diff options
Diffstat (limited to 'autoload')
80 files changed, 1879 insertions, 741 deletions
diff --git a/autoload/ale.vim b/autoload/ale.vim index 6d1e8521..f6c23d72 100644 --- a/autoload/ale.vim +++ b/autoload/ale.vim @@ -10,8 +10,7 @@ let g:ale_echo_msg_warning_str = get(g:, 'ale_echo_msg_warning_str', 'Warning') let g:ale_linters_ignore = get(g:, 'ale_linters_ignore', {}) let s:lint_timer = -1 -let s:queued_buffer_number = -1 -let s:should_lint_file_for_buffer = {} +let s:getcmdwintype_exists = exists('*getcmdwintype') " Return 1 if a file is too large for ALE to handle. function! ale#FileTooLarge(buffer) abort @@ -20,14 +19,12 @@ function! ale#FileTooLarge(buffer) abort return l:max > 0 ? (line2byte(line('$') + 1) > l:max) : 0 endfunction -let s:getcmdwintype_exists = exists('*getcmdwintype') - " A function for checking various conditions whereby ALE just shouldn't " attempt to do anything, say if particular buffer types are open in Vim. function! ale#ShouldDoNothing(buffer) abort " The checks are split into separate if statements to make it possible to " profile each check individually with Vim's profiling tools. - + " " Do nothing if ALE is disabled. if !getbufvar(a:buffer, 'ale_enabled', get(g:, 'ale_enabled', 0)) return 1 @@ -62,6 +59,11 @@ function! ale#ShouldDoNothing(buffer) abort return 1 endif + " Don't start linting and so on when an operator is pending. + if ale#util#Mode(1) is# 'no' + return 1 + endif + " Do nothing if running in the sandbox. if ale#util#InSandbox() return 1 @@ -81,21 +83,47 @@ function! ale#ShouldDoNothing(buffer) abort return 0 endfunction +function! s:Lint(buffer, should_lint_file, timer_id) abort + " Use the filetype from the buffer + let l:filetype = getbufvar(a:buffer, '&filetype') + let l:linters = ale#linter#Get(l:filetype) + + " Apply ignore lists for linters only if needed. + let l:ignore_config = ale#Var(a:buffer, 'linters_ignore') + let l:linters = !empty(l:ignore_config) + \ ? ale#engine#ignore#Exclude(l:filetype, l:linters, l:ignore_config) + \ : l:linters + + " Tell other sources that they can start checking the buffer now. + let g:ale_want_results_buffer = a:buffer + silent doautocmd <nomodeline> User ALEWantResults + unlet! g:ale_want_results_buffer + + " Don't set up buffer data and so on if there are no linters to run. + if !has_key(g:ale_buffer_info, a:buffer) && empty(l:linters) + return + endif + + " Clear lint_file linters, or only run them if the file exists. + let l:lint_file = empty(l:linters) + \ || (a:should_lint_file && filereadable(expand('#' . a:buffer . ':p'))) + + call ale#engine#RunLinters(a:buffer, l:linters, l:lint_file) +endfunction + " (delay, [linting_flag, buffer_number]) function! ale#Queue(delay, ...) abort if a:0 > 2 throw 'too many arguments!' endif - " Default linting_flag to '' - let l:linting_flag = get(a:000, 0, '') - let l:buffer = get(a:000, 1, bufnr('')) + let l:buffer = get(a:000, 1, v:null) - if l:linting_flag isnot# '' && l:linting_flag isnot# 'lint_file' - throw "linting_flag must be either '' or 'lint_file'" + if l:buffer is v:null + let l:buffer = bufnr('') endif - if type(l:buffer) != type(0) + if type(l:buffer) isnot v:t_number throw 'buffer_number must be a Number' endif @@ -103,80 +131,24 @@ function! ale#Queue(delay, ...) abort return endif - " Remember that we want to check files for this buffer. - " We will remember this until we finally run the linters, via any event. - if l:linting_flag is# 'lint_file' - let s:should_lint_file_for_buffer[l:buffer] = 1 - endif + " Default linting_flag to '' + let l:should_lint_file = get(a:000, 0) is# 'lint_file' if s:lint_timer != -1 call timer_stop(s:lint_timer) let s:lint_timer = -1 endif - let l:linters = ale#linter#Get(getbufvar(l:buffer, '&filetype')) - - " Don't set up buffer data and so on if there are no linters to run. - if empty(l:linters) - " If we have some previous buffer data, then stop any jobs currently - " running and clear everything. - if has_key(g:ale_buffer_info, l:buffer) - call ale#engine#RunLinters(l:buffer, [], 1) - endif - - return - endif - if a:delay > 0 - let s:queued_buffer_number = l:buffer - let s:lint_timer = timer_start(a:delay, function('ale#Lint')) + let s:lint_timer = timer_start( + \ a:delay, + \ function('s:Lint', [l:buffer, l:should_lint_file]) + \) else - call ale#Lint(-1, l:buffer) + call s:Lint(l:buffer, l:should_lint_file, 0) endif endfunction -function! ale#Lint(...) abort - if a:0 > 1 - " Use the buffer number given as the optional second argument. - let l:buffer = a:2 - elseif a:0 > 0 && a:1 == s:lint_timer - " Use the buffer number for the buffer linting was queued for. - let l:buffer = s:queued_buffer_number - else - " Use the current buffer number. - let l:buffer = bufnr('') - endif - - if ale#ShouldDoNothing(l:buffer) - return - endif - - " Use the filetype from the buffer - let l:filetype = getbufvar(l:buffer, '&filetype') - let l:linters = ale#linter#Get(l:filetype) - let l:should_lint_file = 0 - - " Check if we previously requested checking the file. - if has_key(s:should_lint_file_for_buffer, l:buffer) - unlet s:should_lint_file_for_buffer[l:buffer] - " Lint files if they exist. - let l:should_lint_file = filereadable(expand('#' . l:buffer . ':p')) - endif - - " Apply ignore lists for linters only if needed. - let l:ignore_config = ale#Var(l:buffer, 'linters_ignore') - let l:linters = !empty(l:ignore_config) - \ ? ale#engine#ignore#Exclude(l:filetype, l:linters, l:ignore_config) - \ : l:linters - - call ale#engine#RunLinters(l:buffer, l:linters, l:should_lint_file) -endfunction - -" Reset flags indicating that files should be checked for all buffers. -function! ale#ResetLintFileMarkers() abort - let s:should_lint_file_for_buffer = {} -endfunction - let g:ale_has_override = get(g:, 'ale_has_override', {}) " Call has(), but check a global Dictionary so we can force flags on or off @@ -197,6 +169,11 @@ function! ale#Var(buffer, variable_name) abort return get(l:vars, l:full_name, g:[l:full_name]) endfunction +" As above, but curry the arguments so only the buffer number is required. +function! ale#VarFunc(variable_name) abort + return {buf -> ale#Var(buf, a:variable_name)} +endfunction + " Initialize a variable with a default value, if it isn't already set. " " Every variable name will be prefixed with 'ale_'. diff --git a/autoload/ale/assert.vim b/autoload/ale/assert.vim index 55c39ee3..ed08ed09 100644 --- a/autoload/ale/assert.vim +++ b/autoload/ale/assert.vim @@ -30,7 +30,7 @@ function! ale#assert#Linter(expected_executable, expected_command) abort let l:callbacks = map(copy(l:linter.command_chain), 'v:val.callback') " If the expected command is a string, just check the last one. - if type(a:expected_command) is type('') + if type(a:expected_command) is v:t_string if len(l:callbacks) is 1 let l:command = call(l:callbacks[0], [l:buffer]) else @@ -54,9 +54,14 @@ function! ale#assert#Linter(expected_executable, expected_command) abort endif else let l:command = ale#linter#GetCommand(l:buffer, l:linter) + endif + + if type(l:command) is v:t_string " Replace %e with the escaped executable, so tests keep passing after " linters are changed to use %e. let l:command = substitute(l:command, '%e', '\=ale#Escape(l:executable)', 'g') + else + call map(l:command, 'substitute(v:val, ''%e'', ''\=ale#Escape(l:executable)'', ''g'')') endif AssertEqual @@ -80,6 +85,14 @@ function! ale#assert#LSPOptions(expected_options) abort AssertEqual a:expected_options, l:initialization_options endfunction +function! ale#assert#LSPConfig(expected_config) abort + let l:buffer = bufnr('') + let l:linter = s:GetLinter() + let l:config = ale#lsp_linter#GetConfig(l:buffer, l:linter) + + AssertEqual a:expected_config, l:config +endfunction + function! ale#assert#LSPLanguage(expected_language) abort let l:buffer = bufnr('') let l:linter = s:GetLinter() @@ -96,6 +109,14 @@ function! ale#assert#LSPProject(expected_root) abort AssertEqual a:expected_root, l:root endfunction +function! ale#assert#LSPAddress(expected_address) abort + let l:buffer = bufnr('') + let l:linter = s:GetLinter() + let l:address = ale#util#GetFunction(l:linter.address_callback)(l:buffer) + + AssertEqual a:expected_address, l:address +endfunction + " A dummy function for making sure this module is loaded. function! ale#assert#SetUpLinterTest(filetype, name) abort " Set up a marker so ALE doesn't create real random temporary filenames. @@ -126,28 +147,59 @@ function! ale#assert#SetUpLinterTest(filetype, name) abort execute 'runtime ale_linters/' . a:filetype . '/' . a:name . '.vim' - call ale#test#SetDirectory('/testplugin/test/command_callback') + if !exists('g:dir') + call ale#test#SetDirectory('/testplugin/test/command_callback') + endif command! -nargs=+ WithChainResults :call ale#assert#WithChainResults(<args>) command! -nargs=+ AssertLinter :call ale#assert#Linter(<args>) command! -nargs=0 AssertLinterNotExecuted :call ale#assert#LinterNotExecuted() command! -nargs=+ AssertLSPOptions :call ale#assert#LSPOptions(<args>) + command! -nargs=+ AssertLSPConfig :call ale#assert#LSPConfig(<args>) command! -nargs=+ AssertLSPLanguage :call ale#assert#LSPLanguage(<args>) command! -nargs=+ AssertLSPProject :call ale#assert#LSPProject(<args>) + command! -nargs=+ AssertLSPAddress :call ale#assert#LSPAddress(<args>) endfunction function! ale#assert#TearDownLinterTest() abort unlet! g:ale_create_dummy_temporary_file let s:chain_results = [] - delcommand WithChainResults - delcommand AssertLinter - delcommand AssertLinterNotExecuted - delcommand AssertLSPOptions - delcommand AssertLSPLanguage - delcommand AssertLSPProject + if exists(':WithChainResults') + delcommand WithChainResults + endif + + if exists(':AssertLinter') + delcommand AssertLinter + endif + + if exists(':AssertLinterNotExecuted') + delcommand AssertLinterNotExecuted + endif + + if exists(':AssertLSPOptions') + delcommand AssertLSPOptions + endif + + if exists(':AssertLSPConfig') + delcommand AssertLSPConfig + endif - call ale#test#RestoreDirectory() + if exists(':AssertLSPLanguage') + delcommand AssertLSPLanguage + endif + + if exists(':AssertLSPProject') + delcommand AssertLSPProject + endif + + if exists(':AssertLSPAddress') + delcommand AssertLSPAddress + endif + + if exists('g:dir') + call ale#test#RestoreDirectory() + endif Restore diff --git a/autoload/ale/c.vim b/autoload/ale/c.vim index fbfe9509..ce5105b6 100644 --- a/autoload/ale/c.vim +++ b/autoload/ale/c.vim @@ -2,11 +2,31 @@ " Description: Functions for integrating with C-family linters. call ale#Set('c_parse_makefile', 0) +call ale#Set('c_parse_compile_commands', 0) let s:sep = has('win32') ? '\' : '/' " Set just so tests can override it. let g:__ale_c_project_filenames = ['.git/HEAD', 'configure', 'Makefile', 'CMakeLists.txt'] +function! ale#c#GetBuildDirectory(buffer) abort + " Don't include build directory for header files, as compile_commands.json + " files don't consider headers to be translation units, and provide no + " commands for compiling header files. + if expand('#' . a:buffer) =~# '\v\.(h|hpp)$' + return '' + endif + + let l:build_dir = ale#Var(a:buffer, 'c_build_dir') + + " c_build_dir has the priority if defined + if !empty(l:build_dir) + return l:build_dir + endif + + return ale#path#Dirname(ale#c#FindCompileCommands(a:buffer)) +endfunction + + function! ale#c#FindProjectRoot(buffer) abort for l:project_filename in g:__ale_c_project_filenames let l:full_path = ale#path#FindNearestFile(a:buffer, l:project_filename) @@ -26,15 +46,21 @@ function! ale#c#FindProjectRoot(buffer) abort return '' endfunction -function! ale#c#ParseCFlagsToList(path_prefix, cflags) abort +function! ale#c#ParseCFlags(path_prefix, cflag_line) abort let l:cflags_list = [] let l:previous_options = [] - for l:option in a:cflags + let l:split_lines = split(a:cflag_line, '-') + let l:option_index = 0 + + while l:option_index < len(l:split_lines) + let l:option = l:split_lines[l:option_index] + let l:option_index = l:option_index + 1 call add(l:previous_options, l:option) " Check if cflag contained a '-' and should not have been splitted let l:option_list = split(l:option, '\zs') - if l:option_list[-1] isnot# ' ' + + if len(l:option_list) > 0 && l:option_list[-1] isnot# ' ' && l:option_index < len(l:split_lines) continue endif @@ -60,34 +86,134 @@ function! ale#c#ParseCFlagsToList(path_prefix, cflags) abort call add(l:cflags_list, l:option) endif endif - endfor + endwhile - return l:cflags_list + return join(l:cflags_list, ' ') endfunction -function! ale#c#ParseCFlags(buffer, stdout_make) abort +function! ale#c#ParseCFlagsFromMakeOutput(buffer, make_output) abort if !g:ale_c_parse_makefile - return [] + return '' endif let l:buffer_filename = expand('#' . a:buffer . ':t') - let l:cflags = [] - for l:lines in split(a:stdout_make, '\\n') - if stridx(l:lines, l:buffer_filename) >= 0 - let l:cflags = split(l:lines, '-') + let l:cflag_line = '' + + " Find a line matching this buffer's filename in the make output. + for l:line in a:make_output + if stridx(l:line, l:buffer_filename) >= 0 + let l:cflag_line = l:line break endif endfor let l:makefile_path = ale#path#FindNearestFile(a:buffer, 'Makefile') - return ale#c#ParseCFlagsToList(fnamemodify(l:makefile_path, ':p:h'), l:cflags) + let l:makefile_dir = fnamemodify(l:makefile_path, ':p:h') + + return ale#c#ParseCFlags(l:makefile_dir, l:cflag_line) +endfunction + +" Given a buffer number, find the build subdirectory with compile commands +" The subdirectory is returned without the trailing / +function! ale#c#FindCompileCommands(buffer) abort + " Look above the current source file to find compile_commands.json + let l:json_file = ale#path#FindNearestFile(a:buffer, 'compile_commands.json') + + if !empty(l:json_file) + return l:json_file + endif + + " Search in build directories if we can't find it in the project. + for l:path in ale#path#Upwards(expand('#' . a:buffer . ':p:h')) + for l:dirname in ale#Var(a:buffer, 'c_build_dir_names') + let l:c_build_dir = l:path . s:sep . l:dirname + let l:json_file = l:c_build_dir . s:sep . 'compile_commands.json' + + if filereadable(l:json_file) + return l:json_file + endif + endfor + endfor + + return '' +endfunction + +" Cache compile_commands.json data in a Dictionary, so we don't need to read +" the same files over and over again. The key in the dictionary will include +" the last modified time of the file. +if !exists('s:compile_commands_cache') + let s:compile_commands_cache = {} +endif + +function! s:GetListFromCompileCommandsFile(compile_commands_file) abort + if empty(a:compile_commands_file) + return [] + endif + + let l:time = getftime(a:compile_commands_file) + + if l:time < 0 + return [] + endif + + let l:key = a:compile_commands_file . ':' . l:time + + if has_key(s:compile_commands_cache, l:key) + return s:compile_commands_cache[l:key] + endif + + let l:data = [] + silent! let l:data = json_decode(join(readfile(a:compile_commands_file), '')) + + if !empty(l:data) + let s:compile_commands_cache[l:key] = l:data + + return l:data + endif + + return [] +endfunction + +function! ale#c#ParseCompileCommandsFlags(buffer, dir, json_list) abort + " Search for an exact file match first. + for l:item in a:json_list + if bufnr(l:item.file) is a:buffer + return ale#c#ParseCFlags(a:dir, l:item.command) + 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 a:json_list + if ale#path#Simplify(fnamemodify(l:item.file, ':h')) is? l:dir + return ale#c#ParseCFlags(a:dir, l:item.command) + endif + endfor + + return '' +endfunction + +function! ale#c#FlagsFromCompileCommands(buffer, compile_commands_file) abort + let l:dir = ale#path#Dirname(a:compile_commands_file) + let l:json_list = s:GetListFromCompileCommandsFile(a:compile_commands_file) + + return ale#c#ParseCompileCommandsFlags(a:buffer, l:dir, l:json_list) endfunction function! ale#c#GetCFlags(buffer, output) abort let l:cflags = ' ' - if g:ale_c_parse_makefile && !empty(a:output) - let l:cflags = join(ale#c#ParseCFlags(a:buffer, join(a:output, '\n')), ' ') . ' ' + if ale#Var(a:buffer, 'c_parse_makefile') && !empty(a:output) + let l:cflags = ale#c#ParseCFlagsFromMakeOutput(a:buffer, a:output) + endif + + if ale#Var(a:buffer, 'c_parse_compile_commands') + let l:json_file = ale#c#FindCompileCommands(a:buffer) + + if !empty(l:json_file) + let l:cflags = ale#c#FlagsFromCompileCommands(a:buffer, l:json_file) + endif endif if l:cflags is# ' ' @@ -98,8 +224,9 @@ function! ale#c#GetCFlags(buffer, output) abort endfunction function! ale#c#GetMakeCommand(buffer) abort - if g:ale_c_parse_makefile + if ale#Var(a:buffer, 'c_parse_makefile') let l:makefile_path = ale#path#FindNearestFile(a:buffer, 'Makefile') + if !empty(l:makefile_path) return 'cd '. fnamemodify(l:makefile_path, ':p:h') . ' && make -n' endif @@ -154,26 +281,10 @@ function! ale#c#IncludeOptions(include_paths) abort return '' endif - return ' ' . join(l:option_list) . ' ' + return join(l:option_list) endfunction let g:ale_c_build_dir_names = get(g:, 'ale_c_build_dir_names', [ \ 'build', \ 'bin', \]) - -" Given a buffer number, find the build subdirectory with compile commands -" The subdirectory is returned without the trailing / -function! ale#c#FindCompileCommands(buffer) abort - for l:path in ale#path#Upwards(expand('#' . a:buffer . ':p:h')) - for l:dirname in ale#Var(a:buffer, 'c_build_dir_names') - let l:c_build_dir = l:path . s:sep . l:dirname - - if filereadable(l:c_build_dir . '/compile_commands.json') - return l:c_build_dir - endif - endfor - endfor - - return '' -endfunction diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index 7440f8cd..9dd913f5 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -39,10 +39,14 @@ let s:LSP_COMPLETION_COLOR_KIND = 16 let s:LSP_COMPLETION_FILE_KIND = 17 let s:LSP_COMPLETION_REFERENCE_KIND = 18 +let s:lisp_regex = '\v[a-zA-Z_\-][a-zA-Z_\-0-9]*$' + " Regular expressions for checking the characters in the line before where " the insert cursor is. If one of these matches, we'll check for completions. let s:should_complete_map = { \ '<default>': '\v[a-zA-Z$_][a-zA-Z$_0-9]*$|\.$', +\ 'clojure': s:lisp_regex, +\ 'lisp': s:lisp_regex, \ 'typescript': '\v[a-zA-Z$_][a-zA-Z$_0-9]*$|\.$|''$|"$', \ 'rust': '\v[a-zA-Z$_][a-zA-Z$_0-9]*$|\.$|::$', \} @@ -75,6 +79,7 @@ endfunction " Check if we should look for completions for a language. function! ale#completion#GetPrefix(filetype, line, column) abort let l:regex = s:GetFiletypeValue(s:should_complete_map, a:filetype) + " The column we're using completions for is where we are inserting text, " like so: " abc @@ -93,14 +98,15 @@ function! ale#completion#GetTriggerCharacter(filetype, prefix) abort return '' endfunction -function! ale#completion#Filter(buffer, suggestions, prefix) abort +function! ale#completion#Filter(buffer, filetype, suggestions, prefix) abort let l:excluded_words = ale#Var(a:buffer, 'completion_excluded_words') + let l:triggers = s:GetFiletypeValue(s:trigger_character_map, a:filetype) " For completing... " foo. " ^ " We need to include all of the given suggestions. - if a:prefix is# '.' + if index(l:triggers, a:prefix) >= 0 let l:filtered_suggestions = a:suggestions else let l:filtered_suggestions = [] @@ -113,7 +119,7 @@ function! ale#completion#Filter(buffer, suggestions, prefix) abort for l:item in a:suggestions " A List of String values or a List of completion item Dictionaries " is accepted here. - let l:word = type(l:item) == type('') ? l:item : l:item.word + 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. @@ -133,7 +139,7 @@ function! ale#completion#Filter(buffer, suggestions, prefix) abort " Remove suggestions with words in the exclusion List. call filter( \ l:filtered_suggestions, - \ 'index(l:excluded_words, type(v:val) is type('''') ? v:val : v:val.word) < 0', + \ 'index(l:excluded_words, type(v:val) is v:t_string ? v:val : v:val.word) < 0', \) endif @@ -214,8 +220,10 @@ function! ale#completion#Show(response, completion_parser) abort " function, and then start omni-completion. let b:ale_completion_response = a:response let b:ale_completion_parser = a:completion_parser + " Replace completion options shortly before opening the menu. call s:ReplaceCompletionOptions() - call ale#util#FeedKeys("\<Plug>(ale_show_completion_menu)") + + call timer_start(0, {-> ale#util#FeedKeys("\<Plug>(ale_show_completion_menu)")}) endfunction function! s:CompletionStillValid(request_id) abort @@ -257,7 +265,7 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort call add(l:documentationParts, l:part.text) endfor - if l:suggestion.kind is# 'clasName' + if l:suggestion.kind is# 'className' let l:kind = 'f' elseif l:suggestion.kind is# 'parameterName' let l:kind = 'f' @@ -315,10 +323,10 @@ function! ale#completion#ParseLSPCompletions(response) abort let l:item_list = [] - if type(get(a:response, 'result')) is type([]) + if type(get(a:response, 'result')) is v:t_list let l:item_list = a:response.result - elseif type(get(a:response, 'result')) is type({}) - \&& type(get(a:response.result, 'items')) is type([]) + elseif type(get(a:response, 'result')) is v:t_dict + \&& type(get(a:response.result, 'items')) is v:t_list let l:item_list = a:response.result.items endif @@ -352,17 +360,23 @@ function! ale#completion#ParseLSPCompletions(response) abort let l:kind = 'v' endif + let l:doc = get(l:item, 'documentation', '') + + if type(l:doc) is v:t_dict && has_key(l:doc, 'value') + let l:doc = l:doc.value + endif + call add(l:results, { \ 'word': l:word, \ 'kind': l:kind, \ 'icase': 1, \ 'menu': get(l:item, 'detail', ''), - \ 'info': get(l:item, 'documentation', ''), + \ 'info': (type(l:doc) is v:t_string ? l:doc : ''), \}) endfor if has_key(l:info, 'prefix') - return ale#completion#Filter(l:buffer, l:results, l:info.prefix) + return ale#completion#Filter(l:buffer, &filetype, l:results, l:info.prefix) endif return l:results @@ -383,6 +397,7 @@ function! ale#completion#HandleTSServerResponse(conn_id, response) abort if l:command is# 'completions' let l:names = ale#completion#Filter( \ l:buffer, + \ &filetype, \ ale#completion#ParseTSServerCompletions(a:response), \ b:ale_completion_info.prefix, \)[: g:ale_completion_max_suggestions - 1] @@ -422,6 +437,58 @@ function! ale#completion#HandleLSPResponse(conn_id, response) abort \) endfunction +function! s:OnReady(linter, lsp_details, ...) abort + let l:buffer = a:lsp_details.buffer + let l:id = a:lsp_details.connection_id + + " If we have sent a completion request already, don't send another. + if b:ale_completion_info.request_id + return + endif + + let l:Callback = a:linter.lsp is# 'tsserver' + \ ? function('ale#completion#HandleTSServerResponse') + \ : function('ale#completion#HandleLSPResponse') + call ale#lsp#RegisterCallback(l:id, l:Callback) + + if a:linter.lsp is# 'tsserver' + let l:message = ale#lsp#tsserver_message#Completions( + \ l:buffer, + \ b:ale_completion_info.line, + \ b:ale_completion_info.column, + \ b:ale_completion_info.prefix, + \) + else + " Send a message saying the buffer has changed first, otherwise + " completions won't know what text is nearby. + call ale#lsp#NotifyForChanges(l:id, l:buffer) + + " For LSP completions, we need to clamp the column to the length of + " the line. python-language-server and perhaps others do not implement + " this correctly. + let l:message = ale#lsp#message#Completion( + \ l:buffer, + \ b:ale_completion_info.line, + \ min([ + \ b:ale_completion_info.line_length, + \ b:ale_completion_info.column, + \ ]), + \ ale#completion#GetTriggerCharacter(&filetype, b:ale_completion_info.prefix), + \) + endif + + let l:request_id = ale#lsp#Send(l:id, l:message) + + if l:request_id + let b:ale_completion_info.conn_id = l:id + let b:ale_completion_info.request_id = l:request_id + + if has_key(a:linter, 'completion_filter') + let b:ale_completion_info.completion_filter = a:linter.completion_filter + endif + endif +endfunction + function! s:GetLSPCompletions(linter) abort let l:buffer = bufnr('') let l:lsp_details = ale#lsp_linter#StartLSP(l:buffer, a:linter) @@ -431,58 +498,10 @@ function! s:GetLSPCompletions(linter) abort endif let l:id = l:lsp_details.connection_id - let l:root = l:lsp_details.project_root - - function! OnReady(...) abort closure - " If we have sent a completion request already, don't send another. - if b:ale_completion_info.request_id - return - endif - - let l:Callback = a:linter.lsp is# 'tsserver' - \ ? function('ale#completion#HandleTSServerResponse') - \ : function('ale#completion#HandleLSPResponse') - call ale#lsp#RegisterCallback(l:id, l:Callback) - - if a:linter.lsp is# 'tsserver' - let l:message = ale#lsp#tsserver_message#Completions( - \ l:buffer, - \ b:ale_completion_info.line, - \ b:ale_completion_info.column, - \ b:ale_completion_info.prefix, - \) - else - " Send a message saying the buffer has changed first, otherwise - " completions won't know what text is nearby. - call ale#lsp#NotifyForChanges(l:id, l:root, l:buffer) - - " For LSP completions, we need to clamp the column to the length of - " the line. python-language-server and perhaps others do not implement - " this correctly. - let l:message = ale#lsp#message#Completion( - \ l:buffer, - \ b:ale_completion_info.line, - \ min([ - \ b:ale_completion_info.line_length, - \ b:ale_completion_info.column, - \ ]), - \ ale#completion#GetTriggerCharacter(&filetype, b:ale_completion_info.prefix), - \) - endif - let l:request_id = ale#lsp#Send(l:id, l:message, l:lsp_details.project_root) - - if l:request_id - let b:ale_completion_info.conn_id = l:id - let b:ale_completion_info.request_id = l:request_id - - if has_key(a:linter, 'completion_filter') - let b:ale_completion_info.completion_filter = a:linter.completion_filter - endif - endif - endfunction + let l:OnReady = function('s:OnReady', [a:linter, l:lsp_details]) - call ale#lsp#WaitForCapability(l:id, l:root, 'completion', function('OnReady')) + call ale#lsp#WaitForCapability(l:id, 'completion', l:OnReady) endfunction function! ale#completion#GetCompletions() abort diff --git a/autoload/ale/cursor.vim b/autoload/ale/cursor.vim index 73dbebb2..6672c349 100644 --- a/autoload/ale/cursor.vim +++ b/autoload/ale/cursor.vim @@ -1,4 +1,6 @@ +scriptencoding utf-8 " Author: w0rp <devw0rp@gmail.com> +" Author: João Paulo S. de Souza <joao.paulo.silvasouza@hotmail.com> " Description: Echoes lint message for the current line, if any " Controls the milliseconds delay before echoing a message. @@ -24,7 +26,20 @@ function! ale#cursor#TruncatedEcho(original_message) abort " The message is truncated and saved to the history. setlocal shortmess+=T - exec "norm! :echomsg l:message\n" + + try + exec "norm! :echomsg l:message\n" + catch /^Vim\%((\a\+)\)\=:E523/ + " Fallback into manual truncate (#1987) + let l:winwidth = winwidth(0) + + if l:winwidth < strdisplaywidth(l:message) + " Truncate message longer than window width with trailing '...' + let l:message = l:message[:l:winwidth - 4] . '...' + endif + + exec 'echomsg l:message' + endtry " Reset the cursor position if we moved off the end of the line. " Using :norm and :echomsg can move the cursor off the end of the @@ -37,17 +52,6 @@ function! ale#cursor#TruncatedEcho(original_message) abort endtry endfunction -function! s:FindItemAtCursor() abort - let l:buf = bufnr('') - let l:info = get(g:ale_buffer_info, l:buf, {}) - let l:loclist = get(l:info, 'loclist', []) - let l:pos = getcurpos() - let l:index = ale#util#BinarySearch(l:loclist, l:buf, l:pos[1], l:pos[2]) - let l:loc = l:index >= 0 ? l:loclist[l:index] : {} - - return [l:info, l:loc] -endfunction - function! s:StopCursorTimer() abort if s:cursor_timer != -1 call timer_stop(s:cursor_timer) @@ -56,42 +60,55 @@ function! s:StopCursorTimer() abort endfunction function! ale#cursor#EchoCursorWarning(...) abort - if !g:ale_echo_cursor + let l:buffer = bufnr('') + + if !g:ale_echo_cursor && !g:ale_cursor_detail return endif " Only echo the warnings in normal mode, otherwise we will get problems. - if mode() isnot# 'n' + if mode(1) isnot# 'n' return endif - if ale#ShouldDoNothing(bufnr('')) + if ale#ShouldDoNothing(l:buffer) return endif - let l:buffer = bufnr('') - let [l:info, l:loc] = s:FindItemAtCursor() + let [l:info, l:loc] = ale#util#FindItemAtCursor(l:buffer) + + if g:ale_echo_cursor + if !empty(l:loc) + let l:format = ale#Var(l:buffer, 'echo_msg_format') + let l:msg = ale#GetLocItemMessage(l:loc, l:format) + call ale#cursor#TruncatedEcho(l:msg) + let l:info.echoed = 1 + elseif get(l:info, 'echoed') + " We'll only clear the echoed message when moving off errors once, + " so we don't continually clear the echo line. + execute 'echo' + let l:info.echoed = 0 + endif + endif - if !empty(l:loc) - let l:format = ale#Var(l:buffer, 'echo_msg_format') - let l:msg = ale#GetLocItemMessage(l:loc, l:format) - call ale#cursor#TruncatedEcho(l:msg) - let l:info.echoed = 1 - elseif get(l:info, 'echoed') - " We'll only clear the echoed message when moving off errors once, - " so we don't continually clear the echo line. - execute 'echo' - let l:info.echoed = 0 + if g:ale_cursor_detail + if !empty(l:loc) + call s:ShowCursorDetailForItem(l:loc, {'stay_here': 1}) + else + call ale#preview#CloseIfTypeMatches('ale-preview') + endif endif endfunction function! ale#cursor#EchoCursorWarningWithDelay() abort - if !g:ale_echo_cursor + let l:buffer = bufnr('') + + if !g:ale_echo_cursor && !g:ale_cursor_detail return endif " Only echo the warnings in normal mode, otherwise we will get problems. - if mode() isnot# 'n' + if mode(1) isnot# 'n' return endif @@ -104,7 +121,7 @@ function! ale#cursor#EchoCursorWarningWithDelay() abort " we should echo something. Otherwise we can end up doing processing " the echo message far too frequently. if l:pos != s:last_pos - let l:delay = ale#Var(bufnr(''), 'echo_delay') + let l:delay = ale#Var(l:buffer, 'echo_delay') let s:last_pos = l:pos let s:cursor_timer = timer_start( @@ -114,24 +131,37 @@ function! ale#cursor#EchoCursorWarningWithDelay() abort endif endfunction +function! s:ShowCursorDetailForItem(loc, options) abort + let l:stay_here = get(a:options, 'stay_here', 0) + + let s:last_detailed_line = line('.') + let l:message = get(a:loc, 'detail', a:loc.text) + let l:lines = split(l:message, "\n") + call ale#preview#Show(l:lines, {'stay_here': l:stay_here}) + + " Clear the echo message if we manually displayed details. + if !l:stay_here + execute 'echo' + endif +endfunction + function! ale#cursor#ShowCursorDetail() abort + let l:buffer = bufnr('') + " Only echo the warnings in normal mode, otherwise we will get problems. if mode() isnot# 'n' return endif - if ale#ShouldDoNothing(bufnr('')) + if ale#ShouldDoNothing(l:buffer) return endif call s:StopCursorTimer() - let [l:info, l:loc] = s:FindItemAtCursor() + let [l:info, l:loc] = ale#util#FindItemAtCursor(l:buffer) if !empty(l:loc) - let l:message = get(l:loc, 'detail', l:loc.text) - - call ale#preview#Show(split(l:message, "\n")) - execute 'echo' + call s:ShowCursorDetailForItem(l:loc, {'stay_here': 0}) endif endfunction diff --git a/autoload/ale/d.vim b/autoload/ale/d.vim new file mode 100644 index 00000000..0e232203 --- /dev/null +++ b/autoload/ale/d.vim @@ -0,0 +1,16 @@ +" Author: Auri <me@aurieh.me> +" Description: Functions for integrating with D linters. + +function! ale#d#FindDUBConfig(buffer) abort + " Find a DUB configuration file in ancestor paths. + " The most DUB-specific names will be tried first. + for l:possible_filename in ['dub.sdl', 'dub.json', 'package.json'] + let l:dub_file = ale#path#FindNearestFile(a:buffer, l:possible_filename) + + if !empty(l:dub_file) + return l:dub_file + endif + endfor + + return '' +endfunction diff --git a/autoload/ale/debugging.vim b/autoload/ale/debugging.vim index 34c13770..6c2bfbee 100644 --- a/autoload/ale/debugging.vim +++ b/autoload/ale/debugging.vim @@ -22,14 +22,14 @@ let s:global_variable_list = [ \ 'ale_lint_delay', \ 'ale_lint_on_enter', \ 'ale_lint_on_filetype_changed', +\ 'ale_lint_on_insert_leave', \ 'ale_lint_on_save', \ 'ale_lint_on_text_changed', -\ 'ale_lint_on_insert_leave', \ 'ale_linter_aliases', \ 'ale_linters', \ 'ale_linters_explicit', -\ 'ale_list_window_size', \ 'ale_list_vertical', +\ 'ale_list_window_size', \ 'ale_loclist_msg_format', \ 'ale_max_buffer_history_size', \ 'ale_max_signs', @@ -52,6 +52,7 @@ let s:global_variable_list = [ \ 'ale_statusline_format', \ 'ale_type_map', \ 'ale_use_global_executables', +\ 'ale_virtualtext_cursor', \ 'ale_warn_about_trailing_blank_lines', \ 'ale_warn_about_trailing_whitespace', \] diff --git a/autoload/ale/definition.vim b/autoload/ale/definition.vim index 6c7d7d32..984a4f9d 100644 --- a/autoload/ale/definition.vim +++ b/autoload/ale/definition.vim @@ -40,16 +40,16 @@ function! ale#definition#HandleLSPResponse(conn_id, response) abort " The result can be a Dictionary item, a List of the same, or null. let l:result = get(a:response, 'result', v:null) - if type(l:result) is type({}) + if type(l:result) is v:t_dict let l:result = [l:result] - elseif type(l:result) isnot type([]) + elseif type(l:result) isnot v:t_list let l:result = [] endif for l:item in l:result let l:filename = ale#path#FromURI(l:item.uri) let l:line = l:item.range.start.line + 1 - let l:column = l:item.range.start.character + let l:column = l:item.range.start.character + 1 call ale#util#Open(l:filename, l:line, l:column, l:options) break @@ -57,6 +57,39 @@ function! ale#definition#HandleLSPResponse(conn_id, response) abort endif endfunction +function! s:OnReady(linter, lsp_details, line, column, options, ...) abort + let l:buffer = a:lsp_details.buffer + let l:id = a:lsp_details.connection_id + + let l:Callback = a:linter.lsp is# 'tsserver' + \ ? function('ale#definition#HandleTSServerResponse') + \ : function('ale#definition#HandleLSPResponse') + call ale#lsp#RegisterCallback(l:id, l:Callback) + + if a:linter.lsp is# 'tsserver' + let l:message = ale#lsp#tsserver_message#Definition( + \ l:buffer, + \ a:line, + \ a:column + \) + else + " Send a message saying the buffer has changed first, or the + " definition position probably won't make sense. + call ale#lsp#NotifyForChanges(l:id, l:buffer) + + " For LSP completions, we need to clamp the column to the length of + " the line. python-language-server and perhaps others do not implement + " this correctly. + let l:message = ale#lsp#message#Definition(l:buffer, a:line, a:column) + endif + + let l:request_id = ale#lsp#Send(l:id, l:message) + + let s:go_to_definition_map[l:request_id] = { + \ 'open_in_tab': get(a:options, 'open_in_tab', 0), + \} +endfunction + function! s:GoToLSPDefinition(linter, options) abort let l:buffer = bufnr('') let [l:line, l:column] = getcurpos()[1:2] @@ -71,39 +104,10 @@ function! s:GoToLSPDefinition(linter, options) abort endif let l:id = l:lsp_details.connection_id - let l:root = l:lsp_details.project_root - - function! OnReady(...) abort closure - let l:Callback = a:linter.lsp is# 'tsserver' - \ ? function('ale#definition#HandleTSServerResponse') - \ : function('ale#definition#HandleLSPResponse') - call ale#lsp#RegisterCallback(l:id, l:Callback) - - if a:linter.lsp is# 'tsserver' - let l:message = ale#lsp#tsserver_message#Definition( - \ l:buffer, - \ l:line, - \ l:column - \) - else - " Send a message saying the buffer has changed first, or the - " definition position probably won't make sense. - call ale#lsp#NotifyForChanges(l:id, l:root, l:buffer) - - " For LSP completions, we need to clamp the column to the length of - " the line. python-language-server and perhaps others do not implement - " this correctly. - let l:message = ale#lsp#message#Definition(l:buffer, l:line, l:column) - endif - - let l:request_id = ale#lsp#Send(l:id, l:message, l:lsp_details.project_root) - - let s:go_to_definition_map[l:request_id] = { - \ 'open_in_tab': get(a:options, 'open_in_tab', 0), - \} - endfunction - call ale#lsp#WaitForCapability(l:id, l:root, 'definition', function('OnReady')) + call ale#lsp#WaitForCapability(l:id, 'definition', function('s:OnReady', [ + \ a:linter, l:lsp_details, l:line, l:column, a:options + \])) endfunction function! ale#definition#GoTo(options) abort diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index ec5ccb6d..b44be73c 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -18,6 +18,22 @@ if !has_key(s:, 'executable_cache_map') let s:executable_cache_map = {} endif + +function! ale#engine#CleanupEveryBuffer() abort + for l:key in keys(g:ale_buffer_info) + " The key could be a filename or a buffer number, so try and + " convert it to a number. We need a number for the other + " functions. + let l:buffer = str2nr(l:key) + + if l:buffer > 0 + " Stop all jobs and clear the results for everything, and delete + " all of the data we stored for the buffer. + call ale#engine#Cleanup(l:buffer) + endif + endfor +endfunction + function! ale#engine#ResetExecutableCache() abort let s:executable_cache_map = {} endfunction @@ -63,6 +79,7 @@ function! ale#engine#InitBufferInfo(buffer) abort let g:ale_buffer_info[a:buffer] = { \ 'job_list': [], \ 'active_linter_list': [], + \ 'active_other_sources_list': [], \ 'loclist': [], \ 'temporary_file_list': [], \ 'temporary_directory_list': [], @@ -81,6 +98,7 @@ function! ale#engine#IsCheckingBuffer(buffer) abort let l:info = get(g:ale_buffer_info, a:buffer, {}) return !empty(get(l:info, 'active_linter_list', [])) + \ || !empty(get(l:info, 'active_other_sources_list', [])) endfunction " Register a temporary file to be managed with the ALE engine for @@ -161,20 +179,27 @@ function! s:GatherOutput(job_id, line) abort endif endfunction -function! ale#engine#HandleLoclist(linter_name, buffer, loclist) abort +function! ale#engine#HandleLoclist(linter_name, buffer, loclist, from_other_source) abort let l:info = get(g:ale_buffer_info, a:buffer, {}) if empty(l:info) return endif - " Remove this linter from the list of active linters. - " This may have already been done when the job exits. - call filter(l:info.active_linter_list, 'v:val isnot# a:linter_name') + if !a:from_other_source + " Remove this linter from the list of active linters. + " This may have already been done when the job exits. + call filter(l:info.active_linter_list, 'v:val isnot# a:linter_name') + endif " Make some adjustments to the loclists to fix common problems, and also " to set default values for loclist items. - let l:linter_loclist = ale#engine#FixLocList(a:buffer, a:linter_name, a:loclist) + let l:linter_loclist = ale#engine#FixLocList( + \ a:buffer, + \ a:linter_name, + \ a:from_other_source, + \ a:loclist, + \) " Remove previous items for this linter. call filter(l:info.loclist, 'v:val.linter_name isnot# a:linter_name') @@ -231,6 +256,7 @@ function! s:HandleExit(job_id, exit_code) abort if l:next_chain_index < len(get(l:linter, 'command_chain', [])) call s:InvokeChain(l:buffer, l:executable, l:linter, l:next_chain_index, l:output) + return endif @@ -246,7 +272,7 @@ function! s:HandleExit(job_id, exit_code) abort let l:loclist = [] endtry - call ale#engine#HandleLoclist(l:linter.name, l:buffer, l:loclist) + call ale#engine#HandleLoclist(l:linter.name, l:buffer, l:loclist, 0) endfunction function! ale#engine#SetResults(buffer, loclist) abort @@ -278,6 +304,12 @@ function! ale#engine#SetResults(buffer, loclist) abort call ale#cursor#EchoCursorWarning() endif + if g:ale_virtualtext_cursor + " Try and show the warning now. + " This will only do something meaningful if we're in normal mode. + call ale#virtualtext#ShowCursorWarning() + endif + " Reset the save event marker, used for opening windows, etc. call setbufvar(a:buffer, 'ale_save_event_fired', 0) " Set a marker showing how many times a buffer has been checked. @@ -318,7 +350,7 @@ function! s:RemapItemTypes(type_map, loclist) abort endfor endfunction -function! ale#engine#FixLocList(buffer, linter_name, loclist) abort +function! ale#engine#FixLocList(buffer, linter_name, from_other_source, loclist) abort let l:bufnr_map = {} let l:new_loclist = [] @@ -351,6 +383,10 @@ function! ale#engine#FixLocList(buffer, linter_name, loclist) abort \ 'linter_name': a:linter_name, \} + if a:from_other_source + let l:item.from_other_source = 1 + endif + if has_key(l:old_item, 'code') let l:item.code = l:old_item.code endif @@ -557,7 +593,7 @@ function! s:RunJob(options) abort if get(g:, 'ale_run_synchronously') == 1 " Run a command synchronously if this test option is set. let s:job_info_map[l:job_id].output = systemlist( - \ type(l:command) == type([]) + \ type(l:command) is v:t_list \ ? join(l:command[0:1]) . ' ' . ale#Escape(l:command[2]) \ : l:command \) @@ -595,9 +631,8 @@ function! ale#engine#ProcessChain(buffer, linter, chain_index, input) abort \) endif + " If we have a command to run, execute that. if !empty(l:command) - " We hit a command to run, so we'll execute that - " 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 @@ -675,6 +710,7 @@ endfunction function! s:RemoveProblemsForDisabledLinters(buffer, linters) abort " Figure out which linters are still enabled, and remove " problems for linters which are no longer enabled. + " Problems from other sources will be kept. let l:name_map = {} for l:linter in a:linters @@ -683,7 +719,7 @@ function! s:RemoveProblemsForDisabledLinters(buffer, linters) abort call filter( \ get(g:ale_buffer_info[a:buffer], 'loclist', []), - \ 'get(l:name_map, get(v:val, ''linter_name''))', + \ 'get(v:val, ''from_other_source'') || get(l:name_map, get(v:val, ''linter_name''))', \) endfunction diff --git a/autoload/ale/engine/ignore.vim b/autoload/ale/engine/ignore.vim index 65347e21..2db2c6c1 100644 --- a/autoload/ale/engine/ignore.vim +++ b/autoload/ale/engine/ignore.vim @@ -4,11 +4,11 @@ " Given a filetype and a configuration for ignoring linters, return a List of " Strings for linter names to ignore. function! ale#engine#ignore#GetList(filetype, config) abort - if type(a:config) is type([]) + if type(a:config) is v:t_list return a:config endif - if type(a:config) is type({}) + if type(a:config) is v:t_dict let l:names_to_remove = [] for l:part in split(a:filetype , '\.') diff --git a/autoload/ale/events.vim b/autoload/ale/events.vim index 300aefcc..c3dbd378 100644 --- a/autoload/ale/events.vim +++ b/autoload/ale/events.vim @@ -29,7 +29,7 @@ function! ale#events#SaveEvent(buffer) abort call setbufvar(a:buffer, 'ale_save_event_fired', 1) endif - if ale#Var(a:buffer, 'fix_on_save') + if ale#Var(a:buffer, 'fix_on_save') && !ale#events#QuitRecently(a:buffer) let l:will_fix = ale#fix#Fix(a:buffer, 'save_file') let l:should_lint = l:should_lint && !l:will_fix endif @@ -131,13 +131,25 @@ function! ale#events#Init() abort autocmd InsertLeave * call ale#Queue(0) endif - if g:ale_echo_cursor + if g:ale_echo_cursor || g:ale_cursor_detail autocmd CursorMoved,CursorHold * if exists('*ale#engine#Cleanup') | call ale#cursor#EchoCursorWarningWithDelay() | endif " Look for a warning to echo as soon as we leave Insert mode. " The script's position variable used when moving the cursor will " not be changed here. autocmd InsertLeave * if exists('*ale#engine#Cleanup') | call ale#cursor#EchoCursorWarning() | endif endif + + if g:ale_virtualtext_cursor + autocmd CursorMoved,CursorHold * if exists('*ale#engine#Cleanup') | call ale#virtualtext#ShowCursorWarningWithDelay() | endif + " Look for a warning to echo as soon as we leave Insert mode. + " The script's position variable used when moving the cursor will + " not be changed here. + autocmd InsertLeave * if exists('*ale#engine#Cleanup') | call ale#virtualtext#ShowCursorWarning() | endif + endif + + if g:ale_close_preview_on_insert + autocmd InsertEnter * if exists('*ale#preview#CloseIfTypeMatches') | call ale#preview#CloseIfTypeMatches('ale-preview') | endif + endif endif augroup END endfunction diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index 8dfdeca8..03652ecf 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -30,7 +30,14 @@ function! ale#fix#ApplyQueuedFixes() abort call winrestview(l:save) endif - call setline(1, l:data.output) + " If the file is in DOS mode, we have to remove carriage returns from + " the ends of lines before calling setline(), or we will see them + " twice. + let l:lines_to_set = getbufvar(l:buffer, '&fileformat') is# 'dos' + \ ? map(copy(l:data.output), 'substitute(v:val, ''\r\+$'', '''', '''')') + \ : l:data.output + + call setline(1, l:lines_to_set) if l:data.should_save if empty(&buftype) @@ -71,6 +78,7 @@ function! ale#fix#ApplyFixes(buffer, output) abort if l:data.lines_before != l:lines call remove(g:ale_fix_buffer_data, a:buffer) execute 'echoerr ''The file was changed before fixing finished''' + return endif endif @@ -275,7 +283,7 @@ function! s:RunJob(options) abort if get(g:, 'ale_run_synchronously') == 1 " Run a command synchronously if this test option is set. let l:output = systemlist( - \ type(l:command) == type([]) + \ type(l:command) is v:t_list \ ? join(l:command[0:1]) . ' ' . ale#Escape(l:command[2]) \ : l:command \) @@ -313,10 +321,10 @@ function! s:RunFixer(options) abort \ : call(l:Function, [l:buffer, copy(l:input)]) endif - if type(l:result) == type(0) && l:result == 0 + if type(l:result) is v:t_number && l:result == 0 " When `0` is returned, skip this item. let l:index += 1 - elseif type(l:result) == type([]) + elseif type(l:result) is v:t_list let l:input = l:result let l:index += 1 else @@ -351,9 +359,9 @@ function! s:RunFixer(options) abort endfunction function! s:AddSubCallbacks(full_list, callbacks) abort - if type(a:callbacks) == type('') + if type(a:callbacks) is v:t_string call add(a:full_list, a:callbacks) - elseif type(a:callbacks) == type([]) + elseif type(a:callbacks) is v:t_list call extend(a:full_list, a:callbacks) else return 0 @@ -365,7 +373,7 @@ endfunction function! s:GetCallbacks(buffer, fixers) abort if len(a:fixers) let l:callback_list = a:fixers - elseif type(get(b:, 'ale_fixers')) is type([]) + elseif type(get(b:, 'ale_fixers')) is v:t_list " Lists can be used for buffer-local variables only let l:callback_list = b:ale_fixers else @@ -396,7 +404,7 @@ function! s:GetCallbacks(buffer, fixers) abort " Variables with capital characters are needed, or Vim will complain about " funcref variables. for l:Item in l:callback_list - if type(l:Item) == type('') + if type(l:Item) is v:t_string let l:Func = ale#fix#registry#GetFunc(l:Item) if !empty(l:Func) diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index e148ecd6..a54be420 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -56,7 +56,7 @@ let s:default_registry = { \ }, \ 'prettier': { \ 'function': 'ale#fixers#prettier#Fix', -\ 'suggested_filetypes': ['javascript', 'typescript', 'css', 'less', 'scss', 'json', 'json5', 'graphql', 'markdown', 'vue'], +\ 'suggested_filetypes': ['javascript', 'typescript', 'css', 'less', 'scss', 'json', 'json5', 'graphql', 'markdown', 'vue', 'html', 'yaml'], \ 'description': 'Apply prettier to a file.', \ }, \ 'prettier_eslint': { @@ -145,6 +145,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['go'], \ 'description': 'Fix Go files imports with goimports.', \ }, +\ 'gomod': { +\ 'function': 'ale#fixers#gomod#Fix', +\ 'suggested_filetypes': ['gomod'], +\ 'description': 'Fix Go module files with go mod edit -fmt.', +\ }, \ 'tslint': { \ 'function': 'ale#fixers#tslint#Fix', \ 'suggested_filetypes': ['typescript'], @@ -157,7 +162,7 @@ let s:default_registry = { \ }, \ 'hackfmt': { \ 'function': 'ale#fixers#hackfmt#Fix', -\ 'suggested_filetypes': ['php'], +\ 'suggested_filetypes': ['hack'], \ 'description': 'Fix Hack files with hackfmt.', \ }, \ 'hfmt': { @@ -170,6 +175,21 @@ let s:default_registry = { \ 'suggested_filetypes': ['haskell'], \ 'description': 'Fix Haskell files with brittany.', \ }, +\ 'hlint': { +\ 'function': 'ale#fixers#hlint#Fix', +\ 'suggested_filetypes': ['haskell'], +\ 'description': 'Refactor Haskell files with hlint.', +\ }, +\ 'stylish-haskell': { +\ 'function': 'ale#fixers#stylish_haskell#Fix', +\ 'suggested_filetypes': ['haskell'], +\ 'description': 'Refactor Haskell files with stylish-haskell.', +\ }, +\ 'ocamlformat': { +\ 'function': 'ale#fixers#ocamlformat#Fix', +\ 'suggested_filetypes': ['ocaml'], +\ 'description': 'Fix OCaml files with ocamlformat.', +\ }, \ 'refmt': { \ 'function': 'ale#fixers#refmt#Fix', \ 'suggested_filetypes': ['reason'], @@ -180,6 +200,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['sh'], \ 'description': 'Fix sh files with shfmt.', \ }, +\ 'sqlfmt': { +\ 'function': 'ale#fixers#sqlfmt#Fix', +\ 'suggested_filetypes': ['sql'], +\ 'description': 'Fix SQL files with sqlfmt.', +\ }, \ 'google_java_format': { \ 'function': 'ale#fixers#google_java_format#Fix', \ 'suggested_filetypes': ['java'], @@ -215,6 +240,21 @@ let s:default_registry = { \ 'suggested_filetypes': ['dart'], \ 'description': 'Fix Dart files with dartfmt.', \ }, +\ 'xmllint': { +\ 'function': 'ale#fixers#xmllint#Fix', +\ 'suggested_filetypes': ['xml'], +\ 'description': 'Fix XML files with xmllint.', +\ }, +\ 'uncrustify': { +\ 'function': 'ale#fixers#uncrustify#Fix', +\ 'suggested_filetypes': ['c', 'cpp', 'cs', 'objc', 'objcpp', 'd', 'java', 'p', 'vala' ], +\ 'description': 'Fix C, C++, C#, ObjectiveC, ObjectiveC++, D, Java, Pawn, and VALA files with uncrustify.', +\ }, +\ 'terraform': { +\ 'function': 'ale#fixers#terraform#Fix', +\ 'suggested_filetypes': ['hcl', 'terraform'], +\ 'description': 'Fix tf and hcl files with terraform fmt.', +\ }, \} " Reset the function registry to the default entries. @@ -243,34 +283,34 @@ endfunction " (name, func, filetypes, desc, aliases) function! ale#fix#registry#Add(name, func, filetypes, desc, ...) abort " This command will throw from the sandbox. - let &equalprg=&equalprg + let &l:equalprg=&l:equalprg - if type(a:name) != type('') + if type(a:name) isnot v:t_string throw '''name'' must be a String' endif - if type(a:func) != type('') + if type(a:func) isnot v:t_string throw '''func'' must be a String' endif - if type(a:filetypes) != type([]) + if type(a:filetypes) isnot v:t_list throw '''filetypes'' must be a List' endif for l:type in a:filetypes - if type(l:type) != type('') + if type(l:type) isnot v:t_string throw 'Each entry of ''filetypes'' must be a String' endif endfor - if type(a:desc) != type('') + if type(a:desc) isnot v:t_string throw '''desc'' must be a String' endif let l:aliases = get(a:000, 0, []) - if type(l:aliases) != type([]) - \|| !empty(filter(copy(l:aliases), 'type(v:val) != type('''')')) + if type(l:aliases) isnot v:t_list + \|| !empty(filter(copy(l:aliases), 'type(v:val) isnot v:t_string')) throw '''aliases'' must be a List of String values' endif diff --git a/autoload/ale/fixers/brittany.vim b/autoload/ale/fixers/brittany.vim index 57c77325..c2448348 100644 --- a/autoload/ale/fixers/brittany.vim +++ b/autoload/ale/fixers/brittany.vim @@ -3,11 +3,17 @@ call ale#Set('haskell_brittany_executable', 'brittany') -function! ale#fixers#brittany#Fix(buffer) abort +function! ale#fixers#brittany#GetExecutable(buffer) abort let l:executable = ale#Var(a:buffer, 'haskell_brittany_executable') + return ale#handlers#haskell_stack#EscapeExecutable(l:executable, 'brittany') +endfunction + +function! ale#fixers#brittany#Fix(buffer) abort + let l:executable = ale#fixers#brittany#GetExecutable(a:buffer) + return { - \ 'command': ale#Escape(l:executable) + \ 'command': l:executable \ . ' --write-mode inplace' \ . ' %t', \ 'read_temporary_file': 1, diff --git a/autoload/ale/fixers/eslint.vim b/autoload/ale/fixers/eslint.vim index 36f47510..ea5b2a63 100644 --- a/autoload/ale/fixers/eslint.vim +++ b/autoload/ale/fixers/eslint.vim @@ -25,7 +25,7 @@ endfunction function! ale#fixers#eslint#ProcessEslintDOutput(buffer, output) abort " If the output is an error message, don't use it. for l:line in a:output[:10] - if l:line =~# '^Error:' + if l:line =~# '\v^Error:|^Could not connect' return [] endif endfor diff --git a/autoload/ale/fixers/fixjson.vim b/autoload/ale/fixers/fixjson.vim index 64c6ba81..33ce0af3 100644 --- a/autoload/ale/fixers/fixjson.vim +++ b/autoload/ale/fixers/fixjson.vim @@ -17,6 +17,7 @@ function! ale#fixers#fixjson#Fix(buffer) abort let l:command = l:executable . ' --stdin-filename ' . l:filename let l:options = ale#Var(a:buffer, 'json_fixjson_options') + if l:options isnot# '' let l:command .= ' ' . l:options endif diff --git a/autoload/ale/fixers/generic_python.vim b/autoload/ale/fixers/generic_python.vim index 124146be..d55a23c3 100644 --- a/autoload/ale/fixers/generic_python.vim +++ b/autoload/ale/fixers/generic_python.vim @@ -6,13 +6,28 @@ function! ale#fixers#generic_python#AddLinesBeforeControlStatements(buffer, line let l:new_lines = [] let l:last_indent_size = 0 let l:last_line_is_blank = 0 + let l:in_docstring = 0 for l:line in a:lines let l:indent_size = len(matchstr(l:line, '^ *')) + if !l:in_docstring + " Make sure it is not just a single line docstring and then verify + " it's starting a new docstring + if match(l:line, '\v^ *("""|'''''').*("""|'''''')') == -1 + \&& match(l:line, '\v^ *("""|'''''')') >= 0 + let l:in_docstring = 1 + endif + else + if match(l:line, '\v^ *.*("""|'''''')') >= 0 + let l:in_docstring = 0 + endif + endif + if !l:last_line_is_blank + \&& !l:in_docstring \&& l:indent_size <= l:last_indent_size - \&& match(l:line, '\v^ *(return|if|for|while|break|continue)') >= 0 + \&& match(l:line, '\v^ *(return|if|for|while|break|continue)(\(| |$)') >= 0 call add(l:new_lines, '') endif diff --git a/autoload/ale/fixers/gomod.vim b/autoload/ale/fixers/gomod.vim new file mode 100644 index 00000000..68895f9b --- /dev/null +++ b/autoload/ale/fixers/gomod.vim @@ -0,0 +1,10 @@ +call ale#Set('go_go_executable', 'go') + +function! ale#fixers#gomod#Fix(buffer) abort + let l:executable = ale#Var(a:buffer, 'go_go_executable') + + return { + \ 'command': ale#Escape(l:executable) . ' mod edit -fmt %t', + \ 'read_temporary_file': 1, + \} +endfunction diff --git a/autoload/ale/fixers/google_java_format.vim b/autoload/ale/fixers/google_java_format.vim index 6a2f5491..20086c73 100644 --- a/autoload/ale/fixers/google_java_format.vim +++ b/autoload/ale/fixers/google_java_format.vim @@ -1,13 +1,13 @@ " Author: butlerx <butlerx@notthe,cloud> " Description: Integration of Google-java-format with ALE. -call ale#Set('google_java_format_executable', 'google-java-format') -call ale#Set('google_java_format_use_global', get(g:, 'ale_use_global_executables', 0)) -call ale#Set('google_java_format_options', '') +call ale#Set('java_google_java_format_executable', 'google-java-format') +call ale#Set('java_google_java_format_use_global', get(g:, 'ale_use_global_executables', 0)) +call ale#Set('java_google_java_format_options', '') function! ale#fixers#google_java_format#Fix(buffer) abort - let l:options = ale#Var(a:buffer, 'google_java_format_options') - let l:executable = ale#Var(a:buffer, 'google_java_format_executable') + let l:options = ale#Var(a:buffer, 'java_google_java_format_options') + let l:executable = ale#Var(a:buffer, 'java_google_java_format_executable') if !executable(l:executable) return 0 diff --git a/autoload/ale/fixers/hackfmt.vim b/autoload/ale/fixers/hackfmt.vim index b5bf0dc5..bf2d4f71 100644 --- a/autoload/ale/fixers/hackfmt.vim +++ b/autoload/ale/fixers/hackfmt.vim @@ -1,12 +1,12 @@ " Author: Sam Howie <samhowie@gmail.com> " Description: Integration of hackfmt with ALE. -call ale#Set('php_hackfmt_executable', 'hackfmt') -call ale#Set('php_hackfmt_options', '') +call ale#Set('hack_hackfmt_executable', 'hackfmt') +call ale#Set('hack_hackfmt_options', '') function! ale#fixers#hackfmt#Fix(buffer) abort - let l:executable = ale#Var(a:buffer, 'php_hackfmt_executable') - let l:options = ale#Var(a:buffer, 'php_hackfmt_options') + let l:executable = ale#Var(a:buffer, 'hack_hackfmt_executable') + let l:options = ale#Var(a:buffer, 'hack_hackfmt_options') return { \ 'command': ale#Escape(l:executable) diff --git a/autoload/ale/fixers/hfmt.vim b/autoload/ale/fixers/hfmt.vim index ea061da4..0407b713 100644 --- a/autoload/ale/fixers/hfmt.vim +++ b/autoload/ale/fixers/hfmt.vim @@ -7,7 +7,7 @@ function! ale#fixers#hfmt#Fix(buffer) abort let l:executable = ale#Var(a:buffer, 'haskell_hfmt_executable') return { - \ 'command': ale#Escape(l:executable) + \ 'command': ale#handlers#haskell_stack#EscapeExecutable(l:executable, 'hfmt') \ . ' -w' \ . ' %t', \ 'read_temporary_file': 1, diff --git a/autoload/ale/fixers/hlint.vim b/autoload/ale/fixers/hlint.vim new file mode 100644 index 00000000..88779a55 --- /dev/null +++ b/autoload/ale/fixers/hlint.vim @@ -0,0 +1,13 @@ +" Author: eborden <evan@evan-borden.com> +" Description: Integration of hlint refactor with ALE. +" + +function! ale#fixers#hlint#Fix(buffer) abort + return { + \ 'command': ale#handlers#hlint#GetExecutable(a:buffer) + \ . ' --refactor' + \ . ' --refactor-options="--inplace"' + \ . ' %t', + \ 'read_temporary_file': 1, + \} +endfunction diff --git a/autoload/ale/fixers/importjs.vim b/autoload/ale/fixers/importjs.vim index e8eedb12..b5487b2c 100644 --- a/autoload/ale/fixers/importjs.vim +++ b/autoload/ale/fixers/importjs.vim @@ -1,15 +1,16 @@ " Author: Jeff Willette <jrwillette88@gmail.com> " Description: Integration of importjs with ALE. -call ale#Set('js_importjs_executable', 'importjs') +call ale#Set('javascript_importjs_executable', 'importjs') function! ale#fixers#importjs#ProcessOutput(buffer, output) abort let l:result = ale#util#FuzzyJSONDecode(a:output, []) + return split(get(l:result, 'fileContent', ''), "\n") endfunction function! ale#fixers#importjs#Fix(buffer) abort - let l:executable = ale#Var(a:buffer, 'js_importjs_executable') + let l:executable = ale#Var(a:buffer, 'javascript_importjs_executable') if !executable(l:executable) return 0 diff --git a/autoload/ale/fixers/jq.vim b/autoload/ale/fixers/jq.vim index b0a43fe2..1b743e49 100644 --- a/autoload/ale/fixers/jq.vim +++ b/autoload/ale/fixers/jq.vim @@ -1,5 +1,6 @@ call ale#Set('json_jq_executable', 'jq') call ale#Set('json_jq_options', '') +call ale#Set('json_jq_filters', '.') function! ale#fixers#jq#GetExecutable(buffer) abort return ale#Var(a:buffer, 'json_jq_executable') @@ -7,9 +8,15 @@ endfunction function! ale#fixers#jq#Fix(buffer) abort let l:options = ale#Var(a:buffer, 'json_jq_options') + let l:filters = ale#Var(a:buffer, 'json_jq_filters') + + if empty(l:filters) + return 0 + endif return { \ 'command': ale#Escape(ale#fixers#jq#GetExecutable(a:buffer)) - \ . ' . ' . l:options, + \ . ' ' . l:filters . ' ' + \ . l:options, \} endfunction diff --git a/autoload/ale/fixers/ocamlformat.vim b/autoload/ale/fixers/ocamlformat.vim new file mode 100644 index 00000000..9b7c3e12 --- /dev/null +++ b/autoload/ale/fixers/ocamlformat.vim @@ -0,0 +1,18 @@ +" Author: Stephen Lumenta <@sbl> +" Description: Integration of ocamlformat with ALE. + +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) + \ . ' -' + \} +endfunction diff --git a/autoload/ale/fixers/php_cs_fixer.vim b/autoload/ale/fixers/php_cs_fixer.vim index 26b8e5de..5c59e262 100644 --- a/autoload/ale/fixers/php_cs_fixer.vim +++ b/autoload/ale/fixers/php_cs_fixer.vim @@ -14,6 +14,7 @@ endfunction function! ale#fixers#php_cs_fixer#Fix(buffer) abort let l:executable = ale#fixers#php_cs_fixer#GetExecutable(a:buffer) + return { \ 'command': ale#Escape(l:executable) \ . ' ' . ale#Var(a:buffer, 'php_cs_fixer_options') diff --git a/autoload/ale/fixers/phpcbf.vim b/autoload/ale/fixers/phpcbf.vim index 487f369a..f14b8406 100644 --- a/autoload/ale/fixers/phpcbf.vim +++ b/autoload/ale/fixers/phpcbf.vim @@ -18,6 +18,7 @@ function! ale#fixers#phpcbf#Fix(buffer) abort let l:standard_option = !empty(l:standard) \ ? '--standard=' . l:standard \ : '' + return { \ 'command': ale#Escape(l:executable) . ' --stdin-path=%s ' . l:standard_option . ' -' \} diff --git a/autoload/ale/fixers/prettier.vim b/autoload/ale/fixers/prettier.vim index e8f4e92e..58dea159 100644 --- a/autoload/ale/fixers/prettier.vim +++ b/autoload/ale/fixers/prettier.vim @@ -27,6 +27,17 @@ function! ale#fixers#prettier#Fix(buffer) abort \} endfunction +function! ale#fixers#prettier#ProcessPrettierDOutput(buffer, output) abort + " If the output is an error message, don't use it. + for l:line in a:output[:10] + if l:line =~# '^\w*Error:' + return [] + endif + endfor + + return a:output +endfunction + function! ale#fixers#prettier#ApplyFixForVersion(buffer, version_output) abort let l:executable = ale#fixers#prettier#GetExecutable(a:buffer) let l:options = ale#Var(a:buffer, 'javascript_prettier_options') @@ -36,12 +47,24 @@ function! ale#fixers#prettier#ApplyFixForVersion(buffer, version_output) abort " Append the --parser flag depending on the current filetype (unless it's " already set in g:javascript_prettier_options). if empty(expand('#' . a:buffer . ':e')) && match(l:options, '--parser') == -1 - let l:prettier_parsers = ['typescript', 'css', 'less', 'scss', 'json', 'json5', 'graphql', 'markdown', 'vue'] - let l:parser = 'babylon' + let l:prettier_parsers = { + \ 'typescript': 'typescript', + \ 'css': 'css', + \ 'less': 'less', + \ 'scss': 'scss', + \ 'json': 'json', + \ 'json5': 'json5', + \ 'graphql': 'graphql', + \ 'markdown': 'markdown', + \ 'vue': 'vue', + \ 'yaml': 'yaml', + \ 'html': 'html', + \} + let l:parser = '' for l:filetype in split(getbufvar(a:buffer, '&filetype'), '\.') - if index(l:prettier_parsers, l:filetype) > -1 - let l:parser = l:filetype + if has_key(l:prettier_parsers, l:filetype) + let l:parser = l:prettier_parsers[l:filetype] break endif endfor @@ -51,6 +74,17 @@ function! ale#fixers#prettier#ApplyFixForVersion(buffer, version_output) abort let l:options = (!empty(l:options) ? l:options . ' ' : '') . '--parser ' . l:parser endif + " Special error handling needed for prettier_d + if l:executable =~# 'prettier_d$' + return { + \ 'command': ale#path#BufferCdString(a:buffer) + \ . ale#Escape(l:executable) + \ . (!empty(l:options) ? ' ' . l:options : '') + \ . ' --stdin-filepath %s --stdin', + \ 'process_with': 'ale#fixers#prettier#ProcessPrettierDOutput', + \} + endif + " 1.4.0 is the first version with --stdin-filepath if ale#semver#GTE(l:version, [1, 4, 0]) return { diff --git a/autoload/ale/fixers/puppetlint.vim b/autoload/ale/fixers/puppetlint.vim index 81f34e89..bf36e486 100644 --- a/autoload/ale/fixers/puppetlint.vim +++ b/autoload/ale/fixers/puppetlint.vim @@ -4,6 +4,7 @@ if !exists('g:ale_puppet_puppetlint_executable') let g:ale_puppet_puppetlint_executable = 'puppet-lint' endif + if !exists('g:ale_puppet_puppetlint_options') let g:ale_puppet_puppetlint_options = '' endif diff --git a/autoload/ale/fixers/rubocop.vim b/autoload/ale/fixers/rubocop.vim index 35569b19..33ba6887 100644 --- a/autoload/ale/fixers/rubocop.vim +++ b/autoload/ale/fixers/rubocop.vim @@ -1,16 +1,15 @@ +call ale#Set('ruby_rubocop_options', '') +call ale#Set('ruby_rubocop_executable', 'rubocop') + function! ale#fixers#rubocop#GetCommand(buffer) abort - let l:executable = ale#handlers#rubocop#GetExecutable(a:buffer) - let l:exec_args = l:executable =~? 'bundle$' - \ ? ' exec rubocop' - \ : '' + let l:executable = ale#Var(a:buffer, 'ruby_rubocop_executable') let l:config = ale#path#FindNearestFile(a:buffer, '.rubocop.yml') let l:options = ale#Var(a:buffer, 'ruby_rubocop_options') - return ale#Escape(l:executable) . l:exec_args + return ale#handlers#ruby#EscapeExecutable(l:executable, 'rubocop') \ . (!empty(l:config) ? ' --config ' . ale#Escape(l:config) : '') \ . (!empty(l:options) ? ' ' . l:options : '') - \ . ' --auto-correct %t' - + \ . ' --auto-correct --force-exclusion %t' endfunction function! ale#fixers#rubocop#Fix(buffer) abort diff --git a/autoload/ale/fixers/scalafmt.vim b/autoload/ale/fixers/scalafmt.vim index 07d28275..dd0e7745 100644 --- a/autoload/ale/fixers/scalafmt.vim +++ b/autoload/ale/fixers/scalafmt.vim @@ -15,7 +15,6 @@ function! ale#fixers#scalafmt#GetCommand(buffer) abort return ale#Escape(l:executable) . l:exec_args \ . (empty(l:options) ? '' : ' ' . l:options) \ . ' %t' - endfunction function! ale#fixers#scalafmt#Fix(buffer) abort diff --git a/autoload/ale/fixers/shfmt.vim b/autoload/ale/fixers/shfmt.vim index 882cf3a4..06e8da57 100644 --- a/autoload/ale/fixers/shfmt.vim +++ b/autoload/ale/fixers/shfmt.vim @@ -5,13 +5,27 @@ scriptencoding utf-8 call ale#Set('sh_shfmt_executable', 'shfmt') call ale#Set('sh_shfmt_options', '') +function! s:DefaultOption(buffer) abort + if getbufvar(a:buffer, '&expandtab') == 0 + " Tab is used by default + return '' + endif + + let l:tabsize = getbufvar(a:buffer, '&shiftwidth') + + if l:tabsize == 0 + let l:tabsize = getbufvar(a:buffer, '&tabstop') + endif + + return ' -i ' . l:tabsize +endfunction + function! ale#fixers#shfmt#Fix(buffer) abort let l:executable = ale#Var(a:buffer, 'sh_shfmt_executable') let l:options = ale#Var(a:buffer, 'sh_shfmt_options') return { \ 'command': ale#Escape(l:executable) - \ . (empty(l:options) ? '' : ' ' . l:options) + \ . (empty(l:options) ? s:DefaultOption(a:buffer) : ' ' . l:options) \} - endfunction diff --git a/autoload/ale/fixers/sqlfmt.vim b/autoload/ale/fixers/sqlfmt.vim new file mode 100644 index 00000000..c88a8ec2 --- /dev/null +++ b/autoload/ale/fixers/sqlfmt.vim @@ -0,0 +1,13 @@ +call ale#Set('sql_sqlfmt_executable', 'sqlfmt') +call ale#Set('sql_sqlfmt_options', '') + +function! ale#fixers#sqlfmt#Fix(buffer) abort + let l:executable = ale#Var(a:buffer, 'sql_sqlfmt_executable') + let l:options = ale#Var(a:buffer, 'sql_sqlfmt_options') + + return { + \ 'command': ale#Escape(l:executable) + \ . ' -w' + \ . (empty(l:options) ? '' : ' ' . l:options), + \} +endfunction diff --git a/autoload/ale/fixers/stylish_haskell.vim b/autoload/ale/fixers/stylish_haskell.vim new file mode 100644 index 00000000..ce71c1ce --- /dev/null +++ b/autoload/ale/fixers/stylish_haskell.vim @@ -0,0 +1,21 @@ +" Author: eborden <evan@evan-borden.com> +" Description: Integration of stylish-haskell formatting with ALE. +" +call ale#Set('haskell_stylish_haskell_executable', 'stylish-haskell') + +function! ale#fixers#stylish_haskell#GetExecutable(buffer) abort + let l:executable = ale#Var(a:buffer, 'haskell_stylish_haskell_executable') + + return ale#handlers#haskell_stack#EscapeExecutable(l:executable, 'stylish-haskell') +endfunction + +function! ale#fixers#stylish_haskell#Fix(buffer) abort + let l:executable = ale#fixers#stylish_haskell#GetExecutable(a:buffer) + + return { + \ 'command': l:executable + \ . ' --inplace' + \ . ' %t', + \ 'read_temporary_file': 1, + \} +endfunction diff --git a/autoload/ale/fixers/terraform.vim b/autoload/ale/fixers/terraform.vim new file mode 100644 index 00000000..bc05380a --- /dev/null +++ b/autoload/ale/fixers/terraform.vim @@ -0,0 +1,17 @@ +" Author: dsifford <dereksifford@gmail.com> +" Description: Fixer for terraform and .hcl files + +call ale#Set('terraform_fmt_executable', 'terraform') +call ale#Set('terraform_fmt_options', '') + +function! ale#fixers#terraform#Fix(buffer) abort + let l:executable = ale#Var(a:buffer, 'terraform_fmt_executable') + let l:options = ale#Var(a:buffer, 'terraform_fmt_options') + + return { + \ 'command': ale#Escape(l:executable) + \ . ' fmt' + \ . (empty(l:options) ? '' : ' ' . l:options) + \ . ' -' + \} +endfunction diff --git a/autoload/ale/fixers/uncrustify.vim b/autoload/ale/fixers/uncrustify.vim new file mode 100644 index 00000000..ffec18ef --- /dev/null +++ b/autoload/ale/fixers/uncrustify.vim @@ -0,0 +1,16 @@ +" Author: Derek P Sifford <dereksifford@gmail.com> +" Description: Fixer for C, C++, C#, ObjectiveC, D, Java, Pawn, and VALA. + +call ale#Set('c_uncrustify_executable', 'uncrustify') +call ale#Set('c_uncrustify_options', '') + +function! ale#fixers#uncrustify#Fix(buffer) abort + let l:executable = ale#Var(a:buffer, 'c_uncrustify_executable') + let l:options = ale#Var(a:buffer, 'c_uncrustify_options') + + return { + \ 'command': ale#Escape(l:executable) + \ . ' --no-backup' + \ . (empty(l:options) ? '' : ' ' . l:options) + \} +endfunction diff --git a/autoload/ale/fixers/xmllint.vim b/autoload/ale/fixers/xmllint.vim new file mode 100644 index 00000000..b14ffd36 --- /dev/null +++ b/autoload/ale/fixers/xmllint.vim @@ -0,0 +1,29 @@ +" Author: Cyril Roelandt <tipecaml@gmail.com> +" Description: Integration of xmllint with ALE. + +call ale#Set('xml_xmllint_executable', 'xmllint') +call ale#Set('xml_xmllint_options', '') +call ale#Set('xml_xmllint_indentsize', 2) + +function! ale#fixers#xmllint#Fix(buffer) abort + let l:executable = ale#Escape(ale#Var(a:buffer, 'xml_xmllint_executable')) + let l:filename = ale#Escape(bufname(a:buffer)) + let l:command = l:executable . ' --format ' . l:filename + + let l:indent = ale#Var(a:buffer, 'xml_xmllint_indentsize') + + if l:indent isnot# '' + let l:env = ale#Env('XMLLINT_INDENT', repeat(' ', l:indent)) + let l:command = l:env . l:command + endif + + let l:options = ale#Var(a:buffer, 'xml_xmllint_options') + + if l:options isnot# '' + let l:command .= ' ' . l:options + endif + + return { + \ 'command': l:command + \} +endfunction diff --git a/autoload/ale/go.vim b/autoload/ale/go.vim new file mode 100644 index 00000000..a166480a --- /dev/null +++ b/autoload/ale/go.vim @@ -0,0 +1,27 @@ +" Author: Horacio Sanson https://github.com/hsanson +" Description: Functions for integrating with Go tools + +" Find the nearest dir listed in GOPATH and assume it the root of the go +" project. +function! ale#go#FindProjectRoot(buffer) abort + let l:sep = has('win32') ? ';' : ':' + + let l:filename = ale#path#Simplify(expand('#' . a:buffer . ':p')) + + for l:name in split($GOPATH, l:sep) + let l:path_dir = ale#path#Simplify(l:name) + + " Use the directory from GOPATH if the current filename starts with it. + if l:filename[: len(l:path_dir) - 1] is? l:path_dir + return l:path_dir + endif + endfor + + let l:default_go_path = ale#path#Simplify(expand('~/go')) + + if isdirectory(l:default_go_path) + return l:default_go_path + endif + + return '' +endfunction diff --git a/autoload/ale/handlers/ccls.vim b/autoload/ale/handlers/ccls.vim new file mode 100644 index 00000000..29dd6aed --- /dev/null +++ b/autoload/ale/handlers/ccls.vim @@ -0,0 +1,17 @@ +scriptencoding utf-8 +" Author: Ye Jingchen <ye.jingchen@gmail.com> +" Description: Utilities for ccls + +function! ale#handlers#ccls#GetProjectRoot(buffer) abort + let l:project_root = ale#path#FindNearestFile(a:buffer, '.ccls-root') + + if empty(l:project_root) + let l:project_root = ale#path#FindNearestFile(a:buffer, 'compile_commands.json') + endif + + if empty(l:project_root) + let l:project_root = ale#path#FindNearestFile(a:buffer, '.ccls') + endif + + return !empty(l:project_root) ? fnamemodify(l:project_root, ':h') : '' +endfunction diff --git a/autoload/ale/handlers/elixir.vim b/autoload/ale/handlers/elixir.vim new file mode 100644 index 00000000..2fddf8e7 --- /dev/null +++ b/autoload/ale/handlers/elixir.vim @@ -0,0 +1,28 @@ +" Author: Matteo Centenaro (bugant) - https://github.com/bugant +" Author: Jon Parise <jon@indelible.org> +" Description: Functions for working with Elixir projects + +" Find the root directory for an elixir project that uses mix. +function! ale#handlers#elixir#FindMixProjectRoot(buffer) abort + let l:mix_file = ale#path#FindNearestFile(a:buffer, 'mix.exs') + + if !empty(l:mix_file) + return fnamemodify(l:mix_file, ':p:h') + endif + + return '.' +endfunction + +" Similar to ale#handlers#elixir#FindMixProjectRoot but also continue the +" search upward for a potential umbrella project root. If an umbrella root +" does not exist, the initial project root will be returned. +function! ale#handlers#elixir#FindMixUmbrellaRoot(buffer) abort + let l:app_root = ale#handlers#elixir#FindMixProjectRoot(a:buffer) + let l:umbrella_root = fnamemodify(l:app_root, ':h:h') + + if filereadable(l:umbrella_root . '/mix.exs') + return l:umbrella_root + endif + + return l:app_root +endfunction diff --git a/autoload/ale/handlers/eslint.vim b/autoload/ale/handlers/eslint.vim index bc10ec21..eda033e4 100644 --- a/autoload/ale/handlers/eslint.vim +++ b/autoload/ale/handlers/eslint.vim @@ -99,6 +99,13 @@ function! ale#handlers#eslint#Handle(buffer, lines) abort \}] endif + if a:lines == ['Could not connect'] + return [{ + \ 'lnum': 1, + \ 'text': 'Could not connect to eslint_d. Try updating eslint_d or killing it.', + \}] + endif + " Matches patterns line the following: " " /path/to/some-filename.js:47:14: Missing trailing comma. [Warning/comma-dangle] diff --git a/autoload/ale/handlers/gawk.vim b/autoload/ale/handlers/gawk.vim index 942bc2b2..50bc4c45 100644 --- a/autoload/ale/handlers/gawk.vim +++ b/autoload/ale/handlers/gawk.vim @@ -9,9 +9,11 @@ function! ale#handlers#gawk#HandleGawkFormat(buffer, lines) abort for l:match in ale#util#GetMatches(a:lines, l:pattern) let l:ecode = 'E' + if l:match[2] is? 'warning:' let l:ecode = 'W' endif + call add(l:output, { \ 'lnum': l:match[1] + 0, \ 'col': 0, diff --git a/autoload/ale/handlers/gcc.vim b/autoload/ale/handlers/gcc.vim index 4b53652a..72d639da 100644 --- a/autoload/ale/handlers/gcc.vim +++ b/autoload/ale/handlers/gcc.vim @@ -5,6 +5,13 @@ scriptencoding utf-8 let s:pragma_error = '#pragma once in main file' +" Look for lines like the following. +" +" <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+)?:? ([^:]+): (.+)$' + function! s:IsHeaderFile(filename) abort return a:filename =~? '\v\.(h|hpp)$' endfunction @@ -18,16 +25,63 @@ function! s:RemoveUnicodeQuotes(text) abort return l:text endfunction +" Report problems inside of header files just for gcc and clang +function! s:ParseProblemsInHeaders(buffer, lines) abort + let l:output = [] + let l:include_item = {} + + for l:line in a:lines[: -2] + let l:include_match = matchlist(l:line, '\v^In file included from') + + if !empty(l:include_item) + let l:pattern_match = matchlist(l:line, s:pattern) + + if !empty(l:pattern_match) && l:pattern_match[1] is# '<stdin>' + if has_key(l:include_item, 'lnum') + call add(l:output, l:include_item) + endif + + let l:include_item = {} + + continue + endif + + let l:include_item.detail .= "\n" . l:line + endif + + if !empty(l:include_match) + if empty(l:include_item) + let l:include_item = { + \ 'text': 'Error found in header. See :ALEDetail', + \ 'detail': l:line, + \} + endif + endif + + if !empty(l:include_item) + let l:stdin_match = matchlist(l:line, '\vfrom \<stdin\>:(\d+):(\d*):?$') + + if !empty(l:stdin_match) + let l:include_item.lnum = str2nr(l:stdin_match[1]) + + if str2nr(l:stdin_match[2]) + let l:include_item.col = str2nr(l:stdin_match[2]) + endif + endif + endif + endfor + + if !empty(l:include_item) && has_key(l:include_item, 'lnum') + call add(l:output, l:include_item) + endif + + return l:output +endfunction + function! ale#handlers#gcc#HandleGCCFormat(buffer, lines) abort - " Look for lines like the following. - " - " <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 l:pattern = '\v^([a-zA-Z]?:?[^:]+):(\d+):(\d+)?:? ([^:]+): (.+)$' let l:output = [] - for l:match in ale#util#GetMatches(a:lines, l:pattern) + for l:match in ale#util#GetMatches(a:lines, s:pattern) " Filter out the pragma errors if s:IsHeaderFile(bufname(bufnr(''))) \&& l:match[5][:len(s:pragma_error) - 1] is# s:pragma_error @@ -38,9 +92,12 @@ function! ale#handlers#gcc#HandleGCCFormat(buffer, lines) abort " the previous error parsed in output if l:match[4] is# 'note' if !empty(l:output) - let l:output[-1]['detail'] = - \ get(l:output[-1], 'detail', '') - \ . s:RemoveUnicodeQuotes(l:match[0]) . "\n" + if !has_key(l:output[-1], 'detail') + let l:output[-1].detail = l:output[-1].text + endif + + let l:output[-1].detail = l:output[-1].detail . "\n" + \ . s:RemoveUnicodeQuotes(l:match[0]) endif continue @@ -67,3 +124,12 @@ function! ale#handlers#gcc#HandleGCCFormat(buffer, lines) abort return l:output endfunction + +" Handle problems with the GCC format, but report problems inside of headers. +function! ale#handlers#gcc#HandleGCCFormatWithIncludes(buffer, lines) abort + let l:output = ale#handlers#gcc#HandleGCCFormat(a:buffer, a:lines) + + call extend(l:output, s:ParseProblemsInHeaders(a:buffer, a:lines)) + + return l:output +endfunction diff --git a/autoload/ale/handlers/go.vim b/autoload/ale/handlers/go.vim index 224df664..f17cd862 100644 --- a/autoload/ale/handlers/go.vim +++ b/autoload/ale/handlers/go.vim @@ -21,5 +21,6 @@ function! ale#handlers#go#Handler(buffer, lines) abort \ 'type': 'E', \}) endfor + return l:output endfunction diff --git a/autoload/ale/handlers/haskell.vim b/autoload/ale/handlers/haskell.vim index 9223b650..9e495b36 100644 --- a/autoload/ale/handlers/haskell.vim +++ b/autoload/ale/handlers/haskell.vim @@ -1,5 +1,15 @@ " Author: w0rp <devw0rp@gmail.com> " Description: Error handling for the format GHC outputs. +" +function! ale#handlers#haskell#GetStackExecutable(bufnr) abort + if ale#path#FindNearestFile(a:bufnr, 'stack.yaml') isnot# '' + return 'stack' + endif + + " if there is no stack.yaml file, we don't use stack even if it exists, + " so we return '', because executable('') apparently always fails + return '' +endfunction " Remember the directory used for temporary files for Vim. let s:temp_dir = fnamemodify(ale#util#Tempname(), ':h') diff --git a/autoload/ale/handlers/haskell_stack.vim b/autoload/ale/handlers/haskell_stack.vim new file mode 100644 index 00000000..108301a9 --- /dev/null +++ b/autoload/ale/handlers/haskell_stack.vim @@ -0,0 +1,7 @@ +function! ale#handlers#haskell_stack#EscapeExecutable(executable, stack_exec) abort + let l:exec_args = a:executable =~? 'stack$' + \ ? ' exec ' . ale#Escape(a:stack_exec) . ' --' + \ : '' + + return ale#Escape(a:executable) . l:exec_args +endfunction diff --git a/autoload/ale/handlers/hlint.vim b/autoload/ale/handlers/hlint.vim new file mode 100644 index 00000000..b9a8c322 --- /dev/null +++ b/autoload/ale/handlers/hlint.vim @@ -0,0 +1,8 @@ +call ale#Set('haskell_hlint_executable', 'hlint') +call ale#Set('haskell_hlint_options', get(g:, 'hlint_options', '')) + +function! ale#handlers#hlint#GetExecutable(buffer) abort + let l:executable = ale#Var(a:buffer, 'haskell_hlint_executable') + + return ale#handlers#haskell_stack#EscapeExecutable(l:executable, 'hlint') +endfunction diff --git a/autoload/ale/handlers/ols.vim b/autoload/ale/handlers/ols.vim index 1dda7f92..74130a26 100644 --- a/autoload/ale/handlers/ols.vim +++ b/autoload/ale/handlers/ols.vim @@ -3,6 +3,7 @@ function! ale#handlers#ols#GetExecutable(buffer) abort let l:ols_setting = ale#handlers#ols#GetLanguage(a:buffer) . '_ols' + return ale#node#FindExecutable(a:buffer, l:ols_setting, [ \ 'node_modules/.bin/ocaml-language-server', \]) diff --git a/autoload/ale/handlers/pony.vim b/autoload/ale/handlers/pony.vim index 0ac18e76..ea84ac4b 100644 --- a/autoload/ale/handlers/pony.vim +++ b/autoload/ale/handlers/pony.vim @@ -14,7 +14,6 @@ endfunction function! ale#handlers#pony#HandlePonycFormat(buffer, lines) abort " Look for lines like the following. " /home/code/pony/classes/Wombat.pony:22:30: can't lookup private fields from outside the type - let l:pattern = '\v^([^:]+):(\d+):(\d+)?:? (.+)$' let l:output = [] diff --git a/autoload/ale/handlers/redpen.vim b/autoload/ale/handlers/redpen.vim index c136789c..84e331ed 100644 --- a/autoload/ale/handlers/redpen.vim +++ b/autoload/ale/handlers/redpen.vim @@ -6,15 +6,18 @@ function! ale#handlers#redpen#HandleRedpenOutput(buffer, lines) abort " element. let l:res = json_decode(join(a:lines))[0] let l:output = [] + for l:err in l:res.errors let l:item = { \ 'text': l:err.message, \ 'type': 'W', \ 'code': l:err.validator, \} + if has_key(l:err, 'startPosition') let l:item.lnum = l:err.startPosition.lineNum let l:item.col = l:err.startPosition.offset + 1 + if has_key(l:err, 'endPosition') let l:item.end_lnum = l:err.endPosition.lineNum let l:item.end_col = l:err.endPosition.offset @@ -28,29 +31,35 @@ function! ale#handlers#redpen#HandleRedpenOutput(buffer, lines) abort " Adjust column number for multibyte string let l:line = getline(l:item.lnum) + if l:line is# '' let l:line = l:err.sentence endif + let l:line = split(l:line, '\zs') if l:item.col >= 2 let l:col = 0 + for l:strlen in map(l:line[0:(l:item.col - 2)], 'strlen(v:val)') let l:col = l:col + l:strlen endfor + let l:item.col = l:col + 1 endif if has_key(l:item, 'end_col') let l:col = 0 + for l:strlen in map(l:line[0:(l:item.end_col - 1)], 'strlen(v:val)') let l:col = l:col + l:strlen endfor + let l:item.end_col = l:col endif call add(l:output, l:item) endfor + return l:output endfunction - diff --git a/autoload/ale/handlers/rubocop.vim b/autoload/ale/handlers/rubocop.vim deleted file mode 100644 index f6367cf5..00000000 --- a/autoload/ale/handlers/rubocop.vim +++ /dev/null @@ -1,6 +0,0 @@ -call ale#Set('ruby_rubocop_options', '') -call ale#Set('ruby_rubocop_executable', 'rubocop') - -function! ale#handlers#rubocop#GetExecutable(buffer) abort - return ale#Var(a:buffer, 'ruby_rubocop_executable') -endfunction diff --git a/autoload/ale/handlers/ruby.vim b/autoload/ale/handlers/ruby.vim index 555c13b1..c28b8b75 100644 --- a/autoload/ale/handlers/ruby.vim +++ b/autoload/ale/handlers/ruby.vim @@ -13,8 +13,10 @@ function! s:HandleSyntaxError(buffer, lines) abort for l:line in a:lines let l:match = matchlist(l:line, l:pattern) + if len(l:match) == 0 let l:match = matchlist(l:line, l:column) + if len(l:match) != 0 let l:output[len(l:output) - 1]['col'] = len(l:match[1]) endif @@ -35,3 +37,10 @@ function! ale#handlers#ruby#HandleSyntaxErrors(buffer, lines) abort return s:HandleSyntaxError(a:buffer, a:lines) endfunction +function! ale#handlers#ruby#EscapeExecutable(executable, bundle_exec) abort + let l:exec_args = a:executable =~? 'bundle' + \ ? ' exec ' . a:bundle_exec + \ : '' + + return ale#Escape(a:executable) . l:exec_args +endfunction diff --git a/autoload/ale/handlers/rust.vim b/autoload/ale/handlers/rust.vim index 537bc731..c6a4b670 100644 --- a/autoload/ale/handlers/rust.vim +++ b/autoload/ale/handlers/rust.vim @@ -7,6 +7,10 @@ if !exists('g:ale_rust_ignore_error_codes') let g:ale_rust_ignore_error_codes = [] endif +if !exists('g:ale_rust_ignore_secondary_spans') + let g:ale_rust_ignore_secondary_spans = 0 +endif + function! s:FindSpan(buffer, span) abort if ale#path#IsBufferPath(a:buffer, a:span.file_name) || a:span.file_name is# '<anon>' return a:span @@ -32,7 +36,7 @@ function! ale#handlers#rust#HandleRustErrors(buffer, lines) abort let l:error = json_decode(l:errorline) - if has_key(l:error, 'message') && type(l:error.message) == type({}) + if has_key(l:error, 'message') && type(l:error.message) is v:t_dict let l:error = l:error.message endif @@ -47,6 +51,10 @@ function! ale#handlers#rust#HandleRustErrors(buffer, lines) abort for l:root_span in l:error.spans let l:span = s:FindSpan(a:buffer, l:root_span) + if ale#Var(a:buffer, 'rust_ignore_secondary_spans') && !get(l:span, 'is_primary', 1) + continue + endif + if !empty(l:span) call add(l:output, { \ 'lnum': l:span.line_start, diff --git a/autoload/ale/handlers/sml.vim b/autoload/ale/handlers/sml.vim index 377eade5..92c5f83b 100644 --- a/autoload/ale/handlers/sml.vim +++ b/autoload/ale/handlers/sml.vim @@ -11,8 +11,10 @@ function! ale#handlers#sml#GetCmFile(buffer) abort let l:as_list = 1 let l:cmfile = '' + for l:path in ale#path#Upwards(expand('#' . a:buffer . ':p:h')) let l:results = glob(l:path . '/' . l:pattern, 0, l:as_list) + if len(l:results) > 0 " If there is more than one CM file, we take the first one " See :help ale-sml-smlnj for how to configure this. @@ -46,6 +48,7 @@ endfunction function! ale#handlers#sml#GetExecutableSmlnjCm(buffer) abort return s:GetExecutable(a:buffer, 'smlnj-cm') endfunction + function! ale#handlers#sml#GetExecutableSmlnjFile(buffer) abort return s:GetExecutable(a:buffer, 'smlnj-file') endfunction @@ -53,7 +56,6 @@ endfunction function! ale#handlers#sml#Handle(buffer, lines) abort " Try to match basic sml errors " TODO(jez) We can get better errorfmt strings from Syntastic - let l:out = [] let l:pattern = '^.*\:\([0-9\.]\+\)\ \(\w\+\)\:\ \(.*\)' let l:pattern2 = '^.*\:\([0-9]\+\)\.\?\([0-9]\+\).* \(\(Warning\|Error\): .*\)' @@ -83,7 +85,6 @@ function! ale#handlers#sml#Handle(buffer, lines) abort \}) continue endif - endfor return l:out diff --git a/autoload/ale/handlers/vale.vim b/autoload/ale/handlers/vale.vim index 9dc0872f..2da72fc7 100644 --- a/autoload/ale/handlers/vale.vim +++ b/autoload/ale/handlers/vale.vim @@ -23,6 +23,7 @@ function! ale#handlers#vale#Handle(buffer, lines) abort endif let l:output = [] + for l:error in l:errors[keys(l:errors)[0]] call add(l:output, { \ 'lnum': l:error['Line'], diff --git a/autoload/ale/hover.vim b/autoload/ale/hover.vim index 5e97e16e..69db276e 100644 --- a/autoload/ale/hover.vim +++ b/autoload/ale/hover.vim @@ -63,19 +63,19 @@ function! ale#hover#HandleLSPResponse(conn_id, response) abort let l:result = l:result.contents - if type(l:result) is type('') + if type(l:result) is v:t_string " The result can be just a string. let l:result = [l:result] endif - if type(l:result) is type({}) + if type(l:result) is v:t_dict " If the result is an object, then it's markup content. let l:result = [l:result.value] endif - if type(l:result) is type([]) + if type(l:result) is v:t_list " Replace objects with text values. - call map(l:result, 'type(v:val) is type('''') ? v:val : v:val.value') + call map(l:result, 'type(v:val) is v:t_string ? v:val : v:val.value') let l:str = join(l:result, "\n") let l:str = substitute(l:str, '^\s*\(.\{-}\)\s*$', '\1', '') @@ -92,7 +92,44 @@ function! ale#hover#HandleLSPResponse(conn_id, response) abort endif endfunction -function! s:ShowDetails(linter, buffer, line, column, opt) abort +function! s:OnReady(linter, lsp_details, line, column, opt, ...) abort + let l:buffer = a:lsp_details.buffer + let l:id = a:lsp_details.connection_id + + let l:Callback = a:linter.lsp is# 'tsserver' + \ ? function('ale#hover#HandleTSServerResponse') + \ : function('ale#hover#HandleLSPResponse') + call ale#lsp#RegisterCallback(l:id, l:Callback) + + if a:linter.lsp is# 'tsserver' + let l:column = a:column + + let l:message = ale#lsp#tsserver_message#Quickinfo( + \ l:buffer, + \ a:line, + \ l:column + \) + else + " Send a message saying the buffer has changed first, or the + " 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:message = ale#lsp#message#Hover(l:buffer, a:line, l:column) + endif + + let l:request_id = ale#lsp#Send(l:id, l:message) + + let s:hover_map[l:request_id] = { + \ 'buffer': l:buffer, + \ 'line': a:line, + \ 'column': l:column, + \ 'hover_from_balloonexpr': get(a:opt, 'called_from_balloonexpr', 0), + \} +endfunction + +function! s:ShowDetails(linter, buffer, line, column, opt, ...) abort let l:lsp_details = ale#lsp_linter#StartLSP(a:buffer, a:linter) if empty(l:lsp_details) @@ -100,44 +137,10 @@ function! s:ShowDetails(linter, buffer, line, column, opt) abort endif let l:id = l:lsp_details.connection_id - let l:root = l:lsp_details.project_root - let l:language_id = l:lsp_details.language_id - - function! OnReady(...) abort closure - let l:Callback = a:linter.lsp is# 'tsserver' - \ ? function('ale#hover#HandleTSServerResponse') - \ : function('ale#hover#HandleLSPResponse') - call ale#lsp#RegisterCallback(l:id, l:Callback) - - if a:linter.lsp is# 'tsserver' - let l:column = a:column - - let l:message = ale#lsp#tsserver_message#Quickinfo( - \ a:buffer, - \ a:line, - \ l:column - \) - else - " Send a message saying the buffer has changed first, or the - " hover position probably won't make sense. - call ale#lsp#NotifyForChanges(l:id, l:root, a:buffer) - - let l:column = min([a:column, len(getbufline(a:buffer, a:line)[0])]) - - let l:message = ale#lsp#message#Hover(a:buffer, a:line, l:column) - endif - - let l:request_id = ale#lsp#Send(l:id, l:message, l:lsp_details.project_root) - - let s:hover_map[l:request_id] = { - \ 'buffer': a:buffer, - \ 'line': a:line, - \ 'column': l:column, - \ 'hover_from_balloonexpr': get(a:opt, 'called_from_balloonexpr', 0), - \} - endfunction - call ale#lsp#WaitForCapability(l:id, l:root, 'hover', function('OnReady')) + call ale#lsp#WaitForCapability(l:id, 'hover', function('s:OnReady', [ + \ a:linter, l:lsp_details, a:line, a:column, a:opt + \])) endfunction " Obtain Hover information for the specified position diff --git a/autoload/ale/java.vim b/autoload/ale/java.vim new file mode 100644 index 00000000..b7fd10bd --- /dev/null +++ b/autoload/ale/java.vim @@ -0,0 +1,20 @@ +" Author: Horacio Sanson https://github.com/hsanson +" Description: Functions for integrating with Java tools + +" Find the nearest dir contining a gradle or pom file and asume it +" the root of a java app. +function! ale#java#FindProjectRoot(buffer) abort + let l:gradle_root = ale#gradle#FindProjectRoot(a:buffer) + + if !empty(l:gradle_root) + return l:gradle_root + endif + + let l:maven_pom_file = ale#path#FindNearestFile(a:buffer, 'pom.xml') + + if !empty(l:maven_pom_file) + return fnamemodify(l:maven_pom_file, ':h') + endif + + return '' +endfunction diff --git a/autoload/ale/job.vim b/autoload/ale/job.vim index e0266cba..0117c7dd 100644 --- a/autoload/ale/job.vim +++ b/autoload/ale/job.vim @@ -249,6 +249,11 @@ function! ale#job#Start(command, options) abort let l:job_options.exit_cb = function('s:VimExitCallback') endif + " Use non-blocking writes for Vim versions that support the option. + if has('patch-8.1.350') + let l:job_options.noblock = 1 + endif + " Vim 8 will read the stdin from the file's buffer. let l:job_info.job = job_start(a:command, l:job_options) let l:job_id = ale#job#ParseVim8ProcessID(string(l:job_info.job)) @@ -278,11 +283,13 @@ function! ale#job#IsRunning(job_id) abort try " In NeoVim, if the job isn't running, jobpid() will throw. call jobpid(a:job_id) + return 1 catch endtry elseif has_key(s:job_map, a:job_id) let l:job = s:job_map[a:job_id].job + return job_status(l:job) is# 'run' endif diff --git a/autoload/ale/julia.vim b/autoload/ale/julia.vim new file mode 100644 index 00000000..18dd9ad7 --- /dev/null +++ b/autoload/ale/julia.vim @@ -0,0 +1,19 @@ +" Author: Bartolomeo Stellato bartolomeo.stellato@gmail.com +" Description: Functions for integrating with Julia tools + +" Find the nearest dir containing a julia project +let s:__ale_julia_project_filenames = ['REQUIRE', 'Manifest.toml', 'Project.toml'] + +function! ale#julia#FindProjectRoot(buffer) abort + for l:project_filename in s:__ale_julia_project_filenames + let l:full_path = ale#path#FindNearestFile(a:buffer, l:project_filename) + + if !empty(l:full_path) + let l:path = fnamemodify(l:full_path, ':p:h') + + return l:path + endif + endfor + + return '' +endfunction diff --git a/autoload/ale/linter.vim b/autoload/ale/linter.vim index aa602f7e..1cbc9ffe 100644 --- a/autoload/ale/linter.vim +++ b/autoload/ale/linter.vim @@ -16,6 +16,7 @@ let s:default_ale_linter_aliases = { \ 'systemverilog': 'verilog', \ 'verilog_systemverilog': ['verilog_systemverilog', 'verilog'], \ 'vimwiki': 'markdown', +\ 'vue': ['vue', 'javascript'], \ 'zsh': 'sh', \} @@ -26,17 +27,22 @@ let s:default_ale_linter_aliases = { " " Only cargo is 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. " " NOTE: Update the g:ale_linters documentation when modifying this. let s:default_ale_linters = { \ 'csh': ['shell'], +\ 'elixir': ['credo', 'dialyxir', 'dogma', 'elixir-ls'], \ 'go': ['gofmt', 'golint', 'go vet'], +\ 'hack': ['hack'], \ 'help': [], \ 'perl': ['perlcritic'], +\ 'perl6': [], \ 'python': ['flake8', 'mypy', 'pylint'], \ 'rust': ['cargo'], \ 'spec': [], \ 'text': [], +\ 'vue': ['eslint', 'vls'], \ 'zsh': ['shell'], \} @@ -51,17 +57,17 @@ endfunction " Do not call this function. function! ale#linter#GetLintersLoaded() abort " This command will throw from the sandbox. - let &equalprg=&equalprg + let &l:equalprg=&l:equalprg return s:linters endfunction function! s:IsCallback(value) abort - return type(a:value) == type('') || type(a:value) == type(function('type')) + return type(a:value) is v:t_string || type(a:value) is v:t_func endfunction function! s:IsBoolean(value) abort - return type(a:value) == type(0) && (a:value == 0 || a:value == 1) + return type(a:value) is v:t_number && (a:value == 0 || a:value == 1) endfunction function! s:LanguageGetter(buffer) dict abort @@ -69,7 +75,7 @@ function! s:LanguageGetter(buffer) dict abort endfunction function! ale#linter#PreProcess(filetype, linter) abort - if type(a:linter) != type({}) + if type(a:linter) isnot v:t_dict throw 'The linter object must be a Dictionary' endif @@ -79,7 +85,7 @@ function! ale#linter#PreProcess(filetype, linter) abort \ 'lsp': get(a:linter, 'lsp', ''), \} - if type(l:obj.name) != type('') + if type(l:obj.name) isnot v:t_string throw '`name` must be defined to name the linter' endif @@ -97,7 +103,7 @@ function! ale#linter#PreProcess(filetype, linter) abort endif if index(['', 'socket', 'stdio', 'tsserver'], l:obj.lsp) < 0 - throw '`lsp` must be either `''lsp''` or `''tsserver''` if defined' + throw '`lsp` must be either `''lsp''`, `''stdio''`, `''socket''` or `''tsserver''` if defined' endif if !l:needs_executable @@ -114,7 +120,7 @@ function! ale#linter#PreProcess(filetype, linter) abort elseif has_key(a:linter, 'executable') let l:obj.executable = a:linter.executable - if type(l:obj.executable) != type('') + if type(l:obj.executable) isnot v:t_string throw '`executable` must be a string if defined' endif else @@ -130,7 +136,7 @@ function! ale#linter#PreProcess(filetype, linter) abort elseif has_key(a:linter, 'command_chain') let l:obj.command_chain = a:linter.command_chain - if type(l:obj.command_chain) != type([]) + if type(l:obj.command_chain) isnot v:t_list throw '`command_chain` must be a List' endif @@ -148,7 +154,7 @@ function! ale#linter#PreProcess(filetype, linter) abort endif if has_key(l:link, 'output_stream') - if type(l:link.output_stream) != type('') + 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'" @@ -170,7 +176,7 @@ function! ale#linter#PreProcess(filetype, linter) abort elseif has_key(a:linter, 'command') let l:obj.command = a:linter.command - if type(l:obj.command) != type('') + if type(l:obj.command) isnot v:t_string throw '`command` must be a string if defined' endif else @@ -217,7 +223,7 @@ function! ale#linter#PreProcess(filetype, linter) abort " Default to using the filetype as the language. let l:obj.language = get(a:linter, 'language', a:filetype) - if type(l:obj.language) != type('') + if type(l:obj.language) isnot v:t_string throw '`language` must be a string' endif @@ -253,11 +259,29 @@ function! ale#linter#PreProcess(filetype, linter) abort elseif has_key(a:linter, 'initialization_options') let l:obj.initialization_options = a:linter.initialization_options 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 type(a:linter.lsp_config) isnot v:t_dict + throw '`lsp_config` must be a Dictionary' + endif + + let l:obj.lsp_config = a:linter.lsp_config + endif endif let l:obj.output_stream = get(a:linter, 'output_stream', 'stdout') - if type(l:obj.output_stream) != type('') + if type(l:obj.output_stream) isnot v:t_string \|| index(['stdout', 'stderr', 'both'], l:obj.output_stream) < 0 throw "`output_stream` must be 'stdout', 'stderr', or 'both'" endif @@ -283,8 +307,8 @@ function! ale#linter#PreProcess(filetype, linter) abort let l:obj.aliases = get(a:linter, 'aliases', []) - if type(l:obj.aliases) != type([]) - \|| len(filter(copy(l:obj.aliases), 'type(v:val) != type('''')')) > 0 + if type(l:obj.aliases) isnot v:t_list + \|| len(filter(copy(l:obj.aliases), 'type(v:val) isnot v:t_string')) > 0 throw '`aliases` must be a List of String values' endif @@ -293,7 +317,7 @@ endfunction function! ale#linter#Define(filetype, linter) abort " This command will throw from the sandbox. - let &equalprg=&equalprg + let &l:equalprg=&l:equalprg if !has_key(s:linters, a:filetype) let s:linters[a:filetype] = [] @@ -335,8 +359,9 @@ endfunction function! s:GetAliasedFiletype(original_filetype) abort let l:buffer_aliases = get(b:, 'ale_linter_aliases', {}) - " b:ale_linter_aliases can be set to a List. - if type(l:buffer_aliases) is type([]) + " b:ale_linter_aliases can be set to a List or String. + if type(l:buffer_aliases) is v:t_list + \|| type(l:buffer_aliases) is v:t_string return l:buffer_aliases endif @@ -360,7 +385,7 @@ endfunction function! ale#linter#ResolveFiletype(original_filetype) abort let l:filetype = s:GetAliasedFiletype(a:original_filetype) - if type(l:filetype) != type([]) + if type(l:filetype) isnot v:t_list return [l:filetype] endif @@ -376,7 +401,7 @@ function! s:GetLinterNames(original_filetype) abort endif " b:ale_linters can be set to a List. - if type(l:buffer_ale_linters) is type([]) + if type(l:buffer_ale_linters) is v:t_list return l:buffer_ale_linters endif @@ -414,9 +439,9 @@ function! ale#linter#Get(original_filetypes) abort let l:all_linters = ale#linter#GetAll(l:filetype) let l:filetype_linters = [] - if type(l:linter_names) == type('') && l:linter_names is# 'all' + if type(l:linter_names) is v:t_string && l:linter_names is# 'all' let l:filetype_linters = l:all_linters - elseif type(l:linter_names) == type([]) + elseif type(l:linter_names) is v:t_list " Select only the linters we or the user has specified. for l:linter in l:all_linters let l:name_list = [l:linter.name] + l:linter.aliases diff --git a/autoload/ale/list.vim b/autoload/ale/list.vim index 35304a09..3417575c 100644 --- a/autoload/ale/list.vim +++ b/autoload/ale/list.vim @@ -25,6 +25,7 @@ function! ale#list#IsQuickfixOpen() abort return 1 endif endfor + return 0 endfunction @@ -112,9 +113,11 @@ function! s:SetListsImpl(timer_id, buffer, loclist) abort " open windows vertically instead of default horizontally let l:open_type = '' + if ale#Var(a:buffer, 'list_vertical') == 1 let l:open_type = 'vert ' endif + if g:ale_set_quickfix if !ale#list#IsQuickfixOpen() silent! execute l:open_type . 'copen ' . str2nr(ale#Var(a:buffer, 'list_window_size')) diff --git a/autoload/ale/loclist_jumping.vim b/autoload/ale/loclist_jumping.vim index 7ed9e6ba..fd5ff922 100644 --- a/autoload/ale/loclist_jumping.vim +++ b/autoload/ale/loclist_jumping.vim @@ -66,6 +66,7 @@ function! ale#loclist_jumping#Jump(direction, wrap) abort let l:nearest = ale#loclist_jumping#FindNearest(a:direction, a:wrap) if !empty(l:nearest) + normal! m` call cursor(l:nearest) endif endfunction @@ -82,6 +83,7 @@ function! ale#loclist_jumping#JumpToIndex(index) abort let l:item = l:loclist[a:index] if !empty(l:item) + normal! m` call cursor([l:item.lnum, l:item.col]) endif endfunction diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index 312319ab..f55096c2 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -1,62 +1,70 @@ " Author: w0rp <devw0rp@gmail.com> " Description: Language Server Protocol client code -" A List of connections, used for tracking servers which have been connected -" to, and programs which are run. -let s:connections = get(s:, 'connections', []) +" A Dictionary for tracking connections. +let s:connections = get(s:, 'connections', {}) let g:ale_lsp_next_message_id = 1 -" Exposed only so tests can get at it. -" Do not call this function basically anywhere. -function! ale#lsp#NewConnection(initialization_options) abort - " id: The job ID as a Number, or the server address as a string. - " data: The message data received so far. - " executable: An executable only set for program connections. - " open_documents: A Dictionary mapping buffers to b:changedtick, keeping - " track of when documents were opened, and when we last changed them. - " callback_list: A list of callbacks for handling LSP responses. - " initialization_options: Options to send to the server. - " capabilities: Features the server supports. - let l:conn = { - \ 'is_tsserver': 0, - \ 'id': '', - \ 'data': '', - \ 'projects': {}, - \ 'open_documents': {}, - \ 'callback_list': [], - \ 'initialization_options': a:initialization_options, - \ 'capabilities': { - \ 'hover': 0, - \ 'references': 0, - \ 'completion': 0, - \ 'completion_trigger_characters': [], - \ 'definition': 0, - \ }, - \} - - call add(s:connections, l:conn) +" Given an id, which can be an executable or address, and a project path, +" create a new connection if needed. Return a unique ID for the connection. +function! ale#lsp#Register(executable_or_address, project, init_options) abort + let l:conn_id = a:executable_or_address . ':' . a:project + + if !has_key(s:connections, l:conn_id) + " is_tsserver: 1 if the connection is for tsserver. + " data: The message data received so far. + " root: The project root. + " open_documents: A Dictionary mapping buffers to b:changedtick, keeping + " track of when documents were opened, and when we last changed them. + " initialized: 0 if the connection is ready, 1 otherwise. + " init_request_id: The ID for the init request. + " init_options: Options to send to the server. + " config: Configuration settings to send to the server. + " callback_list: A list of callbacks for handling LSP responses. + " message_queue: Messages queued for sending to callbacks. + " capabilities_queue: The list of callbacks to call with capabilities. + " capabilities: Features the server supports. + let s:connections[l:conn_id] = { + \ 'id': l:conn_id, + \ 'is_tsserver': 0, + \ 'data': '', + \ 'root': a:project, + \ 'open_documents': {}, + \ 'initialized': 0, + \ 'init_request_id': 0, + \ 'init_options': a:init_options, + \ 'config': {}, + \ 'callback_list': [], + \ 'message_queue': [], + \ 'capabilities_queue': [], + \ 'capabilities': { + \ 'hover': 0, + \ 'references': 0, + \ 'completion': 0, + \ 'completion_trigger_characters': [], + \ 'definition': 0, + \ 'symbol_search': 0, + \ }, + \} + endif - return l:conn + return l:conn_id endfunction " Remove an LSP connection with a given ID. This is only for tests. function! ale#lsp#RemoveConnectionWithID(id) abort - call filter(s:connections, 'v:val.id isnot a:id') + if has_key(s:connections, a:id) + call remove(s:connections, a:id) + endif endfunction -function! s:FindConnection(key, value) abort - for l:conn in s:connections - if has_key(l:conn, a:key) && get(l:conn, a:key) is# a:value - return l:conn - endif - endfor - - return {} -endfunction +" This is only needed for tests +function! ale#lsp#MarkDocumentAsOpen(id, buffer) abort + let l:conn = get(s:connections, a:id, {}) -" Get the capabilities for a connection, or an empty Dictionary. -function! ale#lsp#GetConnectionCapabilities(id) abort - return get(s:FindConnection('id', a:id), 'capabilities', {}) + if !empty(l:conn) + let l:conn.open_documents[a:buffer] = -1 + endif endfunction function! ale#lsp#GetNextMessageID() abort @@ -94,13 +102,14 @@ function! s:CreateTSServerMessageData(message) abort endif let l:data = json_encode(l:obj) . "\n" + return [l:is_notification ? 0 : l:obj.seq, l:data] endfunction " Given a List of one or two items, [method_name] or [method_name, params], " return a List containing [message_id, message_data] function! ale#lsp#CreateMessageData(message) abort - if a:message[1] =~# '^ts@' + if a:message[1][:2] is# 'ts@' return s:CreateTSServerMessageData(a:message) endif @@ -167,53 +176,10 @@ function! ale#lsp#ReadMessageData(data) abort return [l:remainder, l:response_list] endfunction -function! s:FindProjectWithInitRequestID(conn, init_request_id) abort - for l:project_root in keys(a:conn.projects) - let l:project = a:conn.projects[l:project_root] - - if l:project.init_request_id == a:init_request_id - return l:project - endif - endfor - - return {} -endfunction - -function! s:MarkProjectAsInitialized(conn, project) abort - let a:project.initialized = 1 - - " After the server starts, send messages we had queued previously. - for l:message_data in a:project.message_queue - call s:SendMessageData(a:conn, l:message_data) - endfor - - " Remove the messages now. - let a:conn.message_queue = [] - - " Call capabilities callbacks queued for the project. - for [l:capability, l:Callback] in a:project.capabilities_queue - if a:conn.is_tsserver || a:conn.capabilities[l:capability] - call call(l:Callback, [a:conn.id, a:project.root]) - endif - endfor - - " Clear the queued callbacks now. - let a:project.capabilities_queue = [] -endfunction - -function! s:HandleInitializeResponse(conn, response) abort - let l:request_id = a:response.request_id - let l:project = s:FindProjectWithInitRequestID(a:conn, l:request_id) - - if !empty(l:project) - call s:MarkProjectAsInitialized(a:conn, l:project) - endif -endfunction - " Update capabilities from the server, so we know which features the server " supports. function! s:UpdateCapabilities(conn, capabilities) abort - if type(a:capabilities) != type({}) + if type(a:capabilities) isnot v:t_dict return endif @@ -229,10 +195,10 @@ function! s:UpdateCapabilities(conn, capabilities) abort let a:conn.capabilities.completion = 1 endif - if type(get(a:capabilities, 'completionProvider')) is type({}) + if type(get(a:capabilities, 'completionProvider')) is v:t_dict let l:chars = get(a:capabilities.completionProvider, 'triggerCharacters') - if type(l:chars) is type([]) + if type(l:chars) is v:t_list let a:conn.capabilities.completion_trigger_characters = l:chars endif endif @@ -240,180 +206,164 @@ function! s:UpdateCapabilities(conn, capabilities) abort if get(a:capabilities, 'definitionProvider') is v:true let a:conn.capabilities.definition = 1 endif -endfunction -function! ale#lsp#HandleOtherInitializeResponses(conn, response) abort - let l:uninitialized_projects = [] + if get(a:capabilities, 'workspaceSymbolProvider') is v:true + let a:conn.capabilities.symbol_search = 1 + endif +endfunction - for [l:key, l:value] in items(a:conn.projects) - if l:value.initialized == 0 - call add(l:uninitialized_projects, [l:key, l:value]) - endif - endfor +" Update a connection's configuration dictionary and notify LSP servers +" of any changes since the last update. Returns 1 if a configuration +" update was sent; otherwise 0 will be returned. +function! ale#lsp#UpdateConfig(conn_id, buffer, config) abort + let l:conn = get(s:connections, a:conn_id, {}) - if empty(l:uninitialized_projects) - return + if empty(l:conn) || a:config ==# l:conn.config " no-custom-checks + return 0 endif - if get(a:response, 'method', '') is# '' - if has_key(get(a:response, 'result', {}), 'capabilities') - call s:UpdateCapabilities(a:conn, a:response.result.capabilities) + let l:conn.config = a:config + let l:message = ale#lsp#message#DidChangeConfiguration(a:buffer, a:config) - for [l:dir, l:project] in l:uninitialized_projects - call s:MarkProjectAsInitialized(a:conn, l:project) - endfor - endif - elseif get(a:response, 'method', '') is# 'textDocument/publishDiagnostics' - let l:filename = ale#path#FromURI(a:response.params.uri) + call ale#lsp#Send(a:conn_id, l:message) - for [l:dir, l:project] in l:uninitialized_projects - if l:filename[:len(l:dir) - 1] is# l:dir - call s:MarkProjectAsInitialized(a:conn, l:project) - endif - endfor - endif + return 1 endfunction -function! ale#lsp#HandleMessage(conn, message) abort - if type(a:message) != type('') - " Ignore messages that aren't strings. - return + +function! ale#lsp#HandleInitResponse(conn, response) abort + if get(a:response, 'method', '') is# 'initialize' + let a:conn.initialized = 1 + elseif type(get(a:response, 'result')) is v:t_dict + \&& has_key(a:response.result, 'capabilities') + call s:UpdateCapabilities(a:conn, a:response.result.capabilities) + + let a:conn.initialized = 1 endif - let a:conn.data .= a:message + if !a:conn.initialized + return + endif - " Parse the objects now if we can, and keep the remaining text. - let [a:conn.data, l:response_list] = ale#lsp#ReadMessageData(a:conn.data) + " After the server starts, send messages we had queued previously. + for l:message_data in a:conn.message_queue + call s:SendMessageData(a:conn, l:message_data) + endfor - " Call our callbacks. - for l:response in l:response_list - if get(l:response, 'method', '') is# 'initialize' - call s:HandleInitializeResponse(a:conn, l:response) - else - call ale#lsp#HandleOtherInitializeResponses(a:conn, l:response) + " Remove the messages now. + let a:conn.message_queue = [] - " Call all of the registered handlers with the response. - for l:Callback in a:conn.callback_list - call ale#util#GetFunction(l:Callback)(a:conn.id, l:response) - endfor + " Call capabilities callbacks queued for the project. + for [l:capability, l:Callback] in a:conn.capabilities_queue + if a:conn.capabilities[l:capability] + call call(l:Callback, [a:conn.id]) endif endfor + + let a:conn.capabilities_queue = [] endfunction -function! s:HandleChannelMessage(channel_id, message) abort - let l:address = ale#socket#GetAddress(a:channel_id) - let l:conn = s:FindConnection('id', l:address) +function! ale#lsp#HandleMessage(conn_id, message) abort + let l:conn = get(s:connections, a:conn_id, {}) - call ale#lsp#HandleMessage(l:conn, a:message) -endfunction + if empty(l:conn) + return + endif -function! s:HandleCommandMessage(job_id, message) abort - let l:conn = s:FindConnection('id', a:job_id) + if type(a:message) isnot v:t_string + " Ignore messages that aren't strings. + return + endif - call ale#lsp#HandleMessage(l:conn, a:message) -endfunction + let l:conn.data .= a:message -" Given a connection ID, mark it as a tsserver connection, so it will be -" handled that way. -function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort - let l:conn = s:FindConnection('id', a:conn_id) + " Parse the objects now if we can, and keep the remaining text. + let [l:conn.data, l:response_list] = ale#lsp#ReadMessageData(l:conn.data) - if !empty(l:conn) - let l:conn.is_tsserver = 1 + " Look for initialize responses first. + if !l:conn.initialized + for l:response in l:response_list + call ale#lsp#HandleInitResponse(l:conn, l:response) + endfor endif -endfunction -" Register a project for an LSP connection. -" -" This function will throw if the connection doesn't exist. -function! ale#lsp#RegisterProject(conn_id, project_root) abort - let l:conn = s:FindConnection('id', a:conn_id) - - " Empty strings can't be used for Dictionary keys in NeoVim, due to E713. - " This appears to be a nonsensical bug in NeoVim. - let l:key = empty(a:project_root) ? '<<EMPTY>>' : a:project_root - - if !has_key(l:conn.projects, l:key) - " Tools without project roots are ready right away, like tsserver. - let l:conn.projects[l:key] = { - \ 'root': a:project_root, - \ 'initialized': empty(a:project_root), - \ 'init_request_id': 0, - \ 'message_queue': [], - \ 'capabilities_queue': [], - \} + " If the connection is marked as initialized, call the callbacks with the + " responses. + if l:conn.initialized + for l:response in l:response_list + " Call all of the registered handlers with the response. + for l:Callback in l:conn.callback_list + call ale#util#GetFunction(l:Callback)(a:conn_id, l:response) + endfor + endfor endif endfunction -function! ale#lsp#GetProject(conn, project_root) abort - if empty(a:conn) - return {} - endif - - let l:key = empty(a:project_root) ? '<<EMPTY>>' : a:project_root - - return get(a:conn.projects, l:key, {}) +" Given a connection ID, mark it as a tsserver connection, so it will be +" handled that way. +function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort + let l:conn = s:connections[a:conn_id] + let l:conn.is_tsserver = 1 + let l:conn.initialized = 1 + " Set capabilities which are supported by tsserver. + let l:conn.capabilities.hover = 1 + let l:conn.capabilities.references = 1 + let l:conn.capabilities.completion = 1 + let l:conn.capabilities.completion_trigger_characters = ['.'] + let l:conn.capabilities.definition = 1 + let l:conn.capabilities.symbol_search = 1 endfunction -" Start a program for LSP servers which run with executables. +" Start a program for LSP servers. " -" The job ID will be returned for for the program if it ran, otherwise -" 0 will be returned. -function! ale#lsp#StartProgram(executable, command, init_options) abort - if !executable(a:executable) - return 0 - endif - - let l:conn = s:FindConnection('executable', a:executable) +" 1 will be returned if the program is running, or 0 if the program could +" not be started. +function! ale#lsp#StartProgram(conn_id, executable, command) abort + let l:conn = s:connections[a:conn_id] - " Get the current connection or a new one. - let l:conn = !empty(l:conn) ? l:conn : ale#lsp#NewConnection(a:init_options) - let l:conn.executable = a:executable - - if !has_key(l:conn, 'id') || !ale#job#IsRunning(l:conn.id) + if !has_key(l:conn, 'job_id') || !ale#job#IsRunning(l:conn.job_id) let l:options = { \ 'mode': 'raw', - \ 'out_cb': function('s:HandleCommandMessage'), + \ 'out_cb': {_, message -> ale#lsp#HandleMessage(a:conn_id, message)}, \} let l:job_id = ale#job#Start(a:command, l:options) else - let l:job_id = l:conn.id + let l:job_id = l:conn.job_id endif - if l:job_id <= 0 - return 0 + if l:job_id > 0 + let l:conn.job_id = l:job_id endif - let l:conn.id = l:job_id - - return l:job_id + return l:job_id > 0 endfunction -" Connect to an address and set up a callback for handling responses. -function! ale#lsp#ConnectToAddress(address, init_options) abort - let l:conn = s:FindConnection('id', a:address) - " Get the current connection or a new one. - let l:conn = !empty(l:conn) ? l:conn : ale#lsp#NewConnection(a:init_options) +" Connect to an LSP server via TCP. +" +" 1 will be returned if the connection is running, or 0 if the connection could +" not be opened. +function! ale#lsp#ConnectToAddress(conn_id, address) abort + let l:conn = s:connections[a:conn_id] if !has_key(l:conn, 'channel_id') || !ale#socket#IsOpen(l:conn.channel_id) - let l:conn.channel_id = ale#socket#Open(a:address, { - \ 'callback': function('s:HandleChannelMessage'), + let l:channel_id = ale#socket#Open(a:address, { + \ 'callback': {_, mess -> ale#lsp#HandleMessage(a:conn_id, mess)}, \}) + else + let l:channel_id = l:conn.channel_id endif - if l:conn.channel_id < 0 - return '' + if l:channel_id >= 0 + let l:conn.channel_id = l:channel_id endif - let l:conn.id = a:address - - return a:address + return l:channel_id >= 0 endfunction " Given a connection ID and a callback, register that callback for handling " messages if the connection exists. function! ale#lsp#RegisterCallback(conn_id, callback) abort - let l:conn = s:FindConnection('id', a:conn_id) + let l:conn = get(s:connections, a:conn_id, {}) if !empty(l:conn) " Add the callback to the List if it's not there already. @@ -421,23 +371,33 @@ function! ale#lsp#RegisterCallback(conn_id, callback) abort endif endfunction -" Stop all LSP connections, closing all jobs and channels, and removing any -" queued messages. -function! ale#lsp#StopAll() abort - for l:conn in s:connections +" Stop a single LSP connection. +function! ale#lsp#Stop(conn_id) abort + if has_key(s:connections, a:conn_id) + let l:conn = remove(s:connections, a:conn_id) + if has_key(l:conn, 'channel_id') call ale#socket#Close(l:conn.channel_id) - else - call ale#job#Stop(l:conn.id) + elseif has_key(l:conn, 'job_id') + call ale#job#Stop(l:conn.job_id) endif - endfor + endif +endfunction - let s:connections = [] +function! ale#lsp#CloseDocument(conn_id) abort +endfunction + +" Stop all LSP connections, closing all jobs and channels, and removing any +" queued messages. +function! ale#lsp#StopAll() abort + for l:conn_id in keys(s:connections) + call ale#lsp#Stop(l:conn_id) + endfor endfunction function! s:SendMessageData(conn, data) abort - if has_key(a:conn, 'executable') - call ale#job#SendRaw(a:conn.id, a:data) + if has_key(a:conn, 'job_id') + call ale#job#SendRaw(a:conn.job_id, a:data) elseif has_key(a:conn, 'channel_id') && ale#socket#IsOpen(a:conn.channel_id) " Send the message to the server call ale#socket#Send(a:conn.channel_id, a:data) @@ -454,38 +414,32 @@ endfunction " Returns -1 when a message is sent, but no response is expected " 0 when the message is not sent and " >= 1 with the message ID when a response is expected. -function! ale#lsp#Send(conn_id, message, ...) abort - let l:project_root = get(a:000, 0, '') +function! ale#lsp#Send(conn_id, message) abort + let l:conn = get(s:connections, a:conn_id, {}) - let l:conn = s:FindConnection('id', a:conn_id) - let l:project = ale#lsp#GetProject(l:conn, l:project_root) - - if empty(l:project) + if empty(l:conn) return 0 endif " If we haven't initialized the server yet, then send the message for it. - if !l:project.initialized - " Only send the init message once. - if !l:project.init_request_id - let [l:init_id, l:init_data] = ale#lsp#CreateMessageData( - \ ale#lsp#message#Initialize(l:project_root, l:conn.initialization_options), - \) + if !l:conn.initialized && !l:conn.init_request_id + let [l:init_id, l:init_data] = ale#lsp#CreateMessageData( + \ ale#lsp#message#Initialize(l:conn.root, l:conn.init_options), + \) - let l:project.init_request_id = l:init_id + let l:conn.init_request_id = l:init_id - call s:SendMessageData(l:conn, l:init_data) - endif + call s:SendMessageData(l:conn, l:init_data) endif let [l:id, l:data] = ale#lsp#CreateMessageData(a:message) - if l:project.initialized + if l:conn.initialized " Send the message now. call s:SendMessageData(l:conn, l:data) else " Add the message we wanted to send to a List to send later. - call add(l:project.message_queue, l:data) + call add(l:conn.message_queue, l:data) endif return l:id == 0 ? -1 : l:id @@ -493,11 +447,10 @@ endfunction " Notify LSP servers or tsserver if a document is opened, if needed. " If a document is opened, 1 will be returned, otherwise 0 will be returned. -function! ale#lsp#OpenDocument(conn_id, project_root, buffer, language_id) abort - let l:conn = s:FindConnection('id', a:conn_id) +function! ale#lsp#OpenDocument(conn_id, buffer, language_id) abort + let l:conn = get(s:connections, a:conn_id, {}) let l:opened = 0 - " FIXME: Return 1 if the document is already open? if !empty(l:conn) && !has_key(l:conn.open_documents, a:buffer) if l:conn.is_tsserver let l:message = ale#lsp#tsserver_message#Open(a:buffer) @@ -505,7 +458,7 @@ function! ale#lsp#OpenDocument(conn_id, project_root, buffer, language_id) abort let l:message = ale#lsp#message#DidOpen(a:buffer, a:language_id) endif - call ale#lsp#Send(a:conn_id, l:message, a:project_root) + call ale#lsp#Send(a:conn_id, l:message) let l:conn.open_documents[a:buffer] = getbufvar(a:buffer, 'changedtick') let l:opened = 1 endif @@ -515,8 +468,8 @@ endfunction " Notify LSP servers or tsserver that a document has changed, if needed. " If a notification is sent, 1 will be returned, otherwise 0 will be returned. -function! ale#lsp#NotifyForChanges(conn_id, project_root, buffer) abort - let l:conn = s:FindConnection('id', a:conn_id) +function! ale#lsp#NotifyForChanges(conn_id, buffer) abort + let l:conn = get(s:connections, a:conn_id, {}) let l:notified = 0 if !empty(l:conn) && has_key(l:conn.open_documents, a:buffer) @@ -529,7 +482,7 @@ function! ale#lsp#NotifyForChanges(conn_id, project_root, buffer) abort let l:message = ale#lsp#message#DidChange(a:buffer) endif - call ale#lsp#Send(a:conn_id, l:message, a:project_root) + call ale#lsp#Send(a:conn_id, l:message) let l:conn.open_documents[a:buffer] = l:new_tick let l:notified = 1 endif @@ -540,25 +493,24 @@ endfunction " Given some LSP details that must contain at least `connection_id` and " `project_root` keys, -function! ale#lsp#WaitForCapability(conn_id, project_root, capability, callback) abort - let l:conn = s:FindConnection('id', a:conn_id) - let l:project = ale#lsp#GetProject(l:conn, a:project_root) +function! ale#lsp#WaitForCapability(conn_id, capability, callback) abort + let l:conn = get(s:connections, a:conn_id, {}) - if empty(l:project) - return 0 + if empty(l:conn) + return endif - if type(get(l:conn.capabilities, a:capability, v:null)) isnot type(0) + if type(get(l:conn.capabilities, a:capability, v:null)) isnot v:t_number throw 'Invalid capability ' . a:capability endif - if l:project.initialized - if l:conn.is_tsserver || l:conn.capabilities[a:capability] + if l:conn.initialized + if l:conn.capabilities[a:capability] " The project has been initialized, so call the callback now. - call call(a:callback, [a:conn_id, a:project_root]) + call call(a:callback, [a:conn_id]) endif else " Call the callback later, once we have the information we need. - call add(l:project.capabilities_queue, [a:capability, a:callback]) + call add(l:conn.capabilities_queue, [a:capability, a:callback]) endif endfunction diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim index 9e05156d..9fffb83a 100644 --- a/autoload/ale/lsp/message.vim +++ b/autoload/ale/lsp/message.vim @@ -130,6 +130,12 @@ function! ale#lsp#message#References(buffer, line, column) abort \}] endfunction +function! ale#lsp#message#Symbol(query) abort + return [0, 'workspace/symbol', { + \ 'query': a:query, + \}] +endfunction + function! ale#lsp#message#Hover(buffer, line, column) abort return [0, 'textDocument/hover', { \ 'textDocument': { @@ -138,3 +144,9 @@ function! ale#lsp#message#Hover(buffer, line, column) abort \ 'position': {'line': a:line - 1, 'character': a:column}, \}] endfunction + +function! ale#lsp#message#DidChangeConfiguration(buffer, config) abort + return [0, 'workspace/didChangeConfiguration', { + \ 'settings': a:config, + \}] +endfunction diff --git a/autoload/ale/lsp/reset.vim b/autoload/ale/lsp/reset.vim index c7c97a47..2fc7f0a2 100644 --- a/autoload/ale/lsp/reset.vim +++ b/autoload/ale/lsp/reset.vim @@ -17,7 +17,7 @@ function! ale#lsp#reset#StopAllLSPs() abort for l:linter in ale#linter#Get(getbufvar(l:buffer, '&filetype')) if !empty(l:linter.lsp) - call ale#engine#HandleLoclist(l:linter.name, l:buffer, []) + call ale#engine#HandleLoclist(l:linter.name, l:buffer, [], 0) endif endfor endfor diff --git a/autoload/ale/lsp/response.vim b/autoload/ale/lsp/response.vim index a0e1984d..08b36808 100644 --- a/autoload/ale/lsp/response.vim +++ b/autoload/ale/lsp/response.vim @@ -47,7 +47,23 @@ function! ale#lsp#response#ReadDiagnostics(response) abort endif if has_key(l:diagnostic, 'code') - let l:loclist_item.nr = l:diagnostic.code + if type(l:diagnostic.code) == v:t_string + let l:loclist_item.code = l:diagnostic.code + elseif type(l:diagnostic.code) == v:t_number && l:diagnostic.code != -1 + let l:loclist_item.code = string(l:diagnostic.code) + let l:loclist_item.nr = l:diagnostic.code + endif + endif + + if has_key(l:diagnostic, 'relatedInformation') + let l:related = deepcopy(l:diagnostic.relatedInformation) + call map(l:related, {key, val -> + \ ale#path#FromURI(val.location.uri) . + \ ':' . (val.location.range.start.line + 1) . + \ ':' . (val.location.range.start.character + 1) . + \ ":\n\t" . val.message + \ }) + let l:loclist_item.detail = l:diagnostic.message . "\n" . join(l:related, "\n") endif if has_key(l:diagnostic, 'source') @@ -74,7 +90,12 @@ function! ale#lsp#response#ReadTSServerDiagnostics(response) abort \} if has_key(l:diagnostic, 'code') - let l:loclist_item.nr = l:diagnostic.code + if type(l:diagnostic.code) == v:t_string + let l:loclist_item.code = l:diagnostic.code + elseif type(l:diagnostic.code) == v:t_number && l:diagnostic.code != -1 + let l:loclist_item.code = string(l:diagnostic.code) + let l:loclist_item.nr = l:diagnostic.code + endif endif if get(l:diagnostic, 'category') is# 'warning' @@ -92,7 +113,7 @@ function! ale#lsp#response#ReadTSServerDiagnostics(response) abort endfunction function! ale#lsp#response#GetErrorMessage(response) abort - if type(get(a:response, 'error', 0)) isnot type({}) + if type(get(a:response, 'error', 0)) isnot v:t_dict return '' endif @@ -112,12 +133,12 @@ function! ale#lsp#response#GetErrorMessage(response) abort " Include the traceback or error data as details, if present. let l:error_data = get(a:response.error, 'data', {}) - if type(l:error_data) is type('') + if type(l:error_data) is v:t_string let l:message .= "\n" . l:error_data - else + elseif type(l:error_data) is v:t_dict let l:traceback = get(l:error_data, 'traceback', []) - if type(l:traceback) is type([]) && !empty(l:traceback) + if type(l:traceback) is v:t_list && !empty(l:traceback) let l:message .= "\n" . join(l:traceback, "\n") endif endif diff --git a/autoload/ale/lsp_linter.vim b/autoload/ale/lsp_linter.vim index 87aee759..42d67398 100644 --- a/autoload/ale/lsp_linter.vim +++ b/autoload/ale/lsp_linter.vim @@ -38,7 +38,7 @@ function! s:HandleLSPDiagnostics(conn_id, response) abort let l:loclist = ale#lsp#response#ReadDiagnostics(a:response) - call ale#engine#HandleLoclist(l:linter_name, l:buffer, l:loclist) + call ale#engine#HandleLoclist(l:linter_name, l:buffer, l:loclist, 0) endfunction function! s:HandleTSServerDiagnostics(response, error_type) abort @@ -55,20 +55,33 @@ function! s:HandleTSServerDiagnostics(response, error_type) abort endif let l:thislist = ale#lsp#response#ReadTSServerDiagnostics(a:response) + let l:no_changes = 0 " tsserver sends syntax and semantic errors in separate messages, so we " have to collect the messages separately for each buffer and join them " back together again. if a:error_type is# 'syntax' + if len(l:thislist) is 0 && len(get(l:info, 'syntax_loclist', [])) is 0 + let l:no_changes = 1 + endif + let l:info.syntax_loclist = l:thislist else + if len(l:thislist) is 0 && len(get(l:info, 'semantic_loclist', [])) is 0 + let l:no_changes = 1 + endif + let l:info.semantic_loclist = l:thislist endif + if l:no_changes + return + endif + let l:loclist = get(l:info, 'semantic_loclist', []) \ + get(l:info, 'syntax_loclist', []) - call ale#engine#HandleLoclist(l:linter_name, l:buffer, l:loclist) + call ale#engine#HandleLoclist(l:linter_name, l:buffer, l:loclist, 0) endfunction function! s:HandleLSPErrorMessage(linter_name, response) abort @@ -99,9 +112,10 @@ endfunction function! ale#lsp_linter#HandleLSPResponse(conn_id, response) abort let l:method = get(a:response, 'method', '') - let l:linter_name = get(s:lsp_linter_map, a:conn_id, '') if get(a:response, 'jsonrpc', '') is# '2.0' && has_key(a:response, 'error') + let l:linter_name = get(s:lsp_linter_map, a:conn_id, '') + call s:HandleLSPErrorMessage(l:linter_name, a:response) elseif l:method is# 'textDocument/publishDiagnostics' call s:HandleLSPDiagnostics(a:conn_id, a:response) @@ -126,6 +140,18 @@ function! ale#lsp_linter#GetOptions(buffer, linter) abort return l:initialization_options endfunction +function! ale#lsp_linter#GetConfig(buffer, linter) abort + let l:config = {} + + if has_key(a:linter, 'lsp_config_callback') + let l:config = ale#util#GetFunction(a:linter.lsp_config_callback)(a:buffer) + elseif has_key(a:linter, 'lsp_config') + let l:config = a:linter.lsp_config + endif + + return l:config +endfunction + " Given a buffer, an LSP linter, start up an LSP linter and get ready to " receive messages for the document. function! ale#lsp_linter#StartLSP(buffer, linter) abort @@ -143,7 +169,8 @@ function! ale#lsp_linter#StartLSP(buffer, linter) abort if a:linter.lsp is# 'socket' let l:address = ale#linter#GetAddress(a:buffer, a:linter) - let l:conn_id = ale#lsp#ConnectToAddress(l:address, l:init_options) + let l:conn_id = ale#lsp#Register(l:address, l:root, l:init_options) + let l:ready = ale#lsp#ConnectToAddress(l:conn_id, l:address) else let l:executable = ale#linter#GetExecutable(a:buffer, a:linter) @@ -151,18 +178,16 @@ function! ale#lsp_linter#StartLSP(buffer, linter) abort return {} endif + let l:conn_id = ale#lsp#Register(l:executable, l:root, l:init_options) + let l:command = ale#linter#GetCommand(a:buffer, a:linter) " Format the command, so %e can be formatted into it. let l:command = ale#command#FormatCommand(a:buffer, l:executable, l:command, 0)[1] let l:command = ale#job#PrepareCommand(a:buffer, l:command) - let l:conn_id = ale#lsp#StartProgram( - \ l:executable, - \ l:command, - \ l:init_options, - \) + let l:ready = ale#lsp#StartProgram(l:conn_id, l:executable, l:command) endif - if empty(l:conn_id) + if !l:ready if g:ale_history_enabled && !empty(l:command) call ale#history#Add(a:buffer, 'failed', l:conn_id, l:command) endif @@ -175,9 +200,7 @@ function! ale#lsp_linter#StartLSP(buffer, linter) abort call ale#lsp#MarkConnectionAsTsserver(l:conn_id) endif - " Register the project now the connection is ready. - call ale#lsp#RegisterProject(l:conn_id, l:root) - + let l:config = ale#lsp_linter#GetConfig(a:buffer, a:linter) let l:language_id = ale#util#GetFunction(a:linter.language_callback)(a:buffer) let l:details = { @@ -188,7 +211,9 @@ function! ale#lsp_linter#StartLSP(buffer, linter) abort \ 'language_id': l:language_id, \} - if ale#lsp#OpenDocument(l:conn_id, l:root, a:buffer, l:language_id) + call ale#lsp#UpdateConfig(l:conn_id, a:buffer, l:config) + + if ale#lsp#OpenDocument(l:conn_id, a:buffer, l:language_id) if g:ale_history_enabled && !empty(l:command) call ale#history#Add(a:buffer, 'started', l:conn_id, l:command) endif @@ -196,7 +221,7 @@ function! ale#lsp_linter#StartLSP(buffer, linter) abort " The change message needs to be sent for tsserver before doing anything. if a:linter.lsp is# 'tsserver' - call ale#lsp#NotifyForChanges(l:conn_id, l:root, a:buffer) + call ale#lsp#NotifyForChanges(l:conn_id, a:buffer) endif return l:details @@ -211,7 +236,6 @@ function! ale#lsp_linter#CheckWithLSP(buffer, linter) abort endif let l:id = l:lsp_details.connection_id - let l:root = l:lsp_details.project_root " Register a callback now for handling errors now. let l:Callback = function('ale#lsp_linter#HandleLSPResponse') @@ -222,16 +246,16 @@ function! ale#lsp_linter#CheckWithLSP(buffer, linter) abort if a:linter.lsp is# 'tsserver' let l:message = ale#lsp#tsserver_message#Geterr(a:buffer) - let l:notified = ale#lsp#Send(l:id, l:message, l:root) != 0 + let l:notified = ale#lsp#Send(l:id, l:message) != 0 else - let l:notified = ale#lsp#NotifyForChanges(l:id, l:root, a:buffer) + let l:notified = ale#lsp#NotifyForChanges(l:id, a:buffer) endif " If this was a file save event, also notify the server of that. if a:linter.lsp isnot# 'tsserver' \&& getbufvar(a:buffer, 'ale_save_event_fired', 0) let l:save_message = ale#lsp#message#DidSave(a:buffer) - let l:notified = ale#lsp#Send(l:id, l:save_message, l:root) != 0 + let l:notified = ale#lsp#Send(l:id, l:save_message) != 0 endif if l:notified diff --git a/autoload/ale/node.vim b/autoload/ale/node.vim index f75280b7..5c579c75 100644 --- a/autoload/ale/node.vim +++ b/autoload/ale/node.vim @@ -23,6 +23,11 @@ function! ale#node#FindExecutable(buffer, base_var_name, path_list) abort return ale#Var(a:buffer, a:base_var_name . '_executable') endfunction +" As above, but curry the arguments so only the buffer number is required. +function! ale#node#FindExecutableFunc(base_var_name, path_list) abort + return {buf -> ale#node#FindExecutable(buf, a:base_var_name, a:path_list)} +endfunction + " Create a executable string which executes a Node.js script command with a " Node.js executable if needed. " diff --git a/autoload/ale/other_source.vim b/autoload/ale/other_source.vim new file mode 100644 index 00000000..1a092034 --- /dev/null +++ b/autoload/ale/other_source.vim @@ -0,0 +1,21 @@ +" Tell ALE that another source has started checking a buffer. +function! ale#other_source#StartChecking(buffer, linter_name) abort + call ale#engine#InitBufferInfo(a:buffer) + let l:list = g:ale_buffer_info[a:buffer].active_other_sources_list + + call add(l:list, a:linter_name) + call uniq(sort(l:list)) +endfunction + +" Show some results, and stop checking a buffer. +" To clear results or cancel checking a buffer, an empty List can be given. +function! ale#other_source#ShowResults(buffer, linter_name, loclist) abort + call ale#engine#InitBufferInfo(a:buffer) + let l:info = g:ale_buffer_info[a:buffer] + + " Remove this linter name from the active list. + let l:list = l:info.active_other_sources_list + call filter(l:list, 'v:val isnot# a:linter_name') + + call ale#engine#HandleLoclist(a:linter_name, a:buffer, a:loclist, 1) +endfunction diff --git a/autoload/ale/path.vim b/autoload/ale/path.vim index 45da3709..89b119f4 100644 --- a/autoload/ale/path.vim +++ b/autoload/ale/path.vim @@ -65,7 +65,11 @@ endfunction " Output 'cd <directory> && ' " This function can be used changing the directory for a linter command. function! ale#path#CdString(directory) abort - return 'cd ' . ale#Escape(a:directory) . ' && ' + if has('win32') + return 'cd /d ' . ale#Escape(a:directory) . ' && ' + else + return 'cd ' . ale#Escape(a:directory) . ' && ' + endif endfunction " Output 'cd <buffer_filename_directory> && ' @@ -105,6 +109,21 @@ function! ale#path#GetAbsPath(base_directory, filename) abort return ale#path#Simplify(a:base_directory . l:sep . a:filename) endfunction +" Given a path, return the directory name for that path, with no trailing +" slashes. If the argument is empty(), return an empty string. +function! ale#path#Dirname(path) abort + if empty(a:path) + return '' + endif + + " For /foo/bar/ we need :h:h to get /foo + if a:path[-1:] is# '/' + return fnamemodify(a:path, ':h:h') + endif + + return fnamemodify(a:path, ':h') +endfunction + " Given a buffer number and a relative or absolute path, return 1 if the " two paths represent the same file on disk. function! ale#path#IsBufferPath(buffer, complex_filename) abort diff --git a/autoload/ale/preview.vim b/autoload/ale/preview.vim index aefbb691..1f50e0ad 100644 --- a/autoload/ale/preview.vim +++ b/autoload/ale/preview.vim @@ -15,13 +15,13 @@ function! ale#preview#Show(lines, ...) abort setlocal modifiable setlocal noreadonly setlocal nobuflisted - let &l:filetype = get(l:options, 'filetype', 'ale-preview') setlocal buftype=nofile setlocal bufhidden=wipe :%d call setline(1, a:lines) setlocal nomodifiable setlocal readonly + let &l:filetype = get(l:options, 'filetype', 'ale-preview') if get(l:options, 'stay_here') wincmd p @@ -46,11 +46,14 @@ function! ale#preview#ShowSelection(item_list) abort " Create lines to display to users. for l:item in a:item_list + let l:match = get(l:item, 'match', '') + call add( \ l:lines, \ l:item.filename \ . ':' . l:item.line - \ . ':' . l:item.column, + \ . ':' . l:item.column + \ . (!empty(l:match) ? ' ' . l:match : ''), \) endfor diff --git a/autoload/ale/python.vim b/autoload/ale/python.vim index bc1cc980..8d6bf1f0 100644 --- a/autoload/ale/python.vim +++ b/autoload/ale/python.vim @@ -1,6 +1,8 @@ " Author: w0rp <devw0rp@gmail.com> " Description: Functions for integrating with Python linters. +call ale#Set('python_auto_pipenv', '0') + let s:sep = has('win32') ? '\' : '/' " bin is used for Unix virtualenv directories, and Scripts is for Windows. let s:bin_dir = has('unix') ? 'bin' : 'Scripts' @@ -24,6 +26,7 @@ function! ale#python#FindProjectRootIni(buffer) abort \|| filereadable(l:path . '/mypy.ini') \|| filereadable(l:path . '/pycodestyle.cfg') \|| filereadable(l:path . '/flake8.cfg') + \|| filereadable(l:path . '/.flake8rc') \|| filereadable(l:path . '/Pipfile') \|| filereadable(l:path . '/Pipfile.lock') return l:path @@ -106,3 +109,8 @@ function! ale#python#FindExecutable(buffer, base_var_name, path_list) abort return ale#Var(a:buffer, a:base_var_name . '_executable') endfunction + +" Detects whether a pipenv environment is present. +function! ale#python#PipenvPresent(buffer) abort + return findfile('Pipfile.lock', expand('#' . a:buffer . ':p:h') . ';') isnot# '' +endfunction diff --git a/autoload/ale/references.vim b/autoload/ale/references.vim index 3a710b7b..d00a1fa9 100644 --- a/autoload/ale/references.vim +++ b/autoload/ale/references.vim @@ -64,6 +64,35 @@ function! ale#references#HandleLSPResponse(conn_id, response) abort endif endfunction +function! s:OnReady(linter, lsp_details, line, column, ...) abort + let l:buffer = a:lsp_details.buffer + let l:id = a:lsp_details.connection_id + + let l:Callback = a:linter.lsp is# 'tsserver' + \ ? function('ale#references#HandleTSServerResponse') + \ : function('ale#references#HandleLSPResponse') + + call ale#lsp#RegisterCallback(l:id, l:Callback) + + if a:linter.lsp is# 'tsserver' + let l:message = ale#lsp#tsserver_message#References( + \ l:buffer, + \ a:line, + \ a:column + \) + else + " Send a message saying the buffer has changed first, or the + " references position probably won't make sense. + call ale#lsp#NotifyForChanges(l:id, l:buffer) + + let l:message = ale#lsp#message#References(l:buffer, a:line, a:column) + endif + + let l:request_id = ale#lsp#Send(l:id, l:message) + + let s:references_map[l:request_id] = {} +endfunction + function! s:FindReferences(linter) abort let l:buffer = bufnr('') let [l:line, l:column] = getcurpos()[1:2] @@ -79,35 +108,10 @@ function! s:FindReferences(linter) abort endif let l:id = l:lsp_details.connection_id - let l:root = l:lsp_details.project_root - - function! OnReady(...) abort closure - let l:Callback = a:linter.lsp is# 'tsserver' - \ ? function('ale#references#HandleTSServerResponse') - \ : function('ale#references#HandleLSPResponse') - - call ale#lsp#RegisterCallback(l:id, l:Callback) - - if a:linter.lsp is# 'tsserver' - let l:message = ale#lsp#tsserver_message#References( - \ l:buffer, - \ l:line, - \ l:column - \) - else - " Send a message saying the buffer has changed first, or the - " references position probably won't make sense. - call ale#lsp#NotifyForChanges(l:id, l:root, l:buffer) - - let l:message = ale#lsp#message#References(l:buffer, l:line, l:column) - endif - - let l:request_id = ale#lsp#Send(l:id, l:message, l:lsp_details.project_root) - - let s:references_map[l:request_id] = {} - endfunction - call ale#lsp#WaitForCapability(l:id, l:root, 'references', function('OnReady')) + call ale#lsp#WaitForCapability(l:id, 'references', function('s:OnReady', [ + \ a:linter, l:lsp_details, l:line, l:column + \])) endfunction function! ale#references#Find() abort diff --git a/autoload/ale/ruby.vim b/autoload/ale/ruby.vim index b981ded6..5f0aa50d 100644 --- a/autoload/ale/ruby.vim +++ b/autoload/ale/ruby.vim @@ -20,3 +20,25 @@ function! ale#ruby#FindRailsRoot(buffer) abort return '' endfunction + +" Find the nearest dir containing a potential ruby project. +function! ale#ruby#FindProjectRoot(buffer) abort + let l:dir = ale#ruby#FindRailsRoot(a:buffer) + + if isdirectory(l:dir) + return l:dir + endif + + for l:name in ['.solargraph.yml', 'Rakefile', 'Gemfile'] + let l:dir = fnamemodify( + \ ale#path#FindNearestFile(a:buffer, l:name), + \ ':h' + \) + + if l:dir isnot# '.' && isdirectory(l:dir) + return l:dir + endif + endfor + + return '' +endfunction diff --git a/autoload/ale/sign.vim b/autoload/ale/sign.vim index a0dde359..af863682 100644 --- a/autoload/ale/sign.vim +++ b/autoload/ale/sign.vim @@ -211,7 +211,7 @@ function! s:BuildSignMap(buffer, current_sign_list, grouped_items) abort if l:max_signs is 0 let l:selected_grouped_items = [] - elseif type(l:max_signs) is type(0) && l:max_signs > 0 + elseif type(l:max_signs) is v:t_number && l:max_signs > 0 let l:selected_grouped_items = a:grouped_items[:l:max_signs - 1] else let l:selected_grouped_items = a:grouped_items diff --git a/autoload/ale/socket.vim b/autoload/ale/socket.vim index 0ca4dea6..7e069fb5 100644 --- a/autoload/ale/socket.vim +++ b/autoload/ale/socket.vim @@ -55,11 +55,18 @@ function! ale#socket#Open(address, options) abort if !has('nvim') " Vim - let l:channel_info.channel = ch_open(a:address, { + let l:channel_options = { \ 'mode': l:mode, \ 'waittime': 0, \ 'callback': function('s:VimOutputCallback'), - \}) + \} + + " Use non-blocking writes for Vim versions that support the option. + if has('patch-8.1.350') + let l:channel_options.noblock = 1 + endif + + let l:channel_info.channel = ch_open(a:address, l:channel_options) let l:vim_info = ch_info(l:channel_info.channel) let l:channel_id = !empty(l:vim_info) ? l:vim_info.id : -1 elseif exists('*chansend') && exists('*sockconnect') @@ -104,6 +111,7 @@ function! ale#socket#IsOpen(channel_id) abort endif let l:channel = s:channel_map[a:channel_id].channel + return ch_status(l:channel) is# 'open' endfunction diff --git a/autoload/ale/symbol.vim b/autoload/ale/symbol.vim new file mode 100644 index 00000000..5180cb86 --- /dev/null +++ b/autoload/ale/symbol.vim @@ -0,0 +1,109 @@ +let s:symbol_map = {} + +" Used to get the symbol map in tests. +function! ale#symbol#GetMap() abort + return deepcopy(s:symbol_map) +endfunction + +" Used to set the symbol map in tests. +function! ale#symbol#SetMap(map) abort + let s:symbol_map = a:map +endfunction + +function! ale#symbol#ClearLSPData() abort + let s:symbol_map = {} +endfunction + +function! ale#symbol#HandleLSPResponse(conn_id, response) abort + if has_key(a:response, 'id') + \&& has_key(s:symbol_map, a:response.id) + let l:options = remove(s:symbol_map, a:response.id) + + let l:result = get(a:response, 'result', v:null) + let l:item_list = [] + + if type(l:result) is v:t_list + " Each item looks like this: + " { + " 'name': 'foo', + " 'kind': 123, + " 'deprecated': v:false, + " 'location': { + " 'uri': 'file://...', + " 'range': { + " 'start': {'line': 0, 'character': 0}, + " 'end': {'line': 0, 'character': 0}, + " }, + " }, + " 'containerName': 'SomeContainer', + " } + for l:response_item in l:result + let l:location = l:response_item.location + + call add(l:item_list, { + \ 'filename': ale#path#FromURI(l:location.uri), + \ 'line': l:location.range.start.line + 1, + \ 'column': l:location.range.start.character + 1, + \ 'match': l:response_item.name, + \}) + endfor + endif + + if empty(l:item_list) + call ale#util#Execute('echom ''No symbols found.''') + else + call ale#preview#ShowSelection(l:item_list) + endif + endif +endfunction + +function! s:OnReady(linter, lsp_details, query, ...) abort + let l:buffer = a:lsp_details.buffer + + " If we already made a request, stop here. + if getbufvar(l:buffer, 'ale_symbol_request_made', 0) + return + endif + + let l:id = a:lsp_details.connection_id + + let l:Callback = function('ale#symbol#HandleLSPResponse') + call ale#lsp#RegisterCallback(l:id, l:Callback) + + let l:message = ale#lsp#message#Symbol(a:query) + let l:request_id = ale#lsp#Send(l:id, l:message) + + call setbufvar(l:buffer, 'ale_symbol_request_made', 1) + let s:symbol_map[l:request_id] = { + \ 'buffer': l:buffer, + \} +endfunction + +function! s:Search(linter, buffer, query) abort + let l:lsp_details = ale#lsp_linter#StartLSP(a:buffer, a:linter) + + if !empty(l:lsp_details) + call ale#lsp#WaitForCapability( + \ l:lsp_details.connection_id, + \ 'symbol_search', + \ function('s:OnReady', [a:linter, l:lsp_details, a:query]), + \) + endif +endfunction + +function! ale#symbol#Search(query) abort + if type(a:query) isnot v:t_string || empty(a:query) + throw 'A non-empty string must be provided!' + endif + + let l:buffer = bufnr('') + + " Set a flag so we only make one request. + call setbufvar(l:buffer, 'ale_symbol_request_made', 0) + + for l:linter in ale#linter#Get(getbufvar(l:buffer, '&filetype')) + if !empty(l:linter.lsp) && l:linter.lsp isnot# 'tsserver' + call s:Search(l:linter, l:buffer, a:query) + endif + endfor +endfunction diff --git a/autoload/ale/toggle.vim b/autoload/ale/toggle.vim index da108782..8e642b3f 100644 --- a/autoload/ale/toggle.vim +++ b/autoload/ale/toggle.vim @@ -15,21 +15,6 @@ function! s:DisablePostamble() abort endif endfunction -function! s:CleanupEveryBuffer() abort - for l:key in keys(g:ale_buffer_info) - " The key could be a filename or a buffer number, so try and - " convert it to a number. We need a number for the other - " functions. - let l:buffer = str2nr(l:key) - - if l:buffer > 0 - " Stop all jobs and clear the results for everything, and delete - " all of the data we stored for the buffer. - call ale#engine#Cleanup(l:buffer) - endif - endfor -endfunction - function! ale#toggle#Toggle() abort let g:ale_enabled = !get(g:, 'ale_enabled') @@ -40,7 +25,7 @@ function! ale#toggle#Toggle() abort call ale#balloon#Enable() endif else - call s:CleanupEveryBuffer() + call ale#engine#CleanupEveryBuffer() call s:DisablePostamble() if exists('*ale#balloon#Disable') @@ -64,7 +49,7 @@ function! ale#toggle#Disable() abort endfunction function! ale#toggle#Reset() abort - call s:CleanupEveryBuffer() + call ale#engine#CleanupEveryBuffer() call ale#highlight#UpdateHighlights() endfunction @@ -76,6 +61,7 @@ function! ale#toggle#ToggleBuffer(buffer) abort " linting locally when linting is disabled globally if l:enabled && !g:ale_enabled execute 'echom ''ALE cannot be enabled locally when disabled globally''' + return endif diff --git a/autoload/ale/util.vim b/autoload/ale/util.vim index fb6dc085..bb478957 100644 --- a/autoload/ale/util.vim +++ b/autoload/ale/util.vim @@ -54,6 +54,7 @@ endif function! ale#util#JoinNeovimOutput(job, last_line, data, mode, callback) abort if a:mode is# 'raw' call a:callback(a:job, join(a:data, "\n")) + return '' endif @@ -79,7 +80,7 @@ function! ale#util#GetLineCount(buffer) abort endfunction function! ale#util#GetFunction(string_or_ref) abort - if type(a:string_or_ref) == type('') + if type(a:string_or_ref) is v:t_string return function(a:string_or_ref) endif @@ -88,12 +89,12 @@ endfunction function! ale#util#Open(filename, line, column, options) abort if get(a:options, 'open_in_tab', 0) - call ale#util#Execute('tabedit ' . fnameescape(a:filename)) - else + call ale#util#Execute('tabedit +' . a:line . ' ' . fnameescape(a:filename)) + elseif bufnr(a:filename) isnot bufnr('') " Open another file only if we need to. - if bufnr(a:filename) isnot bufnr('') - call ale#util#Execute('edit ' . fnameescape(a:filename)) - endif + call ale#util#Execute('edit +' . a:line . ' ' . fnameescape(a:filename)) + else + normal! m` endif call cursor(a:line, a:column) @@ -268,7 +269,7 @@ endfunction " See :help sandbox function! ale#util#InSandbox() abort try - let &equalprg=&equalprg + let &l:equalprg=&l:equalprg catch /E48/ " E48 is the sandbox error. return 1 @@ -303,8 +304,8 @@ endfunction " Only the first pattern which matches a line will be returned. function! ale#util#GetMatches(lines, patterns) abort let l:matches = [] - let l:lines = type(a:lines) == type([]) ? a:lines : [a:lines] - let l:patterns = type(a:patterns) == type([]) ? a:patterns : [a:patterns] + let l:lines = type(a:lines) is v:t_list ? a:lines : [a:lines] + let l:patterns = type(a:patterns) is v:t_list ? a:patterns : [a:patterns] for l:line in l:lines for l:pattern in l:patterns @@ -382,7 +383,7 @@ function! ale#util#FuzzyJSONDecode(data, default) abort return a:default endif - let l:str = type(a:data) == type('') ? a:data : join(a:data, '') + let l:str = type(a:data) is v:t_string ? a:data : join(a:data, '') try let l:result = json_decode(l:str) @@ -404,7 +405,7 @@ endfunction " the buffer. function! ale#util#Writefile(buffer, lines, filename) abort let l:corrected_lines = getbufvar(a:buffer, '&fileformat') is# 'dos' - \ ? map(copy(a:lines), 'v:val . "\r"') + \ ? map(copy(a:lines), 'substitute(v:val, ''\r*$'', ''\r'', '''')') \ : a:lines call writefile(l:corrected_lines, a:filename) " no-custom-checks @@ -451,3 +452,14 @@ function! ale#util#Col(str, chr) abort return strlen(join(split(a:str, '\zs')[0:a:chr - 2], '')) + 1 endfunction + +function! ale#util#FindItemAtCursor(buffer) abort + let l:info = get(g:ale_buffer_info, a:buffer, {}) + let l:loclist = get(l:info, 'loclist', []) + let l:pos = getcurpos() + let l:index = ale#util#BinarySearch(l:loclist, a:buffer, l:pos[1], l:pos[2]) + let l:loc = l:index >= 0 ? l:loclist[l:index] : {} + + return [l:info, l:loc] +endfunction + diff --git a/autoload/ale/virtualtext.vim b/autoload/ale/virtualtext.vim new file mode 100644 index 00000000..c4ce37dd --- /dev/null +++ b/autoload/ale/virtualtext.vim @@ -0,0 +1,136 @@ +scriptencoding utf-8 +" Author: w0rp <devw0rp@gmail.com> +" Author: Luan Santos <cfcluan@gmail.com> +" Description: Shows lint message for the current line as virtualtext, if any + +" Controls the milliseconds delay before showing a message. +let g:ale_virtualtext_delay = get(g:, 'ale_virtualtext_delay', 10) +let s:cursor_timer = -1 +let s:last_pos = [0, 0, 0] + +if has('nvim-0.3.2') + let s:ns_id = nvim_create_namespace('ale') +endif + +if !hlexists('ALEVirtualTextError') + highlight link ALEVirtualTextError ALEError +endif + +if !hlexists('ALEVirtualTextStyleError') + highlight link ALEVirtualTextStyleError ALEVirtualTextError +endif + +if !hlexists('ALEVirtualTextWarning') + highlight link ALEVirtualTextWarning ALEWarning +endif + +if !hlexists('ALEVirtualTextStyleWarning') + highlight link ALEVirtualTextStyleWarning ALEVirtualTextWarning +endif + +if !hlexists('ALEVirtualTextInfo') + highlight link ALEVirtualTextInfo ALEVirtualTextWarning +endif + +function! ale#virtualtext#Clear() abort + if !has('nvim-0.3.2') + return + endif + + let l:buffer = bufnr('') + + call nvim_buf_clear_highlight(l:buffer, s:ns_id, 0, -1) +endfunction + +function! ale#virtualtext#ShowMessage(message, hl_group) abort + if !has('nvim-0.3.2') + return + endif + + let l:cursor_position = getcurpos() + let l:line = line('.') + let l:buffer = bufnr('') + let l:prefix = get(g:, 'ale_virtualtext_prefix', '> ') + + call nvim_buf_set_virtual_text(l:buffer, s:ns_id, l:line-1, [[l:prefix.a:message, a:hl_group]], {}) +endfunction + +function! s:StopCursorTimer() abort + if s:cursor_timer != -1 + call timer_stop(s:cursor_timer) + let s:cursor_timer = -1 + endif +endfunction + +function! ale#virtualtext#ShowCursorWarning(...) abort + if !g:ale_virtualtext_cursor + return + endif + + let l:buffer = bufnr('') + + if mode(1) isnot# 'n' + return + endif + + if ale#ShouldDoNothing(l:buffer) + return + endif + + let [l:info, l:loc] = ale#util#FindItemAtCursor(l:buffer) + + call ale#virtualtext#Clear() + + if !empty(l:loc) + let l:msg = get(l:loc, 'detail', l:loc.text) + let l:hl_group = 'ALEVirtualTextInfo' + let l:type = get(l:loc, 'type', 'E') + + if l:type is# 'E' + if get(l:loc, 'sub_type', '') is# 'style' + let l:hl_group = 'ALEVirtualTextStyleError' + else + let l:hl_group = 'ALEVirtualTextError' + endif + elseif l:type is# 'W' + if get(l:loc, 'sub_type', '') is# 'style' + let l:hl_group = 'ALEVirtualTextStyleWarning' + else + let l:hl_group = 'ALEVirtualTextWarning' + endif + endif + + call ale#virtualtext#ShowMessage(l:msg, l:hl_group) + endif +endfunction + +function! ale#virtualtext#ShowCursorWarningWithDelay() abort + let l:buffer = bufnr('') + + if !g:ale_virtualtext_cursor + return + endif + + if mode(1) isnot# 'n' + return + endif + + call s:StopCursorTimer() + + let l:pos = getcurpos()[0:2] + + " Check the current buffer, line, and column number against the last + " recorded position. If the position has actually changed, *then* + " we should show something. Otherwise we can end up doing processing + " the show message far too frequently. + if l:pos != s:last_pos + let l:delay = ale#Var(l:buffer, 'virtualtext_delay') + + let s:last_pos = l:pos + let s:cursor_timer = timer_start( + \ l:delay, + \ function('ale#virtualtext#ShowCursorWarning') + \) + endif +endfunction + |