From b800d24212d4c34b42dd388b8cab120b39893866 Mon Sep 17 00:00:00 2001 From: Tim Bedard Date: Thu, 2 Apr 2020 22:52:47 -0500 Subject: [PATCH 01/12] add stdin option for supported vint versions --- ale_linters/vim/vint.vim | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ale_linters/vim/vint.vim b/ale_linters/vim/vint.vim index 65e19126..6b996fa4 100644 --- a/ale_linters/vim/vint.vim +++ b/ale_linters/vim/vint.vim @@ -13,12 +13,17 @@ function! ale_linters#vim#vint#GetCommand(buffer, version) abort let l:warning_flag = ale#Var(a:buffer, 'vim_vint_show_style_issues') ? '-s' : '-w' + " Use the --stdin-display-name argument if supported, temp file otherwise. + let l:stdin_or_temp = ale#semver#GTE(a:version, [0, 4, 0]) + \ ? ' --stdin-display-name %s -' + \ : ' %t' + return '%e' \ . ' ' . l:warning_flag \ . (l:can_use_no_color_flag ? ' --no-color' : '') \ . s:enable_neovim \ . ' ' . s:format - \ . ' %t' + \ . l:stdin_or_temp endfunction let s:word_regex_list = [ From 0b55098bd0ad1428581cb5ae3d0634d40167feba Mon Sep 17 00:00:00 2001 From: Jeremiasz Nelz Date: Fri, 4 Sep 2020 22:52:29 +0200 Subject: [PATCH 02/12] Fix asciidoc languagetool integration --- ale_linters/asciidoc/languagetool.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ale_linters/asciidoc/languagetool.vim b/ale_linters/asciidoc/languagetool.vim index f16df6c0..8e8de7f3 100644 --- a/ale_linters/asciidoc/languagetool.vim +++ b/ale_linters/asciidoc/languagetool.vim @@ -2,4 +2,4 @@ " Description: languagetool for asciidoc files, copied from markdown. -call ale#handlers#languagetool#DefineLinter('asciidoctor') +call ale#handlers#languagetool#DefineLinter('asciidoc') From 152b2cb6910d9173d75a195c5a185b7f3f8aa696 Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 5 Sep 2020 19:06:07 +0100 Subject: [PATCH 03/12] Clean up embertemplatelint code Alias ember-template-lint to embertemplatelint so users can use either string to enable the linter. --- ale_linters/handlebars/embertemplatelint.vim | 13 ++++++----- doc/ale-handlebars.txt | 6 +++-- ..._embertemplatelint_command_callbacks.vader | 17 ++++++++++++++ ...bertemplatelint_executable_detection.vader | 22 ------------------- 4 files changed, 28 insertions(+), 30 deletions(-) create mode 100644 test/command_callback/test_embertemplatelint_command_callbacks.vader delete mode 100644 test/test_embertemplatelint_executable_detection.vader diff --git a/ale_linters/handlebars/embertemplatelint.vim b/ale_linters/handlebars/embertemplatelint.vim index 31d65b70..bd4d1d31 100644 --- a/ale_linters/handlebars/embertemplatelint.vim +++ b/ale_linters/handlebars/embertemplatelint.vim @@ -19,11 +19,11 @@ endfunction function! ale_linters#handlebars#embertemplatelint#GetCommandWithVersionCheck(buffer) abort return ale#semver#RunWithVersionCheck( - \ a:buffer, - \ ale_linters#handlebars#embertemplatelint#GetExecutable(a:buffer), - \ '%e --version', - \ function('ale_linters#handlebars#embertemplatelint#GetCommand'), - \ ) + \ a:buffer, + \ ale_linters#handlebars#embertemplatelint#GetExecutable(a:buffer), + \ '%e --version', + \ function('ale_linters#handlebars#embertemplatelint#GetCommand'), + \) endfunction function! ale_linters#handlebars#embertemplatelint#Handle(buffer, lines) abort @@ -52,7 +52,8 @@ function! ale_linters#handlebars#embertemplatelint#Handle(buffer, lines) abort endfunction call ale#linter#Define('handlebars', { -\ 'name': 'ember-template-lint', +\ 'name': 'embertemplatelint', +\ 'aliases': ['ember-template-lint'], \ 'executable': function('ale_linters#handlebars#embertemplatelint#GetExecutable'), \ 'command': function('ale_linters#handlebars#embertemplatelint#GetCommandWithVersionCheck'), \ 'callback': 'ale_linters#handlebars#embertemplatelint#Handle', diff --git a/doc/ale-handlebars.txt b/doc/ale-handlebars.txt index 5daec5b3..4a5a3870 100644 --- a/doc/ale-handlebars.txt +++ b/doc/ale-handlebars.txt @@ -14,7 +14,8 @@ ember-template-lint *ale-handlebars-embertemplatelint* g:ale_handlebars_embertemplatelint_executable *g:ale_handlebars_embertemplatelint_executable* - Type: |String| *b:ale_handlebars_embertemplatelint_executable* + *b:ale_handlebars_embertemplatelint_executable* + Type: |String| Default: `'ember-template-lint'` See |ale-integrations-local-executables| @@ -22,7 +23,8 @@ g:ale_handlebars_embertemplatelint_executable g:ale_handlebars_embertemplatelint_use_global *g:ale_handlebars_embertemplatelint_use_global* - Type: |Number| *b:ale_handlebars_embertemplatelint_use_global* + *b:ale_handlebars_embertemplatelint_use_global* + Type: |Number| Default: `get(g:, 'ale_use_global_executables', 0)` See |ale-integrations-local-executables| diff --git a/test/command_callback/test_embertemplatelint_command_callbacks.vader b/test/command_callback/test_embertemplatelint_command_callbacks.vader new file mode 100644 index 00000000..97687d29 --- /dev/null +++ b/test/command_callback/test_embertemplatelint_command_callbacks.vader @@ -0,0 +1,17 @@ +Before: + call ale#assert#SetUpLinterTest('handlebars', 'embertemplatelint') + + GivenCommandOutput ['1.6.0'] + +After: + call ale#assert#TearDownLinterTest() + +Execute(ember-template-lint executables runs the right command): + AssertLinter 'ember-template-lint', + \ ale#Escape('ember-template-lint') . ' --json --filename %s' + +Execute(old ember-template-lint executables runs the right command): + GivenCommandOutput [] + + AssertLinter 'ember-template-lint', + \ ale#Escape('ember-template-lint') . ' --json %t' diff --git a/test/test_embertemplatelint_executable_detection.vader b/test/test_embertemplatelint_executable_detection.vader deleted file mode 100644 index bd0f5dd9..00000000 --- a/test/test_embertemplatelint_executable_detection.vader +++ /dev/null @@ -1,22 +0,0 @@ -Before: - call ale#test#SetDirectory('/testplugin/test') - - runtime ale_linters/handlebars/embertemplatelint.vim - -After: - call ale#test#RestoreDirectory() - call ale#linter#Reset() - -Execute(ember-template-lint executables runs the right command): - call ale#test#SetFilename('ember-template-lint-test-files/app/template.hbs') - - AssertEqual - \ ale_linters#handlebars#embertemplatelint#GetCommand(bufnr(''), [2, 0, 0]), - \ '%e --json --filename %s' - -Execute(old ember-template-lint executables runs the right command): - call ale#test#SetFilename('ember-template-lint-test-files/app/template.hbs') - - AssertEqual - \ ale_linters#handlebars#embertemplatelint#GetCommand(bufnr(''), [1, 5, 0]), - \ '%e --json %t' From 5bc49d204782617e01da65d83c0d9700a1b0a72f Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 5 Sep 2020 21:46:39 +0100 Subject: [PATCH 04/12] Fix #3183 - Escape filename characters from LSP/tsserver --- autoload/ale/lsp_linter.vim | 12 +++- .../test_engine_lsp_response_handling.vader | 56 +++++++++++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/autoload/ale/lsp_linter.vim b/autoload/ale/lsp_linter.vim index db640654..dcd76e8f 100644 --- a/autoload/ale/lsp_linter.vim +++ b/autoload/ale/lsp_linter.vim @@ -34,7 +34,11 @@ endfunction function! s:HandleLSPDiagnostics(conn_id, response) abort let l:linter_name = s:lsp_linter_map[a:conn_id] let l:filename = ale#path#FromURI(a:response.params.uri) - let l:buffer = bufnr('^' . l:filename . '$') + let l:escaped_name = escape( + \ fnameescape(l:filename), + \ has('win32') ? '^' : '^,}]' + \) + let l:buffer = bufnr('^' . l:escaped_name . '$') let l:info = get(g:ale_buffer_info, l:buffer, {}) if empty(l:info) @@ -52,7 +56,11 @@ endfunction function! s:HandleTSServerDiagnostics(response, error_type) abort let l:linter_name = 'tsserver' - let l:buffer = bufnr('^' . a:response.body.file . '$') + let l:escaped_name = escape( + \ fnameescape(a:response.body.file), + \ has('win32') ? '^' : '^,}]' + \) + let l:buffer = bufnr('^' . l:escaped_name . '$') let l:info = get(g:ale_buffer_info, l:buffer, {}) if empty(l:info) diff --git a/test/lsp/test_engine_lsp_response_handling.vader b/test/lsp/test_engine_lsp_response_handling.vader index 9abfa087..8261f1da 100644 --- a/test/lsp/test_engine_lsp_response_handling.vader +++ b/test/lsp/test_engine_lsp_response_handling.vader @@ -44,9 +44,21 @@ After: Given foobar(An empty file): Execute(tsserver syntax error responses should be handled correctly): runtime ale_linters/typescript/tsserver.vim - call ale#test#SetFilename('filename.ts') + + if has('win32') + call ale#test#SetFilename('filename,[]^$.ts') + else + call ale#test#SetFilename('filename*?,{}[]^$.ts') + endif + call ale#engine#InitBufferInfo(bufnr('')) + if has('win32') + AssertEqual 'filename,[]^$.ts', expand('%:p:t') + else + AssertEqual 'filename*?,{}[]^$.ts', expand('%:p:t') + endif + " When we get syntax errors and no semantic errors, we should keep the " syntax errors. call ale#lsp_linter#HandleLSPResponse(1, { @@ -54,7 +66,7 @@ Execute(tsserver syntax error responses should be handled correctly): \ 'type': 'event', \ 'event': 'syntaxDiag', \ 'body': { - \ 'file': g:dir . '/filename.ts', + \ 'file': expand('%:p'), \ 'diagnostics':[ \ { \ 'start': { @@ -76,7 +88,7 @@ Execute(tsserver syntax error responses should be handled correctly): \ 'type': 'event', \ 'event': 'semanticDiag', \ 'body': { - \ 'file': g:dir . '/filename.ts', + \ 'file': expand('%:p'), \ 'diagnostics':[ \ ], \ }, @@ -104,7 +116,7 @@ Execute(tsserver syntax error responses should be handled correctly): \ 'type': 'event', \ 'event': 'syntaxDiag', \ 'body': { - \ 'file': g:dir . '/filename.ts', + \ 'file': expand('%:p'), \ 'diagnostics':[ \ ], \ }, @@ -146,9 +158,21 @@ Execute(tsserver syntax error responses should be handled correctly): Execute(tsserver semantic error responses should be handled correctly): runtime ale_linters/typescript/tsserver.vim - call ale#test#SetFilename('filename.ts') + + if has('win32') + call ale#test#SetFilename('filename,[]^$.ts') + else + call ale#test#SetFilename('filename*?,{}[]^$.ts') + endif + call ale#engine#InitBufferInfo(bufnr('')) + if has('win32') + AssertEqual 'filename,[]^$.ts', expand('%:p:t') + else + AssertEqual 'filename*?,{}[]^$.ts', expand('%:p:t') + endif + " When we get syntax errors and no semantic errors, we should keep the " syntax errors. call ale#lsp_linter#HandleLSPResponse(1, { @@ -156,7 +180,7 @@ Execute(tsserver semantic error responses should be handled correctly): \ 'type': 'event', \ 'event': 'syntaxDiag', \ 'body': { - \ 'file': g:dir . '/filename.ts', + \ 'file': expand('%:p'), \ 'diagnostics':[ \ ], \ }, @@ -166,7 +190,7 @@ Execute(tsserver semantic error responses should be handled correctly): \ 'type': 'event', \ 'event': 'semanticDiag', \ 'body': { - \ 'file': g:dir . '/filename.ts', + \ 'file': expand('%:p'), \ 'diagnostics':[ \ { \ 'start': { @@ -206,7 +230,7 @@ Execute(tsserver semantic error responses should be handled correctly): \ 'type': 'event', \ 'event': 'semanticDiag', \ 'body': { - \ 'file': g:dir . '/filename.ts', + \ 'file': expand('%:p'), \ 'diagnostics':[ \ ], \ }, @@ -270,15 +294,27 @@ Execute(tsserver errors should mark tsserver no longer active): Execute(LSP diagnostics responses should be handled correctly): let b:ale_linters = ['eclipselsp'] runtime ale_linters/java/eclipselsp.vim - call ale#test#SetFilename('filename.java') + + if has('win32') + call ale#test#SetFilename('filename,[]^$.ts') + else + call ale#test#SetFilename('filename*?,{}[]^$.java') + endif + call ale#engine#InitBufferInfo(bufnr('')) call ale#lsp_linter#SetLSPLinterMap({'1': 'eclipselsp'}) + if has('win32') + AssertEqual 'filename,[]^$.ts', expand('%:p:t') + else + AssertEqual 'filename*?,{}[]^$.java', expand('%:p:t') + endif + call ale#lsp_linter#HandleLSPResponse(1, { \ 'jsonrpc':'2.0', \ 'method':'textDocument/publishDiagnostics', \ 'params': { - \ 'uri':'file://' . g:dir . '/filename.java', + \ 'uri': ale#path#ToURI(expand('%:p')), \ 'diagnostics': [ \ { \ 'range': { From c36053d4cc28fbc5fc0ee70ccb2c9a1b349eaaa3 Mon Sep 17 00:00:00 2001 From: w0rp Date: Sun, 6 Sep 2020 22:37:37 +0100 Subject: [PATCH 05/12] Close #3268 - Implement :ALEImport A new command, `:ALEImport`, has been added, which lets you import words at your cursor if a completion provider can provide a completion for that word which includes some additional text changes. --- autoload/ale/completion.vim | 185 ++++-- doc/ale.txt | 28 +- plugin/ale.vim | 5 + test/completion/test_ale_import_command.vader | 562 ++++++++++++++++++ .../test_completion_filtering.vader | 50 +- 5 files changed, 775 insertions(+), 55 deletions(-) create mode 100644 test/completion/test_ale_import_command.vader diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index c2cfd74a..96415202 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -188,7 +188,13 @@ function! ale#completion#GetTriggerCharacter(filetype, prefix) abort return '' endfunction -function! ale#completion#Filter(buffer, filetype, suggestions, prefix) abort +function! ale#completion#Filter( +\ buffer, +\ filetype, +\ suggestions, +\ prefix, +\ exact_prefix_match, +\) abort let l:excluded_words = ale#Var(a:buffer, 'completion_excluded_words') if empty(a:prefix) @@ -215,10 +221,17 @@ function! ale#completion#Filter(buffer, filetype, suggestions, prefix) abort " Dictionaries is accepted here. let l:word = type(l:item) is v:t_string ? l:item : l:item.word - " Add suggestions if the suggestion starts with a - " case-insensitive match for the prefix. - if l:word[: len(a:prefix) - 1] is? a:prefix - call add(l:filtered_suggestions, l:item) + if a:exact_prefix_match + " Add suggestions if the word is an exact match. + if l:word is# a:prefix + call add(l:filtered_suggestions, l:item) + endif + else + " Add suggestions if the suggestion starts with a + " case-insensitive match for the prefix. + if l:word[: len(a:prefix) - 1] is? a:prefix + call add(l:filtered_suggestions, l:item) + endif endif endfor endif @@ -241,21 +254,17 @@ function! ale#completion#Filter(buffer, filetype, suggestions, prefix) abort return l:filtered_suggestions endfunction -function! s:ReplaceCompletionOptions() abort - let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') - - if l:source is# 'ale-automatic' || l:source is# 'ale-manual' - " Remember the old omnifunc value, if there is one. - " If we don't store an old one, we'll just never reset the option. - " This will stop some random exceptions from appearing. - if !exists('b:ale_old_omnifunc') && !empty(&l:omnifunc) - let b:ale_old_omnifunc = &l:omnifunc - endif - - let &l:omnifunc = 'ale#completion#AutomaticOmniFunc' +function! s:ReplaceCompletionOptions(source) abort + " Remember the old omnifunc value, if there is one. + " If we don't store an old one, we'll just never reset the option. + " This will stop some random exceptions from appearing. + if !exists('b:ale_old_omnifunc') && !empty(&l:omnifunc) + let b:ale_old_omnifunc = &l:omnifunc endif - if l:source is# 'ale-automatic' + let &l:omnifunc = 'ale#completion#AutomaticOmniFunc' + + if a:source is# 'ale-automatic' if !exists('b:ale_old_completeopt') let b:ale_old_completeopt = &l:completeopt endif @@ -318,7 +327,11 @@ function! ale#completion#AutomaticOmniFunc(findstart, base) abort else let l:result = ale#completion#GetCompletionResult() - call s:ReplaceCompletionOptions() + let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') + + if l:source is# 'ale-automatic' || l:source is# 'ale-manual' + call s:ReplaceCompletionOptions(l:source) + endif return l:result isnot v:null ? l:result : [] endif @@ -331,31 +344,53 @@ function! s:OpenCompletionMenu(...) abort endfunction function! ale#completion#Show(result) abort - if ale#util#Mode() isnot# 'i' + let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') + + if ale#util#Mode() isnot# 'i' && l:source isnot# 'ale-import' return endif - " Set the list in the buffer, temporarily replace omnifunc with our - " function, and then start omni-completion. + " Set the list in the buffer. let b:ale_completion_result = a:result " Don't try to open the completion menu if there's nothing to show. if empty(b:ale_completion_result) + if l:source is# 'ale-import' + " If we ran completion from :ALEImport, + " tell the user that nothing is going to happen. + call s:message('No possible imports found.') + endif + return endif " Replace completion options shortly before opening the menu. - call s:ReplaceCompletionOptions() - - let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') - if l:source is# 'ale-automatic' || l:source is# 'ale-manual' + call s:ReplaceCompletionOptions(l:source) + call timer_start(0, function('s:OpenCompletionMenu')) endif if l:source is# 'ale-callback' call b:CompleteCallback(b:ale_completion_result) endif + + if l:source is# 'ale-import' + call ale#completion#HandleUserData(b:ale_completion_result[0]) + + let l:text_changed = '' . g:ale_lint_on_text_changed + + " Check the buffer again right away, if linting is enabled. + if g:ale_enabled + \&& ( + \ l:text_changed is# '1' + \ || l:text_changed is# 'always' + \ || l:text_changed is# 'normal' + \ || l:text_changed is# 'insert' + \) + call ale#Queue(0, '') + endif + endif endfunction function! ale#completion#GetAllTriggers() abort @@ -386,7 +421,10 @@ endfunction function! s:CompletionStillValid(request_id) abort let [l:line, l:column] = getpos('.')[1:2] - return ale#util#Mode() is# 'i' + return ( + \ ale#util#Mode() is# 'i' + \ || b:ale_completion_info.source is# 'ale-import' + \) \&& has_key(b:, 'ale_completion_info') \&& b:ale_completion_info.request_id == a:request_id \&& b:ale_completion_info.line == l:line @@ -394,6 +432,7 @@ function! s:CompletionStillValid(request_id) abort \ b:ale_completion_info.column == l:column \ || b:ale_completion_info.source is# 'ale-omnifunc' \ || b:ale_completion_info.source is# 'ale-callback' + \ || b:ale_completion_info.source is# 'ale-import' \) endfunction @@ -418,6 +457,7 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort let l:buffer = bufnr('') let l:results = [] let l:names_with_details = [] + let l:info = get(b:, 'ale_completion_info', {}) for l:suggestion in a:response.body let l:displayParts = [] @@ -459,7 +499,8 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort \ 'kind': ale#completion#GetCompletionSymbols(l:suggestion.kind), \ 'icase': 1, \ 'menu': join(l:displayParts, ''), - \ 'dup': g:ale_completion_autoimport, + \ 'dup': get(l:info, 'additional_edits_only', 0) + \ || g:ale_completion_autoimport, \ 'info': join(l:documentationParts, ''), \} @@ -469,7 +510,12 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort \ }) endif - call add(l:results, l:result) + " Include this item if we'll accept any items, + " or if we only want items with additional edits, and this has them. + if !get(l:info, 'additional_edits_only', 0) + \|| has_key(l:result, 'user_data') + call add(l:results, l:result) + endif endfor let l:names = getbufvar(l:buffer, 'ale_tsserver_completion_names', []) @@ -544,7 +590,10 @@ function! ale#completion#ParseLSPCompletions(response) abort " Don't use LSP items with additional text edits when autoimport for " completions is turned off. if !empty(get(l:item, 'additionalTextEdits')) - \&& !g:ale_completion_autoimport + \&& !( + \ get(l:info, 'additional_edits_only', 0) + \ || g:ale_completion_autoimport + \) continue endif @@ -594,11 +643,22 @@ function! ale#completion#ParseLSPCompletions(response) abort endif endif - call add(l:results, l:result) + " Include this item if we'll accept any items, + " or if we only want items with additional edits, and this has them. + if !get(l:info, 'additional_edits_only', 0) + \|| has_key(l:result, 'user_data') + call add(l:results, l:result) + endif endfor if has_key(l:info, 'prefix') - let l:results = ale#completion#Filter(l:buffer, &filetype, l:results, l:info.prefix) + let l:results = ale#completion#Filter( + \ l:buffer, + \ &filetype, + \ l:results, + \ l:info.prefix, + \ get(l:info, 'additional_edits_only', 0), + \) endif return l:results[: g:ale_completion_max_suggestions - 1] @@ -622,13 +682,18 @@ function! ale#completion#HandleTSServerResponse(conn_id, response) abort \ &filetype, \ ale#completion#ParseTSServerCompletions(a:response), \ b:ale_completion_info.prefix, + \ get(b:ale_completion_info, 'additional_edits_only', 0), \)[: g:ale_completion_max_suggestions - 1] " We need to remember some names for tsserver, as it doesn't send " details back for everything we send. call setbufvar(l:buffer, 'ale_tsserver_completion_names', l:names) - if !empty(l:names) + if empty(l:names) + " Response with no results now and skip making a redundant request + " for nothing. + call ale#completion#Show([]) + else let l:identifiers = [] for l:name in l:names @@ -702,7 +767,8 @@ function! s:OnReady(linter, lsp_details) abort \ b:ale_completion_info.line, \ b:ale_completion_info.column, \ b:ale_completion_info.prefix, - \ g:ale_completion_autoimport, + \ get(b:ale_completion_info, 'additional_edits_only', 0) + \ || g:ale_completion_autoimport, \) else " Send a message saying the buffer has changed first, otherwise @@ -761,9 +827,19 @@ function! ale#completion#GetCompletions(...) abort let b:CompleteCallback = l:CompleteCallback endif - let [l:line, l:column] = getpos('.')[1:2] + if has_key(l:options, 'line') && has_key(l:options, 'column') + " Use a provided line and column, if given. + let l:line = l:options.line + let l:column = l:options.column + else + let [l:line, l:column] = getpos('.')[1:2] + endif - let l:prefix = ale#completion#GetPrefix(&filetype, l:line, l:column) + if has_key(l:options, 'prefix') + let l:prefix = l:options.prefix + else + let l:prefix = ale#completion#GetPrefix(&filetype, l:line, l:column) + endif if l:source is# 'ale-automatic' && empty(l:prefix) return 0 @@ -782,6 +858,11 @@ function! ale#completion#GetCompletions(...) abort \} unlet! b:ale_completion_result + if has_key(l:options, 'additional_edits_only') + let b:ale_completion_info.additional_edits_only = + \ l:options.additional_edits_only + endif + let l:buffer = bufnr('') let l:Callback = function('s:OnReady') @@ -798,6 +879,37 @@ function! ale#completion#GetCompletions(...) abort return l:started endfunction +function! s:message(message) abort + call ale#util#Execute('echom ' . string(a:message)) +endfunction + +" This function implements the :ALEImport command. +function! ale#completion#Import() abort + let l:word = expand('') + + if empty(l:word) + call s:message('Nothing to complete at cursor!') + + return + endif + + let [l:line, l:column] = getpos('.')[1:2] + let l:column = searchpos('\V' . escape(l:word, '/\'), 'bn', l:line)[1] + + if l:column isnot 0 + let l:started = ale#completion#GetCompletions('ale-import', { + \ 'line': l:line, + \ 'column': l:column, + \ 'prefix': l:word, + \ 'additional_edits_only': 1, + \}) + + if !l:started + call s:message('No completion providers are available.') + endif + endif +endfunction + function! ale#completion#OmniFunc(findstart, base) abort if a:findstart let l:started = ale#completion#GetCompletions('ale-omnifunc') @@ -876,6 +988,7 @@ function! ale#completion#HandleUserData(completed_item) abort if l:source isnot# 'ale-automatic' \&& l:source isnot# 'ale-manual' \&& l:source isnot# 'ale-callback' + \&& l:source isnot# 'ale-import' return endif diff --git a/doc/ale.txt b/doc/ale.txt index d45cada7..7549dc01 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -526,6 +526,12 @@ completion information with Deoplete, consult Deoplete's documentation. ALE by can support automatic imports from external modules. This behavior can be enabled by setting the |g:ale_completion_autoimport| variable to `1`. +You can manually request imports for symbols at the cursor with the +|ALEImport| command. The word at the cursor must be an exact match for some +potential completion result which includes additional text to insert into the +current buffer, which ALE will assume is code for an import line. This command +can be useful when your code already contains something you need to import. + When working with TypeScript files, ALE can remove warnings from your completions by setting the |g:ale_completion_tsserver_remove_warnings| variable to 1. @@ -3052,6 +3058,23 @@ ALEHover *ALEHover* A plug mapping `(ale_hover)` is defined for this command. +ALEImport *ALEImport* + + Try to import a symbol using `tsserver` or a Language Server. + + ALE will look for completions for the word at the cursor which contain + additional text edits that possible insert lines to import the symbol. The + first match with additional text edits will be used, and may add other code + to the current buffer other than import lines. + + If linting is enabled, and |g:ale_lint_on_text_changed| is set to ever check + buffers when text is changed, the buffer will be checked again after changes + are made. + + A Plug mapping `(ale_import)` is defined for this command. This + mapping should only be bound for normal mode. + + ALEOrganizeImports *ALEOrganizeImports* Organize imports using tsserver. Currently not implemented for LSPs. @@ -3059,9 +3082,10 @@ ALEOrganizeImports *ALEOrganizeImports* ALERename *ALERename* - Rename a symbol using TypeScript server or Language Server. + Rename a symbol using `tsserver` or a Language Server. - The user will be prompted for a new name. + The symbol where the cursor is resting will be the symbol renamed, and a + prompt will open to request a new name. ALERepeatSelection *ALERepeatSelection* diff --git a/plugin/ale.vim b/plugin/ale.vim index 6a947ea7..9cebb71f 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -229,8 +229,12 @@ command! -bar ALEDocumentation :call ale#hover#ShowDocumentationAtCursor() " Search for appearances of a symbol, such as a type name or function name. command! -nargs=1 ALESymbolSearch :call ale#symbol#Search() +" Complete text with tsserver and LSP command! -bar ALEComplete :call ale#completion#GetCompletions('ale-manual') +" Try to find completions for the current symbol that add additional text. +command! -bar ALEImport :call ale#completion#Import() + " Rename symbols using tsserver and LSP command! -bar ALERename :call ale#rename#Execute() @@ -275,6 +279,7 @@ nnoremap (ale_find_references) :ALEFindReferences nnoremap (ale_hover) :ALEHover nnoremap (ale_documentation) :ALEDocumentation inoremap (ale_complete) :ALEComplete +nnoremap (ale_import) :ALEImport nnoremap (ale_rename) :ALERename nnoremap (ale_repeat_selection) :ALERepeatSelection diff --git a/test/completion/test_ale_import_command.vader b/test/completion/test_ale_import_command.vader new file mode 100644 index 00000000..2ba9b8d7 --- /dev/null +++ b/test/completion/test_ale_import_command.vader @@ -0,0 +1,562 @@ +Before: + Save g:ale_enabled + Save b:ale_enabled + Save g:ale_lint_on_text_changed + Save g:ale_completion_enabled + Save g:ale_completion_autoimport + Save g:ale_completion_max_suggestions + Save g:ale_linters + Save b:ale_linters + + let g:ale_enabled = 0 + let b:ale_enabled = 0 + let g:ale_lint_on_text_changed = 'always' + let g:ale_completion_enabled = 0 + let g:ale_completion_autoimport = 0 + let g:ale_completion_max_suggestions = 50 + let g:ale_linters = {'typescript': ['tsserver'], 'python': ['pyre']} + unlet! b:ale_linters + + let g:server_started_value = 1 + let g:request_id = 0 + let g:LastCallback = v:null + let g:LastHandleCallback = v:null + let g:sent_message_list = [] + let g:code_action_list = [] + let g:execute_list = [] + let g:ale_queue_call_list = [] + + runtime autoload/ale.vim + runtime autoload/ale/util.vim + runtime autoload/ale/code_action.vim + runtime autoload/ale/lsp.vim + runtime autoload/ale/lsp_linter.vim + + function! ale#util#Execute(expr) abort + call add(g:execute_list, a:expr) + endfunction + + function! ale#Queue(...) abort + call add(g:ale_queue_call_list, a:000) + endfunction + + function! ale#lsp#RegisterCallback(id, Callback) abort + let g:LastHandleCallback = a:Callback + endfunction + + function! ale#lsp#NotifyForChanges(id, buffer) abort + endfunction + + function! ale#lsp#HasCapability(id, capability) abort + return 1 + endfunction + + function! ale#lsp#Send(id, message) abort + let g:request_id += 1 + + call add(g:sent_message_list, a:message) + + return g:request_id + endfunction + + function! ale#lsp_linter#StartLSP(buffer, linter, Callback) abort + let g:LastCallback = a:Callback + + return g:server_started_value + endfunction + + function! ale#code_action#HandleCodeAction(code_action, should_save) abort + Assert !a:should_save + + call add(g:code_action_list, a:code_action) + endfunction + + function GetLastMessage() + return get(g:execute_list, -1, '') + endfunction + + function CheckLintStates(conn_id, message) + " Check that we request more linter results after adding completions. + AssertEqual [[0, '']], g:ale_queue_call_list + + let g:ale_enabled = 0 + + let g:ale_queue_call_list = [] + call g:LastHandleCallback(a:conn_id, a:message) + AssertEqual [], g:ale_queue_call_list + + let g:ale_enabled = 1 + let g:ale_lint_on_text_changed = 1 + + let g:ale_queue_call_list = [] + call g:LastHandleCallback(a:conn_id, a:message) + AssertEqual [[0, '']], g:ale_queue_call_list + + let g:ale_lint_on_text_changed = 'normal' + + let g:ale_queue_call_list = [] + call g:LastHandleCallback(a:conn_id, a:message) + AssertEqual [[0, '']], g:ale_queue_call_list + + let g:ale_lint_on_text_changed = 'insert' + + let g:ale_queue_call_list = [] + call g:LastHandleCallback(a:conn_id, a:message) + AssertEqual [[0, '']], g:ale_queue_call_list + + let g:ale_queue_call_list = [] + let g:ale_lint_on_text_changed = 'never' + + call g:LastHandleCallback(a:conn_id, a:message) + AssertEqual [], g:ale_queue_call_list + + let g:ale_lint_on_text_changed = '0' + + call g:LastHandleCallback(a:conn_id, a:message) + AssertEqual [], g:ale_queue_call_list + + let g:ale_lint_on_text_changed = 0 + + call g:LastHandleCallback(a:conn_id, a:message) + AssertEqual [], g:ale_queue_call_list + + let g:ale_lint_on_text_changed = 'xxx' + + call g:LastHandleCallback(a:conn_id, a:message) + AssertEqual [], g:ale_queue_call_list + endfunction + +After: + call ale#linter#Reset() + + Restore + + delfunction GetLastMessage + delfunction CheckLintStates + + unlet! g:LastCallback + unlet! g:LastHandleCallback + unlet! g:request_id + unlet! g:server_started_value + unlet! g:sent_message_list + unlet! g:code_action_list + unlet! g:ale_queue_call_list + unlet! g:execute_list + unlet! g:received_message + unlet! b:ale_old_omnifunc + unlet! b:ale_old_completeopt + unlet! b:ale_completion_info + unlet! b:ale_completion_result + unlet! b:ale_complete_done_time + + runtime autoload/ale.vim + runtime autoload/ale/util.vim + runtime autoload/ale/code_action.vim + runtime autoload/ale/lsp.vim + runtime autoload/ale/lsp_linter.vim + +Given typescript(Some example TypeScript code): + let xyz = 123 + let foo = missingword + + let abc = 456 + +Execute(ALEImport should complain when there's no word at the cursor): + call setpos('.', [bufnr(''), 3, 1, 0]) + ALEImport + + AssertEqual 'echom ''Nothing to complete at cursor!''', GetLastMessage() + +Execute(ALEImport should tell the user if no LSP is available): + let g:server_started_value = 0 + + call setpos('.', [bufnr(''), 2, 16, 0]) + ALEImport + + AssertEqual + \ 'echom ''No completion providers are available.''', + \ GetLastMessage() + +Execute(ALEImport should request imports correctly for tsserver): + call setpos('.', [bufnr(''), 2, 16, 0]) + + ALEImport + + AssertEqual + \ { + \ 'conn_id': 0, + \ 'request_id': 0, + \ 'source': 'ale-import', + \ 'column': 11, + \ 'line': 2, + \ 'line_length': 21, + \ 'prefix': 'missingword', + \ 'additional_edits_only': 1, + \ }, + \ b:ale_completion_info + Assert g:LastCallback isnot v:null + + call g:LastCallback(ale#linter#Get(&filetype)[0], { + \ 'connection_id': 347, + \ 'buffer': bufnr(''), + \}) + + AssertEqual + \ { + \ 'conn_id': 347, + \ 'request_id': 1, + \ 'source': 'ale-import', + \ 'column': 11, + \ 'line': 2, + \ 'line_length': 21, + \ 'prefix': 'missingword', + \ 'additional_edits_only': 1, + \ }, + \ b:ale_completion_info + Assert g:LastHandleCallback isnot v:null + + call g:LastHandleCallback(347, { + \ 'request_seq': 1, + \ 'command': 'completions', + \ 'body': [ + \ {'name': 'missingwordIgnoreMe'}, + \ {'name': 'missingword'}, + \ ], + \}) + + AssertEqual + \ [ + \ [0, 'ts@completions', { + \ 'file': expand('%:p'), + \ 'includeExternalModuleExports': 1, + \ 'offset': 11, + \ 'line': 2, + \ 'prefix': 'missingword', + \ }], + \ [0, 'ts@completionEntryDetails', { + \ 'file': expand('%:p'), + \ 'entryNames': [{'name': 'missingword'}], + \ 'offset': 11, + \ 'line': 2, + \ }] + \ ], + \ g:sent_message_list + AssertEqual 2, b:ale_completion_info.request_id + + let g:ale_enabled = 1 + let g:received_message = { + \ 'request_seq': 2, + \ 'command': 'completionEntryDetails', + \ 'body': [ + \ { + \ 'name': 'missingword', + \ 'kind': 'className', + \ 'displayParts': [], + \ 'codeActions': [{ + \ 'description': 'import { missingword } from "./Something";', + \ 'changes': [], + \ }], + \ }, + \ ], + \} + call g:LastHandleCallback(347, g:received_message) + + AssertEqual + \ [ + \ { + \ 'description': 'import { missingword } from "./Something";', + \ 'changes': [], + \ }, + \ ], + \ g:code_action_list + + call CheckLintStates(347, g:received_message) + +Execute(ALEImport should tell the user when no completions were found from tsserver): + call setpos('.', [bufnr(''), 2, 16, 0]) + + ALEImport + + AssertEqual + \ { + \ 'conn_id': 0, + \ 'request_id': 0, + \ 'source': 'ale-import', + \ 'column': 11, + \ 'line': 2, + \ 'line_length': 21, + \ 'prefix': 'missingword', + \ 'additional_edits_only': 1, + \ }, + \ b:ale_completion_info + Assert g:LastCallback isnot v:null + + call g:LastCallback(ale#linter#Get(&filetype)[0], { + \ 'connection_id': 347, + \ 'buffer': bufnr(''), + \}) + + AssertEqual + \ { + \ 'conn_id': 347, + \ 'request_id': 1, + \ 'source': 'ale-import', + \ 'column': 11, + \ 'line': 2, + \ 'line_length': 21, + \ 'prefix': 'missingword', + \ 'additional_edits_only': 1, + \ }, + \ b:ale_completion_info + Assert g:LastHandleCallback isnot v:null + + call g:LastHandleCallback(347, { + \ 'request_seq': 1, + \ 'command': 'completions', + \ 'body': [ + \ {'name': 'missingwordIgnoreMe'}, + \ ], + \}) + + AssertEqual 'echom ''No possible imports found.''', GetLastMessage() + +Given python(Some example Python code): + xyz = 123 + foo = missingword + + abc = 456 + +Execute(ALEImport should request imports correctly for language servers): + call setpos('.', [bufnr(''), 2, 12, 0]) + + ALEImport + + AssertEqual + \ { + \ 'conn_id': 0, + \ 'request_id': 0, + \ 'source': 'ale-import', + \ 'column': 7, + \ 'line': 2, + \ 'line_length': 17, + \ 'prefix': 'missingword', + \ 'additional_edits_only': 1, + \ }, + \ b:ale_completion_info + Assert g:LastCallback isnot v:null + + call g:LastCallback(ale#linter#Get(&filetype)[0], { + \ 'connection_id': 347, + \ 'buffer': bufnr(''), + \}) + + AssertEqual + \ { + \ 'conn_id': 347, + \ 'request_id': 1, + \ 'source': 'ale-import', + \ 'column': 7, + \ 'line': 2, + \ 'line_length': 17, + \ 'prefix': 'missingword', + \ 'additional_edits_only': 1, + \ 'completion_filter': 'ale#completion#python#CompletionItemFilter', + \ }, + \ b:ale_completion_info + Assert g:LastHandleCallback isnot v:null + + AssertEqual + \ [ + \ [0, 'textDocument/completion', { + \ 'textDocument': {'uri': ale#path#ToURI(expand('%:p'))}, + \ 'position': {'character': 6, 'line': 1} + \ }], + \ ], + \ g:sent_message_list + AssertEqual 1, b:ale_completion_info.request_id + + let g:ale_enabled = 1 + let g:received_message = { + \ 'id': 1, + \ 'jsonrpc': '2.0', + \ 'result': { + \ 'isIncomplete': v:false, + \ 'items': [ + \ { + \ 'detail': 'Some other word we should ignore', + \ 'filterText': 'missingwordIgnoreMe', + \ 'insertText': 'missingwordIgnoreMe', + \ 'insertTextFormat': 1, + \ 'kind': 6, + \ 'label': ' missingwordIgnoreMe', + \ 'sortText': '3ee19999missingword', + \ 'additionalTextEdits': [ + \ { + \ 'range': { + \ 'start': {'line': 1, 'character': 1}, + \ 'end': {'line': 2, 'character': 1}, + \ }, + \ 'newText': 'from something import missingwordIgnoreMe', + \ }, + \ ], + \ }, + \ { + \ 'detail': 'Some word without text edits', + \ 'filterText': 'missingword', + \ 'insertText': 'missingword', + \ 'insertTextFormat': 1, + \ 'kind': 6, + \ 'label': ' missingword', + \ 'sortText': '3ee19999missingword', + \ }, + \ { + \ 'detail': 'The word we should use', + \ 'filterText': 'missingword', + \ 'insertText': 'missingword', + \ 'insertTextFormat': 1, + \ 'kind': 6, + \ 'label': ' missingword', + \ 'sortText': '3ee19999missingword', + \ 'additionalTextEdits': [ + \ { + \ 'range': { + \ 'start': {'line': 1, 'character': 1}, + \ 'end': {'line': 2, 'character': 1}, + \ }, + \ 'newText': 'from something import missingword', + \ }, + \ ], + \ }, + \ { + \ 'detail': 'The other word we should not use', + \ 'filterText': 'missingword', + \ 'insertText': 'missingword', + \ 'insertTextFormat': 1, + \ 'kind': 6, + \ 'label': ' missingword', + \ 'sortText': '3ee19999missingword', + \ 'additionalTextEdits': [ + \ { + \ 'range': { + \ 'start': {'line': 1, 'character': 1}, + \ 'end': {'line': 2, 'character': 1}, + \ }, + \ 'newText': 'from something_else import missingword', + \ }, + \ ], + \ }, + \ ], + \ }, + \} + call g:LastHandleCallback(347, g:received_message) + + AssertEqual + \ [ + \ { + \ 'description': 'completion', + \ 'changes': [ + \ { + \ 'fileName': expand('%:p'), + \ 'textChanges': [ + \ { + \ 'start': {'line': 2, 'offset': 2}, + \ 'end': {'line': 3, 'offset': 2}, + \ 'newText': 'from something import missingword', + \ }, + \ ], + \ }, + \ ], + \ }, + \ ], + \ g:code_action_list + + call CheckLintStates(347, g:received_message) + +Execute(ALEImport should tell the user when no completions were found from a language server): + call setpos('.', [bufnr(''), 2, 12, 0]) + + ALEImport + + AssertEqual + \ { + \ 'conn_id': 0, + \ 'request_id': 0, + \ 'source': 'ale-import', + \ 'column': 7, + \ 'line': 2, + \ 'line_length': 17, + \ 'prefix': 'missingword', + \ 'additional_edits_only': 1, + \ }, + \ b:ale_completion_info + Assert g:LastCallback isnot v:null + + call g:LastCallback(ale#linter#Get(&filetype)[0], { + \ 'connection_id': 347, + \ 'buffer': bufnr(''), + \}) + + AssertEqual + \ { + \ 'conn_id': 347, + \ 'request_id': 1, + \ 'source': 'ale-import', + \ 'column': 7, + \ 'line': 2, + \ 'line_length': 17, + \ 'prefix': 'missingword', + \ 'additional_edits_only': 1, + \ 'completion_filter': 'ale#completion#python#CompletionItemFilter', + \ }, + \ b:ale_completion_info + Assert g:LastHandleCallback isnot v:null + + AssertEqual + \ [ + \ [0, 'textDocument/completion', { + \ 'textDocument': {'uri': ale#path#ToURI(expand('%:p'))}, + \ 'position': {'character': 6, 'line': 1} + \ }], + \ ], + \ g:sent_message_list + AssertEqual 1, b:ale_completion_info.request_id + + let g:received_message = { + \ 'id': 1, + \ 'jsonrpc': '2.0', + \ 'result': { + \ 'isIncomplete': v:false, + \ 'items': [ + \ { + \ 'detail': 'Some other word we should ignore', + \ 'filterText': 'missingwordIgnoreMe', + \ 'insertText': 'missingwordIgnoreMe', + \ 'insertTextFormat': 1, + \ 'kind': 6, + \ 'label': ' missingwordIgnoreMe', + \ 'sortText': '3ee19999missingword', + \ 'additionalTextEdits': [ + \ { + \ 'range': { + \ 'start': {'line': 1, 'character': 1}, + \ 'end': {'line': 2, 'character': 1}, + \ }, + \ 'newText': 'from something import missingwordIgnoreMe', + \ }, + \ ], + \ }, + \ { + \ 'detail': 'Some word without text edits', + \ 'filterText': 'missingword', + \ 'insertText': 'missingword', + \ 'insertTextFormat': 1, + \ 'kind': 6, + \ 'label': ' missingword', + \ 'sortText': '3ee19999missingword', + \ }, + \ ], + \ }, + \} + call g:LastHandleCallback(347, g:received_message) + + AssertEqual 'echom ''No possible imports found.''', GetLastMessage() diff --git a/test/completion/test_completion_filtering.vader b/test/completion/test_completion_filtering.vader index c5f14266..172203a4 100644 --- a/test/completion/test_completion_filtering.vader +++ b/test/completion/test_completion_filtering.vader @@ -12,13 +12,24 @@ After: Execute(Prefix filtering should work for Lists of strings): AssertEqual \ ['FooBar', 'foo'], - \ ale#completion#Filter(bufnr(''), '', ['FooBar', 'FongBar', 'baz', 'foo'], 'foo') + \ ale#completion#Filter(bufnr(''), '', ['FooBar', 'FongBar', 'baz', 'foo'], 'foo', 0) AssertEqual \ ['FooBar', 'FongBar', 'baz', 'foo'], - \ ale#completion#Filter(bufnr(''), '', ['FooBar', 'FongBar', 'baz', 'foo'], '.') + \ ale#completion#Filter(bufnr(''), '', ['FooBar', 'FongBar', 'baz', 'foo'], '.', 0) AssertEqual \ ['FooBar', 'FongBar', 'baz', 'foo'], - \ ale#completion#Filter(bufnr(''), '', ['FooBar', 'FongBar', 'baz', 'foo'], '') + \ ale#completion#Filter(bufnr(''), '', ['FooBar', 'FongBar', 'baz', 'foo'], '', 0) + +Execute(Exact filtering should work): + AssertEqual + \ ['foo'], + \ ale#completion#Filter(bufnr(''), '', ['FooBar', 'FongBar', 'baz', 'foo'], 'foo', 1) + AssertEqual + \ ['FooBar', 'FongBar', 'baz', 'foo'], + \ ale#completion#Filter(bufnr(''), '', ['FooBar', 'FongBar', 'baz', 'foo'], '.', 1) + AssertEqual + \ ['Foo'], + \ ale#completion#Filter(bufnr(''), '', ['FooBar', 'FongBar', 'Foo', 'foo'], 'Foo', 1) Execute(Prefix filtering should work for completion items): AssertEqual @@ -32,7 +43,8 @@ Execute(Prefix filtering should work for completion items): \ {'word': 'baz'}, \ {'word': 'foo'}, \ ], - \ 'foo' + \ 'foo', + \ 0, \ ) AssertEqual @@ -51,7 +63,8 @@ Execute(Prefix filtering should work for completion items): \ {'word': 'baz'}, \ {'word': 'foo'}, \ ], - \ '.' + \ '.', + \ 0, \ ) Execute(Excluding words from completion results should work): @@ -66,7 +79,8 @@ Execute(Excluding words from completion results should work): \ {'word': 'Italian'}, \ {'word': 'it'}, \ ], - \ 'it' + \ 'it', + \ 0, \ ) AssertEqual @@ -78,7 +92,8 @@ Execute(Excluding words from completion results should work): \ {'word': 'describe'}, \ {'word': 'Deutsch'}, \ ], - \ 'de' + \ 'de', + \ 0, \ ) AssertEqual @@ -90,7 +105,8 @@ Execute(Excluding words from completion results should work): \ {'word': 'describe'}, \ {'word': 'Deutsch'}, \ ], - \ '.' + \ '.', + \ 0, \ ) Execute(Excluding words from completion results should work with lists of Strings): @@ -98,29 +114,29 @@ Execute(Excluding words from completion results should work with lists of String AssertEqual \ ['Italian'], - \ ale#completion#Filter(bufnr(''), '', ['Italian', 'it'], 'it') + \ ale#completion#Filter(bufnr(''), '', ['Italian', 'it'], 'it', 0) AssertEqual \ ['Deutsch'], - \ ale#completion#Filter(bufnr(''), '', ['describe', 'Deutsch'], 'de') + \ ale#completion#Filter(bufnr(''), '', ['describe', 'Deutsch'], 'de', 0) AssertEqual \ ['Deutsch'], - \ ale#completion#Filter(bufnr(''), '', ['describe', 'Deutsch'], '.') + \ ale#completion#Filter(bufnr(''), '', ['describe', 'Deutsch'], '.', 0) AssertEqual \ ['Deutsch'], - \ ale#completion#Filter(bufnr(''), '', ['Deutsch'], '') + \ ale#completion#Filter(bufnr(''), '', ['Deutsch'], '', 0) Execute(Filtering shouldn't modify the original list): let b:ale_completion_excluded_words = ['it', 'describe'] let b:suggestions = [{'word': 'describe'}] - AssertEqual [], ale#completion#Filter(bufnr(''), '', b:suggestions, '.') + AssertEqual [], ale#completion#Filter(bufnr(''), '', b:suggestions, '.', 0) AssertEqual b:suggestions, [{'word': 'describe'}] - AssertEqual [], ale#completion#Filter(bufnr(''), '', b:suggestions, 'de') + AssertEqual [], ale#completion#Filter(bufnr(''), '', b:suggestions, 'de', 0) AssertEqual b:suggestions, [{'word': 'describe'}] Execute(Filtering should respect filetype triggers): let b:suggestions = [{'word': 'describe'}] - AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), '', b:suggestions, '.') - AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), 'rust', b:suggestions, '.') - AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), 'rust', b:suggestions, '::') + AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), '', b:suggestions, '.', 0) + AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), 'rust', b:suggestions, '.', 0) + AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), 'rust', b:suggestions, '::', 0) From 417761b4157e1ce0fa230b4da1e489379ccd0512 Mon Sep 17 00:00:00 2001 From: Andrew Haust Date: Mon, 7 Sep 2020 02:24:39 -0400 Subject: [PATCH 06/12] Fix typo --- doc/ale-elixir.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ale-elixir.txt b/doc/ale-elixir.txt index 5864f728..de9daacf 100644 --- a/doc/ale-elixir.txt +++ b/doc/ale-elixir.txt @@ -6,7 +6,7 @@ ALE Elixir Integration *ale-elixir-options* mix *ale-elixir-mix* -The `mix` linter is disabled by default, as it can bee too expensive to run. +The `mix` linter is disabled by default, as it can be too expensive to run. See `:help g:ale_linters` From b4b75126f9eae30da8f5e0cb9ec100feb38c1cb6 Mon Sep 17 00:00:00 2001 From: w0rp Date: Mon, 7 Sep 2020 10:00:36 +0100 Subject: [PATCH 07/12] Fix a completion error --- autoload/ale/completion.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index 96415202..bdade95c 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -421,11 +421,11 @@ endfunction function! s:CompletionStillValid(request_id) abort let [l:line, l:column] = getpos('.')[1:2] - return ( + return has_key(b:, 'ale_completion_info') + \&& ( \ ale#util#Mode() is# 'i' \ || b:ale_completion_info.source is# 'ale-import' \) - \&& has_key(b:, 'ale_completion_info') \&& b:ale_completion_info.request_id == a:request_id \&& b:ale_completion_info.line == l:line \&& ( From 7d90ff56d96d601478f00895e009ae63a7d8b4bb Mon Sep 17 00:00:00 2001 From: w0rp Date: Tue, 8 Sep 2020 21:40:10 +0100 Subject: [PATCH 08/12] Close #3333 - Add an ALECompletePost event Add an `ALECompletePost` event along with everything needed to make it useful for its primary purpose: fixing code after inserting completions. * `ALEFix` can now be called with a bang (`!`) to suppress errors. * A new `ALELintStop` command lets you stop linting, and start it later. --- autoload/ale/completion.vim | 72 +++++++++--------- autoload/ale/engine.vim | 4 + autoload/ale/fix.vim | 26 ++++--- doc/ale.txt | 33 ++++++++ plugin/ale.vim | 4 +- rplugin/python3/deoplete/sources/ale.py | 5 +- test/completion/test_complete_events.vader | 35 +++++++++ test/completion/test_completion_events.vader | 48 ++++++++++-- .../test_lsp_completion_parsing.vader | 76 ++++++++++--------- .../test_tsserver_completion_parsing.vader | 11 ++- test/fix/test_ale_fix.vader | 24 ++++++ test/test_ale_lint_stop_command.vader | 27 +++++++ 12 files changed, 273 insertions(+), 92 deletions(-) create mode 100644 test/completion/test_complete_events.vader create mode 100644 test/test_ale_lint_stop_command.vader diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index bdade95c..ecd93600 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -503,17 +503,19 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort \ || g:ale_completion_autoimport, \ 'info': join(l:documentationParts, ''), \} + " This flag is used to tell if this completion came from ALE or not. + let l:user_data = {'_ale_completion_item': 1} if has_key(l:suggestion, 'codeActions') - let l:result.user_data = json_encode({ - \ 'codeActions': l:suggestion.codeActions, - \ }) + let l:user_data.code_actions = l:suggestion.codeActions endif + let l:result.user_data = json_encode(l:user_data) + " Include this item if we'll accept any items, " or if we only want items with additional edits, and this has them. if !get(l:info, 'additional_edits_only', 0) - \|| has_key(l:result, 'user_data') + \|| has_key(l:user_data, 'code_actions') call add(l:results, l:result) endif endfor @@ -534,6 +536,7 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort \ 'icase': 1, \ 'menu': '', \ 'info': '', + \ 'user_data': json_encode({'_ale_completion_item': 1}), \}) endfor endif @@ -610,6 +613,8 @@ function! ale#completion#ParseLSPCompletions(response) abort \ 'menu': get(l:item, 'detail', ''), \ 'info': (type(l:doc) is v:t_string ? l:doc : ''), \} + " This flag is used to tell if this completion came from ALE or not. + let l:user_data = {'_ale_completion_item': 1} if has_key(l:item, 'additionalTextEdits') let l:text_changes = [] @@ -629,24 +634,24 @@ function! ale#completion#ParseLSPCompletions(response) abort endfor if !empty(l:text_changes) - let l:result.user_data = json_encode({ - \ 'codeActions': [{ - \ 'description': 'completion', - \ 'changes': [ - \ { - \ 'fileName': expand('#' . l:buffer . ':p'), - \ 'textChanges': l:text_changes, - \ } - \ ], - \ }], - \}) + let l:user_data.code_actions = [{ + \ 'description': 'completion', + \ 'changes': [ + \ { + \ 'fileName': expand('#' . l:buffer . ':p'), + \ 'textChanges': l:text_changes, + \ }, + \ ], + \}] endif endif + let l:result.user_data = json_encode(l:user_data) + " Include this item if we'll accept any items, " or if we only want items with additional edits, and this has them. if !get(l:info, 'additional_edits_only', 0) - \|| has_key(l:result, 'user_data') + \|| has_key(l:user_data, 'code_actions') call add(l:results, l:result) endif endfor @@ -983,30 +988,29 @@ function! ale#completion#Queue() abort endfunction function! ale#completion#HandleUserData(completed_item) abort - let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') - - if l:source isnot# 'ale-automatic' - \&& l:source isnot# 'ale-manual' - \&& l:source isnot# 'ale-callback' - \&& l:source isnot# 'ale-import' - return - endif - let l:user_data_json = get(a:completed_item, 'user_data', '') - - if empty(l:user_data_json) - return - endif - - let l:user_data = json_decode(l:user_data_json) + let l:user_data = !empty(l:user_data_json) + \ ? json_decode(l:user_data_json) + \ : v:null if type(l:user_data) isnot v:t_dict + \|| get(l:user_data, '_ale_completion_item', 0) isnot 1 return endif - for l:code_action in get(l:user_data, 'codeActions', []) - call ale#code_action#HandleCodeAction(l:code_action, v:false) - endfor + let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') + + if l:source is# 'ale-automatic' + \|| l:source is# 'ale-manual' + \|| l:source is# 'ale-callback' + \|| l:source is# 'ale-import' + \|| l:source is# 'ale-omnifunc' + for l:code_action in get(l:user_data, 'code_actions', []) + call ale#code_action#HandleCodeAction(l:code_action, v:false) + endfor + endif + + silent doautocmd User ALECompletePost endfunction function! ale#completion#Done() abort diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index ae0354b8..63195d0f 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -458,6 +458,10 @@ function! s:StopCurrentJobs(buffer, clear_lint_file_jobs) abort endif endfunction +function! ale#engine#Stop(buffer) abort + call s:StopCurrentJobs(a:buffer, 1) +endfunction + function! s:RemoveProblemsForDisabledLinters(buffer, linters) abort " Figure out which linters are still enabled, and remove " problems for linters which are no longer enabled. diff --git a/autoload/ale/fix.vim b/autoload/ale/fix.vim index 8b841b13..c3338fc5 100644 --- a/autoload/ale/fix.vim +++ b/autoload/ale/fix.vim @@ -75,7 +75,10 @@ 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''' + + if !l:data.ignore_file_changed_errors + execute 'echoerr ''The file was changed before fixing finished''' + endif return endif @@ -329,6 +332,7 @@ function! ale#fix#InitBufferData(buffer, fixing_flag) abort \ 'lines_before': getbufline(a:buffer, 1, '$'), \ 'done': 0, \ 'should_save': a:fixing_flag is# 'save_file', + \ 'ignore_file_changed_errors': a:fixing_flag is# '!', \ 'temporary_directory_list': [], \} endfunction @@ -337,19 +341,23 @@ endfunction " " Returns 0 if no fixes can be applied, and 1 if fixing can be done. function! ale#fix#Fix(buffer, fixing_flag, ...) abort - if a:fixing_flag isnot# '' && a:fixing_flag isnot# 'save_file' - throw "fixing_flag must be either '' or 'save_file'" + if a:fixing_flag isnot# '' + \&& a:fixing_flag isnot# '!' + \&& a:fixing_flag isnot# 'save_file' + throw "fixing_flag must be '', '!', or 'save_file'" endif try let l:callback_list = s:GetCallbacks(a:buffer, a:fixing_flag, a:000) catch /E700\|BADNAME/ - let l:function_name = join(split(split(v:exception, ':')[3])) - let l:echo_message = printf( - \ 'There is no fixer named `%s`. Check :ALEFixSuggest', - \ l:function_name, - \) - execute 'echom l:echo_message' + if a:fixing_flag isnot# '!' + let l:function_name = join(split(split(v:exception, ':')[3])) + let l:echo_message = printf( + \ 'There is no fixer named `%s`. Check :ALEFixSuggest', + \ l:function_name, + \) + execute 'echom l:echo_message' + endif return 0 endtry diff --git a/doc/ale.txt b/doc/ale.txt index 7549dc01..e6b91be8 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -147,6 +147,8 @@ ALE offers several options for controlling which linters are run. * Disabling only a subset of linters. - |g:ale_linters_ignore| * Disabling LSP linters and `tsserver`. - |g:ale_disable_lsp| +You can stop ALE any currently running linters with the |ALELintStop| command. +Any existing problems will be kept. ------------------------------------------------------------------------------- 3.1 Linting On Other Machines *ale-lint-other-machines* @@ -532,6 +534,9 @@ potential completion result which includes additional text to insert into the current buffer, which ALE will assume is code for an import line. This command can be useful when your code already contains something you need to import. +You can execute other commands whenever ALE inserts some completion text with +the |ALECompletePost| event. + When working with TypeScript files, ALE can remove warnings from your completions by setting the |g:ale_completion_tsserver_remove_warnings| variable to 1. @@ -2976,6 +2981,10 @@ ALEFix *ALEFix* Fix problems with the current buffer. See |ale-fix| for more information. + If the command is run with a bang (`:ALEFix!`), all warnings will be + suppressed, including warnings about no fixers being defined, and warnings + about not being able to apply fixes to a file because it has been changed. + A plug mapping `(ale_fix)` is defined for this command. @@ -3115,6 +3124,13 @@ ALELint *ALELint* A plug mapping `(ale_lint)` is defined for this command. +ALELintStop *ALELintStop* + + Stop any currently running jobs for checking the current buffer. + + Any problems from previous linter results will continue to be shown. + + ALEPrevious *ALEPrevious* ALEPreviousWrap *ALEPreviousWrap* ALENext *ALENext* @@ -3980,6 +3996,23 @@ g:ale_want_results_buffer *g:ale_want_results_buffer* figure out which buffer other sources should lint. +ALECompletePost *ALECompletePost-autocmd* + *ALECompletePost* + + This |User| autocmd is triggered after ALE inserts an item on + |CompleteDone|. This event can be used to run commands after a buffer + is changed by ALE as the result of completion. For example, |ALEFix| can + be configured to run automatically when completion is done: > + + augroup FixAfterComplete + autocmd! + " Run ALEFix when completion items are added. + autocmd User ALECompletePost ALEFix! + " If ALE starts fixing a file, stop linters running for now. + autocmd User ALEFixPre ALELintStop + augroup END +< + ALELintPre *ALELintPre-autocmd* *ALELintPre* ALELintPost *ALELintPost-autocmd* diff --git a/plugin/ale.vim b/plugin/ale.vim index 9cebb71f..18d867ee 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -195,6 +195,8 @@ command! -bar ALEStopAllLSPs :call ale#lsp#reset#StopAllLSPs() " A command for linting manually. command! -bar ALELint :call ale#Queue(0, 'lint_file') +" Stop current jobs when linting. +command! -bar ALELintStop :call ale#engine#Stop(bufnr('')) " Define a command to get information about current filetype. command! -bar ALEInfo :call ale#debugging#Info() @@ -204,7 +206,7 @@ command! -bar ALEInfoToClipboard :call ale#debugging#InfoToClipboard() command! -bar -nargs=1 ALEInfoToFile :call ale#debugging#InfoToFile() " Fix problems in files. -command! -bar -nargs=* -complete=customlist,ale#fix#registry#CompleteFixers ALEFix :call ale#fix#Fix(bufnr(''), '', ) +command! -bar -bang -nargs=* -complete=customlist,ale#fix#registry#CompleteFixers ALEFix :call ale#fix#Fix(bufnr(''), '', ) " Suggest registered functions to use for fixing problems. command! -bar ALEFixSuggest :call ale#fix#registry#Suggest(&filetype) diff --git a/rplugin/python3/deoplete/sources/ale.py b/rplugin/python3/deoplete/sources/ale.py index ae1f4039..82d9bbf2 100644 --- a/rplugin/python3/deoplete/sources/ale.py +++ b/rplugin/python3/deoplete/sources/ale.py @@ -49,12 +49,13 @@ class Source(Base): if event == 'Async': result = self.vim.call('ale#completion#GetCompletionResult') + return result or [] if context.get('is_refresh'): self.vim.command( - "call ale#completion#GetCompletions('ale-callback', " + \ - "{'callback': {completions -> deoplete#auto_complete() }})" + "call ale#completion#GetCompletions('ale-callback', " + + "{'callback': {completions -> deoplete#auto_complete() }})" ) return [] diff --git a/test/completion/test_complete_events.vader b/test/completion/test_complete_events.vader new file mode 100644 index 00000000..cee15985 --- /dev/null +++ b/test/completion/test_complete_events.vader @@ -0,0 +1,35 @@ +Before: + let g:complete_post_triggered = 0 + + augroup VaderTest + autocmd! + autocmd User ALECompletePost let g:complete_post_triggered = 1 + augroup END + +After: + unlet! b:ale_completion_info + unlet! g:complete_post_triggered + + augroup VaderTest + autocmd! + augroup END + + augroup! VaderTest + +Execute(ALECompletePost should not be triggered when completion is cancelled): + call ale#completion#HandleUserData({}) + + Assert !g:complete_post_triggered + +Execute(ALECompletePost should not be triggered when tools other than ALE insert completions): + call ale#completion#HandleUserData({'user_data': ''}) + call ale#completion#HandleUserData({'user_data': '{}'}) + + Assert !g:complete_post_triggered + +Execute(ALECompletePost should be triggered when ALE inserts completions): + call ale#completion#HandleUserData({ + \ 'user_data': json_encode({'_ale_completion_item': 1}), + \}) + + Assert g:complete_post_triggered diff --git a/test/completion/test_completion_events.vader b/test/completion/test_completion_events.vader index 87bd10ad..f678e773 100644 --- a/test/completion/test_completion_events.vader +++ b/test/completion/test_completion_events.vader @@ -407,41 +407,75 @@ Execute(HandleUserData should call ale#code_action#HandleCodeAction): AssertEqual g:handle_code_action_called, 0 call ale#completion#HandleUserData({ - \ 'user_data': '' + \ 'user_data': '' \}) AssertEqual g:handle_code_action_called, 0 call ale#completion#HandleUserData({ - \ 'user_data': '{}' + \ 'user_data': json_encode({}), \}) AssertEqual g:handle_code_action_called, 0 call ale#completion#HandleUserData({ - \ 'user_data': '{"codeActions": []}' + \ 'user_data': json_encode({ + \ '_ale_completion_item': 1, + \ 'code_actions': [], + \ }), \}) AssertEqual g:handle_code_action_called, 0 call ale#completion#HandleUserData({ - \ 'user_data': '{"codeActions": [{"description":"", "changes": []}]}' + \ 'user_data': json_encode({ + \ '_ale_completion_item': 1, + \ 'code_actions': [ + \ {'description': '', 'changes': []}, + \ ], + \ }), \}) AssertEqual g:handle_code_action_called, 1 let b:ale_completion_info = {'source': 'ale-automatic'} call ale#completion#HandleUserData({ - \ 'user_data': '{"codeActions": [{"description":"", "changes": []}]}' + \ 'user_data': json_encode({ + \ '_ale_completion_item': 1, + \ 'code_actions': [ + \ {'description': '', 'changes': []}, + \ ], + \ }), \}) AssertEqual g:handle_code_action_called, 2 let b:ale_completion_info = {'source': 'ale-callback'} call ale#completion#HandleUserData({ - \ 'user_data': '{"codeActions": [{"description":"", "changes": []}]}' + \ 'user_data': json_encode({ + \ '_ale_completion_item': 1, + \ 'code_actions': [ + \ {'description': '', 'changes': []}, + \ ], + \ }), \}) AssertEqual g:handle_code_action_called, 3 + let b:ale_completion_info = {'source': 'ale-omnifunc'} + call ale#completion#HandleUserData({ + \ 'user_data': json_encode({ + \ '_ale_completion_item': 1, + \ 'code_actions': [ + \ {'description': '', 'changes': []}, + \ ], + \ }), + \}) + AssertEqual g:handle_code_action_called, 4 + Execute(ale#code_action#HandleCodeAction should not be called when when source is not ALE): call MockHandleCodeAction() let b:ale_completion_info = {'source': 'syntastic'} call ale#completion#HandleUserData({ - \ 'user_data': '{"codeActions": [{"description":"", "changes": []}]}' + \ 'user_data': json_encode({ + \ '_ale_completion_item': 1, + \ 'code_actions': [ + \ {'description': '', 'changes': []}, + \ ], + \ }), \}) AssertEqual g:handle_code_action_called, 0 diff --git a/test/completion/test_lsp_completion_parsing.vader b/test/completion/test_lsp_completion_parsing.vader index b8e71320..d989aefe 100644 --- a/test/completion/test_lsp_completion_parsing.vader +++ b/test/completion/test_lsp_completion_parsing.vader @@ -12,34 +12,34 @@ After: Execute(Should handle Rust completion results correctly): AssertEqual \ [ - \ {'word': 'new', 'menu': 'pub fn new() -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'with_capacity', 'menu': 'pub fn with_capacity(capacity: usize) -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from_utf8', 'menu': 'pub fn from_utf8(vec: Vec) -> Result', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from_utf8_lossy', 'menu': 'pub fn from_utf8_lossy<''a>(v: &''a [u8]) -> Cow<''a, str>', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from_utf16', 'menu': 'pub fn from_utf16(v: &[u16]) -> Result', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from_utf16_lossy', 'menu': 'pub fn from_utf16_lossy(v: &[u16]) -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from_raw_parts', 'menu': 'pub unsafe fn from_raw_parts(buf: *mut u8, length: usize, capacity: usize) -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from_utf8_unchecked', 'menu': 'pub unsafe fn from_utf8_unchecked(bytes: Vec) -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from_iter', 'menu': 'fn from_iter>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from_iter', 'menu': 'fn from_iter>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from_iter', 'menu': 'fn from_iter>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from_iter', 'menu': 'fn from_iter>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from_iter', 'menu': 'fn from_iter>>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'Searcher', 'menu': 'type Searcher = <&''b str as Pattern<''a>>::Searcher;', 'info': '', 'kind': 't', 'icase': 1}, - \ {'word': 'default', 'menu': 'fn default() -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'Output', 'menu': 'type Output = String;', 'info': '', 'kind': 't', 'icase': 1}, - \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 't', 'icase': 1}, - \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 't', 'icase': 1}, - \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 't', 'icase': 1}, - \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 't', 'icase': 1}, - \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 't', 'icase': 1}, - \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 't', 'icase': 1}, - \ {'word': 'Target', 'menu': 'type Target = str;', 'info': '', 'kind': 't', 'icase': 1}, - \ {'word': 'Err', 'menu': 'type Err = ParseError;', 'info': '', 'kind': 't', 'icase': 1}, - \ {'word': 'from_str', 'menu': 'fn from_str(s: &str) -> Result', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from', 'menu': 'fn from(s: &''a str) -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from', 'menu': 'fn from(s: Box) -> String', 'info': '', 'kind': 'f', 'icase': 1}, - \ {'word': 'from', 'menu': 'fn from(s: Cow<''a, str>) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'new', 'menu': 'pub fn new() -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'with_capacity', 'menu': 'pub fn with_capacity(capacity: usize) -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from_utf8', 'menu': 'pub fn from_utf8(vec: Vec) -> Result', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from_utf8_lossy', 'menu': 'pub fn from_utf8_lossy<''a>(v: &''a [u8]) -> Cow<''a, str>', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from_utf16', 'menu': 'pub fn from_utf16(v: &[u16]) -> Result', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from_utf16_lossy', 'menu': 'pub fn from_utf16_lossy(v: &[u16]) -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from_raw_parts', 'menu': 'pub unsafe fn from_raw_parts(buf: *mut u8, length: usize, capacity: usize) -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from_utf8_unchecked', 'menu': 'pub unsafe fn from_utf8_unchecked(bytes: Vec) -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from_iter', 'menu': 'fn from_iter>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from_iter', 'menu': 'fn from_iter>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from_iter', 'menu': 'fn from_iter>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from_iter', 'menu': 'fn from_iter>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from_iter', 'menu': 'fn from_iter>>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'Searcher', 'menu': 'type Searcher = <&''b str as Pattern<''a>>::Searcher;', 'info': '', 'kind': 't', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'default', 'menu': 'fn default() -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'Output', 'menu': 'type Output = String;', 'info': '', 'kind': 't', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 't', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 't', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 't', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 't', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 't', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 't', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'Target', 'menu': 'type Target = str;', 'info': '', 'kind': 't', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'Err', 'menu': 'type Err = ParseError;', 'info': '', 'kind': 't', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from_str', 'menu': 'fn from_str(s: &str) -> Result', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from', 'menu': 'fn from(s: &''a str) -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from', 'menu': 'fn from(s: Box) -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'from', 'menu': 'fn from(s: Cow<''a, str>) -> String', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, \], \ ale#completion#ParseLSPCompletions({ \ "jsonrpc":"2.0", @@ -195,7 +195,7 @@ Execute(Should handle Python completion results correctly): AssertEqual \ [ - \ {'word': 'what', 'menu': 'example-python-project.bar.Bar', 'info': "what()\n\n", 'kind': 'f', 'icase': 1}, + \ {'word': 'what', 'menu': 'example-python-project.bar.Bar', 'info': "what()\n\n", 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, \ ], \ ale#completion#ParseLSPCompletions({ \ "jsonrpc":"2.0", @@ -399,7 +399,7 @@ Execute(Should handle Python completion results correctly): \ } \ }) -Execute(Should handle Python completion results correctly): +Execute(Should handle extra Python completion results correctly): let b:ale_completion_info = { \ 'completion_filter': 'ale#completion#python#CompletionItemFilter', \ 'prefix': 'mig', @@ -407,8 +407,8 @@ Execute(Should handle Python completion results correctly): AssertEqual \ [ - \ {'word': 'migrations', 'menu': 'xxx', 'info': 'migrations', 'kind': 'f', 'icase': 1}, - \ {'word': 'MigEngine', 'menu': 'xxx', 'info': 'mig engine', 'kind': 'f', 'icase': 1}, + \ {'word': 'migrations', 'menu': 'xxx', 'info': 'migrations', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, + \ {'word': 'MigEngine', 'menu': 'xxx', 'info': 'mig engine', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, \ ], \ ale#completion#ParseLSPCompletions({ \ 'jsonrpc': '2.0', @@ -441,7 +441,7 @@ Execute(Should handle Python completion results correctly): Execute(Should handle missing keys): AssertEqual \ [ - \ {'word': 'x', 'menu': '', 'info': '', 'kind': 'v', 'icase': 1}, + \ {'word': 'x', 'menu': '', 'info': '', 'kind': 'v', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, \ ], \ ale#completion#ParseLSPCompletions({ \ 'jsonrpc': '2.0', @@ -459,7 +459,7 @@ Execute(Should handle missing keys): Execute(Should handle documentation in the markdown format): AssertEqual \ [ - \ {'word': 'migrations', 'menu': 'xxx', 'info': 'Markdown documentation', 'kind': 'f', 'icase': 1}, + \ {'word': 'migrations', 'menu': 'xxx', 'info': 'Markdown documentation', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, \ ], \ ale#completion#ParseLSPCompletions({ \ 'jsonrpc': '2.0', @@ -483,7 +483,7 @@ Execute(Should handle documentation in the markdown format): Execute(Should handle completion messages with textEdit objects): AssertEqual \ [ - \ {'word': 'next_callback', 'menu': 'PlayTimeCallback', 'info': '', 'kind': 'v', 'icase': 1}, + \ {'word': 'next_callback', 'menu': 'PlayTimeCallback', 'info': '', 'kind': 'v', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, \ ], \ ale#completion#ParseLSPCompletions({ \ 'id': 226, @@ -514,7 +514,7 @@ Execute(Should handle completion messages with textEdit objects): Execute(Should handle completion messages with the deprecated insertText attribute): AssertEqual \ [ - \ {'word': 'next_callback', 'menu': 'PlayTimeCallback', 'info': '', 'kind': 'v', 'icase': 1}, + \ {'word': 'next_callback', 'menu': 'PlayTimeCallback', 'info': '', 'kind': 'v', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, \ ], \ ale#completion#ParseLSPCompletions({ \ 'id': 226, @@ -547,7 +547,8 @@ Execute(Should handle completion messages with additionalTextEdits when ale_comp \ 'kind': 'v', \ 'icase': 1, \ 'user_data': json_encode({ - \ 'codeActions': [ + \ '_ale_completion_item': 1, + \ 'code_actions': [ \ { \ 'description': 'completion', \ 'changes': [ @@ -658,6 +659,7 @@ Execute(Should still handle completion messages with empty additionalTextEdits w \ 'info': '', \ 'kind': 'v', \ 'icase': 1, + \ 'user_data': json_encode({'_ale_completion_item': 1}), \ } \ ], \ ale#completion#ParseLSPCompletions({ diff --git a/test/completion/test_tsserver_completion_parsing.vader b/test/completion/test_tsserver_completion_parsing.vader index aaaaae95..231c0f95 100644 --- a/test/completion/test_tsserver_completion_parsing.vader +++ b/test/completion/test_tsserver_completion_parsing.vader @@ -90,6 +90,7 @@ Execute(TypeScript completion details responses should be parsed correctly): \ 'info': '', \ 'kind': 'v', \ 'icase': 1, + \ 'user_data': json_encode({'_ale_completion_item': 1}), \ 'dup': g:ale_completion_autoimport, \ }, \ { @@ -98,6 +99,7 @@ Execute(TypeScript completion details responses should be parsed correctly): \ 'info': 'foo bar baz', \ 'kind': 'v', \ 'icase': 1, + \ 'user_data': json_encode({'_ale_completion_item': 1}), \ 'dup': g:ale_completion_autoimport, \ }, \ { @@ -106,6 +108,7 @@ Execute(TypeScript completion details responses should be parsed correctly): \ 'info': '', \ 'kind': 'v', \ 'icase': 1, + \ 'user_data': json_encode({'_ale_completion_item': 1}), \ 'dup': g:ale_completion_autoimport, \ }, \ ], @@ -179,7 +182,8 @@ Execute(Entries without details should be included in the responses): \ 'kind': 'v', \ 'icase': 1, \ 'user_data': json_encode({ - \ 'codeActions': [{ + \ '_ale_completion_item': 1, + \ 'code_actions': [{ \ 'description': 'import { def } from "./Foo";', \ 'changes': [], \ }], @@ -192,6 +196,7 @@ Execute(Entries without details should be included in the responses): \ 'info': 'foo bar baz', \ 'kind': 'v', \ 'icase': 1, + \ 'user_data': json_encode({'_ale_completion_item': 1}), \ 'dup': g:ale_completion_autoimport, \ }, \ { @@ -199,6 +204,7 @@ Execute(Entries without details should be included in the responses): \ 'menu': '', \ 'info': '', \ 'kind': 'v', + \ 'user_data': json_encode({'_ale_completion_item': 1}), \ 'icase': 1, \ }, \ ], @@ -260,7 +266,8 @@ Execute(Default imports should be handled correctly): \ 'kind': 't', \ 'icase': 1, \ 'user_data': json_encode({ - \ 'codeActions': [{ + \ '_ale_completion_item': 1, + \ 'code_actions': [{ \ 'description': 'Import default ''abcd'' from module "./foo"', \ 'changes': [], \ }], diff --git a/test/fix/test_ale_fix.vader b/test/fix/test_ale_fix.vader index a51a5a53..128e3a14 100644 --- a/test/fix/test_ale_fix.vader +++ b/test/fix/test_ale_fix.vader @@ -196,6 +196,10 @@ After: " Clear the messages between tests. echomsg '' + if !exists('g:ale_command_wrapper') + let g:ale_command_wrapper = '' + endif + Given testft (A file with three lines): a b @@ -206,6 +210,13 @@ Execute(ALEFix should complain when there are no functions to call): call ale#test#FlushJobs() AssertEqual 'No fixers have been defined. Try :ALEFixSuggest', GetLastMessage() +Execute(ALEFix should not complain when the command is run with a bang): + echom 'none' + + ALEFix! + call ale#test#FlushJobs() + AssertEqual 'none', GetLastMessage() + Execute(ALEFix should apply simple functions): let g:ale_fixers.testft = ['AddCarets'] ALEFix @@ -715,6 +726,19 @@ Execute(ale#fix#InitBufferData() should set up the correct data): \ 'done': 0, \ 'lines_before': ['a', 'b', 'c'], \ 'should_save': 1, + \ 'ignore_file_changed_errors': 0, + \ }, + \}, g:ale_fix_buffer_data + + call ale#fix#InitBufferData(bufnr(''), '!') + + AssertEqual { + \ bufnr(''): { + \ 'temporary_directory_list': [], + \ 'done': 0, + \ 'lines_before': ['a', 'b', 'c'], + \ 'should_save': 0, + \ 'ignore_file_changed_errors': 1, \ }, \}, g:ale_fix_buffer_data diff --git a/test/test_ale_lint_stop_command.vader b/test/test_ale_lint_stop_command.vader new file mode 100644 index 00000000..c50db5a8 --- /dev/null +++ b/test/test_ale_lint_stop_command.vader @@ -0,0 +1,27 @@ +Before: + Save g:ale_buffer_info + + let g:ale_buffer_info = {} + + call ale#linter#PreventLoading('testft') + call ale#linter#Define('testft', { + \ 'name': 'testlinter', + \ 'callback': {-> []}, + \ 'executable': has('win32') ? 'cmd' : 'true', + \ 'command': 'sleep 9001', + \}) + +After: + Restore + + call ale#linter#Reset() + +Given testft (An empty file): +Execute(ALELintStop should stop ALE from linting): + ALELint + + Assert ale#engine#IsCheckingBuffer(bufnr('')), 'ALE did not start checking the buffer' + + ALELintStop + + Assert !ale#engine#IsCheckingBuffer(bufnr('')), 'ALELintStop didn''t work' From 6a367e44aa0feddd63b0e6d9505e21ca4442247e Mon Sep 17 00:00:00 2001 From: w0rp Date: Tue, 8 Sep 2020 22:19:13 +0100 Subject: [PATCH 09/12] Close #3003 - Show ignored linters in :ALEInfo --- autoload/ale.vim | 8 +------- autoload/ale/debugging.vim | 13 +++++++++++++ autoload/ale/linter.vim | 10 ++++++++++ test/test_ale_info.vader | 27 +++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/autoload/ale.vim b/autoload/ale.vim index 5ec22f57..84003993 100644 --- a/autoload/ale.vim +++ b/autoload/ale.vim @@ -100,13 +100,7 @@ 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:disable_lsp = ale#Var(a:buffer, 'disable_lsp') - let l:linters = !empty(l:ignore_config) || l:disable_lsp - \ ? ale#engine#ignore#Exclude(l:filetype, l:linters, l:ignore_config, l:disable_lsp) - \ : l:linters + let l:linters = ale#linter#RemoveIgnored(a:buffer, l:filetype, l:linters) " Tell other sources that they can start checking the buffer now. let g:ale_want_results_buffer = a:buffer diff --git a/autoload/ale/debugging.vim b/autoload/ale/debugging.vim index 4e134f8c..5e6d5906 100644 --- a/autoload/ale/debugging.vim +++ b/autoload/ale/debugging.vim @@ -8,6 +8,7 @@ let s:global_variable_list = [ \ 'ale_completion_delay', \ 'ale_completion_enabled', \ 'ale_completion_max_suggestions', +\ 'ale_disable_lsp', \ 'ale_echo_cursor', \ 'ale_echo_msg_error_str', \ 'ale_echo_msg_format', @@ -28,6 +29,7 @@ let s:global_variable_list = [ \ 'ale_linter_aliases', \ 'ale_linters', \ 'ale_linters_explicit', +\ 'ale_linters_ignore', \ 'ale_list_vertical', \ 'ale_list_window_size', \ 'ale_loclist_msg_format', @@ -196,6 +198,7 @@ function! s:EchoLSPErrorMessages(all_linter_names) abort endfunction function! ale#debugging#Info() abort + let l:buffer = bufnr('') let l:filetype = &filetype " We get the list of enabled linters for free by the above function. @@ -222,10 +225,20 @@ function! ale#debugging#Info() abort let l:fixers = uniq(sort(l:fixers[0] + l:fixers[1])) let l:fixers_string = join(map(copy(l:fixers), '"\n " . v:val'), '') + let l:non_ignored_names = map( + \ copy(ale#linter#RemoveIgnored(l:buffer, l:filetype, l:enabled_linters)), + \ 'v:val[''name'']', + \) + let l:ignored_names = filter( + \ copy(l:enabled_names), + \ 'index(l:non_ignored_names, v:val) < 0' + \) + call s:Echo(' Current Filetype: ' . l:filetype) call s:Echo('Available Linters: ' . string(l:all_names)) call s:EchoLinterAliases(l:all_linters) call s:Echo(' Enabled Linters: ' . string(l:enabled_names)) + call s:Echo(' Ignored Linters: ' . string(l:ignored_names)) call s:Echo(' Suggested Fixers: ' . l:fixers_string) call s:Echo(' Linter Variables:') call s:Echo('') diff --git a/autoload/ale/linter.vim b/autoload/ale/linter.vim index b483fc19..645c25f9 100644 --- a/autoload/ale/linter.vim +++ b/autoload/ale/linter.vim @@ -394,6 +394,16 @@ function! ale#linter#Get(original_filetypes) abort return reverse(l:combined_linters) endfunction +function! ale#linter#RemoveIgnored(buffer, filetype, linters) abort + " Apply ignore lists for linters only if needed. + let l:ignore_config = ale#Var(a:buffer, 'linters_ignore') + let l:disable_lsp = ale#Var(a:buffer, 'disable_lsp') + + return !empty(l:ignore_config) || l:disable_lsp + \ ? ale#engine#ignore#Exclude(a:filetype, a:linters, l:ignore_config, l:disable_lsp) + \ : a:linters +endfunction + " Given a buffer and linter, get the executable String for the linter. function! ale#linter#GetExecutable(buffer, linter) abort let l:Executable = a:linter.executable diff --git a/test/test_ale_info.vader b/test/test_ale_info.vader index 2bc8c635..895ed2a7 100644 --- a/test/test_ale_info.vader +++ b/test/test_ale_info.vader @@ -6,6 +6,7 @@ Before: Save g:ale_completion_delay Save g:ale_completion_enabled Save g:ale_completion_max_suggestions + Save g:ale_disable_lsp Save g:ale_echo_cursor Save g:ale_echo_msg_error_str Save g:ale_echo_msg_format @@ -24,6 +25,7 @@ Before: Save g:ale_lint_on_text_changed Save g:ale_linters Save g:ale_linters_explicit + Save g:ale_linters_ignore Save g:ale_list_vertical Save g:ale_list_window_size Save g:ale_loclist_msg_format @@ -64,6 +66,7 @@ Before: let g:ale_completion_delay = 100 let g:ale_completion_enabled = 0 let g:ale_completion_max_suggestions = 50 + let g:ale_disable_lsp = 0 let g:ale_echo_cursor = 1 let g:ale_echo_msg_error_str = 'Error' let g:ale_echo_msg_format = '%code: %%s' @@ -80,6 +83,7 @@ Before: let g:ale_lint_on_save = 1 let g:ale_lint_on_text_changed = 'normal' let g:ale_linters_explicit = 0 + let g:ale_linters_ignore = {'python': ['pyright']} let g:ale_list_vertical = 0 let g:ale_list_window_size = 10 let g:ale_loclist_msg_format = '%code: %%s' @@ -138,6 +142,7 @@ Before: \ 'let g:ale_completion_delay = 100', \ 'let g:ale_completion_enabled = 0', \ 'let g:ale_completion_max_suggestions = 50', + \ 'let g:ale_disable_lsp = 0', \ 'let g:ale_echo_cursor = 1', \ 'let g:ale_echo_msg_error_str = ''Error''', \ 'let g:ale_echo_msg_format = ''%code: %%s''', @@ -158,6 +163,7 @@ Before: \ 'let g:ale_linter_aliases = {}', \ 'let g:ale_linters = {}', \ 'let g:ale_linters_explicit = 0', + \ 'let g:ale_linters_ignore = {''python'': [''pyright'']}', \ 'let g:ale_list_vertical = 0', \ 'let g:ale_list_window_size = 10', \ 'let g:ale_loclist_msg_format = ''%code: %%s''', @@ -243,6 +249,7 @@ Execute (ALEInfo with no linters should return the right output): \ ' Current Filetype: nolintersft', \ 'Available Linters: []', \ ' Enabled Linters: []', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -265,6 +272,7 @@ Execute (ALEInfo should return buffer-local global ALE settings): \ ' Current Filetype: ', \ 'Available Linters: []', \ ' Enabled Linters: []', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -279,6 +287,7 @@ Execute (ALEInfo with no filetype should return the right output): \ ' Current Filetype: ', \ 'Available Linters: []', \ ' Enabled Linters: []', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -295,6 +304,7 @@ Execute (ALEInfo with a single linter should return the right output): \ ' Current Filetype: testft', \ 'Available Linters: [''testlinter1'']', \ ' Enabled Linters: [''testlinter1'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -312,6 +322,7 @@ Execute (ALEInfo with two linters should return the right output): \ ' Current Filetype: testft', \ 'Available Linters: [''testlinter1'', ''testlinter2'']', \ ' Enabled Linters: [''testlinter1'', ''testlinter2'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -333,6 +344,7 @@ Execute (ALEInfo should calculate enabled linters correctly): \ ' Current Filetype: testft', \ 'Available Linters: [''testlinter1'', ''testlinter2'']', \ ' Enabled Linters: [''testlinter2'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -350,6 +362,7 @@ Execute (ALEInfo should only return linters for current filetype): \ ' Current Filetype: testft', \ 'Available Linters: [''testlinter1'']', \ ' Enabled Linters: [''testlinter1'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -367,6 +380,7 @@ Execute (ALEInfo with compound filetypes should return linters for both of them) \ ' Current Filetype: testft.testft2', \ 'Available Linters: [''testlinter1'', ''testlinter2'']', \ ' Enabled Linters: [''testlinter1'', ''testlinter2'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -389,6 +403,7 @@ Execute (ALEInfo should return appropriately named global variables): \ ' Current Filetype: testft.testft2', \ 'Available Linters: [''testlinter1'', ''testlinter2'']', \ ' Enabled Linters: [''testlinter1'', ''testlinter2'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + [ @@ -420,6 +435,7 @@ Execute (ALEInfoToFile should write to a file correctly): \ ' Current Filetype: testft.testft2', \ 'Available Linters: [''testlinter1'', ''testlinter2'']', \ ' Enabled Linters: [''testlinter1'', ''testlinter2'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + [ @@ -447,6 +463,7 @@ Execute (ALEInfo should buffer-local linter variables): \ ' Current Filetype: testft.testft2', \ 'Available Linters: [''testlinter1'', ''testlinter2'']', \ ' Enabled Linters: [''testlinter1'', ''testlinter2'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + [ @@ -478,6 +495,7 @@ Execute (ALEInfo should output linter aliases): \ '''testlinter1'' -> [''testftalias1'', ''testftalias2'']', \ '''testlinter2'' -> [''testftalias3'', ''testftalias4'']', \ ' Enabled Linters: [''testlinter1'', ''testlinter2'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + [ @@ -505,6 +523,7 @@ Execute (ALEInfo should return command history): \ ' Current Filetype: testft.testft2', \ 'Available Linters: [''testlinter1'', ''testlinter2'']', \ ' Enabled Linters: [''testlinter1'', ''testlinter2'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -532,6 +551,7 @@ Execute (ALEInfo command history should print exit codes correctly): \ ' Current Filetype: testft.testft2', \ 'Available Linters: [''testlinter1'', ''testlinter2'']', \ ' Enabled Linters: [''testlinter1'', ''testlinter2'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -580,6 +600,7 @@ Execute (ALEInfo command history should print command output if logging is on): \ ' Current Filetype: testft.testft2', \ 'Available Linters: [''testlinter1'', ''testlinter2'']', \ ' Enabled Linters: [''testlinter1'', ''testlinter2'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -618,6 +639,7 @@ Execute (ALEInfo should include executable checks in the history): \ ' Current Filetype: testft.testft2', \ 'Available Linters: [''testlinter1'']', \ ' Enabled Linters: [''testlinter1'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -647,6 +669,7 @@ Execute (The option for caching failing executable checks should work): \ ' Current Filetype: testft.testft2', \ 'Available Linters: [''testlinter1'']', \ ' Enabled Linters: [''testlinter1'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -669,6 +692,7 @@ Execute (LSP errors for a linter should be outputted): \ ' Current Filetype: testft', \ 'Available Linters: [''testlinter1'']', \ ' Enabled Linters: [''testlinter1'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -693,6 +717,7 @@ Execute (LSP errors for other linters shouldn't appear): \ ' Current Filetype: testft', \ 'Available Linters: [''testlinter1'']', \ ' Enabled Linters: [''testlinter1'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -715,6 +740,7 @@ Execute (ALEInfo should include linter global options): \ ' Current Filetype: testft.testft2', \ 'Available Linters: [''testlinter1'', ''testlinter2'']', \ ' Enabled Linters: [''testlinter1'', ''testlinter2'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines @@ -740,6 +766,7 @@ Execute (ALEInfo should include linter global options for enabled linters): \ ' Current Filetype: testft', \ 'Available Linters: [''testlinter1'', ''testlinter2'']', \ ' Enabled Linters: [''testlinter1'']', + \ ' Ignored Linters: []', \ ] \ + g:fixer_lines \ + g:variables_lines From 78fa93bd55be70c00d0342655bcdfada338e6e79 Mon Sep 17 00:00:00 2001 From: w0rp Date: Wed, 9 Sep 2020 20:34:27 +0100 Subject: [PATCH 10/12] Add tests for covering the coming Vint version --- .../test_vint_command_callback.vader | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/test/command_callback/test_vint_command_callback.vader b/test/command_callback/test_vint_command_callback.vader index 4ce277e8..4a224d01 100644 --- a/test/command_callback/test_vint_command_callback.vader +++ b/test/command_callback/test_vint_command_callback.vader @@ -1,17 +1,17 @@ Before: call ale#assert#SetUpLinterTest('vim', 'vint') - let b:command_tail = (has('nvim') ? ' --enable-neovim' : '') - \ . ' -f "{file_path}:{line_number}:{column_number}: {severity}: {policy_name} - {description} (see {reference})" %t' + let b:common_flags = (has('nvim') ? ' --enable-neovim' : '') + \ . ' -f "{file_path}:{line_number}:{column_number}: {severity}: {policy_name} - {description} (see {reference})"' After: - unlet! b:bin_dir - unlet! b:executable + unlet! b:common_flags + call ale#assert#TearDownLinterTest() Execute(The default command should be correct): AssertLinter 'vint', [ \ ale#Escape('vint') .' --version', - \ ale#Escape('vint') .' -s --no-color' . b:command_tail, + \ ale#Escape('vint') .' -s --no-color' . b:common_flags . ' %t', \] Execute(The executable should be configurable): @@ -19,5 +19,16 @@ Execute(The executable should be configurable): AssertLinter 'foobar', [ \ ale#Escape('foobar') .' --version', - \ ale#Escape('foobar') .' -s --no-color' . b:command_tail, + \ ale#Escape('foobar') .' -s --no-color' . b:common_flags . ' %t', \] + +Execute(The --no-color flag should not be used for older Vint versions): + GivenCommandOutput ['v0.3.5'] + + AssertLinter 'vint', ale#Escape('vint') .' -s' . b:common_flags . ' %t' + +Execute(--stdin-display-name should be used in newer versions): + GivenCommandOutput ['v0.4.0'] + + AssertLinter 'vint', ale#Escape('vint') .' -s --no-color' . b:common_flags + \ . ' --stdin-display-name %s -' From 4ddf74264397a0c739b1c6fd5f643505a31e1d11 Mon Sep 17 00:00:00 2001 From: w0rp Date: Wed, 9 Sep 2020 21:42:27 +0100 Subject: [PATCH 11/12] Close #2522 - Check pylint on the fly Newer versions of pylint will now check your code as you type. Older versions will still only check the file on disk. Co-authored-by: Oliver Wiegers --- ale_linters/python/pylint.vim | 38 +++++++-- autoload/ale/engine.vim | 83 +++++++++++-------- doc/ale.txt | 5 +- .../test_pylint_command_callback.vader | 31 ++++--- test/test_computed_lint_file_values.vader | 16 ++++ 5 files changed, 119 insertions(+), 54 deletions(-) diff --git a/ale_linters/python/pylint.vim b/ale_linters/python/pylint.vim index b16d5355..44eea246 100644 --- a/ale_linters/python/pylint.vim +++ b/ale_linters/python/pylint.vim @@ -17,7 +17,7 @@ function! ale_linters#python#pylint#GetExecutable(buffer) abort return ale#python#FindExecutable(a:buffer, 'python_pylint', ['pylint']) endfunction -function! ale_linters#python#pylint#GetCommand(buffer) abort +function! ale_linters#python#pylint#GetCommand(buffer, version) abort let l:cd_string = '' if ale#Var(a:buffer, 'python_pylint_change_directory') @@ -38,17 +38,23 @@ function! ale_linters#python#pylint#GetCommand(buffer) abort return l:cd_string \ . ale#Escape(l:executable) . l:exec_args - \ . ' ' . ale#Var(a:buffer, 'python_pylint_options') + \ . ale#Pad(ale#Var(a:buffer, 'python_pylint_options')) \ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n' + \ . (ale#semver#GTE(a:version, [2, 4, 0]) ? ' --from-stdin' : '') \ . ' %s' endfunction function! ale_linters#python#pylint#Handle(buffer, lines) abort + let l:output = ale#python#HandleTraceback(a:lines, 10) + + if !empty(l:output) + return l:output + endif + " Matches patterns like the following: " " test.py:4:4: W0101 (unreachable) Unreachable code let l:pattern = '\v^[a-zA-Z]?:?[^:]+:(\d+):(\d+): ([[:alnum:]]+) \(([^(]*)\) (.*)$' - let l:output = [] for l:match in ale#util#GetMatches(a:lines, l:pattern) "let l:failed = append(0, l:match) @@ -71,13 +77,19 @@ function! ale_linters#python#pylint#Handle(buffer, lines) abort let l:code_out = l:match[4] endif - call add(l:output, { + let l:item = { \ 'lnum': l:match[1] + 0, \ 'col': l:match[2] + 1, \ 'text': l:match[5], \ 'code': l:code_out, - \ 'type': l:code[:0] is# 'E' ? 'E' : 'W', - \}) + \ 'type': 'W', + \} + + if l:code[:0] is# 'E' + let l:item.type = 'E' + endif + + call add(l:output, l:item) endfor return l:output @@ -86,7 +98,17 @@ endfunction call ale#linter#Define('python', { \ 'name': 'pylint', \ 'executable': function('ale_linters#python#pylint#GetExecutable'), -\ 'command': function('ale_linters#python#pylint#GetCommand'), +\ 'lint_file': {buffer -> ale#semver#RunWithVersionCheck( +\ buffer, +\ ale#Var(buffer, 'python_pylint_executable'), +\ '%e --version', +\ {buffer, version -> !ale#semver#GTE(version, [2, 4, 0])}, +\ )}, +\ 'command': {buffer -> ale#semver#RunWithVersionCheck( +\ buffer, +\ ale#Var(buffer, 'python_pylint_executable'), +\ '%e --version', +\ function('ale_linters#python#pylint#GetCommand'), +\ )}, \ 'callback': 'ale_linters#python#pylint#Handle', -\ 'lint_file': 1, \}) diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index 63195d0f..3cafa25c 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -444,7 +444,7 @@ function! s:RunJob(command, options) abort return 1 endfunction -function! s:StopCurrentJobs(buffer, clear_lint_file_jobs) abort +function! s:StopCurrentJobs(buffer, clear_lint_file_jobs, linter_slots) abort let l:info = get(g:ale_buffer_info, a:buffer, {}) call ale#command#StopJobs(a:buffer, 'linter') @@ -453,13 +453,23 @@ function! s:StopCurrentJobs(buffer, clear_lint_file_jobs) abort call ale#command#StopJobs(a:buffer, 'file_linter') let l:info.active_linter_list = [] else + let l:lint_file_map = {} + + " Use a previously computed map of `lint_file` values to find + " linters that are used for linting files. + for [l:lint_file, l:linter] in a:linter_slots + if l:lint_file is 1 + let l:lint_file_map[l:linter.name] = 1 + endif + endfor + " Keep jobs for linting files when we're only linting buffers. - call filter(l:info.active_linter_list, 'get(v:val, ''lint_file'')') + call filter(l:info.active_linter_list, 'get(l:lint_file_map, v:val.name)') endif endfunction function! ale#engine#Stop(buffer) abort - call s:StopCurrentJobs(a:buffer, 1) + call s:StopCurrentJobs(a:buffer, 1, []) endfunction function! s:RemoveProblemsForDisabledLinters(buffer, linters) abort @@ -562,6 +572,22 @@ function! s:RunLinter(buffer, linter, lint_file) abort return 0 endfunction +function! s:GetLintFileSlots(buffer, linters) abort + let l:linter_slots = [] + + for l:linter in a:linters + let l:LintFile = l:linter.lint_file + + if type(l:LintFile) is v:t_func + let l:LintFile = l:LintFile(a:buffer) + endif + + call add(l:linter_slots, [l:LintFile, l:linter]) + endfor + + return l:linter_slots +endfunction + function! s:GetLintFileValues(slots, Callback) abort let l:deferred_list = [] let l:new_slots = [] @@ -595,12 +621,18 @@ endfunction function! s:RunLinters( \ buffer, +\ linters, \ slots, \ should_lint_file, \ new_buffer, -\ can_clear_results \) abort - let l:can_clear_results = a:can_clear_results + call s:StopCurrentJobs(a:buffer, a:should_lint_file, a:slots) + call s:RemoveProblemsForDisabledLinters(a:buffer, a:linters) + + " We can only clear the results if we aren't checking the buffer. + let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer) + + silent doautocmd User ALELintPre for [l:lint_file, l:linter] in a:slots " Only run lint_file linters if we should. @@ -631,36 +663,19 @@ endfunction function! ale#engine#RunLinters(buffer, linters, should_lint_file) abort " Initialise the buffer information if needed. let l:new_buffer = ale#engine#InitBufferInfo(a:buffer) - call s:StopCurrentJobs(a:buffer, a:should_lint_file) - call s:RemoveProblemsForDisabledLinters(a:buffer, a:linters) - " We can only clear the results if we aren't checking the buffer. - let l:can_clear_results = !ale#engine#IsCheckingBuffer(a:buffer) - - silent doautocmd User ALELintPre - - " Handle `lint_file` callbacks first. - let l:linter_slots = [] - - for l:linter in a:linters - let l:LintFile = l:linter.lint_file - - if type(l:LintFile) is v:t_func - let l:LintFile = l:LintFile(a:buffer) - endif - - call add(l:linter_slots, [l:LintFile, l:linter]) - endfor - - call s:GetLintFileValues(l:linter_slots, { - \ new_slots -> s:RunLinters( - \ a:buffer, - \ new_slots, - \ a:should_lint_file, - \ l:new_buffer, - \ l:can_clear_results, - \ ) - \}) + call s:GetLintFileValues( + \ s:GetLintFileSlots(a:buffer, a:linters), + \ { + \ slots -> s:RunLinters( + \ a:buffer, + \ a:linters, + \ slots, + \ a:should_lint_file, + \ l:new_buffer, + \ ) + \ } + \) endfunction " Clean up a buffer. diff --git a/doc/ale.txt b/doc/ale.txt index e6b91be8..6ef137c1 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -179,8 +179,11 @@ script like so. > #!/usr/bin/env bash - exec docker run --rm -v "$(pwd):/data" cytopia/pylint "$@" + exec docker run -i --rm -v "$(pwd):/data" cytopia/pylint "$@" < + +You will run to run Docker commands with `-i` in order to read from stdin. + With the above script in mind, you might configure ALE to lint your Python project with `pylint` by providing the path to the script to execute, and mappings which describe how to between the two file systems in your diff --git a/test/command_callback/test_pylint_command_callback.vader b/test/command_callback/test_pylint_command_callback.vader index 755dd292..15f004b6 100644 --- a/test/command_callback/test_pylint_command_callback.vader +++ b/test/command_callback/test_pylint_command_callback.vader @@ -8,6 +8,8 @@ Before: let b:bin_dir = has('win32') ? 'Scripts' : 'bin' let b:command_tail = ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s' + GivenCommandOutput ['pylint 2.3.0'] + After: unlet! b:bin_dir unlet! b:executable @@ -17,26 +19,33 @@ After: Execute(The pylint callbacks should return the correct default values): AssertLinter 'pylint', - \ ale#path#CdString(expand('#' . bufnr('') . ':p:h')) - \ . ale#Escape('pylint') . ' ' . b:command_tail + \ ale#path#CdString(expand('%:p:h')) + \ . ale#Escape('pylint') . b:command_tail + +Execute(Pylint should run with the --from-stdin in new enough versions): + GivenCommandOutput ['pylint 2.4.0'] + + AssertLinter 'pylint', + \ ale#path#CdString(expand('%:p:h')) + \ . ale#Escape('pylint') . b:command_tail[:-3] . '--from-stdin %s' Execute(The option for disabling changing directories should work): let g:ale_python_pylint_change_directory = 0 - AssertLinter 'pylint', ale#Escape('pylint') . ' ' . b:command_tail + AssertLinter 'pylint', ale#Escape('pylint') . b:command_tail Execute(The pylint executable should be configurable, and escaped properly): let g:ale_python_pylint_executable = 'executable with spaces' AssertLinter 'executable with spaces', - \ ale#path#CdString(expand('#' . bufnr('') . ':p:h')) - \ . ale#Escape('executable with spaces') . ' ' . b:command_tail + \ ale#path#CdString(expand('%:p:h')) + \ . ale#Escape('executable with spaces') . b:command_tail Execute(The pylint command callback should let you set options): let g:ale_python_pylint_options = '--some-option' AssertLinter 'pylint', - \ ale#path#CdString(expand('#' . bufnr('') . ':p:h')) + \ ale#path#CdString(expand('%:p:h')) \ . ale#Escape('pylint') . ' --some-option' . b:command_tail Execute(The pylint callbacks shouldn't detect virtualenv directories where they don't exist): @@ -44,7 +53,7 @@ Execute(The pylint callbacks shouldn't detect virtualenv directories where they AssertLinter 'pylint', \ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/no_virtualenv/subdir')) - \ . ale#Escape('pylint') . ' ' . b:command_tail + \ . ale#Escape('pylint') . b:command_tail Execute(The pylint callbacks should detect virtualenv directories): silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.py') @@ -55,7 +64,7 @@ Execute(The pylint callbacks should detect virtualenv directories): AssertLinter b:executable, \ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/subdir')) - \ . ale#Escape(b:executable) . ' ' . b:command_tail + \ . ale#Escape(b:executable) . b:command_tail Execute(You should able able to use the global pylint instead): silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.py') @@ -63,7 +72,7 @@ Execute(You should able able to use the global pylint instead): AssertLinter 'pylint', \ ale#path#CdString(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/subdir')) - \ . ale#Escape('pylint') . ' ' . b:command_tail + \ . ale#Escape('pylint') . b:command_tail Execute(Setting executable to 'pipenv' appends 'run pylint'): let g:ale_python_pylint_executable = 'path/to/pipenv' @@ -71,7 +80,7 @@ Execute(Setting executable to 'pipenv' appends 'run pylint'): AssertLinter 'path/to/pipenv', \ ale#path#CdString(expand('#' . bufnr('') . ':p:h')) \ . ale#Escape('path/to/pipenv') . ' run pylint' - \ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s' + \ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s' Execute(Pipenv is detected when python_pylint_auto_pipenv is set): let g:ale_python_pylint_auto_pipenv = 1 @@ -80,4 +89,4 @@ Execute(Pipenv is detected when python_pylint_auto_pipenv is set): AssertLinter 'pipenv', \ ale#path#CdString(expand('#' . bufnr('') . ':p:h')) \ . ale#Escape('pipenv') . ' run pylint' - \ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s' + \ . ' --output-format text --msg-template="{path}:{line}:{column}: {msg_id} ({symbol}) {msg}" --reports n %s' diff --git a/test/test_computed_lint_file_values.vader b/test/test_computed_lint_file_values.vader index 399e96fe..ed0d4c0c 100644 --- a/test/test_computed_lint_file_values.vader +++ b/test/test_computed_lint_file_values.vader @@ -132,3 +132,19 @@ Execute(Linters where lint_file eventually evaluates to 1 shouldn't be run if we call ale#test#FlushJobs() AssertEqual [], ale#test#GetLoclistWithoutModule() + +Execute(Keeping computed lint_file jobs running should work): + AssertEqual 'testlinter2', ale#linter#Get('foobar')[1].name + + call ale#engine#InitBufferInfo(bufnr('')) + + call ale#engine#MarkLinterActive( + \ g:ale_buffer_info[bufnr('')], + \ ale#linter#Get('foobar')[1] + \) + call ale#engine#RunLinters(bufnr(''), ale#linter#Get('foobar'), 0) + + Assert !empty(g:ale_buffer_info[bufnr('')].active_linter_list), + \ 'The active linter list was empty' + Assert ale#engine#IsCheckingBuffer(bufnr('')), + \ 'The IsCheckingBuffer function returned 0' From 08295ce17405cb5f6c80d2f726262493bfd21210 Mon Sep 17 00:00:00 2001 From: w0rp Date: Wed, 9 Sep 2020 22:06:38 +0100 Subject: [PATCH 12/12] Bump the ALE version to 3.0.0 --- autoload/ale.vim | 2 +- test/test_ale_has.vader | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/autoload/ale.vim b/autoload/ale.vim index 84003993..3f59a6a4 100644 --- a/autoload/ale.vim +++ b/autoload/ale.vim @@ -157,7 +157,7 @@ function! ale#Queue(delay, ...) abort endif endfunction -let s:current_ale_version = [2, 7, 0] +let s:current_ale_version = [3, 0, 0] " A function used to check for ALE features in files outside of the project. function! ale#Has(feature) abort diff --git a/test/test_ale_has.vader b/test/test_ale_has.vader index 633c7186..6e73641d 100644 --- a/test/test_ale_has.vader +++ b/test/test_ale_has.vader @@ -1,4 +1,5 @@ Execute(Checks for versions below the current version should succeed): + AssertEqual 1, ale#Has('ale-3.0.0') AssertEqual 1, ale#Has('ale-2.7.0') AssertEqual 1, ale#Has('ale-2.6.0') AssertEqual 1, ale#Has('ale-2.5.0')