diff options
author | toastal <toastal@protonmail.com> | 2020-09-08 10:08:00 +0700 |
---|---|---|
committer | toastal <toastal@protonmail.com> | 2020-09-08 10:08:00 +0700 |
commit | f07ecbc579a216a0fff18bdc010fe1a4de91fa39 (patch) | |
tree | 13c59e33534ae7b7a79fdf0edaa180222f0ec86b /autoload | |
parent | 167e2e77506c55831921ee40dc30c92f7f2aaae8 (diff) | |
parent | b4b75126f9eae30da8f5e0cb9ec100feb38c1cb6 (diff) | |
download | ale-f07ecbc579a216a0fff18bdc010fe1a4de91fa39.zip |
merge master -- apparently someone else added dhall?
Diffstat (limited to 'autoload')
31 files changed, 891 insertions, 537 deletions
diff --git a/autoload/ale.vim b/autoload/ale.vim index 6251b47b..5ec22f57 100644 --- a/autoload/ale.vim +++ b/autoload/ale.vim @@ -263,6 +263,28 @@ function! ale#GetLocItemMessage(item, format_string) abort let l:msg = substitute(l:msg, '\V%linter%', '\=l:linter_name', 'g') " Replace %s with the text. let l:msg = substitute(l:msg, '\V%s', '\=a:item.text', 'g') + " Windows may insert carriage return line endings (^M), strip these characters. + let l:msg = substitute(l:msg, '\r', '', 'g') return l:msg endfunction + +" Given a buffer and a linter or fixer name, return an Array of two-item +" Arrays describing how to map filenames to and from the local to foreign file +" systems. +function! ale#GetFilenameMappings(buffer, name) abort + let l:linter_mappings = ale#Var(a:buffer, 'filename_mappings') + + if type(l:linter_mappings) is v:t_list + return l:linter_mappings + endif + + let l:name = a:name + + if !has_key(l:linter_mappings, l:name) + " Use * as a default setting for all tools. + let l:name = '*' + endif + + return get(l:linter_mappings, l:name, []) +endfunction diff --git a/autoload/ale/assert.vim b/autoload/ale/assert.vim index 291edcee..934fcaa8 100644 --- a/autoload/ale/assert.vim +++ b/autoload/ale/assert.vim @@ -130,7 +130,7 @@ endfunction function! ale#assert#LSPLanguage(expected_language) abort let l:buffer = bufnr('') let l:linter = s:GetLinter() - let l:language = ale#util#GetFunction(l:linter.language_callback)(l:buffer) + let l:language = ale#linter#GetLanguage(l:buffer, l:linter) AssertEqual a:expected_language, l:language endfunction diff --git a/autoload/ale/c.vim b/autoload/ale/c.vim index 39892d42..cff53125 100644 --- a/autoload/ale/c.vim +++ b/autoload/ale/c.vim @@ -2,12 +2,27 @@ " Description: Functions for integrating with C-family linters. call ale#Set('c_parse_makefile', 0) +call ale#Set('c_always_make', has('unix') && !has('macunix')) call ale#Set('c_parse_compile_commands', 1) + let s:sep = has('win32') ? '\' : '/' " Set just so tests can override it. let g:__ale_c_project_filenames = ['.git/HEAD', 'configure', 'Makefile', 'CMakeLists.txt'] +let g:ale_c_build_dir_names = get(g:, 'ale_c_build_dir_names', [ +\ 'build', +\ 'bin', +\]) + +function! s:CanParseMakefile(buffer) abort + " Something somewhere seems to delete this setting in tests, so ensure we + " always have a default value. + call ale#Set('c_parse_makefile', 0) + + return ale#Var(a:buffer, 'c_parse_makefile') +endfunction + function! ale#c#GetBuildDirectory(buffer) abort let l:build_dir = ale#Var(a:buffer, 'c_build_dir') @@ -61,14 +76,73 @@ function! ale#c#ShellSplit(line) abort return l:args endfunction -function! ale#c#ParseCFlags(path_prefix, cflag_line) abort - let l:cflags_list = [] +" Takes the path prefix and a list of cflags and expands @file arguments to +" the contents of the file. +" +" @file arguments are command line arguments recognised by gcc and clang. For +" instance, if @./path/to/file was given to gcc, it would load .path/to/file +" and use the contents of that file as arguments. +function! ale#c#ExpandAtArgs(path_prefix, raw_split_lines) abort + let l:out_lines = [] + + for l:option in a:raw_split_lines + if stridx(l:option, '@') == 0 + " This is an argument specifying a location of a file containing other arguments + let l:path = join(split(l:option, '\zs')[1:], '') + + " Make path absolute + if !ale#path#IsAbsolute(l:path) + let l:rel_path = substitute(l:path, '"', '', 'g') + let l:rel_path = substitute(l:rel_path, '''', '', 'g') + let l:path = ale#path#GetAbsPath(a:path_prefix, l:rel_path) + endif + + " Read the file and add all the arguments + try + let l:additional_args = readfile(l:path) + catch + continue " All we can really do is skip this argument + endtry + + let l:file_lines = [] + + for l:line in l:additional_args + let l:file_lines += ale#c#ShellSplit(l:line) + endfor + + " @file arguments can include other @file arguments, so we must + " recurse. + let l:out_lines += ale#c#ExpandAtArgs(a:path_prefix, l:file_lines) + else + " This is not an @file argument, so don't touch it. + let l:out_lines += [l:option] + endif + endfor + + return l:out_lines +endfunction + +" Quote C/C++ a compiler argument, if needed. +" +" Quoting arguments might cause issues with some systems/compilers, so we only +" quote them if we need to. +function! ale#c#QuoteArg(arg) abort + if a:arg !~# '\v[#$&*()\\|[\]{};''"<>/?! ^%]' + return a:arg + endif + + return ale#Escape(a:arg) +endfunction - let l:split_lines = ale#c#ShellSplit(a:cflag_line) +function! ale#c#ParseCFlags(path_prefix, should_quote, raw_arguments) abort + " Expand @file arguments now before parsing + let l:arguments = ale#c#ExpandAtArgs(a:path_prefix, a:raw_arguments) + " A list of [already_quoted, argument] + let l:items = [] let l:option_index = 0 - while l:option_index < len(l:split_lines) - let l:option = l:split_lines[l:option_index] + while l:option_index < len(l:arguments) + let l:option = l:arguments[l:option_index] let l:option_index = l:option_index + 1 " Include options, that may need relative path fix @@ -76,56 +150,67 @@ function! ale#c#ParseCFlags(path_prefix, cflag_line) abort \ || stridx(l:option, '-iquote') == 0 \ || stridx(l:option, '-isystem') == 0 \ || stridx(l:option, '-idirafter') == 0 + \ || stridx(l:option, '-iframework') == 0 + \ || stridx(l:option, '-include') == 0 if stridx(l:option, '-I') == 0 && l:option isnot# '-I' let l:arg = join(split(l:option, '\zs')[2:], '') let l:option = '-I' else - let l:arg = l:split_lines[l:option_index] + let l:arg = l:arguments[l:option_index] let l:option_index = l:option_index + 1 endif " Fix relative paths if needed - if stridx(l:arg, s:sep) != 0 && stridx(l:arg, '/') != 0 + if !ale#path#IsAbsolute(l:arg) let l:rel_path = substitute(l:arg, '"', '', 'g') let l:rel_path = substitute(l:rel_path, '''', '', 'g') - let l:arg = ale#Escape(a:path_prefix . s:sep . l:rel_path) + let l:arg = ale#path#GetAbsPath(a:path_prefix, l:rel_path) endif - call add(l:cflags_list, l:option) - call add(l:cflags_list, l:arg) + call add(l:items, [1, l:option]) + call add(l:items, [1, ale#Escape(l:arg)]) " Options with arg that can be grouped with the option or separate elseif stridx(l:option, '-D') == 0 || stridx(l:option, '-B') == 0 - call add(l:cflags_list, l:option) - if l:option is# '-D' || l:option is# '-B' - call add(l:cflags_list, l:split_lines[l:option_index]) + call add(l:items, [1, l:option]) + call add(l:items, [0, l:arguments[l:option_index]]) let l:option_index = l:option_index + 1 + else + call add(l:items, [0, l:option]) endif " Options that have an argument (always separate) elseif l:option is# '-iprefix' || stridx(l:option, '-iwithprefix') == 0 \ || l:option is# '-isysroot' || l:option is# '-imultilib' - call add(l:cflags_list, l:option) - call add(l:cflags_list, l:split_lines[l:option_index]) + call add(l:items, [0, l:option]) + call add(l:items, [0, l:arguments[l:option_index]]) let l:option_index = l:option_index + 1 " Options without argument elseif (stridx(l:option, '-W') == 0 && stridx(l:option, '-Wa,') != 0 && stridx(l:option, '-Wl,') != 0 && stridx(l:option, '-Wp,') != 0) \ || l:option is# '-w' || stridx(l:option, '-pedantic') == 0 \ || l:option is# '-ansi' || stridx(l:option, '-std=') == 0 - \ || (stridx(l:option, '-f') == 0 && stridx(l:option, '-fdump') != 0 && stridx(l:option, '-fdiagnostics') != 0 && stridx(l:option, '-fno-show-column') != 0) + \ || stridx(l:option, '-f') == 0 && l:option !~# '\v^-f(dump|diagnostics|no-show-column|stack-usage)' \ || stridx(l:option, '-O') == 0 \ || l:option is# '-C' || l:option is# '-CC' || l:option is# '-trigraphs' \ || stridx(l:option, '-nostdinc') == 0 || stridx(l:option, '-iplugindir=') == 0 \ || stridx(l:option, '--sysroot=') == 0 || l:option is# '--no-sysroot-suffix' \ || stridx(l:option, '-m') == 0 - call add(l:cflags_list, l:option) + call add(l:items, [0, l:option]) endif endwhile - return join(l:cflags_list, ' ') + if a:should_quote + " Quote C arguments that haven't already been quoted above. + " If and only if we've been asked to quote them. + call map(l:items, 'v:val[0] ? v:val[1] : ale#c#QuoteArg(v:val[1])') + else + call map(l:items, 'v:val[1]') + endif + + return join(l:items, ' ') endfunction function! ale#c#ParseCFlagsFromMakeOutput(buffer, make_output) abort - if !g:ale_c_parse_makefile + if !s:CanParseMakefile(a:buffer) return v:null endif @@ -143,7 +228,7 @@ function! ale#c#ParseCFlagsFromMakeOutput(buffer, make_output) abort let l:makefile_path = ale#path#FindNearestFile(a:buffer, 'Makefile') let l:makefile_dir = fnamemodify(l:makefile_path, ':p:h') - return ale#c#ParseCFlags(l:makefile_dir, l:cflag_line) + return ale#c#ParseCFlags(l:makefile_dir, 0, ale#c#ShellSplit(l:cflag_line)) endfunction " Given a buffer number, find the project directory containing @@ -211,6 +296,10 @@ if !exists('s:compile_commands_cache') let s:compile_commands_cache = {} endif +function! ale#c#ResetCompileCommandsCache() abort + let s:compile_commands_cache = {} +endfunction + function! s:GetLookupFromCompileCommandsFile(compile_commands_file) abort let l:empty = [{}, {}] @@ -241,9 +330,20 @@ function! s:GetLookupFromCompileCommandsFile(compile_commands_file) abort let l:dir_lookup = {} for l:entry in (type(l:raw_data) is v:t_list ? l:raw_data : []) + let l:filename = ale#path#GetAbsPath(l:entry.directory, l:entry.file) + + " Store a key for lookups by the absolute path to the filename. + let l:file_lookup[l:filename] = get(l:file_lookup, l:filename, []) + [l:entry] + + " Store a key for fuzzy lookups by the absolute path to the directory. + let l:dirname = fnamemodify(l:filename, ':h') + let l:dir_lookup[l:dirname] = get(l:dir_lookup, l:dirname, []) + [l:entry] + + " Store a key for fuzzy lookups by just the basename of the file. let l:basename = tolower(fnamemodify(l:entry.file, ':t')) let l:file_lookup[l:basename] = get(l:file_lookup, l:basename, []) + [l:entry] + " Store a key for fuzzy lookups by just the basename of the directory. let l:dirbasename = tolower(fnamemodify(l:entry.directory, ':p:h:t')) let l:dir_lookup[l:dirbasename] = get(l:dir_lookup, l:dirbasename, []) + [l:entry] endfor @@ -258,28 +358,80 @@ function! s:GetLookupFromCompileCommandsFile(compile_commands_file) abort return l:empty endfunction -function! ale#c#GetCompileCommand(json_item) abort - if has_key(a:json_item, 'command') - return a:json_item.command - elseif has_key(a:json_item, 'arguments') - return join(a:json_item.arguments, ' ') +" Get [should_quote, arguments] from either 'command' or 'arguments' +" 'arguments' should be quoted later, the split 'command' strings should not. +function! s:GetArguments(json_item) abort + if has_key(a:json_item, 'arguments') + return [1, a:json_item.arguments] + elseif has_key(a:json_item, 'command') + return [0, ale#c#ShellSplit(a:json_item.command)] endif - return '' + return [0, []] endfunction function! ale#c#ParseCompileCommandsFlags(buffer, file_lookup, dir_lookup) abort + let l:buffer_filename = ale#path#Simplify(expand('#' . a:buffer . ':p')) + let l:basename = tolower(fnamemodify(l:buffer_filename, ':t')) + " Look for any file in the same directory if we can't find an exact match. + let l:dir = fnamemodify(l:buffer_filename, ':h') + " Search for an exact file match first. - let l:basename = tolower(expand('#' . a:buffer . ':t')) - let l:file_list = get(a:file_lookup, l:basename, []) + let l:file_list = get(a:file_lookup, l:buffer_filename, []) + + " We may have to look for /foo/bar instead of C:\foo\bar + if empty(l:file_list) && has('win32') + let l:file_list = get( + \ a:file_lookup, + \ ale#path#RemoveDriveLetter(l:buffer_filename), + \ [] + \) + endif + + " Try the absolute path to the directory second. + let l:dir_list = get(a:dir_lookup, l:dir, []) + + if empty(l:dir_list) && has('win32') + let l:dir_list = get( + \ a:dir_lookup, + \ ale#path#RemoveDriveLetter(l:dir), + \ [] + \) + endif + + if empty(l:file_list) && empty(l:dir_list) + " If we can't find matches with the path to the file, try a + " case-insensitive match for any similarly-named file. + let l:file_list = get(a:file_lookup, l:basename, []) + + " If we can't find matches with the path to the directory, try a + " case-insensitive match for anything in similarly-named directory. + let l:dir_list = get(a:dir_lookup, tolower(fnamemodify(l:dir, ':t')), []) + endif + " A source file matching the header filename. let l:source_file = '' if empty(l:file_list) && l:basename =~? '\.h$\|\.hpp$' for l:suffix in ['.c', '.cpp'] - let l:key = fnamemodify(l:basename, ':r') . l:suffix + " Try to find a source file by an absolute path first. + let l:key = fnamemodify(l:buffer_filename, ':r') . l:suffix let l:file_list = get(a:file_lookup, l:key, []) + if empty(l:file_list) && has('win32') + let l:file_list = get( + \ a:file_lookup, + \ ale#path#RemoveDriveLetter(l:key), + \ [] + \) + endif + + if empty(l:file_list) + " Look fuzzy matches on the basename second. + let l:key = fnamemodify(l:basename, ':r') . l:suffix + let l:file_list = get(a:file_lookup, l:key, []) + endif + if !empty(l:file_list) let l:source_file = l:key break @@ -288,28 +440,31 @@ function! ale#c#ParseCompileCommandsFlags(buffer, file_lookup, dir_lookup) abort endif for l:item in l:file_list + let l:filename = ale#path#GetAbsPath(l:item.directory, l:item.file) + " Load the flags for this file, or for a source file matching the " header file. if ( - \ bufnr(l:item.file) is a:buffer + \ bufnr(l:filename) is a:buffer \ || ( \ !empty(l:source_file) - \ && l:item.file[-len(l:source_file):] is? l:source_file + \ && l:filename[-len(l:source_file):] is? l:source_file \ ) \) - return ale#c#ParseCFlags(l:item.directory, ale#c#GetCompileCommand(l:item)) + let [l:should_quote, l:args] = s:GetArguments(l:item) + + return ale#c#ParseCFlags(l:item.directory, l:should_quote, l:args) endif endfor - " Look for any file in the same directory if we can't find an exact match. - let l:dir = ale#path#Simplify(expand('#' . a:buffer . ':p:h')) + for l:item in l:dir_list + let l:filename = ale#path#GetAbsPath(l:item.directory, l:item.file) - let l:dirbasename = tolower(expand('#' . a:buffer . ':p:h:t')) - let l:dir_list = get(a:dir_lookup, l:dirbasename, []) + if ale#path#RemoveDriveLetter(fnamemodify(l:filename, ':h')) + \ is? ale#path#RemoveDriveLetter(l:dir) + let [l:should_quote, l:args] = s:GetArguments(l:item) - for l:item in l:dir_list - if ale#path#Simplify(fnamemodify(l:item.file, ':h')) is? l:dir - return ale#c#ParseCFlags(l:item.directory, ale#c#GetCompileCommand(l:item)) + return ale#c#ParseCFlags(l:item.directory, l:should_quote, l:args) endif endfor @@ -335,9 +490,7 @@ function! ale#c#GetCFlags(buffer, output) abort endif endif - if ale#Var(a:buffer, 'c_parse_makefile') - \&& !empty(a:output) - \&& !empty(l:cflags) + if s:CanParseMakefile(a:buffer) && !empty(a:output) && !empty(l:cflags) let l:cflags = ale#c#ParseCFlagsFromMakeOutput(a:buffer, a:output) endif @@ -349,11 +502,14 @@ function! ale#c#GetCFlags(buffer, output) abort endfunction function! ale#c#GetMakeCommand(buffer) abort - if ale#Var(a:buffer, 'c_parse_makefile') - let l:makefile_path = ale#path#FindNearestFile(a:buffer, 'Makefile') + if s:CanParseMakefile(a:buffer) + let l:path = ale#path#FindNearestFile(a:buffer, 'Makefile') + + if !empty(l:path) + let l:always_make = ale#Var(a:buffer, 'c_always_make') - if !empty(l:makefile_path) - return 'cd '. fnamemodify(l:makefile_path, ':p:h') . ' && make -n' + return ale#path#CdString(fnamemodify(l:path, ':h')) + \ . 'make -n' . (l:always_make ? ' --always-make' : '') endif endif @@ -422,8 +578,3 @@ function! ale#c#IncludeOptions(include_paths) abort return join(l:option_list) endfunction - -let g:ale_c_build_dir_names = get(g:, 'ale_c_build_dir_names', [ -\ 'build', -\ 'bin', -\]) diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index 60c3bbef..8c7263f3 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -24,6 +24,42 @@ function! ale#code_action#HandleCodeAction(code_action, should_save) abort endfor endfunction +function! s:ChangeCmp(left, right) abort + if a:left.start.line < a:right.start.line + return -1 + endif + + if a:left.start.line > a:right.start.line + return 1 + endif + + if a:left.start.offset < a:right.start.offset + return -1 + endif + + if a:left.start.offset > a:right.start.offset + return 1 + endif + + if a:left.end.line < a:right.end.line + return -1 + endif + + if a:left.end.line > a:right.end.line + return 1 + endif + + if a:left.end.offset < a:right.end.offset + return -1 + endif + + if a:left.end.offset > a:right.end.offset + return 1 + endif + + return 0 +endfunction + function! ale#code_action#ApplyChanges(filename, changes, should_save) abort let l:current_buffer = bufnr('') " The buffer is used to determine the fileformat, if available. @@ -48,7 +84,8 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort let l:column_offset = 0 let l:last_end_line = 0 - for l:code_edit in a:changes + " Changes have to be sorted so we apply them from top-to-bottom. + for l:code_edit in sort(copy(a:changes), function('s:ChangeCmp')) if l:code_edit.start.line isnot l:last_end_line let l:column_offset = 0 endif diff --git a/autoload/ale/command.vim b/autoload/ale/command.vim index 1bbc4f4c..8f497169 100644 --- a/autoload/ale/command.vim +++ b/autoload/ale/command.vim @@ -133,11 +133,36 @@ function! ale#command#EscapeCommandPart(command_part) abort return substitute(a:command_part, '%', '%%', 'g') endfunction +" Format a filename, converting it with filename mappings, if non-empty, +" and escaping it for putting into a command string. +" +" The filename can be modified. +function! s:FormatFilename(filename, mappings, modifiers) abort + let l:filename = a:filename + + if !empty(a:mappings) + let l:filename = ale#filename_mapping#Map(l:filename, a:mappings) + endif + + if !empty(a:modifiers) + let l:filename = fnamemodify(l:filename, a:modifiers) + endif + + return ale#Escape(l:filename) +endfunction + " Given a command string, replace every... " %s -> with the current filename " %t -> with the name of an unused file in a temporary directory " %% -> with a literal % -function! ale#command#FormatCommand(buffer, executable, command, pipe_file_if_needed, input) abort +function! ale#command#FormatCommand( +\ buffer, +\ executable, +\ command, +\ pipe_file_if_needed, +\ input, +\ mappings, +\) abort let l:temporary_file = '' let l:command = a:command @@ -154,14 +179,24 @@ function! ale#command#FormatCommand(buffer, executable, command, pipe_file_if_ne " file. if l:command =~# '%s' let l:filename = fnamemodify(bufname(a:buffer), ':p') - let l:command = substitute(l:command, '%s', '\=ale#Escape(l:filename)', 'g') + let l:command = substitute( + \ l:command, + \ '\v\%s(%(:h|:t|:r|:e)*)', + \ '\=s:FormatFilename(l:filename, a:mappings, submatch(1))', + \ 'g' + \) endif if a:input isnot v:false && l:command =~# '%t' " Create a temporary filename, <temp_dir>/<original_basename> " The file itself will not be created by this function. let l:temporary_file = s:TemporaryFilename(a:buffer) - let l:command = substitute(l:command, '%t', '\=ale#Escape(l:temporary_file)', 'g') + let l:command = substitute( + \ l:command, + \ '\v\%t(%(:h|:t|:r|:e)*)', + \ '\=s:FormatFilename(l:temporary_file, a:mappings, submatch(1))', + \ 'g' + \) endif " Finish formatting so %% becomes %. @@ -265,6 +300,7 @@ function! ale#command#Run(buffer, command, Callback, ...) abort \ a:command, \ get(l:options, 'read_buffer', 0), \ get(l:options, 'input', v:null), + \ get(l:options, 'filename_mappings', []), \) let l:command = ale#job#PrepareCommand(a:buffer, l:command) let l:job_options = { diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index bffcdc8a..bdade95c 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -5,7 +5,7 @@ scriptencoding utf-8 " The omnicompletion menu is shown through a special Plug mapping which is " only valid in Insert mode. This way, feedkeys() won't send these keys if you " quit Insert mode quickly enough. -inoremap <silent> <Plug>(ale_show_completion_menu) <C-x><C-o> +inoremap <silent> <Plug>(ale_show_completion_menu) <C-x><C-o><C-p> " If we hit the key sequence in normal mode, then we won't show the menu, so " we should restore the old settings right away. nnoremap <silent> <Plug>(ale_show_completion_menu) :call ale#completion#RestoreCompletionOptions()<CR> @@ -188,7 +188,13 @@ function! ale#completion#GetTriggerCharacter(filetype, prefix) abort return '' endfunction -function! ale#completion#Filter(buffer, filetype, suggestions, prefix) abort +function! ale#completion#Filter( +\ buffer, +\ filetype, +\ suggestions, +\ prefix, +\ exact_prefix_match, +\) abort let l:excluded_words = ale#Var(a:buffer, 'completion_excluded_words') if empty(a:prefix) @@ -215,10 +221,17 @@ function! ale#completion#Filter(buffer, filetype, suggestions, prefix) abort " Dictionaries is accepted here. let l:word = type(l:item) is v:t_string ? l:item : l:item.word - " Add suggestions if the suggestion starts with a - " case-insensitive match for the prefix. - if l:word[: len(a:prefix) - 1] is? a:prefix - call add(l:filtered_suggestions, l:item) + if a:exact_prefix_match + " Add suggestions if the word is an exact match. + if l:word is# a:prefix + call add(l:filtered_suggestions, l:item) + endif + else + " Add suggestions if the suggestion starts with a + " case-insensitive match for the prefix. + if l:word[: len(a:prefix) - 1] is? a:prefix + call add(l:filtered_suggestions, l:item) + endif endif endfor endif @@ -241,21 +254,17 @@ function! ale#completion#Filter(buffer, filetype, suggestions, prefix) abort return l:filtered_suggestions endfunction -function! s:ReplaceCompletionOptions() abort - let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') - - if l:source is# 'ale-automatic' || l:source is# 'ale-manual' - " Remember the old omnifunc value, if there is one. - " If we don't store an old one, we'll just never reset the option. - " This will stop some random exceptions from appearing. - if !exists('b:ale_old_omnifunc') && !empty(&l:omnifunc) - let b:ale_old_omnifunc = &l:omnifunc - endif - - let &l:omnifunc = 'ale#completion#AutomaticOmniFunc' +function! s:ReplaceCompletionOptions(source) abort + " Remember the old omnifunc value, if there is one. + " If we don't store an old one, we'll just never reset the option. + " This will stop some random exceptions from appearing. + if !exists('b:ale_old_omnifunc') && !empty(&l:omnifunc) + let b:ale_old_omnifunc = &l:omnifunc endif - if l:source is# 'ale-automatic' + let &l:omnifunc = 'ale#completion#AutomaticOmniFunc' + + if a:source is# 'ale-automatic' if !exists('b:ale_old_completeopt') let b:ale_old_completeopt = &l:completeopt endif @@ -318,41 +327,70 @@ function! ale#completion#AutomaticOmniFunc(findstart, base) abort else let l:result = ale#completion#GetCompletionResult() - call s:ReplaceCompletionOptions() + let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') + + if l:source is# 'ale-automatic' || l:source is# 'ale-manual' + call s:ReplaceCompletionOptions(l:source) + endif return l:result isnot v:null ? l:result : [] endif endfunction +function! s:OpenCompletionMenu(...) abort + if !&l:paste + call ale#util#FeedKeys("\<Plug>(ale_show_completion_menu)") + endif +endfunction + function! ale#completion#Show(result) abort - if ale#util#Mode() isnot# 'i' + let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') + + if ale#util#Mode() isnot# 'i' && l:source isnot# 'ale-import' return endif - " Set the list in the buffer, temporarily replace omnifunc with our - " function, and then start omni-completion. + " Set the list in the buffer. let b:ale_completion_result = a:result " Don't try to open the completion menu if there's nothing to show. if empty(b:ale_completion_result) + if l:source is# 'ale-import' + " If we ran completion from :ALEImport, + " tell the user that nothing is going to happen. + call s:message('No possible imports found.') + endif + return endif " Replace completion options shortly before opening the menu. - call s:ReplaceCompletionOptions() - - let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') - if l:source is# 'ale-automatic' || l:source is# 'ale-manual' - call timer_start( - \ 0, - \ {-> ale#util#FeedKeys("\<Plug>(ale_show_completion_menu)")} - \) + call s:ReplaceCompletionOptions(l:source) + + call timer_start(0, function('s:OpenCompletionMenu')) endif if l:source is# 'ale-callback' call b:CompleteCallback(b:ale_completion_result) endif + + if l:source is# 'ale-import' + call ale#completion#HandleUserData(b:ale_completion_result[0]) + + let l:text_changed = '' . g:ale_lint_on_text_changed + + " Check the buffer again right away, if linting is enabled. + if g:ale_enabled + \&& ( + \ l:text_changed is# '1' + \ || l:text_changed is# 'always' + \ || l:text_changed is# 'normal' + \ || l:text_changed is# 'insert' + \) + call ale#Queue(0, '') + endif + endif endfunction function! ale#completion#GetAllTriggers() abort @@ -383,14 +421,18 @@ endfunction function! s:CompletionStillValid(request_id) abort let [l:line, l:column] = getpos('.')[1:2] - return ale#util#Mode() is# 'i' - \&& has_key(b:, 'ale_completion_info') + return has_key(b:, 'ale_completion_info') + \&& ( + \ ale#util#Mode() is# 'i' + \ || b:ale_completion_info.source is# 'ale-import' + \) \&& b:ale_completion_info.request_id == a:request_id \&& b:ale_completion_info.line == l:line \&& ( \ b:ale_completion_info.column == l:column \ || b:ale_completion_info.source is# 'ale-omnifunc' \ || b:ale_completion_info.source is# 'ale-callback' + \ || b:ale_completion_info.source is# 'ale-import' \) endfunction @@ -415,15 +457,26 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort let l:buffer = bufnr('') let l:results = [] let l:names_with_details = [] + let l:info = get(b:, 'ale_completion_info', {}) for l:suggestion in a:response.body let l:displayParts = [] + let l:local_name = v:null for l:action in get(l:suggestion, 'codeActions', []) call add(l:displayParts, l:action.description . ' ') endfor for l:part in l:suggestion.displayParts + " Stop on stop on line breaks for the menu. + if get(l:part, 'kind') is# 'lineBreak' + break + endif + + if get(l:part, 'kind') is# 'localName' + let l:local_name = l:part.text + endif + call add(l:displayParts, l:part.text) endfor @@ -436,11 +489,18 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort " See :help complete-items let l:result = { - \ 'word': l:suggestion.name, + \ 'word': ( + \ l:suggestion.name is# 'default' + \ && l:suggestion.kind is# 'alias' + \ && !empty(l:local_name) + \ ? l:local_name + \ : l:suggestion.name + \ ), \ 'kind': ale#completion#GetCompletionSymbols(l:suggestion.kind), \ 'icase': 1, \ 'menu': join(l:displayParts, ''), - \ 'dup': g:ale_completion_autoimport, + \ 'dup': get(l:info, 'additional_edits_only', 0) + \ || g:ale_completion_autoimport, \ 'info': join(l:documentationParts, ''), \} @@ -450,7 +510,12 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort \ }) endif - call add(l:results, l:result) + " Include this item if we'll accept any items, + " or if we only want items with additional edits, and this has them. + if !get(l:info, 'additional_edits_only', 0) + \|| has_key(l:result, 'user_data') + call add(l:results, l:result) + endif endfor let l:names = getbufvar(l:buffer, 'ale_tsserver_completion_names', []) @@ -524,7 +589,11 @@ function! ale#completion#ParseLSPCompletions(response) abort " Don't use LSP items with additional text edits when autoimport for " completions is turned off. - if has_key(l:item, 'additionalTextEdits') && !g:ale_completion_autoimport + if !empty(get(l:item, 'additionalTextEdits')) + \&& !( + \ get(l:info, 'additional_edits_only', 0) + \ || g:ale_completion_autoimport + \) continue endif @@ -546,38 +615,50 @@ function! ale#completion#ParseLSPCompletions(response) abort let l:text_changes = [] for l:edit in l:item.additionalTextEdits - let l:range = l:edit.range call add(l:text_changes, { \ 'start': { - \ 'line': l:range.start.line + 1, - \ 'offset': l:range.start.character + 1, + \ 'line': l:edit.range.start.line + 1, + \ 'offset': l:edit.range.start.character + 1, \ }, \ 'end': { - \ 'line': l:range.end.line + 1, - \ 'offset': l:range.end.character + 1, + \ 'line': l:edit.range.end.line + 1, + \ 'offset': l:edit.range.end.character + 1, \ }, \ 'newText': l:edit.newText, \}) endfor - let l:changes = [{ - \ 'fileName': expand('#' . l:buffer . ':p'), - \ 'textChanges': l:text_changes, - \}] - \ - let l:result.user_data = json_encode({ - \ 'codeActions': [{ - \ 'description': 'completion', - \ 'changes': l:changes, - \ }], - \ }) + if !empty(l:text_changes) + let l:result.user_data = json_encode({ + \ 'codeActions': [{ + \ 'description': 'completion', + \ 'changes': [ + \ { + \ 'fileName': expand('#' . l:buffer . ':p'), + \ 'textChanges': l:text_changes, + \ } + \ ], + \ }], + \}) + endif endif - call add(l:results, l:result) + " Include this item if we'll accept any items, + " or if we only want items with additional edits, and this has them. + if !get(l:info, 'additional_edits_only', 0) + \|| has_key(l:result, 'user_data') + call add(l:results, l:result) + endif endfor if has_key(l:info, 'prefix') - let l:results = ale#completion#Filter(l:buffer, &filetype, l:results, l:info.prefix) + let l:results = ale#completion#Filter( + \ l:buffer, + \ &filetype, + \ l:results, + \ l:info.prefix, + \ get(l:info, 'additional_edits_only', 0), + \) endif return l:results[: g:ale_completion_max_suggestions - 1] @@ -601,13 +682,18 @@ function! ale#completion#HandleTSServerResponse(conn_id, response) abort \ &filetype, \ ale#completion#ParseTSServerCompletions(a:response), \ b:ale_completion_info.prefix, + \ get(b:ale_completion_info, 'additional_edits_only', 0), \)[: g:ale_completion_max_suggestions - 1] " We need to remember some names for tsserver, as it doesn't send " details back for everything we send. call setbufvar(l:buffer, 'ale_tsserver_completion_names', l:names) - if !empty(l:names) + if empty(l:names) + " Response with no results now and skip making a redundant request + " for nothing. + call ale#completion#Show([]) + else let l:identifiers = [] for l:name in l:names @@ -681,7 +767,8 @@ function! s:OnReady(linter, lsp_details) abort \ b:ale_completion_info.line, \ b:ale_completion_info.column, \ b:ale_completion_info.prefix, - \ g:ale_completion_autoimport, + \ get(b:ale_completion_info, 'additional_edits_only', 0) + \ || g:ale_completion_autoimport, \) else " Send a message saying the buffer has changed first, otherwise @@ -740,9 +827,19 @@ function! ale#completion#GetCompletions(...) abort let b:CompleteCallback = l:CompleteCallback endif - let [l:line, l:column] = getpos('.')[1:2] + if has_key(l:options, 'line') && has_key(l:options, 'column') + " Use a provided line and column, if given. + let l:line = l:options.line + let l:column = l:options.column + else + let [l:line, l:column] = getpos('.')[1:2] + endif - let l:prefix = ale#completion#GetPrefix(&filetype, l:line, l:column) + if has_key(l:options, 'prefix') + let l:prefix = l:options.prefix + else + let l:prefix = ale#completion#GetPrefix(&filetype, l:line, l:column) + endif if l:source is# 'ale-automatic' && empty(l:prefix) return 0 @@ -761,6 +858,11 @@ function! ale#completion#GetCompletions(...) abort \} unlet! b:ale_completion_result + if has_key(l:options, 'additional_edits_only') + let b:ale_completion_info.additional_edits_only = + \ l:options.additional_edits_only + endif + let l:buffer = bufnr('') let l:Callback = function('s:OnReady') @@ -777,6 +879,37 @@ function! ale#completion#GetCompletions(...) abort return l:started endfunction +function! s:message(message) abort + call ale#util#Execute('echom ' . string(a:message)) +endfunction + +" This function implements the :ALEImport command. +function! ale#completion#Import() abort + let l:word = expand('<cword>') + + if empty(l:word) + call s:message('Nothing to complete at cursor!') + + return + endif + + let [l:line, l:column] = getpos('.')[1:2] + let l:column = searchpos('\V' . escape(l:word, '/\'), 'bn', l:line)[1] + + if l:column isnot 0 + let l:started = ale#completion#GetCompletions('ale-import', { + \ 'line': l:line, + \ 'column': l:column, + \ 'prefix': l:word, + \ 'additional_edits_only': 1, + \}) + + if !l:started + call s:message('No completion providers are available.') + endif + endif +endfunction + function! ale#completion#OmniFunc(findstart, base) abort if a:findstart let l:started = ale#completion#GetCompletions('ale-omnifunc') @@ -855,6 +988,7 @@ function! ale#completion#HandleUserData(completed_item) abort if l:source isnot# 'ale-automatic' \&& l:source isnot# 'ale-manual' \&& l:source isnot# 'ale-callback' + \&& l:source isnot# 'ale-import' return endif @@ -884,6 +1018,8 @@ function! ale#completion#Done() abort endfunction augroup ALECompletionActions + autocmd! + autocmd CompleteDone * call ale#completion#HandleUserData(v:completed_item) augroup END diff --git a/autoload/ale/cursor.vim b/autoload/ale/cursor.vim index 8c331c5c..9ca6fb15 100644 --- a/autoload/ale/cursor.vim +++ b/autoload/ale/cursor.vim @@ -39,6 +39,8 @@ function! ale#cursor#TruncatedEcho(original_message) abort endif exec 'echomsg l:message' + catch /E481/ + " Do nothing if running from a visual selection. endtry " Reset the cursor position if we moved off the end of the line. diff --git a/autoload/ale/definition.vim b/autoload/ale/definition.vim index ffcd9d10..0c1fb7cf 100644 --- a/autoload/ale/definition.vim +++ b/autoload/ale/definition.vim @@ -135,10 +135,6 @@ function! s:GoToLSPDefinition(linter, options, capability) abort endfunction function! ale#definition#GoTo(options) abort - if !get(g:, 'ale_ignore_2_7_warnings') && has_key(a:options, 'deprecated_command') - execute 'echom '':' . a:options.deprecated_command . ' is deprecated. Use `let g:ale_ignore_2_7_warnings = 1` to disable this message.''' - endif - for l:linter in ale#linter#Get(&filetype) if !empty(l:linter.lsp) call s:GoToLSPDefinition(l:linter, a:options, 'definition') @@ -147,10 +143,6 @@ function! ale#definition#GoTo(options) abort endfunction function! ale#definition#GoToType(options) abort - if !get(g:, 'ale_ignore_2_7_warnings') && has_key(a:options, 'deprecated_command') - execute 'echom '':' . a:options.deprecated_command . ' is deprecated. Use `let g:ale_ignore_2_7_warnings = 1` to disable this message.''' - endif - for l:linter in ale#linter#Get(&filetype) if !empty(l:linter.lsp) " TODO: handle typeDefinition for tsserver if supported by the diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index 491d3c2e..ae0354b8 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -4,6 +4,7 @@ " Remapping of linter problems. let g:ale_type_map = get(g:, 'ale_type_map', {}) +let g:ale_filename_mappings = get(g:, 'ale_filename_mappings', {}) if !has_key(s:, 'executable_cache_map') let s:executable_cache_map = {} @@ -104,42 +105,6 @@ function! ale#engine#IsCheckingBuffer(buffer) abort \ || !empty(get(l:info, 'active_other_sources_list', [])) endfunction -" Register a temporary file to be managed with the ALE engine for -" a current job run. -function! ale#engine#ManageFile(buffer, filename) abort - if !get(g:, 'ale_ignore_2_4_warnings') - execute 'echom ''ale#engine#ManageFile is deprecated. Use `let g:ale_ignore_2_4_warnings = 1` to disable this message.''' - endif - - call ale#command#ManageFile(a:buffer, a:filename) -endfunction - -" Same as the above, but manage an entire directory. -function! ale#engine#ManageDirectory(buffer, directory) abort - if !get(g:, 'ale_ignore_2_4_warnings') - execute 'echom ''ale#engine#ManageDirectory is deprecated. Use `let g:ale_ignore_2_4_warnings = 1` to disable this message.''' - endif - - call ale#command#ManageDirectory(a:buffer, a:directory) -endfunction - -function! ale#engine#CreateFile(buffer) abort - if !get(g:, 'ale_ignore_2_4_warnings') - execute 'echom ''ale#engine#CreateFile is deprecated. Use `let g:ale_ignore_2_4_warnings = 1` to disable this message.''' - endif - - return ale#command#CreateFile(a:buffer) -endfunction - -" Create a new temporary directory and manage it in one go. -function! ale#engine#CreateDirectory(buffer) abort - if !get(g:, 'ale_ignore_2_4_warnings') - execute 'echom ''ale#engine#CreateDirectory is deprecated. Use `let g:ale_ignore_2_4_warnings = 1` to disable this message.''' - endif - - return ale#command#CreateDirectory(a:buffer) -endfunction - function! ale#engine#HandleLoclist(linter_name, buffer, loclist, from_other_source) abort let l:info = get(g:ale_buffer_info, a:buffer, {}) @@ -192,7 +157,6 @@ function! s:HandleExit(job_info, buffer, output, data) abort let l:linter = a:job_info.linter let l:executable = a:job_info.executable - let l:next_chain_index = a:job_info.next_chain_index " Remove this job from the list. call ale#engine#MarkLinterInactive(l:buffer_info, l:linter.name) @@ -207,20 +171,6 @@ function! s:HandleExit(job_info, buffer, output, data) abort call remove(a:output, -1) endif - if l:next_chain_index < len(get(l:linter, 'command_chain', [])) - let [l:command, l:options] = ale#engine#ProcessChain( - \ a:buffer, - \ l:executable, - \ l:linter, - \ l:next_chain_index, - \ a:output, - \) - - call s:RunJob(l:command, l:options) - - return - endif - try let l:loclist = ale#util#GetFunction(l:linter.callback)(a:buffer, a:output) " Handle the function being unknown, or being deleted. @@ -307,6 +257,13 @@ function! s:RemapItemTypes(type_map, loclist) abort endfunction function! ale#engine#FixLocList(buffer, linter_name, from_other_source, loclist) abort + let l:mappings = ale#GetFilenameMappings(a:buffer, a:linter_name) + + if !empty(l:mappings) + " We need to apply reverse filename mapping here. + let l:mappings = ale#filename_mapping#Invert(l:mappings) + endif + let l:bufnr_map = {} let l:new_loclist = [] @@ -347,13 +304,19 @@ function! ale#engine#FixLocList(buffer, linter_name, from_other_source, loclist) let l:item.code = l:old_item.code endif - if has_key(l:old_item, 'filename') - \&& !ale#path#IsTempName(l:old_item.filename) + let l:old_name = get(l:old_item, 'filename', '') + + " Map parsed from output to local filesystem files. + if !empty(l:old_name) && !empty(l:mappings) + let l:old_name = ale#filename_mapping#Map(l:old_name, l:mappings) + endif + + if !empty(l:old_name) && !ale#path#IsTempName(l:old_name) " Use the filename given. " Temporary files are assumed to be for this buffer, " and the filename is not included then, because it looks bad " in the loclist window. - let l:filename = l:old_item.filename + let l:filename = l:old_name let l:item.filename = l:filename if has_key(l:old_item, 'bufnr') @@ -454,20 +417,19 @@ function! s:RunJob(command, options) abort let l:buffer = a:options.buffer let l:linter = a:options.linter let l:output_stream = a:options.output_stream - let l:next_chain_index = a:options.next_chain_index - let l:read_buffer = a:options.read_buffer + let l:read_buffer = a:options.read_buffer && !a:options.lint_file let l:info = g:ale_buffer_info[l:buffer] let l:Callback = function('s:HandleExit', [{ \ 'linter': l:linter, \ 'executable': l:executable, - \ 'next_chain_index': l:next_chain_index, \}]) let l:result = ale#command#Run(l:buffer, l:command, l:Callback, { \ 'output_stream': l:output_stream, \ 'executable': l:executable, \ 'read_buffer': l:read_buffer, - \ 'log_output': l:next_chain_index >= len(get(l:linter, 'command_chain', [])), + \ 'log_output': 1, + \ 'filename_mappings': ale#GetFilenameMappings(l:buffer, l:linter.name), \}) " Only proceed if the job is being run. @@ -482,68 +444,6 @@ function! s:RunJob(command, options) abort return 1 endfunction -" Determine which commands to run for a link in a command chain, or -" just a regular command. -function! ale#engine#ProcessChain(buffer, executable, linter, chain_index, input) abort - let l:output_stream = get(a:linter, 'output_stream', 'stdout') - let l:read_buffer = a:linter.read_buffer - let l:chain_index = a:chain_index - let l:input = a:input - - while l:chain_index < len(a:linter.command_chain) - " Run a chain of commands, one asynchronous command after the other, - " so that many programs can be run in a sequence. - let l:chain_item = a:linter.command_chain[l:chain_index] - - if l:chain_index == 0 - " The first callback in the chain takes only a buffer number. - let l:command = ale#util#GetFunction(l:chain_item.callback)( - \ a:buffer - \) - else - " The second callback in the chain takes some input too. - let l:command = ale#util#GetFunction(l:chain_item.callback)( - \ a:buffer, - \ l:input - \) - endif - - " If we have a command to run, execute that. - if !empty(l:command) - " The chain item can override the output_stream option. - if has_key(l:chain_item, 'output_stream') - let l:output_stream = l:chain_item.output_stream - endif - - " The chain item can override the read_buffer option. - if has_key(l:chain_item, 'read_buffer') - let l:read_buffer = l:chain_item.read_buffer - elseif l:chain_index != len(a:linter.command_chain) - 1 - " Don't read the buffer for commands besides the last one - " in the chain by default. - let l:read_buffer = 0 - endif - - break - endif - - " Command chain items can return an empty string to indicate that - " a command should be skipped, so we should try the next item - " with no input. - let l:input = [] - let l:chain_index += 1 - endwhile - - return [l:command, { - \ 'executable': a:executable, - \ 'buffer': a:buffer, - \ 'linter': a:linter, - \ 'output_stream': l:output_stream, - \ 'next_chain_index': l:chain_index + 1, - \ 'read_buffer': l:read_buffer, - \}] -endfunction - function! s:StopCurrentJobs(buffer, clear_lint_file_jobs) abort let l:info = get(g:ale_buffer_info, a:buffer, {}) call ale#command#StopJobs(a:buffer, 'linter') @@ -608,10 +508,15 @@ function! s:AddProblemsFromOtherBuffers(buffer, linters) abort endif endfunction -function! s:RunIfExecutable(buffer, linter, executable) abort +function! s:RunIfExecutable(buffer, linter, lint_file, executable) abort if ale#command#IsDeferred(a:executable) let a:executable.result_callback = { - \ executable -> s:RunIfExecutable(a:buffer, a:linter, executable) + \ executable -> s:RunIfExecutable( + \ a:buffer, + \ a:linter, + \ a:lint_file, + \ executable + \ ) \} return 1 @@ -619,29 +524,17 @@ function! s:RunIfExecutable(buffer, linter, executable) abort if ale#engine#IsExecutable(a:buffer, a:executable) " Use different job types for file or linter jobs. - let l:job_type = a:linter.lint_file ? 'file_linter' : 'linter' + let l:job_type = a:lint_file ? 'file_linter' : 'linter' call setbufvar(a:buffer, 'ale_job_type', l:job_type) - if has_key(a:linter, 'command_chain') - let [l:command, l:options] = ale#engine#ProcessChain( - \ a:buffer, - \ a:executable, - \ a:linter, - \ 0, - \ [] - \) - - return s:RunJob(l:command, l:options) - endif - let l:command = ale#linter#GetCommand(a:buffer, a:linter) let l:options = { \ 'executable': a:executable, \ 'buffer': a:buffer, \ 'linter': a:linter, \ 'output_stream': get(a:linter, 'output_stream', 'stdout'), - \ 'next_chain_index': 1, \ 'read_buffer': a:linter.read_buffer, + \ 'lint_file': a:lint_file, \} return s:RunJob(l:command, l:options) @@ -653,33 +546,62 @@ endfunction " Run a linter for a buffer. " " Returns 1 if the linter was successfully run. -function! s:RunLinter(buffer, linter) abort +function! s:RunLinter(buffer, linter, lint_file) abort if !empty(a:linter.lsp) return ale#lsp_linter#CheckWithLSP(a:buffer, a:linter) else let l:executable = ale#linter#GetExecutable(a:buffer, a:linter) - return s:RunIfExecutable(a:buffer, a:linter, l:executable) + return s:RunIfExecutable(a:buffer, a:linter, a:lint_file, l:executable) endif return 0 endfunction -function! ale#engine#RunLinters(buffer, linters, should_lint_file) abort - " Initialise the buffer information if needed. - let l:new_buffer = ale#engine#InitBufferInfo(a:buffer) - call s:StopCurrentJobs(a:buffer, a:should_lint_file) - call s:RemoveProblemsForDisabledLinters(a:buffer, a:linters) +function! s:GetLintFileValues(slots, Callback) abort + let l:deferred_list = [] + let l:new_slots = [] - " We can only clear the results if we aren't checking the buffer. - let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer) + for [l:lint_file, l:linter] in a:slots + while ale#command#IsDeferred(l:lint_file) && has_key(l:lint_file, 'value') + " If we've already computed the return value, use it. + let l:lint_file = l:lint_file.value + endwhile - silent doautocmd <nomodeline> User ALELintPre + if ale#command#IsDeferred(l:lint_file) + " If we are going to return the result later, wait for it. + call add(l:deferred_list, l:lint_file) + else + " If we have the value now, coerce it to 0 or 1. + let l:lint_file = l:lint_file is 1 + endif - for l:linter in a:linters + call add(l:new_slots, [l:lint_file, l:linter]) + endfor + + if !empty(l:deferred_list) + for l:deferred in l:deferred_list + let l:deferred.result_callback = + \ {-> s:GetLintFileValues(l:new_slots, a:Callback)} + endfor + else + call a:Callback(l:new_slots) + endif +endfunction + +function! s:RunLinters( +\ buffer, +\ slots, +\ should_lint_file, +\ new_buffer, +\ can_clear_results +\) abort + let l:can_clear_results = a:can_clear_results + + for [l:lint_file, l:linter] in a:slots " Only run lint_file linters if we should. - if !l:linter.lint_file || a:should_lint_file - if s:RunLinter(a:buffer, l:linter) + if !l:lint_file || a:should_lint_file + if s:RunLinter(a:buffer, l:linter, l:lint_file) " If a single linter ran, we shouldn't clear everything. let l:can_clear_results = 0 endif @@ -694,11 +616,49 @@ function! ale#engine#RunLinters(buffer, linters, should_lint_file) abort " disabled, or ALE itself is disabled. if l:can_clear_results call ale#engine#SetResults(a:buffer, []) - elseif l:new_buffer - call s:AddProblemsFromOtherBuffers(a:buffer, a:linters) + elseif a:new_buffer + call s:AddProblemsFromOtherBuffers( + \ a:buffer, + \ map(copy(a:slots), 'v:val[1]') + \) endif endfunction +function! ale#engine#RunLinters(buffer, linters, should_lint_file) abort + " Initialise the buffer information if needed. + let l:new_buffer = ale#engine#InitBufferInfo(a:buffer) + call s:StopCurrentJobs(a:buffer, a:should_lint_file) + call s:RemoveProblemsForDisabledLinters(a:buffer, a:linters) + + " We can only clear the results if we aren't checking the buffer. + let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer) + + silent doautocmd <nomodeline> User ALELintPre + + " Handle `lint_file` callbacks first. + let l:linter_slots = [] + + for l:linter in a:linters + let l:LintFile = l:linter.lint_file + + if type(l:LintFile) is v:t_func + let l:LintFile = l:LintFile(a:buffer) + endif + + call add(l:linter_slots, [l:LintFile, l:linter]) + endfor + + call s:GetLintFileValues(l:linter_slots, { + \ new_slots -> s:RunLinters( + \ a:buffer, + \ new_slots, + \ a:should_lint_file, + \ l:new_buffer, + \ l:can_clear_results, + \ ) + \}) +endfunction + " Clean up a buffer. " " This function will stop all current jobs for the buffer, diff --git a/autoload/ale/events.vim b/autoload/ale/events.vim index 731e36f2..3568c117 100644 --- a/autoload/ale/events.vim +++ b/autoload/ale/events.vim @@ -105,11 +105,11 @@ function! ale#events#Init() abort if g:ale_enabled if l:text_changed is? 'always' || l:text_changed is# '1' - autocmd TextChanged,TextChangedI * call ale#Queue(g:ale_lint_delay) + autocmd TextChanged,TextChangedI * call ale#Queue(ale#Var(str2nr(expand('<abuf>')), 'lint_delay')) elseif l:text_changed is? 'normal' - autocmd TextChanged * call ale#Queue(g:ale_lint_delay) + autocmd TextChanged * call ale#Queue(ale#Var(str2nr(expand('<abuf>')), 'lint_delay')) elseif l:text_changed is? 'insert' - autocmd TextChangedI * call ale#Queue(g:ale_lint_delay) + autocmd TextChangedI * call ale#Queue(ale#Var(str2nr(expand('<abuf>')), 'lint_delay')) endif if g:ale_lint_on_enter diff --git a/autoload/ale/filename_mapping.vim b/autoload/ale/filename_mapping.vim new file mode 100644 index 00000000..76d47acc --- /dev/null +++ b/autoload/ale/filename_mapping.vim @@ -0,0 +1,22 @@ +" Author: w0rp <devw0rp@gmail.com> +" Description: Logic for handling mappings between files + +" Invert filesystem mappings so they can be mapped in reverse. +function! ale#filename_mapping#Invert(filename_mappings) abort + return map(copy(a:filename_mappings), '[v:val[1], v:val[0]]') +endfunction + +" Given a filename and some filename_mappings, map a filename. +function! ale#filename_mapping#Map(filename, filename_mappings) abort + let l:simplified_filename = ale#path#Simplify(a:filename) + + for [l:mapping_from, l:mapping_to] in a:filename_mappings + let l:mapping_from = ale#path#Simplify(l:mapping_from) + + if l:simplified_filename[:len(l:mapping_from) - 1] is# l:mapping_from + return l:mapping_to . l:simplified_filename[len(l:mapping_from):] + endif + endfor + + return a:filename +endfunction diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index 69817b36..8b841b13 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -1,4 +1,8 @@ -call ale#Set('fix_on_save_ignore', {}) +" Author: w0rp <devw0rp@gmail.com> +" Description: Functions for fixing code with programs, or other means. + +let g:ale_fix_on_save_ignore = get(g:, 'ale_fix_on_save_ignore', {}) +let g:ale_filename_mappings = get(g:, 'ale_filename_mappings', {}) " Apply fixes queued up for buffers which may be hidden. " Vim doesn't let you modify hidden buffers. @@ -11,22 +15,29 @@ function! ale#fix#ApplyQueuedFixes(buffer) abort call remove(g:ale_fix_buffer_data, a:buffer) - if l:data.changes_made - let l:new_lines = ale#util#SetBufferContents(a:buffer, l:data.output) - - if l:data.should_save - if a:buffer is bufnr('') - if empty(&buftype) - noautocmd :w! + try + if l:data.changes_made + let l:new_lines = ale#util#SetBufferContents(a:buffer, l:data.output) + + if l:data.should_save + if a:buffer is bufnr('') + if empty(&buftype) + noautocmd :w! + else + set nomodified + endif else - set nomodified + call writefile(l:new_lines, expand('#' . a:buffer . ':p')) " no-custom-checks + call setbufvar(a:buffer, '&modified', 0) endif - else - call writefile(l:new_lines, expand('#' . a:buffer . ':p')) " no-custom-checks - call setbufvar(a:buffer, '&modified', 0) endif endif - endif + catch /E21/ + " If we cannot modify the buffer now, try again later. + let g:ale_fix_buffer_data[a:buffer] = l:data + + return + endtry if l:data.should_save let l:should_lint = ale#Var(a:buffer, 'fix_on_save') @@ -90,7 +101,6 @@ function! s:HandleExit(job_info, buffer, job_output, data) abort let l:output = a:job_output endif - let l:ChainCallback = get(a:job_info, 'chain_with', v:null) let l:ProcessWith = get(a:job_info, 'process_with', v:null) " Post-process the output with a function if we have one. @@ -102,27 +112,17 @@ function! s:HandleExit(job_info, buffer, job_output, data) abort " otherwise skip this job and use the input from before. " " We'll use the input from before for chained commands. - if l:ChainCallback is v:null && !empty(split(join(l:output))) + if !empty(split(join(l:output))) let l:input = l:output else let l:input = a:job_info.input endif - if l:ChainCallback isnot v:null && !get(g:, 'ale_ignore_2_4_warnings') - execute 'echom ''chain_with is deprecated. Use `let g:ale_ignore_2_4_warnings = 1` to disable this message.''' - endif - - let l:next_index = l:ChainCallback is v:null - \ ? a:job_info.callback_index + 1 - \ : a:job_info.callback_index - call s:RunFixer({ \ 'buffer': a:buffer, \ 'input': l:input, - \ 'output': l:output, \ 'callback_list': a:job_info.callback_list, - \ 'callback_index': l:next_index, - \ 'chain_callback': l:ChainCallback, + \ 'callback_index': a:job_info.callback_index + 1, \}) endfunction @@ -135,6 +135,7 @@ function! s:RunJob(result, options) abort let l:buffer = a:options.buffer let l:input = a:options.input + let l:fixer_name = a:options.fixer_name if a:result is 0 || type(a:result) is v:t_list if type(a:result) is v:t_list @@ -152,26 +153,21 @@ function! s:RunJob(result, options) abort endif let l:command = get(a:result, 'command', '') - let l:ChainWith = get(a:result, 'chain_with', v:null) if empty(l:command) - " If the command is empty, skip to the next item, or call the - " chain_with function. + " If the command is empty, skip to the next item. call s:RunFixer({ \ 'buffer': l:buffer, \ 'input': l:input, - \ 'callback_index': a:options.callback_index + (l:ChainWith is v:null), + \ 'callback_index': a:options.callback_index, \ 'callback_list': a:options.callback_list, - \ 'chain_callback': l:ChainWith, - \ 'output': [], \}) return endif let l:read_temporary_file = get(a:result, 'read_temporary_file', 0) - " Default to piping the buffer for the last fixer in the chain. - let l:read_buffer = get(a:result, 'read_buffer', l:ChainWith is v:null) + let l:read_buffer = get(a:result, 'read_buffer', 1) let l:output_stream = get(a:result, 'output_stream', 'stdout') if l:read_temporary_file @@ -180,7 +176,6 @@ function! s:RunJob(result, options) abort let l:Callback = function('s:HandleExit', [{ \ 'input': l:input, - \ 'chain_with': l:ChainWith, \ 'callback_index': a:options.callback_index, \ 'callback_list': a:options.callback_list, \ 'process_with': get(a:result, 'process_with', v:null), @@ -192,6 +187,7 @@ function! s:RunJob(result, options) abort \ 'read_buffer': l:read_buffer, \ 'input': l:input, \ 'log_output': 0, + \ 'filename_mappings': ale#GetFilenameMappings(l:buffer, l:fixer_name), \}) if empty(l:run_result) @@ -215,32 +211,22 @@ function! s:RunFixer(options) abort return endif - let l:ChainCallback = get(a:options, 'chain_callback', v:null) - - let l:Function = l:ChainCallback isnot v:null - \ ? ale#util#GetFunction(l:ChainCallback) - \ : a:options.callback_list[l:index] + let [l:fixer_name, l:Function] = a:options.callback_list[l:index] " Record new jobs started as fixer jobs. call setbufvar(l:buffer, 'ale_job_type', 'fixer') - if l:ChainCallback isnot v:null - " Chained commands accept (buffer, output, [input]) - let l:result = ale#util#FunctionArgCount(l:Function) == 2 - \ ? call(l:Function, [l:buffer, a:options.output]) - \ : call(l:Function, [l:buffer, a:options.output, copy(l:input)]) - else - " Regular fixer commands accept (buffer, [input]) - let l:result = ale#util#FunctionArgCount(l:Function) == 1 - \ ? call(l:Function, [l:buffer]) - \ : call(l:Function, [l:buffer, copy(l:input)]) - endif + " Regular fixer commands accept (buffer, [input]) + let l:result = ale#util#FunctionArgCount(l:Function) == 1 + \ ? call(l:Function, [l:buffer]) + \ : call(l:Function, [l:buffer, copy(l:input)]) call s:RunJob(l:result, { \ 'buffer': l:buffer, \ 'input': l:input, \ 'callback_list': a:options.callback_list, \ 'callback_index': l:index, + \ 'fixer_name': l:fixer_name, \}) endfunction @@ -308,16 +294,24 @@ function! s:GetCallbacks(buffer, fixing_flag, fixers) abort " Variables with capital characters are needed, or Vim will complain about " funcref variables. for l:Item in l:callback_list + " Try to capture the names of registered fixer names, so we can use + " them for filename mapping or other purposes later. + let l:fixer_name = v:null + if type(l:Item) is v:t_string let l:Func = ale#fix#registry#GetFunc(l:Item) if !empty(l:Func) + let l:fixer_name = l:Item let l:Item = l:Func endif endif try - call add(l:corrected_list, ale#util#GetFunction(l:Item)) + call add(l:corrected_list, [ + \ l:fixer_name, + \ ale#util#GetFunction(l:Item) + \]) catch /E475/ " Rethrow exceptions for failing to get a function so we can print " a friendly message about it. @@ -389,3 +383,4 @@ endfunction augroup ALEBufferFixGroup autocmd! autocmd BufEnter * call ale#fix#ApplyQueuedFixes(str2nr(expand('<abuf>'))) +augroup END diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index 3dc5c774..0a729cf9 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -175,11 +175,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['php'], \ 'description': 'Fix PHP files with php-cs-fixer.', \ }, -\ 'astyle': { +\ 'astyle': { \ 'function': 'ale#fixers#astyle#Fix', \ 'suggested_filetypes': ['c', 'cpp'], \ 'description': 'Fix C/C++ with astyle.', -\ }, +\ }, \ 'clangtidy': { \ 'function': 'ale#fixers#clangtidy#Fix', \ 'suggested_filetypes': ['c', 'cpp', 'objc'], @@ -380,11 +380,21 @@ let s:default_registry = { \ 'suggested_filetypes': ['nix'], \ 'description': 'A formatter for Nix code', \ }, +\ 'remark-lint': { +\ 'function': 'ale#fixers#remark_lint#Fix', +\ 'suggested_filetypes': ['markdown'], +\ 'description': 'Fix markdown files with remark-lint', +\ }, \ 'html-beautify': { \ 'function': 'ale#fixers#html_beautify#Fix', \ 'suggested_filetypes': ['html', 'htmldjango'], \ 'description': 'Fix HTML files with html-beautify.', \ }, +\ 'dhall': { +\ 'function': 'ale#fixers#dhall#Fix', +\ 'suggested_filetypes': ['dhall'], +\ 'description': 'Fix Dhall files with dhall-format.', +\ }, \} " Reset the function registry to the default entries. diff --git a/autoload/ale/fixers/dhall.vim b/autoload/ale/fixers/dhall.vim new file mode 100644 index 00000000..18f6006c --- /dev/null +++ b/autoload/ale/fixers/dhall.vim @@ -0,0 +1,23 @@ +" Author: Pat Brisbin <pbrisbin@gmail.com> +" Description: Integration of dhall-format with ALE. + +call ale#Set('dhall_format_executable', 'dhall') + +function! ale#fixers#dhall#GetExecutable(buffer) abort + let l:executable = ale#Var(a:buffer, 'dhall_format_executable') + + " Dhall is written in Haskell and commonly installed with Stack + return ale#handlers#haskell_stack#EscapeExecutable(l:executable, 'dhall') +endfunction + +function! ale#fixers#dhall#Fix(buffer) abort + let l:executable = ale#fixers#dhall#GetExecutable(a:buffer) + + return { + \ 'command': l:executable + \ . ' format' + \ . ' --inplace' + \ . ' %t', + \ 'read_temporary_file': 1, + \} +endfunction diff --git a/autoload/ale/fixers/latexindent.vim b/autoload/ale/fixers/latexindent.vim index b0a0884a..54f1231e 100644 --- a/autoload/ale/fixers/latexindent.vim +++ b/autoload/ale/fixers/latexindent.vim @@ -10,9 +10,7 @@ function! ale#fixers#latexindent#Fix(buffer) abort return { \ 'command': ale#Escape(l:executable) - \ . ' -l -w' + \ . ' -l' \ . (empty(l:options) ? '' : ' ' . l:options) - \ . ' %t', - \ 'read_temporary_file': 1, \} endfunction diff --git a/autoload/ale/fixers/ocamlformat.vim b/autoload/ale/fixers/ocamlformat.vim index 9b7c3e12..b12d2eb9 100644 --- a/autoload/ale/fixers/ocamlformat.vim +++ b/autoload/ale/fixers/ocamlformat.vim @@ -5,14 +5,13 @@ call ale#Set('ocaml_ocamlformat_executable', 'ocamlformat') call ale#Set('ocaml_ocamlformat_options', '') function! ale#fixers#ocamlformat#Fix(buffer) abort - let l:filename = expand('#' . a:buffer . ':p') let l:executable = ale#Var(a:buffer, 'ocaml_ocamlformat_executable') let l:options = ale#Var(a:buffer, 'ocaml_ocamlformat_options') return { \ 'command': ale#Escape(l:executable) \ . (empty(l:options) ? '' : ' ' . l:options) - \ . ' --name=' . ale#Escape(l:filename) + \ . ' --name=%s' \ . ' -' \} endfunction diff --git a/autoload/ale/fixers/prettier.vim b/autoload/ale/fixers/prettier.vim index 23120777..e0f4972e 100644 --- a/autoload/ale/fixers/prettier.vim +++ b/autoload/ale/fixers/prettier.vim @@ -34,6 +34,21 @@ function! ale#fixers#prettier#ProcessPrettierDOutput(buffer, output) abort return a:output endfunction +function! ale#fixers#prettier#GetProjectRoot(buffer) abort + let l:config = ale#path#FindNearestFile(a:buffer, '.prettierignore') + + if !empty(l:config) + return fnamemodify(l:config, ':h') + endif + + " Fall back to the directory of the buffer + return fnamemodify(bufname(a:buffer), ':p:h') +endfunction + +function! ale#fixers#prettier#CdProjectRoot(buffer) abort + return ale#path#CdString(ale#fixers#prettier#GetProjectRoot(a:buffer)) +endfunction + function! ale#fixers#prettier#ApplyFixForVersion(buffer, version) abort let l:executable = ale#fixers#prettier#GetExecutable(a:buffer) let l:options = ale#Var(a:buffer, 'javascript_prettier_options') @@ -97,7 +112,7 @@ function! ale#fixers#prettier#ApplyFixForVersion(buffer, version) abort " 1.4.0 is the first version with --stdin-filepath if ale#semver#GTE(a:version, [1, 4, 0]) return { - \ 'command': ale#path#BufferCdString(a:buffer) + \ 'command': ale#fixers#prettier#CdProjectRoot(a:buffer) \ . ale#Escape(l:executable) \ . (!empty(l:options) ? ' ' . l:options : '') \ . ' --stdin-filepath %s --stdin', diff --git a/autoload/ale/fixers/prettier_standard.vim b/autoload/ale/fixers/prettier_standard.vim index b6e0a6f9..9d982ff6 100644 --- a/autoload/ale/fixers/prettier_standard.vim +++ b/autoload/ale/fixers/prettier_standard.vim @@ -17,8 +17,8 @@ function! ale#fixers#prettier_standard#Fix(buffer) abort return { \ 'command': ale#Escape(ale#fixers#prettier_standard#GetExecutable(a:buffer)) - \ . ' %t' + \ . ' --stdin' + \ . ' --stdin-filepath=%s' \ . ' ' . l:options, - \ 'read_temporary_file': 1, \} endfunction diff --git a/autoload/ale/fixers/remark_lint.vim b/autoload/ale/fixers/remark_lint.vim new file mode 100644 index 00000000..3ce442f3 --- /dev/null +++ b/autoload/ale/fixers/remark_lint.vim @@ -0,0 +1,24 @@ +" Author: blyoa <blyoa110@gmail.com> +" Description: Fixing files with remark-lint. + +call ale#Set('markdown_remark_lint_executable', 'remark') +call ale#Set('markdown_remark_lint_use_global', get(g:, 'ale_use_global_executables', 0)) +call ale#Set('markdown_remark_lint_options', '') + +function! ale#fixers#remark_lint#GetExecutable(buffer) abort + return ale#node#FindExecutable(a:buffer, 'markdown_remark_lint', [ + \ 'node_modules/remark-cli/cli.js', + \ 'node_modules/.bin/remark', + \]) +endfunction + +function! ale#fixers#remark_lint#Fix(buffer) abort + let l:executable = ale#fixers#remark_lint#GetExecutable(a:buffer) + let l:options = ale#Var(a:buffer, 'markdown_remark_lint_options') + + return { + \ 'command': ale#Escape(l:executable) + \ . (!empty(l:options) ? ' ' . l:options : ''), + \} +endfunction + diff --git a/autoload/ale/fixers/rubocop.vim b/autoload/ale/fixers/rubocop.vim index d9615256..cdfb014a 100644 --- a/autoload/ale/fixers/rubocop.vim +++ b/autoload/ale/fixers/rubocop.vim @@ -29,8 +29,7 @@ function! ale#fixers#rubocop#GetCommand(buffer) abort \ . (!empty(l:config) ? ' --config ' . ale#Escape(l:config) : '') \ . (!empty(l:options) ? ' ' . l:options : '') \ . (l:auto_correct_all ? ' --auto-correct-all' : ' --auto-correct') - \ . ' --force-exclusion --stdin ' - \ . ale#Escape(expand('#' . a:buffer . ':p')) + \ . ' --force-exclusion --stdin %s' endfunction function! ale#fixers#rubocop#Fix(buffer) abort diff --git a/autoload/ale/fixers/standard.vim b/autoload/ale/fixers/standard.vim index cffa9f9d..46decebf 100644 --- a/autoload/ale/fixers/standard.vim +++ b/autoload/ale/fixers/standard.vim @@ -27,7 +27,7 @@ function! ale#fixers#standard#Fix(buffer) abort return { \ 'command': ale#node#Executable(a:buffer, l:executable) \ . (!empty(l:options) ? ' ' . l:options : '') - \ . ' --fix %t', + \ . ' --fix --stdin < %s > %t', \ 'read_temporary_file': 1, \} endfunction diff --git a/autoload/ale/handlers/gcc.vim b/autoload/ale/handlers/gcc.vim index ec16b977..0b37c98a 100644 --- a/autoload/ale/handlers/gcc.vim +++ b/autoload/ale/handlers/gcc.vim @@ -10,7 +10,7 @@ let s:pragma_error = '#pragma once in main file' " <stdin>:8:5: warning: conversion lacks type at end of format [-Wformat=] " <stdin>:10:27: error: invalid operands to binary - (have ‘int’ and ‘char *’) " -:189:7: note: $/${} is unnecessary on arithmetic variables. [SC2004] -let s:pattern = '\v^([a-zA-Z]?:?[^:]+):(\d+):(\d+)?:? ([^:]+): (.+)$' +let s:pattern = '\v^([a-zA-Z]?:?[^:]+):(\d+)?:?(\d+)?:? ([^:]+): (.+)$' let s:inline_pattern = '\v inlined from .* at \<stdin\>:(\d+):(\d+):$' function! s:IsHeaderFile(filename) abort @@ -117,6 +117,23 @@ function! ale#handlers#gcc#HandleGCCFormat(buffer, lines) abort if !empty(l:output) if !has_key(l:output[-1], 'detail') let l:output[-1].detail = l:output[-1].text + + " handle macro expansion errors/notes + if l:match[5] =~? '^in expansion of macro ‘\w*\w’$' + " if the macro expansion is in the file we're in, add + " the lnum and col keys to the previous error + if l:match[1] is# '<stdin>' + \ && !has_key(l:output[-1], 'col') + let l:output[-1].lnum = str2nr(l:match[2]) + let l:output[-1].col = str2nr(l:match[3]) + else + " the error is not in the current file, and since + " macro expansion errors don't show the full path to + " the error from the current file, we have to just + " give out a generic error message + let l:output[-1].text = 'Error found in macro expansion. See :ALEDetail' + endif + endif endif let l:output[-1].detail = l:output[-1].detail . "\n" diff --git a/autoload/ale/handlers/sh.vim b/autoload/ale/handlers/sh.vim index 75eaf71f..1e50cb89 100644 --- a/autoload/ale/handlers/sh.vim +++ b/autoload/ale/handlers/sh.vim @@ -4,17 +4,24 @@ function! ale#handlers#sh#GetShellType(buffer) abort let l:bang_line = get(getbufline(a:buffer, 1), 0, '') + let l:command = '' + " Take the shell executable from the hashbang, if we can. if l:bang_line[:1] is# '#!' " Remove options like -e, etc. let l:command = substitute(l:bang_line, ' --\?[a-zA-Z0-9]\+', '', 'g') + endif - for l:possible_shell in ['bash', 'dash', 'ash', 'tcsh', 'csh', 'zsh', 'ksh', 'sh'] - if l:command =~# l:possible_shell . '\s*$' - return l:possible_shell - endif - endfor + " If we couldn't find a hashbang, try the filetype + if l:command is# '' + let l:command = &filetype endif + for l:possible_shell in ['bash', 'dash', 'ash', 'tcsh', 'csh', 'zsh', 'ksh', 'sh'] + if l:command =~# l:possible_shell . '\s*$' + return l:possible_shell + endif + endfor + return '' endfunction diff --git a/autoload/ale/hover.vim b/autoload/ale/hover.vim index 168ff424..38b4b866 100644 --- a/autoload/ale/hover.vim +++ b/autoload/ale/hover.vim @@ -264,7 +264,10 @@ function! s:OnReady(line, column, opt, linter, lsp_details) abort " hover position probably won't make sense. call ale#lsp#NotifyForChanges(l:id, l:buffer) - let l:column = min([a:column, len(getbufline(l:buffer, a:line)[0])]) + let l:column = max([ + \ min([a:column, len(getbufline(l:buffer, a:line)[0])]), + \ 1, + \]) let l:message = ale#lsp#message#Hover(l:buffer, a:line, l:column) endif diff --git a/autoload/ale/linter.vim b/autoload/ale/linter.vim index 0e935149..b483fc19 100644 --- a/autoload/ale/linter.vim +++ b/autoload/ale/linter.vim @@ -32,7 +32,7 @@ let s:default_ale_linter_aliases = { " " No linters are used for plaintext files by default. " -" Only cargo is enabled for Rust by default. +" Only cargo and rls are enabled for Rust by default. " rpmlint is disabled by default because it can result in code execution. " hhast is disabled by default because it executes code in the project root. " @@ -46,7 +46,7 @@ let s:default_ale_linters = { \ 'perl': ['perlcritic'], \ 'perl6': [], \ 'python': ['flake8', 'mypy', 'pylint', 'pyright'], -\ 'rust': ['cargo'], +\ 'rust': ['cargo', 'rls'], \ 'spec': [], \ 'text': [], \ 'vue': ['eslint', 'vls'], @@ -77,10 +77,6 @@ function! s:IsBoolean(value) abort return type(a:value) is v:t_number && (a:value == 0 || a:value == 1) endfunction -function! s:LanguageGetter(buffer) dict abort - return l:self.language -endfunction - function! ale#linter#PreProcess(filetype, linter) abort if type(a:linter) isnot v:t_dict throw 'The linter object must be a Dictionary' @@ -114,14 +110,7 @@ function! ale#linter#PreProcess(filetype, linter) abort if !l:needs_executable if has_key(a:linter, 'executable') - \|| has_key(a:linter, 'executable_callback') - throw '`executable` and `executable_callback` cannot be used when lsp == ''socket''' - endif - elseif has_key(a:linter, 'executable_callback') - let l:obj.executable_callback = a:linter.executable_callback - - if !s:IsCallback(l:obj.executable_callback) - throw '`executable_callback` must be a callback if defined' + throw '`executable` cannot be used when lsp == ''socket''' endif elseif has_key(a:linter, 'executable') let l:obj.executable = a:linter.executable @@ -131,54 +120,12 @@ function! ale#linter#PreProcess(filetype, linter) abort throw '`executable` must be a String or Function if defined' endif else - throw 'Either `executable` or `executable_callback` must be defined' + throw '`executable` must be defined' endif if !l:needs_command if has_key(a:linter, 'command') - \|| has_key(a:linter, 'command_callback') - \|| has_key(a:linter, 'command_chain') - throw '`command` and `command_callback` and `command_chain` cannot be used when lsp == ''socket''' - endif - elseif has_key(a:linter, 'command_chain') - let l:obj.command_chain = a:linter.command_chain - - if type(l:obj.command_chain) isnot v:t_list - throw '`command_chain` must be a List' - endif - - if empty(l:obj.command_chain) - throw '`command_chain` must contain at least one item' - endif - - let l:link_index = 0 - - for l:link in l:obj.command_chain - let l:err_prefix = 'The `command_chain` item ' . l:link_index . ' ' - - if !s:IsCallback(get(l:link, 'callback')) - throw l:err_prefix . 'must define a `callback` function' - endif - - if has_key(l:link, 'output_stream') - if type(l:link.output_stream) isnot v:t_string - \|| index(['stdout', 'stderr', 'both'], l:link.output_stream) < 0 - throw l:err_prefix . '`output_stream` flag must be ' - \ . "'stdout', 'stderr', or 'both'" - endif - endif - - if has_key(l:link, 'read_buffer') && !s:IsBoolean(l:link.read_buffer) - throw l:err_prefix . 'value for `read_buffer` must be `0` or `1`' - endif - - let l:link_index += 1 - endfor - elseif has_key(a:linter, 'command_callback') - let l:obj.command_callback = a:linter.command_callback - - if !s:IsCallback(l:obj.command_callback) - throw '`command_callback` must be a callback if defined' + throw '`command` cannot be used when lsp == ''socket''' endif elseif has_key(a:linter, 'command') let l:obj.command = a:linter.command @@ -188,22 +135,12 @@ function! ale#linter#PreProcess(filetype, linter) abort throw '`command` must be a String or Function if defined' endif else - throw 'Either `command`, `executable_callback`, `command_chain` ' - \ . 'must be defined' - endif - - if ( - \ has_key(a:linter, 'command') - \ + has_key(a:linter, 'command_chain') - \ + has_key(a:linter, 'command_callback') - \) > 1 - throw 'Only one of `command`, `command_callback`, or `command_chain` ' - \ . 'should be set' + throw '`command` must be defined' endif if !l:needs_address - if has_key(a:linter, 'address') || has_key(a:linter, 'address_callback') - throw '`address` or `address_callback` cannot be used when lsp != ''socket''' + if has_key(a:linter, 'address') + throw '`address` cannot be used when lsp != ''socket''' endif elseif has_key(a:linter, 'address') if type(a:linter.address) isnot v:t_string @@ -212,41 +149,17 @@ function! ale#linter#PreProcess(filetype, linter) abort endif let l:obj.address = a:linter.address - elseif has_key(a:linter, 'address_callback') - let l:obj.address_callback = a:linter.address_callback - - if !s:IsCallback(l:obj.address_callback) - throw '`address_callback` must be a callback if defined' - endif else - throw '`address` or `address_callback` must be defined for getting the LSP address' + throw '`address` must be defined for getting the LSP address' endif if l:needs_lsp_details - if has_key(a:linter, 'language_callback') - if has_key(a:linter, 'language') - throw 'Only one of `language` or `language_callback` ' - \ . 'should be set' - endif + " Default to using the filetype as the language. + let l:obj.language = get(a:linter, 'language', a:filetype) - let l:obj.language_callback = get(a:linter, 'language_callback') - - if !s:IsCallback(l:obj.language_callback) - throw '`language_callback` must be a callback for LSP linters' - endif - else - " Default to using the filetype as the language. - let l:Language = get(a:linter, 'language', a:filetype) - - if type(l:Language) is v:t_string - " Make 'language_callback' return the 'language' value. - let l:obj.language = l:Language - let l:obj.language_callback = function('s:LanguageGetter') - elseif type(l:Language) is v:t_func - let l:obj.language_callback = l:Language - else - throw '`language` must be a String or Funcref' - endif + if type(l:obj.language) isnot v:t_string + \&& type(l:obj.language) isnot v:t_func + throw '`language` must be a String or Funcref if defined' endif if has_key(a:linter, 'project_root') @@ -254,16 +167,10 @@ function! ale#linter#PreProcess(filetype, linter) abort if type(l:obj.project_root) isnot v:t_string \&& type(l:obj.project_root) isnot v:t_func - throw '`project_root` must be a String or Function if defined' - endif - elseif has_key(a:linter, 'project_root_callback') - let l:obj.project_root_callback = a:linter.project_root_callback - - if !s:IsCallback(l:obj.project_root_callback) - throw '`project_root_callback` must be a callback if defined' + throw '`project_root` must be a String or Function' endif else - throw '`project_root` or `project_root_callback` must be defined for LSP linters' + throw '`project_root` must be defined for LSP linters' endif if has_key(a:linter, 'completion_filter') @@ -274,37 +181,16 @@ function! ale#linter#PreProcess(filetype, linter) abort endif endif - if has_key(a:linter, 'initialization_options_callback') - if has_key(a:linter, 'initialization_options') - throw 'Only one of `initialization_options` or ' - \ . '`initialization_options_callback` should be set' - endif - - let l:obj.initialization_options_callback = a:linter.initialization_options_callback - - if !s:IsCallback(l:obj.initialization_options_callback) - throw '`initialization_options_callback` must be a callback if defined' - endif - elseif has_key(a:linter, 'initialization_options') + if has_key(a:linter, 'initialization_options') let l:obj.initialization_options = a:linter.initialization_options if type(l:obj.initialization_options) isnot v:t_dict \&& type(l:obj.initialization_options) isnot v:t_func - throw '`initialization_options` must be a String or Function if defined' + throw '`initialization_options` must be a Dictionary or Function if defined' endif endif - if has_key(a:linter, 'lsp_config_callback') - if has_key(a:linter, 'lsp_config') - throw 'Only one of `lsp_config` or `lsp_config_callback` should be set' - endif - - let l:obj.lsp_config_callback = a:linter.lsp_config_callback - - if !s:IsCallback(l:obj.lsp_config_callback) - throw '`lsp_config_callback` must be a callback if defined' - endif - elseif has_key(a:linter, 'lsp_config') + if has_key(a:linter, 'lsp_config') if type(a:linter.lsp_config) isnot v:t_dict \&& type(a:linter.lsp_config) isnot v:t_func throw '`lsp_config` must be a Dictionary or Function if defined' @@ -325,21 +211,17 @@ function! ale#linter#PreProcess(filetype, linter) abort " file on disk. let l:obj.lint_file = get(a:linter, 'lint_file', 0) - if !s:IsBoolean(l:obj.lint_file) - throw '`lint_file` must be `0` or `1`' + if !s:IsBoolean(l:obj.lint_file) && type(l:obj.lint_file) isnot v:t_func + throw '`lint_file` must be `0`, `1`, or a Function' endif " An option indicating that the buffer should be read. - let l:obj.read_buffer = get(a:linter, 'read_buffer', !l:obj.lint_file) + let l:obj.read_buffer = get(a:linter, 'read_buffer', 1) if !s:IsBoolean(l:obj.read_buffer) throw '`read_buffer` must be `0` or `1`' endif - if l:obj.lint_file && l:obj.read_buffer - throw 'Only one of `lint_file` or `read_buffer` can be `1`' - endif - let l:obj.aliases = get(a:linter, 'aliases', []) if type(l:obj.aliases) isnot v:t_list @@ -347,14 +229,6 @@ function! ale#linter#PreProcess(filetype, linter) abort throw '`aliases` must be a List of String values' endif - for l:key in filter(keys(a:linter), 'v:val[-9:] is# ''_callback'' || v:val is# ''command_chain''') - if !get(g:, 'ale_ignore_2_4_warnings') - execute 'echom l:key . '' is deprecated. Use `let g:ale_ignore_2_4_warnings = 1` to disable this message.''' - endif - - break - endfor - return l:obj endfunction @@ -522,9 +396,7 @@ endfunction " Given a buffer and linter, get the executable String for the linter. function! ale#linter#GetExecutable(buffer, linter) abort - let l:Executable = has_key(a:linter, 'executable_callback') - \ ? function(a:linter.executable_callback) - \ : a:linter.executable + let l:Executable = a:linter.executable return type(l:Executable) is v:t_func \ ? l:Executable(a:buffer) @@ -532,24 +404,21 @@ function! ale#linter#GetExecutable(buffer, linter) abort endfunction " Given a buffer and linter, get the command String for the linter. -" The command_chain key is not supported. function! ale#linter#GetCommand(buffer, linter) abort - let l:Command = has_key(a:linter, 'command_callback') - \ ? function(a:linter.command_callback) - \ : a:linter.command + let l:Command = a:linter.command - return type(l:Command) is v:t_func - \ ? l:Command(a:buffer) - \ : l:Command + return type(l:Command) is v:t_func ? l:Command(a:buffer) : l:Command endfunction " Given a buffer and linter, get the address for connecting to the server. function! ale#linter#GetAddress(buffer, linter) abort - let l:Address = has_key(a:linter, 'address_callback') - \ ? function(a:linter.address_callback) - \ : a:linter.address + let l:Address = a:linter.address + + return type(l:Address) is v:t_func ? l:Address(a:buffer) : l:Address +endfunction + +function! ale#linter#GetLanguage(buffer, linter) abort + let l:Language = a:linter.language - return type(l:Address) is v:t_func - \ ? l:Address(a:buffer) - \ : l:Address + return type(l:Language) is v:t_func ? l:Language(a:buffer) : l:Language endfunction diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index ae8fd51d..7d99e9d2 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -64,6 +64,9 @@ endfunction " Used only in tests. function! ale#lsp#GetConnections() abort + " This command will throw from the sandbox. + let &l:equalprg=&l:equalprg + return s:connections endfunction @@ -449,6 +452,7 @@ function! ale#lsp#StartProgram(conn_id, executable, command) abort endif if l:started && !l:conn.is_tsserver + let l:conn.initialized = 0 call s:SendInitMessage(l:conn) endif diff --git a/autoload/ale/lsp_linter.vim b/autoload/ale/lsp_linter.vim index e4148ceb..dcd76e8f 100644 --- a/autoload/ale/lsp_linter.vim +++ b/autoload/ale/lsp_linter.vim @@ -34,7 +34,11 @@ endfunction function! s:HandleLSPDiagnostics(conn_id, response) abort let l:linter_name = s:lsp_linter_map[a:conn_id] let l:filename = ale#path#FromURI(a:response.params.uri) - let l:buffer = bufnr('^' . l:filename . '$') + let l:escaped_name = escape( + \ fnameescape(l:filename), + \ has('win32') ? '^' : '^,}]' + \) + let l:buffer = bufnr('^' . l:escaped_name . '$') let l:info = get(g:ale_buffer_info, l:buffer, {}) if empty(l:info) @@ -52,7 +56,11 @@ endfunction function! s:HandleTSServerDiagnostics(response, error_type) abort let l:linter_name = 'tsserver' - let l:buffer = bufnr('^' . a:response.body.file . '$') + let l:escaped_name = escape( + \ fnameescape(a:response.body.file), + \ has('win32') ? '^' : '^,}]' + \) + let l:buffer = bufnr('^' . l:escaped_name . '$') let l:info = get(g:ale_buffer_info, l:buffer, {}) if empty(l:info) @@ -227,7 +235,7 @@ function! ale#lsp_linter#OnInit(linter, details, Callback) abort let l:command = a:details.command let l:config = ale#lsp_linter#GetConfig(l:buffer, a:linter) - let l:language_id = ale#util#GetFunction(a:linter.language_callback)(l:buffer) + let l:language_id = ale#linter#GetLanguage(l:buffer, a:linter) call ale#lsp#UpdateConfig(l:conn_id, l:buffer, l:config) @@ -265,7 +273,14 @@ function! s:StartLSP(options, address, executable, command) abort call ale#lsp#MarkConnectionAsTsserver(l:conn_id) endif - let l:command = ale#command#FormatCommand(l:buffer, a:executable, a:command, 0, v:false)[1] + let l:command = ale#command#FormatCommand( + \ l:buffer, + \ a:executable, + \ a:command, + \ 0, + \ v:false, + \ [], + \)[1] let l:command = ale#job#PrepareCommand(l:buffer, l:command) let l:ready = ale#lsp#StartProgram(l:conn_id, a:executable, l:command) endif diff --git a/autoload/ale/path.vim b/autoload/ale/path.vim index 30550503..fed95ccd 100644 --- a/autoload/ale/path.vim +++ b/autoload/ale/path.vim @@ -24,6 +24,14 @@ function! ale#path#Simplify(path) abort return substitute(simplify(l:win_path), '^\\\+', '\', 'g') " no-custom-checks endfunction +" Simplify a path without a Windows drive letter. +" This function can be used for checking if paths are equal. +function! ale#path#RemoveDriveLetter(path) abort + return has('win32') && a:path[1:2] is# ':\' + \ ? ale#path#Simplify(a:path[2:]) + \ : ale#path#Simplify(a:path) +endfunction + " Given a buffer and a filename, find the nearest file by searching upwards " through the paths relative to the given buffer. function! ale#path#FindNearestFile(buffer, filename) abort @@ -74,15 +82,19 @@ endfunction function! ale#path#CdString(directory) abort if has('win32') return 'cd /d ' . ale#Escape(a:directory) . ' && ' - else - return 'cd ' . ale#Escape(a:directory) . ' && ' endif + + return 'cd ' . ale#Escape(a:directory) . ' && ' endfunction " Output 'cd <buffer_filename_directory> && ' " This function can be used changing the directory for a linter command. function! ale#path#BufferCdString(buffer) abort - return ale#path#CdString(fnamemodify(bufname(a:buffer), ':p:h')) + if has('win32') + return 'cd /d %s:h && ' + endif + + return 'cd %s:h && ' endfunction " Return 1 if a path is an absolute path. @@ -95,7 +107,7 @@ function! ale#path#IsAbsolute(filename) abort return a:filename[:0] is# '/' || a:filename[1:2] is# ':\' endfunction -let s:temp_dir = ale#path#Simplify(fnamemodify(ale#util#Tempname(), ':h')) +let s:temp_dir = ale#path#Simplify(fnamemodify(ale#util#Tempname(), ':h:h')) " Given a filename, return 1 if the file represents some temporary file " created by Vim. diff --git a/autoload/ale/preview.vim b/autoload/ale/preview.vim index faf45cb0..8b94aa7a 100644 --- a/autoload/ale/preview.vim +++ b/autoload/ale/preview.vim @@ -1,14 +1,22 @@ " Author: w0rp <devw0rp@gmail.com> " Description: Preview windows for showing whatever information in. -if !has_key(s:, 'last_selection_list') - let s:last_selection_list = [] +if !has_key(s:, 'last__list') + let s:last_list = [] endif -if !has_key(s:, 'last_selection_open_in') - let s:last_selection_open_in = 'current-buffer' +if !has_key(s:, 'last_options') + let s:last_options = {} endif +function! ale#preview#SetLastSelection(item_list, options) abort + let s:last_list = a:item_list + let s:last_options = { + \ 'open_in': get(a:options, 'open_in', 'current-buffer'), + \ 'use_relative_paths': get(a:options, 'use_relative_paths', 0), + \} +endfunction + " Open a preview window and show some lines in it. " A second argument can be passed as a Dictionary with options. They are... " @@ -81,19 +89,14 @@ function! ale#preview#ShowSelection(item_list, ...) abort let b:ale_preview_item_list = a:item_list let b:ale_preview_item_open_in = get(l:options, 'open_in', 'current-buffer') - " Remove the last preview - let s:last_selection_list = b:ale_preview_item_list - let s:last_selection_open_in = b:ale_preview_item_open_in + " Remember preview state, so we can repeat it later. + call ale#preview#SetLastSelection(a:item_list, l:options) endfunction function! ale#preview#RepeatSelection() abort - if empty(s:last_selection_list) - return + if !empty(s:last_list) + call ale#preview#ShowSelection(s:last_list, s:last_options) endif - - call ale#preview#ShowSelection(s:last_selection_list, { - \ 'open_in': s:last_selection_open_in, - \}) endfunction function! s:Open(open_in) abort diff --git a/autoload/ale/test.vim b/autoload/ale/test.vim index 082d91ff..6fcbf35e 100644 --- a/autoload/ale/test.vim +++ b/autoload/ale/test.vim @@ -145,8 +145,8 @@ function! ale#test#WaitForJobs(deadline) abort " end, but before handlers are run. sleep 10ms - " We must check the buffer data again to see if new jobs started - " for command_chain linters. + " We must check the buffer data again to see if new jobs started for + " linters with chained commands. let l:has_new_jobs = 0 " Check again to see if any jobs are running. diff --git a/autoload/ale/util.vim b/autoload/ale/util.vim index bb9c1961..1f396377 100644 --- a/autoload/ale/util.vim +++ b/autoload/ale/util.vim @@ -423,7 +423,10 @@ function! ale#util#Writefile(buffer, lines, filename) abort \ ? map(copy(a:lines), 'substitute(v:val, ''\r*$'', ''\r'', '''')') \ : a:lines - call writefile(l:corrected_lines, a:filename, 'S') " no-custom-checks + " Set binary flag if buffer doesn't have eol and nofixeol to avoid appending newline + let l:flags = !getbufvar(a:buffer, '&eol') && exists('+fixeol') && !&fixeol ? 'bS' : 'S' + + call writefile(l:corrected_lines, a:filename, l:flags) " no-custom-checks endfunction if !exists('s:patial_timers') |