From 49718e0ec6eb4b9068049048c9373167c19257c9 Mon Sep 17 00:00:00 2001 From: Dmitri Vereshchagin Date: Sun, 1 Sep 2019 17:36:40 +0300 Subject: [PATCH 01/40] Add Elvis handler for Erlang [Elvis][1] is an Erlang style reviewer. [1]: https://github.com/inaka/elvis --- ale_linters/erlang/elvis.vim | 39 +++++++++++++++++++ doc/ale-erlang.txt | 12 ++++++ doc/ale-supported-languages-and-tools.txt | 1 + doc/ale.txt | 1 + supported-tools.md | 1 + .../test_erlang_elvis_command_callback.vader | 16 ++++++++ test/handler/test_erlang_elvis_handler.vader | 37 ++++++++++++++++++ 7 files changed, 107 insertions(+) create mode 100644 ale_linters/erlang/elvis.vim create mode 100644 test/command_callback/test_erlang_elvis_command_callback.vader create mode 100644 test/handler/test_erlang_elvis_handler.vader diff --git a/ale_linters/erlang/elvis.vim b/ale_linters/erlang/elvis.vim new file mode 100644 index 0000000000..31dea3dd7f --- /dev/null +++ b/ale_linters/erlang/elvis.vim @@ -0,0 +1,39 @@ +" Author: Dmitri Vereshchagin +" Description: Elvis linter for Erlang files + +call ale#Set('erlang_elvis_executable', 'elvis') + +function! ale_linters#erlang#elvis#Handle(buffer, lines) abort + let l:pattern = '\v:(\d+):[^:]+:(.+)' + let l:loclist = [] + + for l:match in ale#util#GetMatches(a:lines, l:pattern) + call add(l:loclist, { + \ 'lnum': str2nr(l:match[1]), + \ 'text': s:AbbreviateMessage(l:match[2]), + \ 'type': 'W', + \}) + endfor + + return l:loclist +endfunction + +function! s:AbbreviateMessage(text) abort + let l:pattern = '\v\c^(line \d+ is too long):.*$' + + return substitute(a:text, l:pattern, '\1.', '') +endfunction + +function! s:GetCommand(buffer) abort + let l:file = ale#Escape(expand('#' . a:buffer . ':.')) + + return '%e rock --output-format=parsable ' . l:file +endfunction + +call ale#linter#Define('erlang', { +\ 'name': 'elvis', +\ 'callback': 'ale_linters#erlang#elvis#Handle', +\ 'executable': {b -> ale#Var(b, 'erlang_elvis_executable')}, +\ 'command': function('s:GetCommand'), +\ 'lint_file': 1, +\}) diff --git a/doc/ale-erlang.txt b/doc/ale-erlang.txt index 59993a99ae..38762f086c 100644 --- a/doc/ale-erlang.txt +++ b/doc/ale-erlang.txt @@ -31,6 +31,18 @@ g:ale_erlang_dialyzer_rebar3_profile *g:ale_erlang_dialyzer_rebar3_profile* This variable can be changed to specify the profile that is used to run dialyzer with rebar3. + +------------------------------------------------------------------------------- +elvis *ale-erlang-elvis* + +g:ale_erlang_elvis_executable *g:ale_erlang_elvis_executable* + *b:ale_erlang_elvis_executable* + Type: |String| + Default: `'elvis'` + + This variable can be changed to specify the elvis executable. + + ------------------------------------------------------------------------------- erlc *ale-erlang-erlc* diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index c6bcf42111..73c2e473c5 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -140,6 +140,7 @@ Notes: * `erubis` * `ruumba` * Erlang + * `elvis`!! * `erlc` * `SyntaxErl` * Fish diff --git a/doc/ale.txt b/doc/ale.txt index d45cada7f7..4a1dc7cc01 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -2575,6 +2575,7 @@ documented in additional help files. elm-make..............................|ale-elm-elm-make| erlang..................................|ale-erlang-options| dialyzer..............................|ale-erlang-dialyzer| + elvis.................................|ale-erlang-elvis| erlc..................................|ale-erlang-erlc| syntaxerl.............................|ale-erlang-syntaxerl| eruby...................................|ale-eruby-options| diff --git a/supported-tools.md b/supported-tools.md index 66e463488c..cfbed9ef4a 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -149,6 +149,7 @@ formatting. * [erubis](https://github.com/kwatch/erubis) * [ruumba](https://github.com/ericqweinstein/ruumba) * Erlang + * [elvis](https://github.com/inaka/elvis) :floppy_disk: * [erlc](http://erlang.org/doc/man/erlc.html) * [SyntaxErl](https://github.com/ten0s/syntaxerl) * Fish diff --git a/test/command_callback/test_erlang_elvis_command_callback.vader b/test/command_callback/test_erlang_elvis_command_callback.vader new file mode 100644 index 0000000000..4aab49d6e9 --- /dev/null +++ b/test/command_callback/test_erlang_elvis_command_callback.vader @@ -0,0 +1,16 @@ +Before: + let b:file = fnamemodify(bufname(''), ':.') + call ale#assert#SetUpLinterTest('erlang', 'elvis') + +After: + call ale#assert#TearDownLinterTest() + +Execute(Default command should be correct): + AssertLinter 'elvis', + \ ale#Escape('elvis') . ' rock --output-format=parsable ' . ale#Escape(b:file) + +Execute(Executable should be configurable): + let b:ale_erlang_elvis_executable = '/path/to/elvis' + + AssertLinter '/path/to/elvis', + \ ale#Escape('/path/to/elvis') . ' rock --output-format=parsable ' . ale#Escape(b:file) diff --git a/test/handler/test_erlang_elvis_handler.vader b/test/handler/test_erlang_elvis_handler.vader new file mode 100644 index 0000000000..365376c812 --- /dev/null +++ b/test/handler/test_erlang_elvis_handler.vader @@ -0,0 +1,37 @@ +Before: + runtime ale_linters/erlang/elvis.vim + +After: + call ale#linter#Reset() + +Execute(Warning messages should be handled): + AssertEqual + \ [ + \ { + \ 'lnum': 11, + \ 'text': "Replace the 'if' expression on line 11 with a 'case' expression or function clauses.", + \ 'type': 'W', + \ }, + \ { + \ 'lnum': 20, + \ 'text': 'Remove the debug call to io:format/1 on line 20.', + \ 'type': 'W', + \ }, + \ ], + \ ale_linters#erlang#elvis#Handle(bufnr(''), [ + \ "src/foo.erl:11:no_if_expression:Replace the 'if' expression on line 11 with a 'case' expression or function clauses.", + \ 'src/foo.erl:20:no_debug_call:Remove the debug call to io:format/1 on line 20.', + \ ]) + +Execute(Line length message shouldn't contain the line itself): + AssertEqual + \ [ + \ { + \ 'lnum': 24, + \ 'text': 'Line 24 is too long.', + \ 'type': 'W', + \ }, + \ ], + \ ale_linters#erlang#elvis#Handle(bufnr(''), [ + \ 'src/foo.erl:24:line_length:Line 24 is too long: io:format("Look ma, too long!"),.', + \ ]) From 20f6bebdf22afca71b136a1ce8cff9f8b5ec7410 Mon Sep 17 00:00:00 2001 From: Christian Keil Date: Wed, 9 Sep 2020 17:30:41 +0200 Subject: [PATCH 02/40] Fix handling of ranges at file end. --- autoload/ale/code_action.vim | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index 8c7263f38e..849942caf5 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -127,10 +127,20 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort endif call extend(l:middle, l:insertions[1:]) - let l:middle[-1] .= l:lines[l:end_line - 1][l:end_column - 1 :] + if l:end_line <= len(l:lines) + " Only extend the last line if end_line is within the range of + " lines. + let l:middle[-1] .= l:lines[l:end_line - 1][l:end_column - 1 :] + endif let l:lines_before_change = len(l:lines) - let l:lines = l:start + l:middle + l:lines[l:end_line :] + + if l:end_line < len(l:lines) + let l:end = l:lines[l:end_line :] + else + let l:end = [] + endif + let l:lines = l:start + l:middle + l:end let l:current_line_offset = len(l:lines) - l:lines_before_change let l:line_offset += l:current_line_offset From d3932c02424eec0520faa2afa42371c27122f5a7 Mon Sep 17 00:00:00 2001 From: Christian Keil Date: Wed, 9 Sep 2020 17:44:09 +0200 Subject: [PATCH 03/40] Fix format linting error. --- autoload/ale/code_action.vim | 2 ++ 1 file changed, 2 insertions(+) diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index 849942caf5..1959d1b147 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -127,6 +127,7 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort endif call extend(l:middle, l:insertions[1:]) + if l:end_line <= len(l:lines) " Only extend the last line if end_line is within the range of " lines. @@ -140,6 +141,7 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort else let l:end = [] endif + let l:lines = l:start + l:middle + l:end let l:current_line_offset = len(l:lines) - l:lines_before_change From d1f48e5edef1d622817ffc054e6963e6ca956371 Mon Sep 17 00:00:00 2001 From: Christian Keil Date: Wed, 9 Sep 2020 17:49:21 +0200 Subject: [PATCH 04/40] Remove unnecessary length check. --- autoload/ale/code_action.vim | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index 1959d1b147..359bc0d6a7 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -135,14 +135,7 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort endif let l:lines_before_change = len(l:lines) - - if l:end_line < len(l:lines) - let l:end = l:lines[l:end_line :] - else - let l:end = [] - endif - - let l:lines = l:start + l:middle + l:end + let l:lines = l:start + l:middle + l:lines[l:end_line :] let l:current_line_offset = len(l:lines) - l:lines_before_change let l:line_offset += l:current_line_offset From 63a528eac29c9bc34811eba2e0e09eab24b53591 Mon Sep 17 00:00:00 2001 From: Arthur Arnold Date: Mon, 14 Sep 2020 07:57:22 -0300 Subject: [PATCH 05/40] Fix wording in Rust doc --- doc/ale-rust.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ale-rust.txt b/doc/ale-rust.txt index f4b4e7b744..3aa6367310 100644 --- a/doc/ale-rust.txt +++ b/doc/ale-rust.txt @@ -22,12 +22,12 @@ Integration Information 3. rls -- If you have `rls` installed, you might prefer using this linter over cargo. rls implements the Language Server Protocol for incremental compilation of Rust code, and can check Rust files while you type. `rls` - requires Rust files to contained in Cargo projects. + requires Rust files to be contained in Cargo projects. 4. analyzer -- If you have rust-analyzer installed, you might prefer using this linter over cargo and rls. rust-analyzer also implements the Language Server Protocol for incremental compilation of Rust code, and is the next iteration of rls. rust-analyzer, like rls, requires Rust files - to contained in Cargo projects. + to be contained in Cargo projects. 5. rustfmt -- If you have `rustfmt` installed, you can use it as a fixer to consistently reformat your Rust code. From 9769565f882b43512105d1b63a2f4ceda7a39d86 Mon Sep 17 00:00:00 2001 From: Raphael Nepomuceno <58113327+rphln@users.noreply.github.com> Date: Mon, 14 Sep 2020 12:11:16 -0300 Subject: [PATCH 06/40] Collapse spaces and lines in the completion menu. --- autoload/ale/completion.vim | 5 ++++- test/completion/test_lsp_completion_parsing.vader | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index ecd9360067..882fd5e6d5 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -606,11 +606,14 @@ function! ale#completion#ParseLSPCompletions(response) abort let l:doc = l:doc.value endif + " Collapse whitespaces and line breaks into a single space. + let l:detail = substitute(get(l:item, 'detail', ''), '\_s\+', ' ', 'g') + let l:result = { \ 'word': l:word, \ 'kind': ale#completion#GetCompletionSymbols(get(l:item, 'kind', '')), \ 'icase': 1, - \ 'menu': get(l:item, 'detail', ''), + \ 'menu': l: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. diff --git a/test/completion/test_lsp_completion_parsing.vader b/test/completion/test_lsp_completion_parsing.vader index d989aefe36..36228c1046 100644 --- a/test/completion/test_lsp_completion_parsing.vader +++ b/test/completion/test_lsp_completion_parsing.vader @@ -40,6 +40,7 @@ Execute(Should handle Rust completion results correctly): \ {'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})}, + \ {'word': 'to_vec', 'menu': 'pub fn to_vec(&self) -> Vec where T: Clone,', 'info': '', 'kind': 'f', 'icase': 1, 'user_data': json_encode({'_ale_completion_item': 1})}, \], \ ale#completion#ParseLSPCompletions({ \ "jsonrpc":"2.0", @@ -184,6 +185,11 @@ Execute(Should handle Rust completion results correctly): \ "label":"from", \ "kind":3, \ "detail":"fn from(s: Cow<'a, str>) -> String" + \ }, + \ { + \ "label":"to_vec", + \ "kind":3, + \ "detail":"pub fn to_vec(&self) -> Vec\nwhere\n T: Clone," \ } \ ] \ }) From fec6b63494b4f491279c7a01c0d61e515b8d0f46 Mon Sep 17 00:00:00 2001 From: Arnold Chand Date: Tue, 15 Sep 2020 19:14:12 -0400 Subject: [PATCH 07/40] feat: add intelephense support for php --- ale_linters/php/intelephense.vim | 32 +++++++++++++++++ doc/ale-php.txt | 35 +++++++++++++++++++ doc/ale-supported-languages-and-tools.txt | 1 + doc/ale.txt | 1 + supported-tools.md | 1 + .../with-composer/composer.json | 0 ...st_php_intelephense_command_callback.vader | 23 ++++++++++++ 7 files changed, 93 insertions(+) create mode 100644 ale_linters/php/intelephense.vim create mode 100644 test/command_callback/php-intelephense-project/with-composer/composer.json create mode 100644 test/command_callback/test_php_intelephense_command_callback.vader diff --git a/ale_linters/php/intelephense.vim b/ale_linters/php/intelephense.vim new file mode 100644 index 0000000000..e9e07d1f9f --- /dev/null +++ b/ale_linters/php/intelephense.vim @@ -0,0 +1,32 @@ +" Author: Eric Stern , +" Arnold Chand +" Description: Intelephense language server integration for ALE + +call ale#Set('php_intelephense_executable', 'intelephense') +call ale#Set('php_intelephense_use_global', 1) +call ale#Set('php_intelephense_config', {}) + +function! ale_linters#php#intelephense#GetProjectRoot(buffer) abort + let l:composer_path = ale#path#FindNearestFile(a:buffer, 'composer.json') + + if (!empty(l:composer_path)) + return fnamemodify(l:composer_path, ':h') + endif + + let l:git_path = ale#path#FindNearestDirectory(a:buffer, '.git') + + return !empty(l:git_path) ? fnamemodify(l:git_path, ':h:h') : '' +endfunction + +function! ale_linters#php#intelephense#GetInitializationOptions() abort + return ale#Get('php_intelephense_config') +endfunction + +call ale#linter#Define('php', { +\ 'name': 'intelephense', +\ 'lsp': 'stdio', +\ 'initialization_options': function('ale_linters#php#intelephense#GetInitializationOptions'), +\ 'executable': {b -> ale#node#FindExecutable(b, 'php_intelephense', [])}, +\ 'command': '%e --stdio', +\ 'project_root': function('ale_linters#php#intelephense#GetProjectRoot'), +\}) diff --git a/doc/ale-php.txt b/doc/ale-php.txt index 9fe868f82b..79d9a03307 100644 --- a/doc/ale-php.txt +++ b/doc/ale-php.txt @@ -243,5 +243,40 @@ g:ale_php_php_executable *g:ale_php_php_executable* This variable sets the executable used for php. +=============================================================================== +intelephense *ale-php-intelephense* + +g:ale_php_intelephense_executable *g:ale_php_intelephense_executable* + *b:ale_php_intelephense_executable* + Type: |String| + Default: `'intelephense'` + + The variable can be set to configure the executable that will be used for + running the intelephense language server. `node_modules` directory + executable will be preferred instead of this setting if + |g:ale_php_intelephense_use_global| is `0`. + + See: |ale-integrations-local-executables| + + +g:ale_php_intelephense_use_global *g:ale_php_intelephense_use_global* + *b:ale_php_intelephense_use_global* + Type: |Number| + Default: `get(g:, 'ale_use_global_executables', 0)` + + This variable can be set to `1` to force the language server to be run with + the executable set for |g:ale_php_intelephense_executable|. + + See: |ale-integrations-local-executables| + +g:ale_php_intelephense_config *g:ale_php_intelephense_config* + *b:ale_php_intelephense_config* + Type: |Dictionary| + Default: `{}` + + The initialization options config specified by Intelephense. Refer to the + installation docs provided by intelephense (github.com/bmewburn/intelephense + -docs). + =============================================================================== vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index c6bcf42111..7c8c9bac5a 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -324,6 +324,7 @@ Notes: * Perl6 * `perl6 -c` * PHP + * `intelephense` * `langserver` * `phan` * `phpcbf` diff --git a/doc/ale.txt b/doc/ale.txt index 6ef137c167..67ef531c4c 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -2752,6 +2752,7 @@ documented in additional help files. psalm.................................|ale-php-psalm| php-cs-fixer..........................|ale-php-php-cs-fixer| php...................................|ale-php-php| + intelephense..........................|ale-php-intelephense| po......................................|ale-po-options| write-good............................|ale-po-write-good| pod.....................................|ale-pod-options| diff --git a/supported-tools.md b/supported-tools.md index 66e463488c..559f841a3f 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -333,6 +333,7 @@ formatting. * Perl6 * [perl6 -c](https://perl6.org) :warning: * PHP + * [intelephense](https://github.com/bmewburn/intelephense-docs) * [langserver](https://github.com/felixfbecker/php-language-server) * [phan](https://github.com/phan/phan) see `:help ale-php-phan` to instructions * [phpcbf](https://github.com/squizlabs/PHP_CodeSniffer) diff --git a/test/command_callback/php-intelephense-project/with-composer/composer.json b/test/command_callback/php-intelephense-project/with-composer/composer.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/command_callback/test_php_intelephense_command_callback.vader b/test/command_callback/test_php_intelephense_command_callback.vader new file mode 100644 index 0000000000..283fd61552 --- /dev/null +++ b/test/command_callback/test_php_intelephense_command_callback.vader @@ -0,0 +1,23 @@ +Before: + call ale#assert#SetUpLinterTest('php', 'intelephense') + +After: + call ale#assert#TearDownLinterTest() + +Execute(The default executable path should be correct): + AssertLinter 'intelephense', + \ ale#Escape('intelephense') . ' --stdio' + +Execute(The project path should be correct for .git directories): + call ale#test#SetFilename('php-intelephense-project/with-git/test.php') + silent! call mkdir('php-langserver-project/with-git/.git', 'p') + AssertLSPProject ale#path#Simplify(g:dir . '/php-intelephense-project/with-git') + +Execute(The project path should be correct for composer.json file): + call ale#test#SetFilename('php-intelephense-project/with-composer/test.php') + AssertLSPProject ale#path#Simplify(g:dir . '/php-intelephense-project/with-composer') + +Execute(The project should save to a temp dir): + call ale#test#SetFilename('php-intelephense-project/with-composer/test.php') + let g:ale_php_intelephense_config = { 'storagePath': '/tmp/intelephense' } + AssertLSPProject ale#path#Simplify(g:dir . '/php-intelephense-project/with-composer') From 8a855e3e6442a3b1384a07098d166c114a3e01f3 Mon Sep 17 00:00:00 2001 From: Arnold Chand Date: Thu, 17 Sep 2020 09:35:06 -0400 Subject: [PATCH 08/40] fix: tests --- .../test_php_intelephense_command_callback.vader | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/command_callback/test_php_intelephense_command_callback.vader b/test/command_callback/test_php_intelephense_command_callback.vader index 283fd61552..164ca26e4d 100644 --- a/test/command_callback/test_php_intelephense_command_callback.vader +++ b/test/command_callback/test_php_intelephense_command_callback.vader @@ -10,14 +10,17 @@ Execute(The default executable path should be correct): Execute(The project path should be correct for .git directories): call ale#test#SetFilename('php-intelephense-project/with-git/test.php') - silent! call mkdir('php-langserver-project/with-git/.git', 'p') + silent! call mkdir('php-intelephense-project/with-git/.git') + AssertLSPProject ale#path#Simplify(g:dir . '/php-intelephense-project/with-git') Execute(The project path should be correct for composer.json file): call ale#test#SetFilename('php-intelephense-project/with-composer/test.php') + AssertLSPProject ale#path#Simplify(g:dir . '/php-intelephense-project/with-composer') -Execute(The project should save to a temp dir): +Execute(The project cache should be saved in a temp dir): call ale#test#SetFilename('php-intelephense-project/with-composer/test.php') let g:ale_php_intelephense_config = { 'storagePath': '/tmp/intelephense' } + AssertLSPProject ale#path#Simplify(g:dir . '/php-intelephense-project/with-composer') From f8351c1b22d02f194235388779ad47028d5e7a25 Mon Sep 17 00:00:00 2001 From: Arnold Chand Date: Thu, 17 Sep 2020 09:58:45 -0400 Subject: [PATCH 09/40] fix: test: mkdir should create if it doesn't exist --- .../test_php_intelephense_command_callback.vader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/command_callback/test_php_intelephense_command_callback.vader b/test/command_callback/test_php_intelephense_command_callback.vader index 164ca26e4d..dd6adb3df3 100644 --- a/test/command_callback/test_php_intelephense_command_callback.vader +++ b/test/command_callback/test_php_intelephense_command_callback.vader @@ -10,7 +10,7 @@ Execute(The default executable path should be correct): Execute(The project path should be correct for .git directories): call ale#test#SetFilename('php-intelephense-project/with-git/test.php') - silent! call mkdir('php-intelephense-project/with-git/.git') + silent! call mkdir('php-intelephense-project/with-git/.git', 'p') AssertLSPProject ale#path#Simplify(g:dir . '/php-intelephense-project/with-git') From 6bebdcfa30b492b7c71316b707d36c4f4d19a1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wo=C5=BAniak?= Date: Fri, 18 Sep 2020 12:11:05 +0200 Subject: [PATCH 10/40] Improves fixer performance for large buffers --- autoload/ale/util.vim | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/autoload/ale/util.vim b/autoload/ale/util.vim index 1f39637716..fcc03eb720 100644 --- a/autoload/ale/util.vim +++ b/autoload/ale/util.vim @@ -486,7 +486,7 @@ function! ale#util#Input(message, value) abort endfunction function! ale#util#HasBuflineApi() abort - return exists('*deletebufline') && exists('*setbufline') + return exists('*deletebufline') && exists('*appendbufline') && exists('*getpos') && exists('*setpos') endfunction " Sets buffer contents to lines @@ -507,8 +507,11 @@ function! ale#util#SetBufferContents(buffer, lines) abort " Use a Vim API for setting lines in other buffers, if available. if l:has_bufline_api - call setbufline(a:buffer, 1, l:new_lines) - call deletebufline(a:buffer, l:first_line_to_remove, '$') + let l:save_cursor = getpos('.') + call deletebufline(a:buffer, 1, '$') + call appendbufline(a:buffer, 1, l:new_lines) + call deletebufline(a:buffer, 1, 1) + call setpos('.', l:save_cursor) " Fall back on setting lines the old way, for the current buffer. else let l:old_line_length = line('$') From e08996940468ce90616a9704a439652f1ab73e31 Mon Sep 17 00:00:00 2001 From: Marcus Zanona Date: Wed, 12 Aug 2020 11:06:35 +0200 Subject: [PATCH 11/40] fix(ale_linters/phpcs): add support for multiline error messages --- ale_linters/php/phpcs.vim | 2 +- test/handler/test_phpcs_handler.vader | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ale_linters/php/phpcs.vim b/ale_linters/php/phpcs.vim index 11b81e8498..c5a3faa926 100644 --- a/ale_linters/php/phpcs.vim +++ b/ale_linters/php/phpcs.vim @@ -23,7 +23,7 @@ function! ale_linters#php#phpcs#Handle(buffer, lines) abort " Matches against lines like the following: " " /path/to/some-filename.php:18:3: error - Line indented incorrectly; expected 4 spaces, found 2 (Generic.WhiteSpace.ScopeIndent.IncorrectExact) - let l:pattern = '^.*:\(\d\+\):\(\d\+\): \(.\+\) - \(.\+\) (\(.\+\))$' + let l:pattern = '^.*:\(\d\+\):\(\d\+\): \(.\+\) - \(.\+\) (\(.\+\)).*$' let l:output = [] for l:match in ale#util#GetMatches(a:lines, l:pattern) diff --git a/test/handler/test_phpcs_handler.vader b/test/handler/test_phpcs_handler.vader index 18accece16..26d35cb8c6 100644 --- a/test/handler/test_phpcs_handler.vader +++ b/test/handler/test_phpcs_handler.vader @@ -13,7 +13,16 @@ Execute(phpcs errors should be handled): \ 'type': 'E', \ 'sub_type': 'style', \ 'text': 'Line indented incorrectly; expected 4 spaces, found 2 (Generic.WhiteSpace.ScopeIndent.IncorrectExact)', - \ }], + \ }, + \ { + \ 'lnum': 22, + \ 'col': 3, + \ 'type': 'E', + \ 'sub_type': 'style', + \ 'text': 'All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks)', + \ }, + \ ], \ ale_linters#php#phpcs#Handle(bufnr(''), [ \ '/path/to/some-filename.php:18:3: error - Line indented incorrectly; expected 4 spaces, found 2 (Generic.WhiteSpace.ScopeIndent.IncorrectExact)', + \ "/path/to/some-filename.php:22:3: error - All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found '\"\n'.", \ ]) From 9a8ab764d5a77d1ebf18b176ce57e08a0603a5f5 Mon Sep 17 00:00:00 2001 From: rgossiaux Date: Fri, 2 Oct 2020 22:51:54 -0400 Subject: [PATCH 12/40] Fix typo in ale-python.txt --- doc/ale-python.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ale-python.txt b/doc/ale-python.txt index 6b1a6d3363..5cf7cd7410 100644 --- a/doc/ale-python.txt +++ b/doc/ale-python.txt @@ -687,7 +687,7 @@ g:ale_python_pyre_auto_pipenv *g:ale_python_pyre_auto_pipenv* =============================================================================== pyright *ale-python-pyright* -The `pyrlight` linter requires a recent version of `pyright` which includes +The `pyright` linter requires a recent version of `pyright` which includes the `pyright-langserver` executable. You can install `pyright` on your system through `npm` with `sudo npm install -g pyright` or similar. From 96ec33e6d6119fea01fee7aaed1e8a21ef6ef677 Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Sat, 3 Oct 2020 17:57:59 +0300 Subject: [PATCH 13/40] Use stdin/stdout to communicate with gofmt --- autoload/ale/fixers/gofmt.vim | 3 --- test/fixers/test_gofmt_fixer_callback.vader | 14 +++----------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/autoload/ale/fixers/gofmt.vim b/autoload/ale/fixers/gofmt.vim index d5a539b9c2..b9cfbb586a 100644 --- a/autoload/ale/fixers/gofmt.vim +++ b/autoload/ale/fixers/gofmt.vim @@ -11,9 +11,6 @@ function! ale#fixers#gofmt#Fix(buffer) abort return { \ 'command': l:env . ale#Escape(l:executable) - \ . ' -l -w' \ . (empty(l:options) ? '' : ' ' . l:options) - \ . ' %t', - \ 'read_temporary_file': 1, \} endfunction diff --git a/test/fixers/test_gofmt_fixer_callback.vader b/test/fixers/test_gofmt_fixer_callback.vader index 1665965556..9940717338 100644 --- a/test/fixers/test_gofmt_fixer_callback.vader +++ b/test/fixers/test_gofmt_fixer_callback.vader @@ -21,10 +21,7 @@ Execute(The gofmt callback should return the correct default values): AssertEqual \ { - \ 'read_temporary_file': 1, - \ 'command': ale#Escape('xxxinvalid') - \ . ' -l -w' - \ . ' %t', + \ 'command': ale#Escape('xxxinvalid'), \ }, \ ale#fixers#gofmt#Fix(bufnr('')) @@ -35,11 +32,8 @@ Execute(The gofmt callback should include custom gofmt options): AssertEqual \ { - \ 'read_temporary_file': 1, \ 'command': ale#Escape('xxxinvalid') - \ . ' -l -w' - \ . ' ' . g:ale_go_gofmt_options - \ . ' %t', + \ . ' ' . g:ale_go_gofmt_options, \ }, \ ale#fixers#gofmt#Fix(bufnr('')) @@ -50,9 +44,7 @@ Execute(The gofmt callback should support Go environment variables): AssertEqual \ { - \ 'read_temporary_file': 1, \ 'command': ale#Env('GO111MODULE', 'off') - \ . ale#Escape('xxxinvalid') . ' -l -w' - \ . ' %t', + \ . ale#Escape('xxxinvalid') \ }, \ ale#fixers#gofmt#Fix(bufnr('')) From f384d61c3d4d624fd391a6d877ec913096b2bb10 Mon Sep 17 00:00:00 2001 From: w0rp Date: Thu, 15 Oct 2020 21:24:27 +0100 Subject: [PATCH 14/40] Fix sorting of supported tools --- doc/ale-supported-languages-and-tools.txt | 2 +- supported-tools.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index c6bcf42111..265f7a9929 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -456,8 +456,8 @@ Notes: * `stylelint` * Swift * `sourcekit-lsp` - * `swiftformat` * `swift-format` + * `swiftformat` * `swiftlint` * Tcl * `nagelfar`!! diff --git a/supported-tools.md b/supported-tools.md index 66e463488c..55e7f71fa3 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -465,8 +465,8 @@ formatting. * [stylelint](https://github.com/stylelint/stylelint) * Swift * [sourcekit-lsp](https://github.com/apple/sourcekit-lsp) - * [swiftformat](https://github.com/nicklockwood/SwiftFormat) * [swift-format](https://github.com/apple/swift-format) + * [swiftformat](https://github.com/nicklockwood/SwiftFormat) * [swiftlint](https://github.com/realm/SwiftLint) * Tcl * [nagelfar](http://nagelfar.sourceforge.net) :floppy_disk: From 477eb89793f9be3be95cf89a22c8ff94a88ba9a8 Mon Sep 17 00:00:00 2001 From: w0rp Date: Thu, 15 Oct 2020 21:56:21 +0100 Subject: [PATCH 15/40] #3332 Implement :ALERename! for ignoring errors :ALERename! now ignores errors for files that cannot be modified, and modifies all other files. --- autoload/ale/code_action.vim | 23 ++++-- autoload/ale/completion.vim | 2 +- autoload/ale/organize_imports.vim | 11 ++- autoload/ale/rename.vim | 59 ++++++++------ doc/ale.txt | 6 ++ plugin/ale.vim | 2 +- test/completion/test_ale_import_command.vader | 4 +- test/completion/test_completion_events.vader | 4 +- test/test_code_action.vader | 76 +++++++++++-------- test/test_organize_imports.vader | 4 +- test/test_rename.vader | 10 +-- 11 files changed, 122 insertions(+), 79 deletions(-) diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index 8c7263f38e..42f4f26553 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -1,26 +1,33 @@ " Author: Jerko Steiner " Description: Code action support for LSP / tsserver -function! ale#code_action#HandleCodeAction(code_action, should_save) abort +function! ale#code_action#HandleCodeAction(code_action, options) abort let l:current_buffer = bufnr('') let l:changes = a:code_action.changes + let l:should_save = get(a:options, 'should_save') + let l:force_save = get(a:options, 'force_save') + let l:safe_changes = [] for l:file_code_edit in l:changes let l:buf = bufnr(l:file_code_edit.fileName) if l:buf != -1 && l:buf != l:current_buffer && getbufvar(l:buf, '&mod') - call ale#util#Execute('echom ''Aborting action, file is unsaved''') + if !l:force_save + call ale#util#Execute('echom ''Aborting action, file is unsaved''') - return + return + endif + else + call add(l:safe_changes, l:file_code_edit) endif endfor - for l:file_code_edit in l:changes + for l:file_code_edit in l:safe_changes call ale#code_action#ApplyChanges( - \ l:file_code_edit.fileName, - \ l:file_code_edit.textChanges, - \ a:should_save, - \ ) + \ l:file_code_edit.fileName, + \ l:file_code_edit.textChanges, + \ l:should_save, + \) endfor endfunction diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index ecd9360067..efbf0fd53b 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -1006,7 +1006,7 @@ function! ale#completion#HandleUserData(completed_item) abort \|| 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) + call ale#code_action#HandleCodeAction(l:code_action, {}) endfor endif diff --git a/autoload/ale/organize_imports.vim b/autoload/ale/organize_imports.vim index e89c832cbf..e2b1c0d22a 100644 --- a/autoload/ale/organize_imports.vim +++ b/autoload/ale/organize_imports.vim @@ -12,10 +12,13 @@ function! ale#organize_imports#HandleTSServerResponse(conn_id, response) abort let l:file_code_edits = a:response.body - call ale#code_action#HandleCodeAction({ - \ 'description': 'Organize Imports', - \ 'changes': l:file_code_edits, - \}, v:false) + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'Organize Imports', + \ 'changes': l:file_code_edits, + \ }, + \ {} + \) endfunction function! s:OnReady(linter, lsp_details) abort diff --git a/autoload/ale/rename.vim b/autoload/ale/rename.vim index 64952e63c6..8190411db2 100644 --- a/autoload/ale/rename.vim +++ b/autoload/ale/rename.vim @@ -33,9 +33,10 @@ function! ale#rename#HandleTSServerResponse(conn_id, response) abort return endif - let l:old_name = s:rename_map[a:response.request_seq].old_name - let l:new_name = s:rename_map[a:response.request_seq].new_name - call remove(s:rename_map, a:response.request_seq) + let l:options = remove(s:rename_map, a:response.request_seq) + + let l:old_name = l:options.old_name + let l:new_name = l:options.new_name if get(a:response, 'success', v:false) is v:false let l:message = get(a:response, 'message', 'unknown') @@ -77,10 +78,16 @@ function! ale#rename#HandleTSServerResponse(conn_id, response) abort return endif - call ale#code_action#HandleCodeAction({ - \ 'description': 'rename', - \ 'changes': l:changes, - \}, v:true) + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'rename', + \ 'changes': l:changes, + \ }, + \ { + \ 'should_save': 1, + \ 'force_save': get(l:options, 'force_save'), + \ }, + \) endfunction function! s:getChanges(workspace_edit) abort @@ -111,7 +118,7 @@ endfunction function! ale#rename#HandleLSPResponse(conn_id, response) abort if has_key(a:response, 'id') \&& has_key(s:rename_map, a:response.id) - call remove(s:rename_map, a:response.id) + let l:options = remove(s:rename_map, a:response.id) if !has_key(a:response, 'result') call s:message('No rename result received from server') @@ -156,14 +163,20 @@ function! ale#rename#HandleLSPResponse(conn_id, response) abort \}) endfor - call ale#code_action#HandleCodeAction({ - \ 'description': 'rename', - \ 'changes': l:changes, - \}, v:true) + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'rename', + \ 'changes': l:changes, + \ }, + \ { + \ 'should_save': 1, + \ 'force_save': get(l:options, 'force_save'), + \ }, + \) endif endfunction -function! s:OnReady(line, column, old_name, new_name, linter, lsp_details) abort +function! s:OnReady(line, column, options, linter, lsp_details) abort let l:id = a:lsp_details.connection_id if !ale#lsp#HasCapability(l:id, 'rename') @@ -195,19 +208,16 @@ function! s:OnReady(line, column, old_name, new_name, linter, lsp_details) abort \ l:buffer, \ a:line, \ a:column, - \ a:new_name + \ a:options.new_name \) endif let l:request_id = ale#lsp#Send(l:id, l:message) - let s:rename_map[l:request_id] = { - \ 'new_name': a:new_name, - \ 'old_name': a:old_name, - \} + let s:rename_map[l:request_id] = a:options endfunction -function! s:ExecuteRename(linter, old_name, new_name) abort +function! s:ExecuteRename(linter, options) abort let l:buffer = bufnr('') let [l:line, l:column] = getpos('.')[1:2] @@ -215,12 +225,11 @@ function! s:ExecuteRename(linter, old_name, new_name) abort let l:column = min([l:column, len(getline(l:line))]) endif - let l:Callback = function( - \ 's:OnReady', [l:line, l:column, a:old_name, a:new_name]) + let l:Callback = function('s:OnReady', [l:line, l:column, a:options]) call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) endfunction -function! ale#rename#Execute() abort +function! ale#rename#Execute(options) abort let l:lsp_linters = [] for l:linter in ale#linter#Get(&filetype) @@ -245,6 +254,10 @@ function! ale#rename#Execute() abort endif for l:lsp_linter in l:lsp_linters - call s:ExecuteRename(l:lsp_linter, l:old_name, l:new_name) + call s:ExecuteRename(l:lsp_linter, { + \ 'old_name': l:old_name, + \ 'new_name': l:new_name, + \ 'force_save': get(a:options, 'force_save') is 1, + \}) endfor endfunction diff --git a/doc/ale.txt b/doc/ale.txt index b63c51a2b3..eb8f027575 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -3099,6 +3099,12 @@ ALERename *ALERename* The symbol where the cursor is resting will be the symbol renamed, and a prompt will open to request a new name. + ALE will refuse to complete a rename operation if there are files to modify + which have not yet been saved in Vim. If the command is run with a bang + (`:ALERename!`), all warnings will be suppressed, and files that are still + open in Vim and not saved will be ignored and left in a state where symbols + in those files will not be updated. + ALERepeatSelection *ALERepeatSelection* diff --git a/plugin/ale.vim b/plugin/ale.vim index 18d867eeb8..32ec14ac5f 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -238,7 +238,7 @@ command! -bar ALEComplete :call ale#completion#GetCompletions('ale-manual') command! -bar ALEImport :call ale#completion#Import() " Rename symbols using tsserver and LSP -command! -bar ALERename :call ale#rename#Execute() +command! -bar -bang ALERename :call ale#rename#Execute({'force_save': '' is# '!'}) " Organize import statements using tsserver command! -bar ALEOrganizeImports :call ale#organize_imports#Execute() diff --git a/test/completion/test_ale_import_command.vader b/test/completion/test_ale_import_command.vader index 2ba9b8d7b7..d36caae2e1 100644 --- a/test/completion/test_ale_import_command.vader +++ b/test/completion/test_ale_import_command.vader @@ -65,8 +65,8 @@ Before: return g:server_started_value endfunction - function! ale#code_action#HandleCodeAction(code_action, should_save) abort - Assert !a:should_save + function! ale#code_action#HandleCodeAction(code_action, options) abort + Assert !get(a:options, 'should_save') call add(g:code_action_list, a:code_action) endfunction diff --git a/test/completion/test_completion_events.vader b/test/completion/test_completion_events.vader index f678e77306..30bf603c2e 100644 --- a/test/completion/test_completion_events.vader +++ b/test/completion/test_completion_events.vader @@ -50,8 +50,8 @@ Before: let g:handle_code_action_called = 0 function! MockHandleCodeAction() abort " delfunction! ale#code_action#HandleCodeAction - function! ale#code_action#HandleCodeAction(action, should_save) abort - AssertEqual v:false, a:should_save + function! ale#code_action#HandleCodeAction(action, options) abort + Assert !get(a:options, 'should_save') let g:handle_code_action_called += 1 endfunction endfunction diff --git a/test/test_code_action.vader b/test/test_code_action.vader index 19de72683d..2e5d138152 100644 --- a/test/test_code_action.vader +++ b/test/test_code_action.vader @@ -85,7 +85,8 @@ Execute(It should modify and save multiple files): \ 'import D from "D"', \], g:file2, 'S') - call ale#code_action#HandleCodeAction({ + call ale#code_action#HandleCodeAction( + \ { \ 'changes': [{ \ 'fileName': g:file1, \ 'textChanges': [{ @@ -122,8 +123,10 @@ Execute(It should modify and save multiple files): \ }, \ 'newText': "import {A, B} from 'module'\n\n", \ }] - \ }], - \}, v:true) + \ }], + \ }, + \ {'should_save': 1}, + \) AssertEqual [ \ 'class Value {', @@ -153,7 +156,8 @@ Execute(Beginning of file can be modified): \] call writefile(g:test.text, g:file1, 'S') - call ale#code_action#HandleCodeAction({ + call ale#code_action#HandleCodeAction( + \ { \ 'changes': [{ \ 'fileName': g:file1, \ 'textChanges': [{ @@ -168,7 +172,9 @@ Execute(Beginning of file can be modified): \ 'newText': "type A: string\ntype B: number\n", \ }], \ }] - \}, v:true) + \ }, + \ {'should_save': 1}, + \) AssertEqual [ \ 'type A: string', @@ -184,22 +190,25 @@ Execute(End of file can be modified): \] call writefile(g:test.text, g:file1, 'S') - call ale#code_action#HandleCodeAction({ + call ale#code_action#HandleCodeAction( + \ { \ 'changes': [{ - \ 'fileName': g:file1, - \ 'textChanges': [{ - \ 'start': { - \ 'line': 4, - \ 'offset': 1, - \ }, - \ 'end': { - \ 'line': 4, - \ 'offset': 1, - \ }, - \ 'newText': "type A: string\ntype B: number\n", - \ }], + \ 'fileName': g:file1, + \ 'textChanges': [{ + \ 'start': { + \ 'line': 4, + \ 'offset': 1, + \ }, + \ 'end': { + \ 'line': 4, + \ 'offset': 1, + \ }, + \ 'newText': "type A: string\ntype B: number\n", + \ }], \ }] - \}, v:true) + \ }, + \ {'should_save': 1}, + \) AssertEqual g:test.text + [ \ 'type A: string', @@ -219,7 +228,8 @@ Execute(Current buffer contents will be reloaded): execute 'edit ' . g:file1 let g:test.buffer = bufnr(g:file1) - call ale#code_action#HandleCodeAction({ + call ale#code_action#HandleCodeAction( + \ { \ 'changes': [{ \ 'fileName': g:file1, \ 'textChanges': [{ @@ -234,7 +244,9 @@ Execute(Current buffer contents will be reloaded): \ 'newText': "type A: string\ntype B: number\n", \ }], \ }] - \}, v:true) + \ }, + \ {'should_save': 1}, + \) AssertEqual [ \ 'type A: string', @@ -256,11 +268,11 @@ Execute(Cursor will not move when it is before text change): let g:test.changes = g:test.create_change(2, 3, 2, 8, 'value2') call setpos('.', [0, 1, 1, 0]) - call ale#code_action#HandleCodeAction(g:test.changes, v:true) + call ale#code_action#HandleCodeAction(g:test.changes, {'should_save': 1}) AssertEqual [1, 1], getpos('.')[1:2] call setpos('.', [0, 2, 2, 0]) - call ale#code_action#HandleCodeAction(g:test.changes, v:true) + call ale#code_action#HandleCodeAction(g:test.changes, {'should_save': 1}) AssertEqual [2, 2], getpos('.')[1:2] # ====C==== @@ -271,7 +283,7 @@ Execute(Cursor column will move to the change end when cursor between start/end) call WriteFileAndEdit() call setpos('.', [0, 2, r, 0]) AssertEqual ' value: string', getline('.') - call ale#code_action#HandleCodeAction(g:test.changes, v:true) + call ale#code_action#HandleCodeAction(g:test.changes, {'should_save': 1}) AssertEqual ' value2: string', getline('.') AssertEqual [2, 9], getpos('.')[1:2] endfor @@ -283,7 +295,9 @@ Execute(Cursor column will move back when new text is shorter): call setpos('.', [0, 2, 8, 0]) AssertEqual ' value: string', getline('.') call ale#code_action#HandleCodeAction( - \ g:test.create_change(2, 3, 2, 8, 'val'), v:true) + \ g:test.create_change(2, 3, 2, 8, 'val'), + \ {'should_save': 1}, + \) AssertEqual ' val: string', getline('.') AssertEqual [2, 6], getpos('.')[1:2] @@ -295,7 +309,7 @@ Execute(Cursor column will move forward when new text is longer): call setpos('.', [0, 2, 8, 0]) AssertEqual ' value: string', getline('.') call ale#code_action#HandleCodeAction( - \ g:test.create_change(2, 3, 2, 8, 'longValue'), v:true) + \ g:test.create_change(2, 3, 2, 8, 'longValue'), {'should_save': 1}) AssertEqual ' longValue: string', getline('.') AssertEqual [2, 12], getpos('.')[1:2] @@ -307,7 +321,7 @@ Execute(Cursor line will move when updates are happening on lines above): call setpos('.', [0, 3, 1, 0]) AssertEqual '}', getline('.') call ale#code_action#HandleCodeAction( - \ g:test.create_change(1, 1, 2, 1, "test\ntest\n"), v:true) + \ g:test.create_change(1, 1, 2, 1, "test\ntest\n"), {'should_save': 1}) AssertEqual '}', getline('.') AssertEqual [4, 1], getpos('.')[1:2] @@ -319,7 +333,7 @@ Execute(Cursor line and column will move when change on lines above and just bef call setpos('.', [0, 2, 2, 0]) AssertEqual ' value: string', getline('.') call ale#code_action#HandleCodeAction( - \ g:test.create_change(1, 1, 2, 1, "test\ntest\n123"), v:true) + \ g:test.create_change(1, 1, 2, 1, "test\ntest\n123"), {'should_save': 1}) AssertEqual '123 value: string', getline('.') AssertEqual [3, 5], getpos('.')[1:2] @@ -331,7 +345,7 @@ Execute(Cursor line and column will move at the end of changes): call setpos('.', [0, 2, 10, 0]) AssertEqual ' value: string', getline('.') call ale#code_action#HandleCodeAction( - \ g:test.create_change(1, 1, 3, 1, "test\n"), v:true) + \ g:test.create_change(1, 1, 3, 1, "test\n"), {'should_save': 1}) AssertEqual '}', getline('.') AssertEqual [2, 1], getpos('.')[1:2] @@ -342,14 +356,14 @@ Execute(Cursor will not move when changes happening on lines >= cursor, but afte call setpos('.', [0, 2, 3, 0]) AssertEqual ' value: string', getline('.') call ale#code_action#HandleCodeAction( - \ g:test.create_change(2, 10, 3, 1, "number\n"), v:true) + \ g:test.create_change(2, 10, 3, 1, "number\n"), {'should_save': 1}) AssertEqual ' value: number', getline('.') AssertEqual [2, 3], getpos('.')[1:2] Execute(It should just modify file when should_save is set to v:false): call WriteFileAndEdit() let g:test.change = g:test.create_change(1, 1, 1, 1, "import { writeFile } from 'fs';\n") - call ale#code_action#HandleCodeAction(g:test.change, v:false) + call ale#code_action#HandleCodeAction(g:test.change, {}) AssertEqual 1, getbufvar(bufnr(''), '&modified') AssertEqual [ \ 'import { writeFile } from ''fs'';', diff --git a/test/test_organize_imports.vader b/test/test_organize_imports.vader index c51ff1c0d8..35cd99ffe2 100644 --- a/test/test_organize_imports.vader +++ b/test/test_organize_imports.vader @@ -57,9 +57,9 @@ Before: call add(g:expr_list, a:expr) endfunction - function! ale#code_action#HandleCodeAction(code_action, should_save) abort + function! ale#code_action#HandleCodeAction(code_action, options) abort let g:handle_code_action_called = 1 - AssertEqual v:false, a:should_save + Assert !get(a:options, 'should_save') call add(g:code_actions, a:code_action) endfunction diff --git a/test/test_rename.vader b/test/test_rename.vader index 34d9e32e9e..2e8b746e56 100644 --- a/test/test_rename.vader +++ b/test/test_rename.vader @@ -57,9 +57,9 @@ Before: call add(g:expr_list, a:expr) endfunction - function! ale#code_action#HandleCodeAction(code_action, should_save) abort + function! ale#code_action#HandleCodeAction(code_action, options) abort let g:handle_code_action_called = 1 - AssertEqual v:true, a:should_save + Assert get(a:options, 'should_save') call add(g:code_actions, a:code_action) endfunction @@ -269,7 +269,7 @@ Execute(tsserver rename requests should be sent): \ }] \ ], \ g:message_list - AssertEqual {'42': {'old_name': 'somelongerline', 'new_name': 'a-new-name'}}, + AssertEqual {'42': {'old_name': 'somelongerline', 'new_name': 'a-new-name', 'force_save': 0}}, \ ale#rename#GetMap() Given python(Some Python file): @@ -470,7 +470,7 @@ Execute(LSP rename requests should be sent): let b:ale_linters = ['pyls'] call setpos('.', [bufnr(''), 1, 5, 0]) - ALERename + ALERename! " We shouldn't register the callback yet. AssertEqual '''''', string(g:Callback) @@ -500,5 +500,5 @@ Execute(LSP rename requests should be sent): \ ], \ g:message_list - AssertEqual {'42': {'old_name': 'foo', 'new_name': 'a-new-name'}}, + AssertEqual {'42': {'old_name': 'foo', 'new_name': 'a-new-name', 'force_save': 1}}, \ ale#rename#GetMap() From 557a1ed5da70cb443a8650766f4e8ea95e8c0da3 Mon Sep 17 00:00:00 2001 From: w0rp Date: Thu, 15 Oct 2020 22:02:27 +0100 Subject: [PATCH 16/40] Make the two nearly identical Swift tools easier to distinguish --- doc/ale-supported-languages-and-tools.txt | 2 +- supported-tools.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index 265f7a9929..4f3afd8532 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -455,8 +455,8 @@ Notes: * SugarSS * `stylelint` * Swift + * Apple `swift-format` * `sourcekit-lsp` - * `swift-format` * `swiftformat` * `swiftlint` * Tcl diff --git a/supported-tools.md b/supported-tools.md index 55e7f71fa3..4946089208 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -464,8 +464,8 @@ formatting. * SugarSS * [stylelint](https://github.com/stylelint/stylelint) * Swift + * [Apple swift-format](https://github.com/apple/swift-format) * [sourcekit-lsp](https://github.com/apple/sourcekit-lsp) - * [swift-format](https://github.com/apple/swift-format) * [swiftformat](https://github.com/nicklockwood/SwiftFormat) * [swiftlint](https://github.com/realm/SwiftLint) * Tcl From b496c4b1648cd15f8f6918d7c29853a8a04525c4 Mon Sep 17 00:00:00 2001 From: David Wood Date: Tue, 19 Nov 2019 21:15:35 +0000 Subject: [PATCH 17/40] Add ormolu fixer. This commit adds a fixer for the Haskell language, ormolu (https://github.com/tweag/ormolu). Signed-off-by: David Wood --- autoload/ale/fix/registry.vim | 5 ++++ autoload/ale/fixers/ormolu.vim | 12 ++++++++++ doc/ale-haskell.txt | 20 ++++++++++++++++ doc/ale-supported-languages-and-tools.txt | 1 + doc/ale.txt | 1 + supported-tools.md | 1 + test/fixers/test_ormolu_fixer_callback.vader | 24 ++++++++++++++++++++ 7 files changed, 64 insertions(+) create mode 100644 autoload/ale/fixers/ormolu.vim create mode 100644 test/fixers/test_ormolu_fixer_callback.vader diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index d71668f2f3..377edafdd2 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -380,6 +380,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['dhall'], \ 'description': 'Fix Dhall files with dhall-format.', \ }, +\ 'ormolu': { +\ 'function': 'ale#fixers#ormolu#Fix', +\ 'suggested_filetypes': ['haskell'], +\ 'description': 'A formatter for Haskell source code.', +\ }, \} " Reset the function registry to the default entries. diff --git a/autoload/ale/fixers/ormolu.vim b/autoload/ale/fixers/ormolu.vim new file mode 100644 index 0000000000..69b55c1f12 --- /dev/null +++ b/autoload/ale/fixers/ormolu.vim @@ -0,0 +1,12 @@ +call ale#Set('haskell_ormolu_executable', 'ormolu') +call ale#Set('haskell_ormolu_options', '') + +function! ale#fixers#ormolu#Fix(buffer) abort + let l:executable = ale#Var(a:buffer, 'haskell_ormolu_executable') + let l:options = ale#Var(a:buffer, 'haskell_ormolu_options') + + return { + \ 'command': ale#Escape(l:executable) + \ . (empty(l:options) ? '' : ' ' . l:options), + \} +endfunction diff --git a/doc/ale-haskell.txt b/doc/ale-haskell.txt index 5dd3ec15e8..fde439fe09 100644 --- a/doc/ale-haskell.txt +++ b/doc/ale-haskell.txt @@ -172,5 +172,25 @@ g:ale_haskell_hie_executable *g:ale_haskell_hie_executable* ide engine. i.e. `'hie-wrapper'` +=============================================================================== +ormolu *ale-haskell-ormolu* + +g:ale_haskell_ormolu_executable *g:ale_haskell_ormolu_executable* + *b:ale_haskell_ormolu_executable* + Type: |String| + Default: `'ormolu'` + + This variable can be changed to use a different executable for ormolu. + + +g:ale_haskell_ormolu_options *g:ale_haskell_ormolu_options* + *b:ale_haskell_ormolu_options* + Type: String + Default: '' + + This variable can be used to pass extra options to the underlying ormolu + executable. + + =============================================================================== vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index 4f3afd8532..57a50dd4d5 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -195,6 +195,7 @@ Notes: * `hie` * `hindent` * `hlint` + * `ormolu` * `stack-build`!! * `stack-ghc` * `stylish-haskell` diff --git a/doc/ale.txt b/doc/ale.txt index eb8f027575..01357cd113 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -2642,6 +2642,7 @@ documented in additional help files. stack-ghc.............................|ale-haskell-stack-ghc| stylish-haskell.......................|ale-haskell-stylish-haskell| hie...................................|ale-haskell-hie| + ormolu................................|ale-haskell-ormolu| hcl.....................................|ale-hcl-options| terraform-fmt.........................|ale-hcl-terraform-fmt| html....................................|ale-html-options| diff --git a/supported-tools.md b/supported-tools.md index 4946089208..6aa196b2f7 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -204,6 +204,7 @@ formatting. * [hie](https://github.com/haskell/haskell-ide-engine) * [hindent](https://hackage.haskell.org/package/hindent) * [hlint](https://hackage.haskell.org/package/hlint) + * [ormolu](https://github.com/tweag/ormolu) * [stack-build](https://haskellstack.org/) :floppy_disk: * [stack-ghc](https://haskellstack.org/) * [stylish-haskell](https://github.com/jaspervdj/stylish-haskell) diff --git a/test/fixers/test_ormolu_fixer_callback.vader b/test/fixers/test_ormolu_fixer_callback.vader new file mode 100644 index 0000000000..8df3fca9d8 --- /dev/null +++ b/test/fixers/test_ormolu_fixer_callback.vader @@ -0,0 +1,24 @@ +Before: + Save g:ale_haskell_ormolu_executable + Save g:ale_haskell_ormolu_options + +After: + Restore + +Execute(The ormolu callback should return the correct default values): + AssertEqual + \ { + \ 'command': ale#Escape('ormolu') + \ }, + \ ale#fixers#ormolu#Fix(bufnr('')) + +Execute(The ormolu executable and options should be configurable): + let g:ale_nix_nixpkgsfmt_executable = '/path/to/ormolu' + let g:ale_nix_nixpkgsfmt_options = '-h' + + AssertEqual + \ { + \ 'command': ale#Escape('/path/to/ormolu') + \ . ' -h', + \ }, + \ ale#fixers#nixpkgsfmt#Fix(bufnr('')) From 513e6ee972ea4ee57b28b8b8c10e0b89bb674f25 Mon Sep 17 00:00:00 2001 From: Lyz Date: Fri, 23 Oct 2020 18:53:38 +0200 Subject: [PATCH 18/40] feat: add autoimport fixer --- autoload/ale/fix/registry.vim | 5 ++ autoload/ale/fixers/autoimport.vim | 25 ++++++++++ doc/ale-python.txt | 26 ++++++++++ doc/ale-supported-languages-and-tools.txt | 1 + doc/ale.txt | 1 + supported-tools.md | 1 + .../env/Scripts/autoimport.exe | 0 .../with_virtualenv/env/bin/autoimport | 0 .../test_autoimport_fixer_callback.vader | 50 +++++++++++++++++++ 9 files changed, 109 insertions(+) create mode 100644 autoload/ale/fixers/autoimport.vim create mode 100755 test/command_callback/python_paths/with_virtualenv/env/Scripts/autoimport.exe create mode 100755 test/command_callback/python_paths/with_virtualenv/env/bin/autoimport create mode 100644 test/fixers/test_autoimport_fixer_callback.vader diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index d71668f2f3..ed9c437032 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -12,6 +12,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['help'], \ 'description': 'Align help tags to the right margin', \ }, +\ 'autoimport': { +\ 'function': 'ale#fixers#autoimport#Fix', +\ 'suggested_filetypes': ['python'], +\ 'description': 'Fix import issues with autoimport.', +\ }, \ 'autopep8': { \ 'function': 'ale#fixers#autopep8#Fix', \ 'suggested_filetypes': ['python'], diff --git a/autoload/ale/fixers/autoimport.vim b/autoload/ale/fixers/autoimport.vim new file mode 100644 index 0000000000..37a52db86b --- /dev/null +++ b/autoload/ale/fixers/autoimport.vim @@ -0,0 +1,25 @@ +" Author: lyz-code +" Description: Fixing Python imports with autoimport. + +call ale#Set('python_autoimport_executable', 'autoimport') +call ale#Set('python_autoimport_options', '') +call ale#Set('python_autoimport_use_global', get(g:, 'ale_use_global_executables', 0)) + +function! ale#fixers#autoimport#Fix(buffer) abort + let l:options = ale#Var(a:buffer, 'python_autoimport_options') + + let l:executable = ale#python#FindExecutable( + \ a:buffer, + \ 'python_autoimport', + \ ['autoimport'], + \) + + if !executable(l:executable) + return 0 + endif + + return { + \ 'command': ale#path#BufferCdString(a:buffer) + \ . ale#Escape(l:executable) . (!empty(l:options) ? ' ' . l:options : '') . ' -', + \} +endfunction diff --git a/doc/ale-python.txt b/doc/ale-python.txt index 6b1a6d3363..c6a207ac06 100644 --- a/doc/ale-python.txt +++ b/doc/ale-python.txt @@ -41,6 +41,32 @@ ALE will look for configuration files with the following filenames. > The first directory containing any of the files named above will be used. +=============================================================================== +autoimport *ale-python-autoimport* + +g:ale_python_autoimport_executable *g:ale_python_autoimport_executable* + *b:ale_python_autoimport_executable* + Type: |String| + Default: `'autoimport'` + + See |ale-integrations-local-executables| + + +g:ale_python_autoimport_options *g:ale_python_autoimport_options* + *b:ale_python_autoimport_options* + Type: |String| + Default: `''` + + This variable can be set to pass extra options to autoimport. + + +g:ale_python_autoimport_use_global *g:ale_python_autoimport_use_global* + *b:ale_python_autoimport_use_global* + Type: |Number| + Default: `get(g:, 'ale_use_global_executables', 0)` + + See |ale-integrations-local-executables| + =============================================================================== autopep8 *ale-python-autopep8* diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index 4f3afd8532..62eed26f4b 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -361,6 +361,7 @@ Notes: * `purescript-language-server` * `purty` * Python + * `autoimport` * `autopep8` * `bandit` * `black` diff --git a/doc/ale.txt b/doc/ale.txt index eb8f027575..204bd0e206 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -2777,6 +2777,7 @@ documented in additional help files. pyrex (cython)..........................|ale-pyrex-options| cython................................|ale-pyrex-cython| python..................................|ale-python-options| + autoimport............................|ale-python-autoimport| autopep8..............................|ale-python-autopep8| bandit................................|ale-python-bandit| black.................................|ale-python-black| diff --git a/supported-tools.md b/supported-tools.md index 4946089208..6325e906aa 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -370,6 +370,7 @@ formatting. * [purescript-language-server](https://github.com/nwolverson/purescript-language-server) * [purty](https://gitlab.com/joneshf/purty) * Python + * [autoimport](https://lyz-code.github.io/autoimport/) * [autopep8](https://github.com/hhatto/autopep8) * [bandit](https://github.com/PyCQA/bandit) :warning: * [black](https://github.com/ambv/black) diff --git a/test/command_callback/python_paths/with_virtualenv/env/Scripts/autoimport.exe b/test/command_callback/python_paths/with_virtualenv/env/Scripts/autoimport.exe new file mode 100755 index 0000000000..e69de29bb2 diff --git a/test/command_callback/python_paths/with_virtualenv/env/bin/autoimport b/test/command_callback/python_paths/with_virtualenv/env/bin/autoimport new file mode 100755 index 0000000000..e69de29bb2 diff --git a/test/fixers/test_autoimport_fixer_callback.vader b/test/fixers/test_autoimport_fixer_callback.vader new file mode 100644 index 0000000000..6952cbb8e4 --- /dev/null +++ b/test/fixers/test_autoimport_fixer_callback.vader @@ -0,0 +1,50 @@ +Before: + Save g:ale_python_autoimport_executable + Save g:ale_python_autoimport_options + + " Use an invalid global executable, so we don't match it. + let g:ale_python_autoimport_executable = 'xxxinvalid' + let g:ale_python_autoimport_options = '' + + call ale#test#SetDirectory('/testplugin/test/fixers') + silent cd .. + silent cd command_callback + let g:dir = getcwd() + + let b:bin_dir = has('win32') ? 'Scripts' : 'bin' + +After: + Restore + + unlet! b:bin_dir + + call ale#test#RestoreDirectory() + +Execute(The autoimport callback should return the correct default values): + AssertEqual + \ 0, + \ ale#fixers#autoimport#Fix(bufnr('')) + + silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.py') + AssertEqual + \ { + \ 'command': ale#path#BufferCdString(bufnr('')) + \ . ale#Escape(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/env/' . b:bin_dir . '/autoimport')) . ' -', + \ }, + \ ale#fixers#autoimport#Fix(bufnr('')) + +Execute(The autoimport callback should respect custom options): + let g:ale_python_autoimport_options = '--multi-line=3 --trailing-comma' + + AssertEqual + \ 0, + \ ale#fixers#autoimport#Fix(bufnr('')) + + silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.py') + AssertEqual + \ { + \ 'command': ale#path#BufferCdString(bufnr('')) + \ . ale#Escape(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/env/' . b:bin_dir . '/autoimport')) + \ . ' --multi-line=3 --trailing-comma -', + \ }, + \ ale#fixers#autoimport#Fix(bufnr('')) From 6c22936303a3236b471baee81c83683de4c25f47 Mon Sep 17 00:00:00 2001 From: Jason Franklin Date: Mon, 2 Nov 2020 10:46:28 -0500 Subject: [PATCH 19/40] Remove a noise comment --- autoload/ale/handlers/sh.vim | 1 - 1 file changed, 1 deletion(-) diff --git a/autoload/ale/handlers/sh.vim b/autoload/ale/handlers/sh.vim index 1e50cb8958..20ed5cb8e7 100644 --- a/autoload/ale/handlers/sh.vim +++ b/autoload/ale/handlers/sh.vim @@ -1,6 +1,5 @@ " Author: w0rp -" Get the shell type for a buffer, based on the hashbang line. function! ale#handlers#sh#GetShellType(buffer) abort let l:bang_line = get(getbufline(a:buffer, 1), 0, '') From 73632312c2519d7925dc57e5abcd0eacc5f1e55f Mon Sep 17 00:00:00 2001 From: Jason Franklin Date: Mon, 2 Nov 2020 10:46:56 -0500 Subject: [PATCH 20/40] Use the proper term for the "#!" line --- autoload/ale/handlers/sh.vim | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/autoload/ale/handlers/sh.vim b/autoload/ale/handlers/sh.vim index 20ed5cb8e7..f99c3dbd98 100644 --- a/autoload/ale/handlers/sh.vim +++ b/autoload/ale/handlers/sh.vim @@ -1,17 +1,17 @@ " Author: w0rp function! ale#handlers#sh#GetShellType(buffer) abort - let l:bang_line = get(getbufline(a:buffer, 1), 0, '') + let l:shebang = get(getbufline(a:buffer, 1), 0, '') let l:command = '' - " Take the shell executable from the hashbang, if we can. - if l:bang_line[:1] is# '#!' + " Take the shell executable from the shebang, if we can. + if l:shebang[:1] is# '#!' " Remove options like -e, etc. - let l:command = substitute(l:bang_line, ' --\?[a-zA-Z0-9]\+', '', 'g') + let l:command = substitute(l:shebang, ' --\?[a-zA-Z0-9]\+', '', 'g') endif - " If we couldn't find a hashbang, try the filetype + " If we couldn't find a shebang, try the filetype if l:command is# '' let l:command = &filetype endif From 31b7a2de41d7bfccfd80a563bf3920258230cdc3 Mon Sep 17 00:00:00 2001 From: Jason Franklin Date: Mon, 2 Nov 2020 10:50:14 -0500 Subject: [PATCH 21/40] Move the test for buffer-local variables The "ale#handlers#sh#GetShellType()" function currently falls back to the file type without checking for buffer-local variables first. This causes the function to return "sh" even when a script is known by Vim to be a script of a more specific type (e.g., "bash"). The "ale#handlers#shellcheck#GetDialectArgument()" function then erroneously uses this type even though a more fitting type should be used instead. Files without a "#!" line will be of type "sh" even though they may have a ".bash" suffix. This commit fixes the problem by checking for buffer-local shell type variables (set by Vim) before falling back to the file type. --- autoload/ale/handlers/sh.vim | 11 +++++++++++ autoload/ale/handlers/shellcheck.vim | 9 --------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/autoload/ale/handlers/sh.vim b/autoload/ale/handlers/sh.vim index f99c3dbd98..6ed9fea37d 100644 --- a/autoload/ale/handlers/sh.vim +++ b/autoload/ale/handlers/sh.vim @@ -11,6 +11,17 @@ function! ale#handlers#sh#GetShellType(buffer) abort let l:command = substitute(l:shebang, ' --\?[a-zA-Z0-9]\+', '', 'g') endif + " With no shebang line, attempt to use Vim's buffer-local variables. + if l:command is# '' + if getbufvar(a:buffer, 'is_bash', 0) + let l:command = 'bash' + elseif getbufvar(a:buffer, 'is_sh', 0) + let l:command = 'sh' + elseif getbufvar(a:buffer, 'is_kornshell', 0) + let l:command = 'ksh' + endif + endif + " If we couldn't find a shebang, try the filetype if l:command is# '' let l:command = &filetype diff --git a/autoload/ale/handlers/shellcheck.vim b/autoload/ale/handlers/shellcheck.vim index b16280f0f4..351d6d3fd3 100644 --- a/autoload/ale/handlers/shellcheck.vim +++ b/autoload/ale/handlers/shellcheck.vim @@ -13,15 +13,6 @@ function! ale#handlers#shellcheck#GetDialectArgument(buffer) abort return l:shell_type endif - " If there's no hashbang, try using Vim's buffer variables. - if getbufvar(a:buffer, 'is_bash', 0) - return 'bash' - elseif getbufvar(a:buffer, 'is_sh', 0) - return 'sh' - elseif getbufvar(a:buffer, 'is_kornshell', 0) - return 'ksh' - endif - return '' endfunction From 01800a23addb52788265e0349f519556dab41301 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Sat, 14 Nov 2020 12:15:17 +0200 Subject: [PATCH 22/40] Support for LSP/tsserver Code Actions (#3437) * Added tsserver and LSP code action support. * tsserver refactors support added. * Handling special case when new text is added after new line symbol. * ale#code_action#ApplyChanges simplified. * Initial attempt on LSP Code Actions. * workspace/executeCommand added. * Some null checks added. * Add last column to LSP Code Action message. * Pass diagnostics to LSP code action. Previously ApplyChanges code was applied from top-to-bottom that required extra parameters to track progress and there was bug. I have changed code to bottom-to-top approach as that does not require those extra parameters and solved the bug. Tested with typescript-language-server and it is working. --- ale_linters/python/jedils.vim | 34 + autoload/ale/code_action.vim | 106 ++- autoload/ale/codefix.vim | 388 +++++++++++ autoload/ale/completion.vim | 1 + autoload/ale/lsp.vim | 10 + autoload/ale/lsp/message.vim | 22 + autoload/ale/lsp/response.vim | 1 + autoload/ale/lsp/tsserver_message.vim | 36 ++ autoload/ale/rename.vim | 56 +- doc/ale.txt | 12 + plugin/ale.vim | 3 + ...st_other_initialize_message_handling.vader | 6 +- test/test_code_action.vader | 4 + test/test_code_action_python.vader | 59 ++ test/test_codefix.vader | 604 ++++++++++++++++++ 15 files changed, 1258 insertions(+), 84 deletions(-) create mode 100644 ale_linters/python/jedils.vim create mode 100644 autoload/ale/codefix.vim create mode 100644 test/test_code_action_python.vader create mode 100644 test/test_codefix.vader diff --git a/ale_linters/python/jedils.vim b/ale_linters/python/jedils.vim new file mode 100644 index 0000000000..eae5fb0756 --- /dev/null +++ b/ale_linters/python/jedils.vim @@ -0,0 +1,34 @@ +" Author: Dalius Dobravolskas +" Description: https://github.com/pappasam/jedi-language-server + +call ale#Set('python_jedils_executable', 'jedi-language-server') +call ale#Set('python_jedils_use_global', get(g:, 'ale_use_global_executables', 0)) +call ale#Set('python_jedils_auto_pipenv', 0) + +function! ale_linters#python#jedils#GetExecutable(buffer) abort + if (ale#Var(a:buffer, 'python_auto_pipenv') || ale#Var(a:buffer, 'python_jedils_auto_pipenv')) + \ && ale#python#PipenvPresent(a:buffer) + return 'pipenv' + endif + + return ale#python#FindExecutable(a:buffer, 'python_jedils', ['jedi-language-server']) +endfunction + +function! ale_linters#python#jedils#GetCommand(buffer) abort + let l:executable = ale_linters#python#jedils#GetExecutable(a:buffer) + + let l:exec_args = l:executable =~? 'pipenv$' + \ ? ' run jedi-language-server' + \ : '' + + return ale#Escape(l:executable) . l:exec_args +endfunction + +call ale#linter#Define('python', { +\ 'name': 'jedils', +\ 'lsp': 'stdio', +\ 'executable': function('ale_linters#python#jedils#GetExecutable'), +\ 'command': function('ale_linters#python#jedils#GetCommand'), +\ 'project_root': function('ale#python#FindProjectRoot'), +\ 'completion_filter': 'ale#completion#python#CompletionItemFilter', +\}) diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index 42f4f26553..4dbb2d0844 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -33,35 +33,35 @@ endfunction function! s:ChangeCmp(left, right) abort if a:left.start.line < a:right.start.line - return -1 + return 1 endif if a:left.start.line > a:right.start.line - return 1 + return -1 endif if a:left.start.offset < a:right.start.offset - return -1 + return 1 endif if a:left.start.offset > a:right.start.offset - return 1 + return -1 endif if a:left.end.line < a:right.end.line - return -1 + return 1 endif if a:left.end.line > a:right.end.line - return 1 + return -1 endif if a:left.end.offset < a:right.end.offset - return -1 + return 1 endif if a:left.end.offset > a:right.end.offset - return 1 + return -1 endif return 0 @@ -85,29 +85,14 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort let l:pos = [1, 1] endif - " We have to keep track of how many lines we have added, and offset - " changes accordingly. - let l:line_offset = 0 - let l:column_offset = 0 - let l:last_end_line = 0 - - " Changes have to be sorted so we apply them from top-to-bottom. + " Changes have to be sorted so we apply them from bottom-to-top for l:code_edit in sort(copy(a:changes), function('s:ChangeCmp')) - if l:code_edit.start.line isnot l:last_end_line - let l:column_offset = 0 - endif - - let l:line = l:code_edit.start.line + l:line_offset - let l:column = l:code_edit.start.offset + l:column_offset - let l:end_line = l:code_edit.end.line + l:line_offset - let l:end_column = l:code_edit.end.offset + l:column_offset + let l:line = l:code_edit.start.line + let l:column = l:code_edit.start.offset + let l:end_line = l:code_edit.end.line + let l:end_column = l:code_edit.end.offset let l:text = l:code_edit.newText - let l:cur_line = l:pos[0] - let l:cur_column = l:pos[1] - - let l:last_end_line = l:end_line - " Adjust the ends according to previous edits. if l:end_line > len(l:lines) let l:end_line_len = 0 @@ -125,6 +110,12 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort let l:start = l:lines[: l:line - 2] endif + " Special case when text must be added after new line + if l:column > len(l:lines[l:line - 1]) + call extend(l:start, [l:lines[l:line - 1]]) + let l:column = 1 + endif + if l:column is 1 " We need to handle column 1 specially, because we can't slice an " empty string ending on index 0. @@ -140,7 +131,6 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort let l:lines = l:start + l:middle + l:lines[l:end_line :] let l:current_line_offset = len(l:lines) - l:lines_before_change - let l:line_offset += l:current_line_offset let l:column_offset = len(l:middle[-1]) - l:end_line_len let l:pos = s:UpdateCursor(l:pos, @@ -215,3 +205,61 @@ function! s:UpdateCursor(cursor, start, end, offset) abort return [l:cur_line, l:cur_column] endfunction + +function! ale#code_action#GetChanges(workspace_edit) abort + let l:changes = {} + + if has_key(a:workspace_edit, 'changes') && !empty(a:workspace_edit.changes) + return a:workspace_edit.changes + elseif has_key(a:workspace_edit, 'documentChanges') + let l:document_changes = [] + + if type(a:workspace_edit.documentChanges) is v:t_dict + \ && has_key(a:workspace_edit.documentChanges, 'edits') + call add(l:document_changes, a:workspace_edit.documentChanges) + elseif type(a:workspace_edit.documentChanges) is v:t_list + let l:document_changes = a:workspace_edit.documentChanges + endif + + for l:text_document_edit in l:document_changes + let l:filename = l:text_document_edit.textDocument.uri + let l:edits = l:text_document_edit.edits + let l:changes[l:filename] = l:edits + endfor + endif + + return l:changes +endfunction + +function! ale#code_action#BuildChangesList(changes_map) abort + let l:changes = [] + + for l:file_name in keys(a:changes_map) + let l:text_edits = a:changes_map[l:file_name] + let l:text_changes = [] + + for l:edit in l:text_edits + let l:range = l:edit.range + let l:new_text = l:edit.newText + + call add(l:text_changes, { + \ 'start': { + \ 'line': l:range.start.line + 1, + \ 'offset': l:range.start.character + 1, + \ }, + \ 'end': { + \ 'line': l:range.end.line + 1, + \ 'offset': l:range.end.character + 1, + \ }, + \ 'newText': l:new_text, + \}) + endfor + + call add(l:changes, { + \ 'fileName': ale#path#FromURI(l:file_name), + \ 'textChanges': l:text_changes, + \}) + endfor + + return l:changes +endfunction diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim new file mode 100644 index 0000000000..b58f5e4b36 --- /dev/null +++ b/autoload/ale/codefix.vim @@ -0,0 +1,388 @@ +" Author: Dalius Dobravolskas +" Description: Code Fix support for tsserver + +let s:codefix_map = {} + +" Used to get the codefix map in tests. +function! ale#codefix#GetMap() abort + return deepcopy(s:codefix_map) +endfunction + +" Used to set the codefix map in tests. +function! ale#codefix#SetMap(map) abort + let s:codefix_map = a:map +endfunction + +function! ale#codefix#ClearLSPData() abort + let s:codefix_map = {} +endfunction + +function! s:message(message) abort + call ale#util#Execute('echom ' . string(a:message)) +endfunction + +function! ale#codefix#HandleTSServerResponse(conn_id, response) abort + if !has_key(a:response, 'request_seq') + \ || !has_key(s:codefix_map, a:response.request_seq) + return + endif + + let l:location = remove(s:codefix_map, a:response.request_seq) + + if get(a:response, 'command', '') is# 'getCodeFixes' + if get(a:response, 'success', v:false) is v:false + let l:message = get(a:response, 'message', 'unknown') + call s:message('Error while getting code fixes. Reason: ' . l:message) + + return + endif + + if len(a:response.body) == 0 + call s:message('No code fixes available.') + + return + endif + + let l:code_fix_to_apply = 0 + + if len(a:response.body) == 1 + let l:code_fix_to_apply = 1 + else + let l:codefix_no = 1 + let l:codefixstring = "Code Fixes:\n" + + for l:codefix in a:response.body + let l:codefixstring .= l:codefix_no . ') ' . l:codefix.description . "\n" + let l:codefix_no += 1 + endfor + + let l:codefixstring .= 'Type number and (empty cancels): ' + + let l:code_fix_to_apply = ale#util#Input(l:codefixstring, '') + let l:code_fix_to_apply = str2nr(l:code_fix_to_apply) + + if l:code_fix_to_apply == 0 + return + endif + endif + + let l:changes = a:response.body[l:code_fix_to_apply - 1].changes + + call ale#code_action#HandleCodeAction({ + \ 'description': 'codefix', + \ 'changes': l:changes, + \}, {}) + elseif get(a:response, 'command', '') is# 'getApplicableRefactors' + if get(a:response, 'success', v:false) is v:false + let l:message = get(a:response, 'message', 'unknown') + call s:message('Error while getting applicable refactors. Reason: ' . l:message) + + return + endif + + if len(a:response.body) == 0 + call s:message('No applicable refactors available.') + + return + endif + + let l:refactors = [] + + for l:item in a:response.body + for l:action in l:item.actions + call add(l:refactors, { + \ 'name': l:action.description, + \ 'id': [l:item.name, l:action.name], + \}) + endfor + endfor + + let l:refactor_no = 1 + let l:refactorstring = "Applicable refactors:\n" + + for l:refactor in l:refactors + let l:refactorstring .= l:refactor_no . ') ' . l:refactor.name . "\n" + let l:refactor_no += 1 + endfor + + let l:refactorstring .= 'Type number and (empty cancels): ' + + let l:refactor_to_apply = ale#util#Input(l:refactorstring, '') + let l:refactor_to_apply = str2nr(l:refactor_to_apply) + + if l:refactor_to_apply == 0 + return + endif + + let l:id = l:refactors[l:refactor_to_apply - 1].id + + let l:message = ale#lsp#tsserver_message#GetEditsForRefactor( + \ l:location.buffer, + \ l:location.line, + \ l:location.column, + \ l:location.end_line, + \ l:location.end_column, + \ l:id[0], + \ l:id[1], + \) + + let l:request_id = ale#lsp#Send(l:location.connection_id, l:message) + + let s:codefix_map[l:request_id] = l:location + elseif get(a:response, 'command', '') is# 'getEditsForRefactor' + if get(a:response, 'success', v:false) is v:false + let l:message = get(a:response, 'message', 'unknown') + call s:message('Error while getting edits for refactor. Reason: ' . l:message) + + return + endif + + call ale#code_action#HandleCodeAction({ + \ 'description': 'editsForRefactor', + \ 'changes': a:response.body.edits, + \}, {}) + endif +endfunction + +function! ale#codefix#HandleLSPResponse(conn_id, response) abort + if has_key(a:response, 'method') + \ && a:response.method is# 'workspace/applyEdit' + \ && has_key(a:response, 'params') + let l:params = a:response.params + + let l:changes_map = ale#code_action#GetChanges(l:params.edit) + + if empty(l:changes_map) + return + endif + + let l:changes = ale#code_action#BuildChangesList(l:changes_map) + + call ale#code_action#HandleCodeAction({ + \ 'description': 'applyEdit', + \ 'changes': l:changes, + \}, {}) + elseif has_key(a:response, 'id') + \&& has_key(s:codefix_map, a:response.id) + let l:location = remove(s:codefix_map, a:response.id) + + if !has_key(a:response, 'result') + \ || type(a:response.result) != v:t_list + \ || len(a:response.result) == 0 + call s:message('No code actions received from server') + + return + endif + + let l:codeaction_no = 1 + let l:codeactionstring = "Code Fixes:\n" + + for l:codeaction in a:response.result + let l:codeactionstring .= l:codeaction_no . ') ' . l:codeaction.title . "\n" + let l:codeaction_no += 1 + endfor + + let l:codeactionstring .= 'Type number and (empty cancels): ' + + let l:codeaction_to_apply = ale#util#Input(l:codeactionstring, '') + let l:codeaction_to_apply = str2nr(l:codeaction_to_apply) + + if l:codeaction_to_apply == 0 + return + endif + + let l:item = a:response.result[l:codeaction_to_apply - 1] + + if has_key(l:item, 'command') + \ && type(l:item.command) == v:t_dict + let l:command = l:item.command + let l:message = ale#lsp#message#ExecuteCommand( + \ l:command.command, + \ l:command.arguments, + \) + + let l:request_id = ale#lsp#Send(l:location.connection_id, l:message) + elseif has_key(l:item, 'edit') || has_key(l:item, 'arguments') + if has_key(l:item, 'edit') + let l:topass = l:item.edit + else + let l:topass = l:item.arguments[0] + endif + + let l:changes_map = ale#code_action#GetChanges(l:topass) + + if empty(l:changes_map) + return + endif + + let l:changes = ale#code_action#BuildChangesList(l:changes_map) + + call ale#code_action#HandleCodeAction({ + \ 'description': 'codeaction', + \ 'changes': l:changes, + \}, {}) + endif + endif +endfunction + + +function! s:OnReady(line, column, end_line, end_column, linter, lsp_details) abort + let l:id = a:lsp_details.connection_id + + if !ale#lsp#HasCapability(l:id, 'code_actions') + return + endif + + let l:buffer = a:lsp_details.buffer + + if a:linter.lsp is# 'tsserver' + if a:line == a:end_line && a:column == a:end_column + if !has_key(g:ale_buffer_info, l:buffer) + return + endif + + let l:nearest_error = v:null + let l:nearest_error_diff = -1 + + for l:error in get(g:ale_buffer_info[l:buffer], 'loclist', []) + if has_key(l:error, 'code') && l:error.lnum == a:line + let l:diff = abs(l:error.col - a:column) + + if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff + let l:nearest_error_diff = l:diff + let l:nearest_error = l:error.code + endif + endif + endfor + + let l:message = ale#lsp#tsserver_message#GetCodeFixes( + \ l:buffer, + \ a:line, + \ a:column, + \ a:line, + \ a:column, + \ [l:nearest_error], + \) + else + let l:message = ale#lsp#tsserver_message#GetApplicableRefactors( + \ l:buffer, + \ a:line, + \ a:column, + \ a:end_line, + \ a:end_column, + \) + endif + else + " Send a message saying the buffer has changed first, otherwise + " completions won't know what text is nearby. + call ale#lsp#NotifyForChanges(l:id, l:buffer) + + if a:line == a:end_line && a:column == a:end_column + if !has_key(g:ale_buffer_info, l:buffer) + return + endif + + let l:nearest_error = v:null + let l:nearest_error_diff = -1 + + for l:error in get(g:ale_buffer_info[l:buffer], 'loclist', []) + if has_key(l:error, 'code') && l:error.lnum == a:line + let l:diff = abs(l:error.col - a:column) + + if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff + let l:nearest_error_diff = l:diff + let l:nearest_error = l:error + endif + endif + endfor + + let l:diagnostics = [] + + if l:nearest_error isnot v:null + let l:diagnostics = [{ + \ 'code': l:nearest_error.code, + \ 'message': l:nearest_error.text, + \ 'range': { + \ 'start': { 'line': l:nearest_error.lnum - 1, 'character': l:nearest_error.col - 1 }, + \ 'end': { 'line': l:nearest_error.end_lnum - 1, 'character': l:nearest_error.end_col - 1 } + \} + \}] + endif + + let l:message = ale#lsp#message#CodeAction( + \ l:buffer, + \ a:line, + \ a:column, + \ a:end_line, + \ a:end_column, + \ l:diagnostics, + \) + else + let l:message = ale#lsp#message#CodeAction( + \ l:buffer, + \ a:line, + \ a:column, + \ a:end_line, + \ a:end_column, + \ [], + \) + endif + endif + + let l:Callback = a:linter.lsp is# 'tsserver' + \ ? function('ale#codefix#HandleTSServerResponse') + \ : function('ale#codefix#HandleLSPResponse') + + call ale#lsp#RegisterCallback(l:id, l:Callback) + + let l:request_id = ale#lsp#Send(l:id, l:message) + + let s:codefix_map[l:request_id] = { + \ 'connection_id': l:id, + \ 'buffer': l:buffer, + \ 'line': a:line, + \ 'column': a:column, + \ 'end_line': a:end_line, + \ 'end_column': a:end_column, + \} +endfunction + +function! s:ExecuteGetCodeFix(linter, range) abort + let l:buffer = bufnr('') + + if a:range == 0 + let [l:line, l:column] = getpos('.')[1:2] + let l:end_line = l:line + let l:end_column = l:column + else + let [l:line, l:column] = getpos("'<")[1:2] + let [l:end_line, l:end_column] = getpos("'>")[1:2] + endif + + let l:column = min([l:column, len(getline(l:line))]) + let l:end_column = min([l:end_column, len(getline(l:end_line))]) + + let l:Callback = function( + \ 's:OnReady', [l:line, l:column, l:end_line, l:end_column]) + call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) +endfunction + +function! ale#codefix#Execute(range) abort + let l:lsp_linters = [] + + for l:linter in ale#linter#Get(&filetype) + if !empty(l:linter.lsp) + call add(l:lsp_linters, l:linter) + endif + endfor + + if empty(l:lsp_linters) + call s:message('No active LSPs') + + return + endif + + for l:lsp_linter in l:lsp_linters + call s:ExecuteGetCodeFix(l:lsp_linter, a:range) + endfor +endfunction diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index efbf0fd53b..48e9bf7c8e 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -617,6 +617,7 @@ function! ale#completion#ParseLSPCompletions(response) abort let l:user_data = {'_ale_completion_item': 1} if has_key(l:item, 'additionalTextEdits') + \ && l:item.additionalTextEdits isnot v:null let l:text_changes = [] for l:edit in l:item.additionalTextEdits diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index 7d99e9d2b4..cb0573aa69 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -44,6 +44,7 @@ function! ale#lsp#Register(executable_or_address, project, init_options) abort \ 'definition': 0, \ 'typeDefinition': 0, \ 'symbol_search': 0, + \ 'code_actions': 0, \ }, \} endif @@ -219,6 +220,14 @@ function! s:UpdateCapabilities(conn, capabilities) abort let a:conn.capabilities.rename = 1 endif + if get(a:capabilities, 'codeActionProvider') is v:true + let a:conn.capabilities.code_actions = 1 + endif + + if type(get(a:capabilities, 'codeActionProvider')) is v:t_dict + let a:conn.capabilities.code_actions = 1 + endif + if !empty(get(a:capabilities, 'completionProvider')) let a:conn.capabilities.completion = 1 endif @@ -350,6 +359,7 @@ function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort let l:conn.capabilities.definition = 1 let l:conn.capabilities.symbol_search = 1 let l:conn.capabilities.rename = 1 + let l:conn.capabilities.code_actions = 1 endfunction function! s:SendInitMessage(conn) abort diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim index 5b0cb8b7b8..38be4da67d 100644 --- a/autoload/ale/lsp/message.vim +++ b/autoload/ale/lsp/message.vim @@ -172,3 +172,25 @@ function! ale#lsp#message#Rename(buffer, line, column, new_name) abort \ 'newName': a:new_name, \}] endfunction + +function! ale#lsp#message#CodeAction(buffer, line, column, end_line, end_column, diagnostics) abort + return [0, 'textDocument/codeAction', { + \ 'textDocument': { + \ 'uri': ale#path#ToURI(expand('#' . a:buffer . ':p')), + \ }, + \ 'range': { + \ 'start': {'line': a:line - 1, 'character': a:column - 1}, + \ 'end': {'line': a:end_line - 1, 'character': a:end_column}, + \ }, + \ 'context': { + \ 'diagnostics': a:diagnostics + \ }, + \}] +endfunction + +function! ale#lsp#message#ExecuteCommand(command, arguments) abort + return [0, 'workspace/executeCommand', { + \ 'command': a:command, + \ 'arguments': a:arguments, + \}] +endfunction diff --git a/autoload/ale/lsp/response.vim b/autoload/ale/lsp/response.vim index 30da77e1ca..a4f809802e 100644 --- a/autoload/ale/lsp/response.vim +++ b/autoload/ale/lsp/response.vim @@ -56,6 +56,7 @@ function! ale#lsp#response#ReadDiagnostics(response) abort endif if has_key(l:diagnostic, 'relatedInformation') + \ && l:diagnostic.relatedInformation isnot v:null let l:related = deepcopy(l:diagnostic.relatedInformation) call map(l:related, {key, val -> \ ale#path#FromURI(val.location.uri) . diff --git a/autoload/ale/lsp/tsserver_message.vim b/autoload/ale/lsp/tsserver_message.vim index b9fafaa06b..3c1b47ed3c 100644 --- a/autoload/ale/lsp/tsserver_message.vim +++ b/autoload/ale/lsp/tsserver_message.vim @@ -103,3 +103,39 @@ function! ale#lsp#tsserver_message#OrganizeImports(buffer) abort \ }, \}] endfunction + +function! ale#lsp#tsserver_message#GetCodeFixes(buffer, line, column, end_line, end_column, error_codes) abort + " The lines and columns are 1-based. + " The errors codes must be a list of tsserver error codes to fix. + return [0, 'ts@getCodeFixes', { + \ 'startLine': a:line, + \ 'startOffset': a:column, + \ 'endLine': a:end_line, + \ 'endOffset': a:end_column + 1, + \ 'file': expand('#' . a:buffer . ':p'), + \ 'errorCodes': a:error_codes, + \}] +endfunction + +function! ale#lsp#tsserver_message#GetApplicableRefactors(buffer, line, column, end_line, end_column) abort + " The arguments for this request can also be just 'line' and 'offset' + return [0, 'ts@getApplicableRefactors', { + \ 'startLine': a:line, + \ 'startOffset': a:column, + \ 'endLine': a:end_line, + \ 'endOffset': a:end_column + 1, + \ 'file': expand('#' . a:buffer . ':p'), + \}] +endfunction + +function! ale#lsp#tsserver_message#GetEditsForRefactor(buffer, line, column, end_line, end_column, refactor, action) abort + return [0, 'ts@getEditsForRefactor', { + \ 'startLine': a:line, + \ 'startOffset': a:column, + \ 'endLine': a:end_line, + \ 'endOffset': a:end_column + 1, + \ 'file': expand('#' . a:buffer . ':p'), + \ 'refactor': a:refactor, + \ 'action': a:action, + \}] +endfunction diff --git a/autoload/ale/rename.vim b/autoload/ale/rename.vim index 8190411db2..0d074c245f 100644 --- a/autoload/ale/rename.vim +++ b/autoload/ale/rename.vim @@ -90,31 +90,6 @@ function! ale#rename#HandleTSServerResponse(conn_id, response) abort \) endfunction -function! s:getChanges(workspace_edit) abort - let l:changes = {} - - if has_key(a:workspace_edit, 'changes') && !empty(a:workspace_edit.changes) - return a:workspace_edit.changes - elseif has_key(a:workspace_edit, 'documentChanges') - let l:document_changes = [] - - if type(a:workspace_edit.documentChanges) is v:t_dict - \ && has_key(a:workspace_edit.documentChanges, 'edits') - call add(l:document_changes, a:workspace_edit.documentChanges) - elseif type(a:workspace_edit.documentChanges) is v:t_list - let l:document_changes = a:workspace_edit.documentChanges - endif - - for l:text_document_edit in l:document_changes - let l:filename = l:text_document_edit.textDocument.uri - let l:edits = l:text_document_edit.edits - let l:changes[l:filename] = l:edits - endfor - endif - - return l:changes -endfunction - function! ale#rename#HandleLSPResponse(conn_id, response) abort if has_key(a:response, 'id') \&& has_key(s:rename_map, a:response.id) @@ -126,7 +101,7 @@ function! ale#rename#HandleLSPResponse(conn_id, response) abort return endif - let l:changes_map = s:getChanges(a:response.result) + let l:changes_map = ale#code_action#GetChanges(a:response.result) if empty(l:changes_map) call s:message('No changes received from server') @@ -134,34 +109,7 @@ function! ale#rename#HandleLSPResponse(conn_id, response) abort return endif - let l:changes = [] - - for l:file_name in keys(l:changes_map) - let l:text_edits = l:changes_map[l:file_name] - let l:text_changes = [] - - for l:edit in l:text_edits - let l:range = l:edit.range - let l:new_text = l:edit.newText - - call add(l:text_changes, { - \ 'start': { - \ 'line': l:range.start.line + 1, - \ 'offset': l:range.start.character + 1, - \ }, - \ 'end': { - \ 'line': l:range.end.line + 1, - \ 'offset': l:range.end.character + 1, - \ }, - \ 'newText': l:new_text, - \}) - endfor - - call add(l:changes, { - \ 'fileName': ale#path#FromURI(l:file_name), - \ 'textChanges': l:text_changes, - \}) - endfor + let l:changes = ale#code_action#BuildChangesList(l:changes_map) call ale#code_action#HandleCodeAction( \ { diff --git a/doc/ale.txt b/doc/ale.txt index eb8f027575..4263f69808 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -3106,6 +3106,18 @@ ALERename *ALERename* in those files will not be updated. +ALECodeAction *ALECodeAction* + + Code Actions support for `tsserver`. + + There are two different kind of actions supported for `tsserver`. If run in + normal mode then code fix will be attempted if there is error in that line. + If there are multiple code fixes available use will be shown input to choose + one. In visual mode `tsserver` will be queries for applicable refactors + (e.g. extract to constant or extract to function) and user will be given + choice to select the one he/she likes. + + ALERepeatSelection *ALERepeatSelection* Repeat the last selection displayed in the preview window. diff --git a/plugin/ale.vim b/plugin/ale.vim index 32ec14ac5f..7472ea8cd5 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -240,6 +240,8 @@ command! -bar ALEImport :call ale#completion#Import() " Rename symbols using tsserver and LSP command! -bar -bang ALERename :call ale#rename#Execute({'force_save': '' is# '!'}) +command! -bar -range ALECodeAction :call ale#codefix#Execute() + " Organize import statements using tsserver command! -bar ALEOrganizeImports :call ale#organize_imports#Execute() @@ -283,6 +285,7 @@ nnoremap (ale_documentation) :ALEDocumentation inoremap (ale_complete) :ALEComplete nnoremap (ale_import) :ALEImport nnoremap (ale_rename) :ALERename +nnoremap (ale_code_action) :ALECodeAction nnoremap (ale_repeat_selection) :ALERepeatSelection " Set up autocmd groups now. diff --git a/test/lsp/test_other_initialize_message_handling.vader b/test/lsp/test_other_initialize_message_handling.vader index b6ef852a1a..f3b538435f 100644 --- a/test/lsp/test_other_initialize_message_handling.vader +++ b/test/lsp/test_other_initialize_message_handling.vader @@ -23,6 +23,7 @@ Before: \ 'completion_trigger_characters': [], \ 'definition': 0, \ 'symbol_search': 0, + \ 'code_actions': 0, \ }, \} @@ -102,6 +103,7 @@ Execute(Capabilities should bet set up correctly): \ 'definition': 1, \ 'symbol_search': 1, \ 'rename': 1, + \ 'code_actions': 1, \ }, \ b:conn.capabilities AssertEqual [[1, 'initialized', {}]], g:message_list @@ -125,7 +127,7 @@ Execute(Disabled capabilities should be recognised correctly): \ 'referencesProvider': v:false, \ 'textDocumentSync': 2, \ 'documentFormattingProvider': v:true, - \ 'codeActionProvider': v:true, + \ 'codeActionProvider': v:false, \ 'signatureHelpProvider': { \ 'triggerCharacters': ['(', ','], \ }, @@ -146,6 +148,7 @@ Execute(Disabled capabilities should be recognised correctly): \ 'definition': 0, \ 'symbol_search': 0, \ 'rename': 0, + \ 'code_actions': 0, \ }, \ b:conn.capabilities AssertEqual [[1, 'initialized', {}]], g:message_list @@ -197,6 +200,7 @@ Execute(Capabilities should be enabled when send as Dictionaries): \ 'typeDefinition': 1, \ 'symbol_search': 1, \ 'rename': 1, + \ 'code_actions': 1, \ }, \ b:conn.capabilities AssertEqual [[1, 'initialized', {}]], g:message_list diff --git a/test/test_code_action.vader b/test/test_code_action.vader index 2e5d138152..7eabb75948 100644 --- a/test/test_code_action.vader +++ b/test/test_code_action.vader @@ -3,6 +3,9 @@ Before: let g:ale_enabled = 0 + " Enable fix end-of-line as tests below expect that + set fixeol + runtime autoload/ale/code_action.vim runtime autoload/ale/util.vim @@ -211,6 +214,7 @@ Execute(End of file can be modified): \) AssertEqual g:test.text + [ + \ '', \ 'type A: string', \ 'type B: number', \ '', diff --git a/test/test_code_action_python.vader b/test/test_code_action_python.vader new file mode 100644 index 0000000000..fd30633de2 --- /dev/null +++ b/test/test_code_action_python.vader @@ -0,0 +1,59 @@ +Given python(An example Python file): + def main(): + a = 1 + c = a + 1 + +Execute(): + let g:changes = [ + \ {'end': {'offset': 7, 'line': 1}, 'newText': 'func_qtffgsv', 'start': {'offset': 5, 'line': 1}}, + \ {'end': {'offset': 9, 'line': 1}, 'newText': '', 'start': {'offset': 8, 'line': 1}}, + \ {'end': {'offset': 15, 'line': 3}, 'newText': " return c\n\n\ndef main():\n c = func_qtffgsvi()\n", 'start': {'offset': 15, 'line': 3}} + \] + + call ale#code_action#ApplyChanges(expand('%:p'), g:changes, 0) + +Expect(The changes should be applied correctly): + def func_qtffgsvi(): + a = 1 + c = a + 1 + return c + + + def main(): + c = func_qtffgsvi() + + +Given python(Second python example): + import sys + import exifread + + def main(): + with open(sys.argv[1], 'rb') as f: + exif = exifread.process_file(f) + dt = str(exif['Image DateTime']) + date = dt[:10].replace(':', '-') + +Execute(): + let g:changes = [ + \ {'end': {'offset': 16, 'line': 2}, 'newText': "\n\ndef func_ivlpdpao(f):\n exif = exifread.process_file(f)\n dt = str(exif['Image DateTime'])\n date = dt[:10].replace(':', '-')\n return date\n", 'start': {'offset': 16, 'line': 2}}, + \ {'end': {'offset': 32, 'line': 6}, 'newText': 'date = func', 'start': {'offset': 9, 'line': 6}}, + \ {'end': {'offset': 42, 'line': 8}, 'newText': "ivlpdpao(f)\n", 'start': {'offset': 33, 'line': 6}} + \] + + call ale#code_action#ApplyChanges(expand('%:p'), g:changes, 0) + +Expect(The changes should be applied correctly): + import sys + import exifread + + + def func_ivlpdpao(f): + exif = exifread.process_file(f) + dt = str(exif['Image DateTime']) + date = dt[:10].replace(':', '-') + return date + + + def main(): + with open(sys.argv[1], 'rb') as f: + date = func_ivlpdpao(f) diff --git a/test/test_codefix.vader b/test/test_codefix.vader new file mode 100644 index 0000000000..63275d7f2e --- /dev/null +++ b/test/test_codefix.vader @@ -0,0 +1,604 @@ +Before: + call ale#test#SetDirectory('/testplugin/test') + call ale#test#SetFilename('dummy.txt') + Save g:ale_buffer_info + + let g:ale_buffer_info = {} + + let g:old_filename = expand('%:p') + let g:Callback = '' + let g:expr_list = [] + let g:message_list = [] + let g:handle_code_action_called = 0 + let g:code_actions = [] + let g:options = {} + let g:capability_checked = '' + let g:conn_id = v:null + let g:InitCallback = v:null + + runtime autoload/ale/lsp_linter.vim + runtime autoload/ale/lsp.vim + runtime autoload/ale/util.vim + runtime autoload/ale/codefix.vim + runtime autoload/ale/code_action.vim + + function! ale#lsp_linter#StartLSP(buffer, linter, Callback) abort + let g:conn_id = ale#lsp#Register('executable', '/foo/bar', {}) + call ale#lsp#MarkDocumentAsOpen(g:conn_id, a:buffer) + + if a:linter.lsp is# 'tsserver' + call ale#lsp#MarkConnectionAsTsserver(g:conn_id) + endif + + let l:details = { + \ 'command': 'foobar', + \ 'buffer': a:buffer, + \ 'connection_id': g:conn_id, + \ 'project_root': '/foo/bar', + \} + + let g:InitCallback = {-> ale#lsp_linter#OnInit(a:linter, l:details, a:Callback)} + endfunction + + function! ale#lsp#HasCapability(conn_id, capability) abort + let g:capability_checked = a:capability + + return 1 + endfunction + + function! ale#lsp#RegisterCallback(conn_id, callback) abort + let g:Callback = a:callback + endfunction + + function! ale#lsp#Send(conn_id, message) abort + call add(g:message_list, a:message) + + return 42 + endfunction + + function! ale#util#Execute(expr) abort + call add(g:expr_list, a:expr) + endfunction + + function! ale#code_action#HandleCodeAction(code_action, options) abort + let g:handle_code_action_called = 1 + Assert !get(a:options, 'should_save') + call add(g:code_actions, a:code_action) + endfunction + + function! ale#util#Input(message, value) abort + return '2' + endfunction + +After: + Restore + + if g:conn_id isnot v:null + call ale#lsp#RemoveConnectionWithID(g:conn_id) + endif + + call ale#test#RestoreDirectory() + call ale#linter#Reset() + + unlet! g:capability_checked + unlet! g:InitCallback + unlet! g:old_filename + unlet! g:conn_id + unlet! g:Callback + unlet! g:message_list + unlet! g:expr_list + unlet! b:ale_linters + unlet! g:options + unlet! g:code_actions + unlet! g:handle_code_action_called + + runtime autoload/ale/lsp_linter.vim + runtime autoload/ale/lsp.vim + runtime autoload/ale/util.vim + runtime autoload/ale/codefix.vim + runtime autoload/ale/code_action.vim + +Execute(Failed codefix responses should be handled correctly): + call ale#codefix#HandleTSServerResponse( + \ 1, + \ {'command': 'getCodeFixes', 'request_seq': 3} + \) + AssertEqual g:handle_code_action_called, 0 + + + +Given typescript(Some typescript file): + foo + somelongerline + bazxyzxyzxyz + +Execute(getCodeFixes from tsserver should be handled): + call ale#codefix#SetMap({3: {}}) + call ale#codefix#HandleTSServerResponse(1, { + \ 'command': 'getCodeFixes', + \ 'request_seq': 3, + \ 'success': v:true, + \ 'type': 'response', + \ 'body': [ + \ { + \ 'description': 'Import default "x" from module "./z"', + \ 'fixName': 'import', + \ 'changes': [ + \ { + \ 'fileName': "/foo/bar/file1.ts", + \ 'textChanges': [ + \ { + \ 'end': { + \ 'line': 2, + \ 'offset': 1, + \ }, + \ 'newText': 'import x from "./z";^@', + \ 'start': { + \ 'line': 2, + \ 'offset': 1, + \ } + \ } + \ ] + \ } + \ ] + \ } + \ ] + \}) + + AssertEqual g:handle_code_action_called, 1 + AssertEqual + \ [ + \ { + \ 'description': 'codefix', + \ 'changes': [ + \ { + \ 'fileName': "/foo/bar/file1.ts", + \ 'textChanges': [ + \ { + \ 'end': { + \ 'line': 2, + \ 'offset': 1 + \ }, + \ 'newText': 'import x from "./z";^@', + \ 'start': { + \ 'line': 2, + \ 'offset': 1 + \ } + \ } + \ ] + \ } + \ ] + \ } + \ ], + \ g:code_actions + +Execute(getCodeFixes from tsserver should be handled with user input if there are more than one action): + call ale#codefix#SetMap({3: {}}) + call ale#codefix#HandleTSServerResponse(1, { + \ 'command': 'getCodeFixes', + \ 'request_seq': 3, + \ 'success': v:true, + \ 'type': 'response', + \ 'body': [ + \ { + \ 'description': 'Import default "x" from module "./z"', + \ 'fixName': 'import', + \ 'changes': [ + \ { + \ 'fileName': "/foo/bar/file1.ts", + \ 'textChanges': [ + \ { + \ 'end': { + \ 'line': 2, + \ 'offset': 1, + \ }, + \ 'newText': 'import x from "./z";^@', + \ 'start': { + \ 'line': 2, + \ 'offset': 1, + \ } + \ } + \ ] + \ } + \ ] + \ }, + \ { + \ 'description': 'Import default "x" from module "./y"', + \ 'fixName': 'import', + \ 'changes': [ + \ { + \ 'fileName': "/foo/bar/file1.ts", + \ 'textChanges': [ + \ { + \ 'end': { + \ 'line': 2, + \ 'offset': 1, + \ }, + \ 'newText': 'import x from "./y";^@', + \ 'start': { + \ 'line': 2, + \ 'offset': 1, + \ } + \ } + \ ] + \ } + \ ] + \ } + \ ] + \}) + + AssertEqual g:handle_code_action_called, 1 + AssertEqual + \ [ + \ { + \ 'description': 'codefix', + \ 'changes': [ + \ { + \ 'fileName': "/foo/bar/file1.ts", + \ 'textChanges': [ + \ { + \ 'end': { + \ 'line': 2, + \ 'offset': 1 + \ }, + \ 'newText': 'import x from "./y";^@', + \ 'start': { + \ 'line': 2, + \ 'offset': 1 + \ } + \ } + \ ] + \ } + \ ] + \ } + \ ], + \ g:code_actions + +Execute(Prints a tsserver error message when getCodeFixes unsuccessful): + call ale#codefix#SetMap({3: {}}) + call ale#codefix#HandleTSServerResponse(1, { + \ 'command': 'getCodeFixes', + \ 'request_seq': 3, + \ 'success': v:false, + \ 'message': 'something is wrong', + \}) + + AssertEqual g:handle_code_action_called, 0 + AssertEqual ['echom ''Error while getting code fixes. Reason: something is wrong'''], g:expr_list + +Execute(Does nothing when where are no code fixes): + call ale#codefix#SetMap({3: {}}) + call ale#codefix#HandleTSServerResponse(1, { + \ 'command': 'getCodeFixes', + \ 'request_seq': 3, + \ 'success': v:true, + \ 'body': [] + \}) + + AssertEqual g:handle_code_action_called, 0 + AssertEqual ['echom ''No code fixes available.'''], g:expr_list + +Execute(tsserver codefix requests should be sent): + call ale#linter#Reset() + + runtime ale_linters/typescript/tsserver.vim + let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5, 'code': 2304}]}} + call setpos('.', [bufnr(''), 2, 5, 0]) + + " ALECodeAction + call ale#codefix#Execute(0) + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'code_actions', g:capability_checked + AssertEqual + \ 'function(''ale#codefix#HandleTSServerResponse'')', + \ string(g:Callback) + AssertEqual + \ [ + \ ale#lsp#tsserver_message#Change(bufnr('')), + \ [0, 'ts@getCodeFixes', { + \ 'startLine': 2, + \ 'startOffset': 5, + \ 'endLine': 2, + \ 'endOffset': 6, + \ 'file': expand('%:p'), + \ 'errorCodes': [2304], + \ }] + \ ], + \ g:message_list + +Execute(tsserver codefix requests should be sent only for error with code): + call ale#linter#Reset() + + runtime ale_linters/typescript/tsserver.vim + let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5}, {'lnum': 2, 'col': 5, 'code': 2304}]}} + call setpos('.', [bufnr(''), 2, 5, 0]) + + " ALECodeAction + call ale#codefix#Execute(0) + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'code_actions', g:capability_checked + AssertEqual + \ 'function(''ale#codefix#HandleTSServerResponse'')', + \ string(g:Callback) + AssertEqual + \ [ + \ ale#lsp#tsserver_message#Change(bufnr('')), + \ [0, 'ts@getCodeFixes', { + \ 'startLine': 2, + \ 'startOffset': 5, + \ 'endLine': 2, + \ 'endOffset': 6, + \ 'file': expand('%:p'), + \ 'errorCodes': [2304], + \ }] + \ ], + \ g:message_list + +Execute(getApplicableRefactors from tsserver should be handled): + call ale#codefix#SetMap({3: { + \ 'buffer': expand('%:p'), + \ 'line': 1, + \ 'column': 2, + \ 'end_line': 3, + \ 'end_column': 4, + \ 'connection_id': 0, + \}}) + call ale#codefix#HandleTSServerResponse(1, + \ {'seq': 0, 'request_seq': 3, 'type': 'response', 'success': v:true, 'body': [{'actions': [{'description': 'Extract to constant in enclosing scope', 'name': 'constant_scope_0'}], 'description': 'Extract constant', 'name': 'Extract Symbol'}, {'actions': [{'description': 'Extract to function in module scope', 'name': 'function_scope_1'}], 'description': 'Extract function', 'name': 'Extract Symbol'}], 'command': 'getApplicableRefactors'}) + + AssertEqual + \ [ + \ [0, 'ts@getEditsForRefactor', { + \ 'startLine': 1, + \ 'startOffset': 2, + \ 'endLine': 3, + \ 'endOffset': 5, + \ 'file': expand('%:p'), + \ 'refactor': 'Extract Symbol', + \ 'action': 'function_scope_1', + \ }] + \ ], + \ g:message_list + +Execute(getApplicableRefactors should print error on failure): + call ale#codefix#SetMap({3: { + \ 'buffer': expand('%:p'), + \ 'line': 1, + \ 'column': 2, + \ 'end_line': 3, + \ 'end_column': 4, + \ 'connection_id': 0, + \}}) + call ale#codefix#HandleTSServerResponse(1, + \ {'seq': 0, 'request_seq': 3, 'type': 'response', 'success': v:false, 'message': 'oops', 'command': 'getApplicableRefactors'}) + + AssertEqual ['echom ''Error while getting applicable refactors. Reason: oops'''], g:expr_list + +Execute(getApplicableRefactors should do nothing if there are no refactors): + call ale#codefix#SetMap({3: { + \ 'buffer': expand('%:p'), + \ 'line': 1, + \ 'column': 2, + \ 'end_line': 3, + \ 'end_column': 4, + \ 'connection_id': 0, + \}}) + call ale#codefix#HandleTSServerResponse(1, + \ {'seq': 0, 'request_seq': 3, 'type': 'response', 'success': v:true, 'body': [], 'command': 'getApplicableRefactors'}) + + AssertEqual ['echom ''No applicable refactors available.'''], g:expr_list + +Execute(getEditsForRefactor from tsserver should be handled): + call ale#codefix#SetMap({3: {}}) + call ale#codefix#HandleTSServerResponse(1, + \{'seq': 0, 'request_seq': 3, 'type': 'response', 'success': v:true, 'body': {'edits': [{'fileName': '/foo/bar/file.ts', 'textChanges': [{'end': {'offset': 35, 'line': 9}, 'newText': 'newFunction(app);', 'start': {'offset': 3, 'line': 8}}, {'end': {'offset': 4, 'line': 19}, 'newText': '^@function newFunction(app: Router) {^@ app.use(booExpressCsrf());^@ app.use(booExpressRequireHttps);^@}^@', 'start': {'offset': 4, 'line': 19}}]}], 'renameLocation': {'offset': 3, 'line': 8}, 'renameFilename': '/foo/bar/file.ts'}, 'command': 'getEditsForRefactor' } + \) + + AssertEqual g:handle_code_action_called, 1 + AssertEqual + \ [ + \ { + \ 'description': 'editsForRefactor', + \ 'changes': [{'fileName': '/foo/bar/file.ts', 'textChanges': [{'end': {'offset': 35, 'line': 9}, 'newText': 'newFunction(app);', 'start': {'offset': 3, 'line': 8}}, {'end': {'offset': 4, 'line': 19}, 'newText': '^@function newFunction(app: Router) {^@ app.use(booExpressCsrf());^@ app.use(booExpressRequireHttps);^@}^@', 'start': {'offset': 4, 'line': 19}}]}], + \ } + \ ], + \ g:code_actions + +Execute(getEditsForRefactor should print error on failure): + call ale#codefix#SetMap({3: {}}) + call ale#codefix#HandleTSServerResponse(1, + \{'seq': 0, 'request_seq': 3, 'type': 'response', 'success': v:false, 'message': 'oops', 'command': 'getEditsForRefactor' } + \) + + AssertEqual ['echom ''Error while getting edits for refactor. Reason: oops'''], g:expr_list + +" TODO: I can't figure out how to run ALECodeAction on range +" in test function. Therefore I can't write properly working +" test. If somebody knows how to do that help is appreciated. +" +" Execute(tsserver getApplicableRefactors requests should be sent): +" call ale#linter#Reset() +" +" runtime ale_linters/typescript/tsserver.vim +" let g:ale_buffer_info = {bufnr(''): {'loclist': []}} +" call setpos('.', [bufnr(''), 2, 5, 0]) +" normal "v$" +" +" execute "ALECodeAction" +" +" " We shouldn't register the callback yet. +" AssertEqual '''''', string(g:Callback) +" +" AssertEqual type(function('type')), type(g:InitCallback) +" call g:InitCallback() +" +" AssertEqual 'code_actions', g:capability_checked +" AssertEqual +" \ 'function(''ale#codefix#HandleTSServerResponse'')', +" \ string(g:Callback) +" AssertEqual +" \ [ +" \ ale#lsp#tsserver_message#Change(bufnr('')), +" \ [0, 'ts@getApplicableRefactors', { +" \ 'startLine': 2, +" \ 'startOffset': 5, +" \ 'endLine': 2, +" \ 'endOffset': 15, +" \ 'file': expand('%:p'), +" \ }] +" \ ], +" \ g:message_list + +Execute(Failed LSP responses should be handled correctly): + call ale#codefix#HandleLSPResponse( + \ 1, + \ {'method': 'workspace/applyEdit', 'request_seq': 3} + \) + AssertEqual g:handle_code_action_called, 0 + +Given python(Some python file): + def main(): + a = 1 + b = a + 2 + +Execute("workspace/applyEdit" from LSP should be handled): + call ale#codefix#SetMap({3: {}}) + call ale#codefix#HandleLSPResponse(1, + \ {'id': 0, 'jsonrpc': '2.0', 'method': 'workspace/applyEdit', 'params': {'edit': {'changes': {'file:///foo/bar/file.ts': [{'range': {'end': {'character': 27, 'line': 7}, 'start': {'character': 27, 'line': 7}}, 'newText': ', Config'}, {'range': {'end': {'character': 12, 'line': 96}, 'start': {'character': 2, 'line': 94}}, 'newText': 'await newFunction(redis, imageKey, cover, config);'}, {'range': {'end': {'character': 2, 'line': 99}, 'start': {'character': 2, 'line': 99}}, 'newText': '^@async function newFunction(redis: IRedis, imageKey: string, cover: Buffer, config: Config) {^@ try {^@ await redis.set(imageKey, cover, ''ex'', parseInt(config.coverKeyTTL, 10));^@ }^@ catch { }^@}^@'}]}}}}) + + AssertEqual g:handle_code_action_called, 1 + AssertEqual + \ [{'description': 'applyEdit', 'changes': [{'fileName': '/foo/bar/file.ts', 'textChanges': [{'end': {'offset': 28, 'line': 8}, 'newText': ', Config', 'start': {'offset': 28, 'line': 8}}, {'end': {'offset': 13, 'line': 97}, 'newText': 'await newFunction(redis, imageKey, cover, config);', 'start': {'offset': 3, 'line': 95}}, {'end': {'offset': 3, 'line': 100}, 'newText': '^@async function newFunction(redis: IRedis, imageKey: string, cover: Buffer, config: Config) {^@ try {^@ await redis.set(imageKey, cover, ''ex'', parseInt(config.coverKeyTTL, 10));^@ }^@ catch { }^@}^@', 'start': {'offset': 3, 'line': 100}}]}]}], + \ g:code_actions + +Execute(Code Actions from LSP should be handled with user input if there are more than one action): + call ale#codefix#SetMap({2: {}}) + call ale#codefix#HandleLSPResponse(1, + \ {'id': 2, 'jsonrpc': '2.0', 'result': [{'title': 'fake for testing'}, {'arguments': [{'documentChanges': [{'edits': [{'range': {'end': {'character': 31, 'line': 2}, 'start': {'character': 31, 'line': 2}}, 'newText': ', createVideo'}], 'textDocument': {'uri': 'file:///foo/bar/file.ts', 'version': 1}}]}], 'title': 'Add ''createVideo'' to existing import declaration from "./video"', 'command': '_typescript.applyWorkspaceEdit'}]}) + + AssertEqual g:handle_code_action_called, 1 + AssertEqual + \ [{'description': 'codeaction', 'changes': [{'fileName': '/foo/bar/file.ts', 'textChanges': [{'end': {'offset': 32, 'line': 3}, 'newText': ', createVideo', 'start': {'offset': 32, 'line': 3}}]}]}], + \ g:code_actions + +Execute(Code Actions from LSP should be handled when returned with documentChanges): + call ale#codefix#SetMap({2: {}}) + call ale#codefix#HandleLSPResponse(1, + \ {'id': 2, 'jsonrpc': '2.0', 'result': [{'diagnostics': v:null, 'edit': {'changes': v:null, 'documentChanges': [{'edits': [{'range': {'end': {'character': 4, 'line': 2}, 'start': {'character': 4, 'line': 1}}, 'newText': ''}, {'range': {'end': {'character': 9, 'line': 2}, 'start': {'character': 8, 'line': 2}}, 'newText': '(1)'}], 'textDocument': {'uri': 'file:///foo/bar/test.py', 'version': v:null}}]}, 'kind': 'refactor.inline', 'title': 'Inline variable', 'command': v:null}, {'diagnostics': v:null, 'edit': {'changes': v:null, 'documentChanges': [{'edits': [{'range': {'end': {'character': 0, 'line': 0}, 'start': {'character': 0, 'line': 0}}, 'newText': 'def func_bomdjnxh():^@ a = 1return a^@^@^@'}, {'range': {'end': {'character': 9, 'line': 1}, 'start': {'character': 8, 'line': 1}}, 'newText': 'func_bomdjnxh()^@'}], 'textDocument': {'uri': 'file:///foo/bar/test.py', 'version': v:null}}]}, 'kind': 'refactor.extract', 'title': 'Extract expression into function ''func_bomdjnxh''', 'command': v:null}]}) + + AssertEqual g:handle_code_action_called, 1 + AssertEqual + \ [{'description': 'codeaction', 'changes': [{'fileName': '/foo/bar/test.py', 'textChanges': [{'end': {'offset': 1, 'line': 1}, 'newText': 'def func_bomdjnxh():^@ a = 1return a^@^@^@', 'start': {'offset': 1, 'line': 1}}, {'end': {'offset': 10, 'line': 2}, 'newText': 'func_bomdjnxh()^@', 'start': {'offset': 9, 'line': 2}}]}]}], + \ g:code_actions + +Execute(LSP Code Actions handles command responses): + call ale#codefix#SetMap({3: { + \ 'connection_id': 0, + \}}) + call ale#codefix#HandleLSPResponse(1, + \ {'id': 3, 'jsonrpc': '2.0', 'result': [{'kind': 'refactor', 'title': 'Extract to inner function in function ''getVideo''', 'command': {'arguments': [{'file': '/foo/bar/file.ts', 'endOffset': 0, 'action': 'function_scope_0', 'startOffset': 1, 'startLine': 65, 'refactor': 'Extract Symbol', 'endLine': 68}], 'title': 'Extract to inner function in function ''getVideo''', 'command': '_typescript.applyRefactoring'}}, {'kind': 'refactor', 'title': 'Extract to function in module scope', 'command': {'arguments': [{'file': '/foo/bar/file.ts', 'endOffset': 0, 'action': 'function_scope_1', 'startOffset': 1, 'startLine': 65, 'refactor': 'Extract Symbol', 'endLine': 68}], 'title': 'Extract to function in module scope', 'command': '_typescript.applyRefactoring'}}]}) + + AssertEqual + \ [[0, 'workspace/executeCommand', {'arguments': [{'file': '/foo/bar/file.ts', 'action': 'function_scope_1', 'endOffset': 0, 'refactor': 'Extract Symbol', 'endLine': 68, 'startLine': 65, 'startOffset': 1}], 'command': '_typescript.applyRefactoring'}]], + \ g:message_list + + +Execute(Prints message when LSP code action returns no results): + call ale#codefix#SetMap({3: {}}) + call ale#codefix#HandleLSPResponse(1, + \ {'id': 3, 'jsonrpc': '2.0', 'result': []}) + + AssertEqual g:handle_code_action_called, 0 + AssertEqual ['echom ''No code actions received from server'''], g:expr_list + +Execute(LSP code action requests should be sent): + call ale#linter#Reset() + + runtime ale_linters/python/jedils.vim + let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5, 'end_lnum': 2, 'end_col': 6, 'code': 2304, 'text': 'oops'}]}} + call setpos('.', [bufnr(''), 2, 5, 0]) + + " ALECodeAction + call ale#codefix#Execute(0) + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'code_actions', g:capability_checked + AssertEqual + \ 'function(''ale#codefix#HandleLSPResponse'')', + \ string(g:Callback) + AssertEqual + \ [ + \ [1, 'workspace/didChangeConfiguration', {'settings': {'python': {}}}], + \ [1, 'textDocument/didChange', { + \ 'contentChanges': [{'text': "def main():\n a = 1\n b = a + 2\n"}], + \ 'textDocument': { + \ 'uri': ale#path#ToURI(expand('%:p')), + \ 'version': g:ale_lsp_next_version_id - 1, + \ }, + \ }], + \ [0, 'textDocument/codeAction', { + \ 'context': { + \ 'diagnostics': [{'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, 'code': 2304, 'message': 'oops'}] + \ }, + \ 'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, + \ 'textDocument': {'uri': ale#path#ToURI(expand('%:p'))} + \ }] + \ ], + \ g:message_list + +Execute(LSP code action requests should be sent only for error with code): + call ale#linter#Reset() + + runtime ale_linters/python/jedils.vim + let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5, 'end_lnum': 2, 'end_col': 6, 'code': 2304, 'text': 'oops'}]}} + call setpos('.', [bufnr(''), 2, 5, 0]) + + " ALECodeAction + call ale#codefix#Execute(0) + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'code_actions', g:capability_checked + AssertEqual + \ 'function(''ale#codefix#HandleLSPResponse'')', + \ string(g:Callback) + AssertEqual + \ [ + \ [1, 'workspace/didChangeConfiguration', {'settings': {'python': {}}}], + \ [1, 'textDocument/didChange', { + \ 'contentChanges': [{'text': "def main():\n a = 1\n b = a + 2\n"}], + \ 'textDocument': { + \ 'uri': ale#path#ToURI(expand('%:p')), + \ 'version': g:ale_lsp_next_version_id - 1, + \ }, + \ }], + \ [0, 'textDocument/codeAction', { + \ 'context': { + \ 'diagnostics': [{'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, 'code': 2304, 'message': 'oops'}] + \ }, + \ 'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, + \ 'textDocument': {'uri': ale#path#ToURI(expand('%:p'))} + \ }] + \ ], + \ g:message_list From e9140c740b8deee05c10aab92fb23c7a5747569e Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 14 Nov 2020 10:17:02 +0000 Subject: [PATCH 23/40] cmp forwards, and reverse the code actions --- autoload/ale/code_action.vim | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index 4dbb2d0844..fe6b175faa 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -33,35 +33,35 @@ endfunction function! s:ChangeCmp(left, right) abort if a:left.start.line < a:right.start.line - return 1 + return -1 endif if a:left.start.line > a:right.start.line - return -1 + return 1 endif if a:left.start.offset < a:right.start.offset - return 1 + return -1 endif if a:left.start.offset > a:right.start.offset - return -1 + return 1 endif if a:left.end.line < a:right.end.line - return 1 + return -1 endif if a:left.end.line > a:right.end.line - return -1 + return 1 endif if a:left.end.offset < a:right.end.offset - return 1 + return -1 endif if a:left.end.offset > a:right.end.offset - return -1 + return 1 endif return 0 @@ -86,7 +86,7 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort endif " Changes have to be sorted so we apply them from bottom-to-top - for l:code_edit in sort(copy(a:changes), function('s:ChangeCmp')) + for l:code_edit in reverse(sort(copy(a:changes), function('s:ChangeCmp'))) let l:line = l:code_edit.start.line let l:column = l:code_edit.start.offset let l:end_line = l:code_edit.end.line From 48fe0dd4f629bb1282277ba8a6757a84c13a4dda Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 14 Nov 2020 10:41:51 +0000 Subject: [PATCH 24/40] Update documentation for code actions and rename --- README.md | 13 +++++++++++++ doc/ale.txt | 25 ++++++++++++++++++------- plugin/ale.vim | 1 + 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0f1c613bea..69ff33ae1f 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ other content at [w0rp.com](https://w0rp.com). 5. [Find References](#usage-find-references) 6. [Hovering](#usage-hover) 7. [Symbol Search](#usage-symbol-search) + 8. [Refactoring: Rename, Actions](#usage-refactoring) 3. [Installation](#installation) 1. [Installation with Vim package management](#standard-installation) 2. [Installation with Pathogen](#installation-with-pathogen) @@ -253,6 +254,18 @@ similar to a given query string. See `:help ale-symbol-search` for more information. + + +### 2.viii Refactoring: Rename, Actions + +ALE supports renaming symbols in symbols in code such as variables or class +names with the `ALERename` command. + +`ALECodeAction` will execute actions on the cursor or applied to a visual +range selection, such as automatically fixing errors. + +See `:help ale-refactor` for more information. + ## 3. Installation diff --git a/doc/ale.txt b/doc/ale.txt index 4263f69808..013b39f98d 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -20,6 +20,7 @@ CONTENTS *ale-contents* 5.4 Find References...................|ale-find-references| 5.5 Hovering..........................|ale-hover| 5.6 Symbol Search.....................|ale-symbol-search| + 5.7 Refactoring: Rename, Actions......|ale-refactor| 6. Global Options.......................|ale-options| 6.1 Highlights........................|ale-highlights| 7. Linter/Fixer Options.................|ale-integration-options| @@ -669,6 +670,16 @@ ALE supports searching for workspace symbols via LSP linters with the |ALESymbolSearch| command. See the documentation for the command for a full list of options. +------------------------------------------------------------------------------- +5.7 Refactoring: Rename, Actions *ale-refactor* + +ALE supports renaming symbols in symbols in code such as variables or class +names with the |ALERename| command. + +|ALECodeAction| will execute actions on the cursor or applied to a visual +range selection, such as automatically fixing errors. + + =============================================================================== 6. Global Options *ale-options* @@ -3108,14 +3119,14 @@ ALERename *ALERename* ALECodeAction *ALECodeAction* - Code Actions support for `tsserver`. + Apply a code action via LSP servers or `tsserver`. + + If there is an error present on a line that can be fixed, ALE will + automatically fix a line, unless there are multiple possible code fixes to + apply. - There are two different kind of actions supported for `tsserver`. If run in - normal mode then code fix will be attempted if there is error in that line. - If there are multiple code fixes available use will be shown input to choose - one. In visual mode `tsserver` will be queries for applicable refactors - (e.g. extract to constant or extract to function) and user will be given - choice to select the one he/she likes. + This command can be run in visual mode apply actions, such as applicable + refactors. A menu will be shown to select code action to apply. ALERepeatSelection *ALERepeatSelection* diff --git a/plugin/ale.vim b/plugin/ale.vim index 7472ea8cd5..c5c1d3d3c1 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -240,6 +240,7 @@ command! -bar ALEImport :call ale#completion#Import() " Rename symbols using tsserver and LSP command! -bar -bang ALERename :call ale#rename#Execute({'force_save': '' is# '!'}) +" Apply code actions to a range. command! -bar -range ALECodeAction :call ale#codefix#Execute() " Organize import statements using tsserver From b74827de99e842dc7698d8d6274486550d90f05a Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Wed, 29 Jul 2020 18:23:00 +0200 Subject: [PATCH 25/40] Look for node packages in .yarn/sdks as well --- ale_linters/typescript/tsserver.vim | 1 + autoload/ale/handlers/eslint.vim | 1 + 2 files changed, 2 insertions(+) diff --git a/ale_linters/typescript/tsserver.vim b/ale_linters/typescript/tsserver.vim index 840889f33a..4726e40d11 100644 --- a/ale_linters/typescript/tsserver.vim +++ b/ale_linters/typescript/tsserver.vim @@ -9,6 +9,7 @@ call ale#linter#Define('typescript', { \ 'name': 'tsserver', \ 'lsp': 'tsserver', \ 'executable': {b -> ale#node#FindExecutable(b, 'typescript_tsserver', [ +\ '.yarn/sdks/typescript/bin/tsserver', \ 'node_modules/.bin/tsserver', \ ])}, \ 'command': '%e', diff --git a/autoload/ale/handlers/eslint.vim b/autoload/ale/handlers/eslint.vim index e37d690205..b86106129d 100644 --- a/autoload/ale/handlers/eslint.vim +++ b/autoload/ale/handlers/eslint.vim @@ -5,6 +5,7 @@ let s:executables = [ \ 'node_modules/.bin/eslint_d', \ 'node_modules/eslint/bin/eslint.js', \ 'node_modules/.bin/eslint', +\ '.yarn/sdks/eslint/bin/eslint', \] let s:sep = has('win32') ? '\' : '/' From 7c04ee5c200b64ef0886677216fbec1303ec9475 Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 21 Nov 2020 01:18:27 +0000 Subject: [PATCH 26/40] Close #1466 - Add GVIM refactor menu support Code actions and ALERename now appear in the right click context menu for GVim by default. --- autoload/ale/code_action.vim | 102 +++++++++ autoload/ale/codefix.vim | 392 ++++++++++++++++++++++------------- doc/ale.txt | 35 +++- plugin/ale.vim | 7 + test/test_codefix.vader | 75 +------ 5 files changed, 396 insertions(+), 215 deletions(-) diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index fe6b175faa..506107f415 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -263,3 +263,105 @@ function! ale#code_action#BuildChangesList(changes_map) abort return l:changes endfunction + +function! s:EscapeMenuName(text) abort + return substitute(a:text, '\\\| \|\.\|&', '\\\0', 'g') +endfunction + +function! s:UpdateMenu(data, menu_items) abort + silent! aunmenu PopUp.Refactor\.\.\. + + if empty(a:data) + return + endif + + for [l:type, l:item] in a:menu_items + let l:name = l:type is# 'tsserver' ? l:item.name : l:item.title + let l:func_name = l:type is# 'tsserver' + \ ? 'ale#codefix#ApplyTSServerCodeAction' + \ : 'ale#codefix#ApplyLSPCodeAction' + + execute printf( + \ 'anoremenu PopUp.&Refactor\.\.\..%s' + \ . ' :call %s(%s, %s)', + \ s:EscapeMenuName(l:name), + \ l:func_name, + \ string(a:data), + \ string(l:item), + \) + endfor + + if empty(a:menu_items) + silent! anoremenu PopUp.Refactor\.\.\..(None) :silent + endif +endfunction + +function! s:GetCodeActions(linter, options) abort + let l:buffer = bufnr('') + let [l:line, l:column] = getpos('.')[1:2] + let l:column = min([l:column, len(getline(l:line))]) + + let l:location = { + \ 'buffer': l:buffer, + \ 'line': l:line, + \ 'column': l:column, + \ 'end_line': l:line, + \ 'end_column': l:column, + \} + let l:Callback = function('s:OnReady', [l:location, a:options]) + call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) +endfunction + +function! ale#code_action#GetCodeActions(options) abort + silent! aunmenu PopUp.Rename + silent! aunmenu PopUp.Refactor\.\.\. + + " Only display the menu items if there's an LSP server. + let l:has_lsp = 0 + + for l:linter in ale#linter#Get(&filetype) + if !empty(l:linter.lsp) + let l:has_lsp = 1 + + break + endif + endfor + + if l:has_lsp + if !empty(expand('')) + silent! anoremenu PopUp.Rename :ALERename + endif + + silent! anoremenu PopUp.Refactor\.\.\..(None) :silent + + call ale#codefix#Execute( + \ mode() is# 'v' || mode() is# "\", + \ function('s:UpdateMenu') + \) + endif +endfunction + +function! s:Setup(enabled) abort + augroup ALECodeActionsGroup + autocmd! + + if a:enabled + autocmd MenuPopup * :call ale#code_action#GetCodeActions({}) + endif + augroup END + + if !a:enabled + silent! augroup! ALECodeActionsGroup + + silent! aunmenu PopUp.Rename + silent! aunmenu PopUp.Refactor\.\.\. + endif +endfunction + +function! ale#code_action#EnablePopUpMenu() abort + call s:Setup(1) +endfunction + +function! ale#code_action#DisablePopUpMenu() abort + call s:Setup(0) +endfunction diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index b58f5e4b36..3120e7cb39 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -1,5 +1,5 @@ " Author: Dalius Dobravolskas -" Description: Code Fix support for tsserver +" Description: Code Fix support for tsserver and LSP servers let s:codefix_map = {} @@ -21,23 +21,65 @@ function! s:message(message) abort call ale#util#Execute('echom ' . string(a:message)) endfunction +function! ale#codefix#ApplyTSServerCodeAction(data, item) abort + if has_key(a:item, 'changes') + let l:changes = a:item.changes + + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'codefix', + \ 'changes': l:changes, + \ }, + \ {}, + \) + else + let l:message = ale#lsp#tsserver_message#GetEditsForRefactor( + \ a:data.buffer, + \ a:data.line, + \ a:data.column, + \ a:data.end_line, + \ a:data.end_column, + \ a:item.id[0], + \ a:item.id[1], + \) + + let l:request_id = ale#lsp#Send(a:data.connection_id, l:message) + + let s:codefix_map[l:request_id] = a:data + endif +endfunction + function! ale#codefix#HandleTSServerResponse(conn_id, response) abort if !has_key(a:response, 'request_seq') \ || !has_key(s:codefix_map, a:response.request_seq) return endif - let l:location = remove(s:codefix_map, a:response.request_seq) + let l:data = remove(s:codefix_map, a:response.request_seq) + let l:MenuCallback = get(l:data, 'menu_callback', v:null) if get(a:response, 'command', '') is# 'getCodeFixes' if get(a:response, 'success', v:false) is v:false + \&& l:MenuCallback is v:null let l:message = get(a:response, 'message', 'unknown') call s:message('Error while getting code fixes. Reason: ' . l:message) return endif - if len(a:response.body) == 0 + let l:result = get(a:response, 'body', []) + call filter(l:result, 'has_key(v:val, ''changes'')') + + if l:MenuCallback isnot v:null + call l:MenuCallback( + \ l:data, + \ map(copy(l:result), '[''tsserver'', v:val]') + \) + + return + endif + + if len(l:result) == 0 call s:message('No code fixes available.') return @@ -45,14 +87,15 @@ function! ale#codefix#HandleTSServerResponse(conn_id, response) abort let l:code_fix_to_apply = 0 - if len(a:response.body) == 1 + if len(l:result) == 1 let l:code_fix_to_apply = 1 else let l:codefix_no = 1 let l:codefixstring = "Code Fixes:\n" - for l:codefix in a:response.body - let l:codefixstring .= l:codefix_no . ') ' . l:codefix.description . "\n" + for l:codefix in l:result + let l:codefixstring .= l:codefix_no . ') ' + \ . l:codefix.description . "\n" let l:codefix_no += 1 endfor @@ -66,21 +109,22 @@ function! ale#codefix#HandleTSServerResponse(conn_id, response) abort endif endif - let l:changes = a:response.body[l:code_fix_to_apply - 1].changes - - call ale#code_action#HandleCodeAction({ - \ 'description': 'codefix', - \ 'changes': l:changes, - \}, {}) + call ale#codefix#ApplyTSServerCodeAction( + \ l:data, + \ l:result[l:code_fix_to_apply - 1], + \) elseif get(a:response, 'command', '') is# 'getApplicableRefactors' if get(a:response, 'success', v:false) is v:false + \&& l:MenuCallback is v:null let l:message = get(a:response, 'message', 'unknown') call s:message('Error while getting applicable refactors. Reason: ' . l:message) return endif - if len(a:response.body) == 0 + let l:result = get(a:response, 'body', []) + + if len(l:result) == 0 call s:message('No applicable refactors available.') return @@ -88,7 +132,7 @@ function! ale#codefix#HandleTSServerResponse(conn_id, response) abort let l:refactors = [] - for l:item in a:response.body + for l:item in l:result for l:action in l:item.actions call add(l:refactors, { \ 'name': l:action.description, @@ -97,11 +141,21 @@ function! ale#codefix#HandleTSServerResponse(conn_id, response) abort endfor endfor + if l:MenuCallback isnot v:null + call l:MenuCallback( + \ l:data, + \ map(copy(l:refactors), '[''tsserver'', v:val]') + \) + + return + endif + let l:refactor_no = 1 let l:refactorstring = "Applicable refactors:\n" for l:refactor in l:refactors - let l:refactorstring .= l:refactor_no . ') ' . l:refactor.name . "\n" + let l:refactorstring .= l:refactor_no . ') ' + \ . l:refactor.name . "\n" let l:refactor_no += 1 endfor @@ -116,19 +170,10 @@ function! ale#codefix#HandleTSServerResponse(conn_id, response) abort let l:id = l:refactors[l:refactor_to_apply - 1].id - let l:message = ale#lsp#tsserver_message#GetEditsForRefactor( - \ l:location.buffer, - \ l:location.line, - \ l:location.column, - \ l:location.end_line, - \ l:location.end_column, - \ l:id[0], - \ l:id[1], + call ale#codefix#ApplyTSServerCodeAction( + \ l:data, + \ l:refactors[l:refactor_to_apply - 1], \) - - let l:request_id = ale#lsp#Send(l:location.connection_id, l:message) - - let s:codefix_map[l:request_id] = l:location elseif get(a:response, 'command', '') is# 'getEditsForRefactor' if get(a:response, 'success', v:false) is v:false let l:message = get(a:response, 'message', 'unknown') @@ -137,10 +182,48 @@ function! ale#codefix#HandleTSServerResponse(conn_id, response) abort return endif - call ale#code_action#HandleCodeAction({ - \ 'description': 'editsForRefactor', - \ 'changes': a:response.body.edits, - \}, {}) + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'editsForRefactor', + \ 'changes': a:response.body.edits, + \ }, + \ {}, + \) + endif +endfunction + +function! ale#codefix#ApplyLSPCodeAction(data, item) abort + if has_key(a:item, 'command') + \&& type(a:item.command) == v:t_dict + let l:command = a:item.command + let l:message = ale#lsp#message#ExecuteCommand( + \ l:command.command, + \ l:command.arguments, + \) + + let l:request_id = ale#lsp#Send(a:data.connection_id, l:message) + elseif has_key(a:item, 'edit') || has_key(a:item, 'arguments') + if has_key(a:item, 'edit') + let l:topass = a:item.edit + else + let l:topass = a:item.arguments[0] + endif + + let l:changes_map = ale#code_action#GetChanges(l:topass) + + if empty(l:changes_map) + return + endif + + let l:changes = ale#code_action#BuildChangesList(l:changes_map) + + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'codeaction', + \ 'changes': l:changes, + \ }, + \ {}, + \) endif endfunction @@ -158,17 +241,32 @@ function! ale#codefix#HandleLSPResponse(conn_id, response) abort let l:changes = ale#code_action#BuildChangesList(l:changes_map) - call ale#code_action#HandleCodeAction({ - \ 'description': 'applyEdit', - \ 'changes': l:changes, - \}, {}) + call ale#code_action#HandleCodeAction( + \ { + \ 'description': 'applyEdit', + \ 'changes': l:changes, + \ }, + \ {} + \) elseif has_key(a:response, 'id') \&& has_key(s:codefix_map, a:response.id) - let l:location = remove(s:codefix_map, a:response.id) + let l:data = remove(s:codefix_map, a:response.id) + let l:MenuCallback = get(l:data, 'menu_callback', v:null) + + let l:result = get(a:response, 'result') + + if type(l:result) != v:t_list + let l:result = [] + endif - if !has_key(a:response, 'result') - \ || type(a:response.result) != v:t_list - \ || len(a:response.result) == 0 + " Send the results to the menu callback, if set. + if l:MenuCallback isnot v:null + call l:MenuCallback(map(copy(l:result), '[''lsp'', v:val]')) + + return + endif + + if len(l:result) == 0 call s:message('No code actions received from server') return @@ -177,8 +275,9 @@ function! ale#codefix#HandleLSPResponse(conn_id, response) abort let l:codeaction_no = 1 let l:codeactionstring = "Code Fixes:\n" - for l:codeaction in a:response.result - let l:codeactionstring .= l:codeaction_no . ') ' . l:codeaction.title . "\n" + for l:codeaction in l:result + let l:codeactionstring .= l:codeaction_no . ') ' + \ . l:codeaction.title . "\n" let l:codeaction_no += 1 endfor @@ -191,42 +290,44 @@ function! ale#codefix#HandleLSPResponse(conn_id, response) abort return endif - let l:item = a:response.result[l:codeaction_to_apply - 1] - - if has_key(l:item, 'command') - \ && type(l:item.command) == v:t_dict - let l:command = l:item.command - let l:message = ale#lsp#message#ExecuteCommand( - \ l:command.command, - \ l:command.arguments, - \) + let l:item = l:result[l:codeaction_to_apply - 1] - let l:request_id = ale#lsp#Send(l:location.connection_id, l:message) - elseif has_key(l:item, 'edit') || has_key(l:item, 'arguments') - if has_key(l:item, 'edit') - let l:topass = l:item.edit - else - let l:topass = l:item.arguments[0] - endif + call ale#codefix#ApplyLSPCodeAction(l:data, l:item) + endif +endfunction - let l:changes_map = ale#code_action#GetChanges(l:topass) +function! s:FindError(buffer, line, column, end_line, end_column) abort + let l:nearest_error = v:null - if empty(l:changes_map) - return - endif + if a:line == a:end_line + \&& a:column == a:end_column + \&& has_key(g:ale_buffer_info, a:buffer) + let l:nearest_error_diff = -1 - let l:changes = ale#code_action#BuildChangesList(l:changes_map) + for l:error in get(g:ale_buffer_info[a:buffer], 'loclist', []) + if has_key(l:error, 'code') && l:error.lnum == a:line + let l:diff = abs(l:error.col - a:column) - call ale#code_action#HandleCodeAction({ - \ 'description': 'codeaction', - \ 'changes': l:changes, - \}, {}) - endif + if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff + let l:nearest_error_diff = l:diff + let l:nearest_error = l:error + endif + endif + endfor endif -endfunction + return l:nearest_error +endfunction -function! s:OnReady(line, column, end_line, end_column, linter, lsp_details) abort +function! s:OnReady( +\ line, +\ column, +\ end_line, +\ end_column, +\ MenuCallback, +\ linter, +\ lsp_details, +\) abort let l:id = a:lsp_details.connection_id if !ale#lsp#HasCapability(l:id, 'code_actions') @@ -236,32 +337,17 @@ function! s:OnReady(line, column, end_line, end_column, linter, lsp_details) abo let l:buffer = a:lsp_details.buffer if a:linter.lsp is# 'tsserver' - if a:line == a:end_line && a:column == a:end_column - if !has_key(g:ale_buffer_info, l:buffer) - return - endif - - let l:nearest_error = v:null - let l:nearest_error_diff = -1 - - for l:error in get(g:ale_buffer_info[l:buffer], 'loclist', []) - if has_key(l:error, 'code') && l:error.lnum == a:line - let l:diff = abs(l:error.col - a:column) - - if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff - let l:nearest_error_diff = l:diff - let l:nearest_error = l:error.code - endif - endif - endfor + let l:nearest_error = + \ s:FindError(l:buffer, a:line, a:column, a:end_line, a:end_column) + if l:nearest_error isnot v:null let l:message = ale#lsp#tsserver_message#GetCodeFixes( \ l:buffer, \ a:line, \ a:column, \ a:line, \ a:column, - \ [l:nearest_error], + \ [l:nearest_error.code], \) else let l:message = ale#lsp#tsserver_message#GetApplicableRefactors( @@ -277,56 +363,37 @@ function! s:OnReady(line, column, end_line, end_column, linter, lsp_details) abo " completions won't know what text is nearby. call ale#lsp#NotifyForChanges(l:id, l:buffer) - if a:line == a:end_line && a:column == a:end_column - if !has_key(g:ale_buffer_info, l:buffer) - return - endif - - let l:nearest_error = v:null - let l:nearest_error_diff = -1 - - for l:error in get(g:ale_buffer_info[l:buffer], 'loclist', []) - if has_key(l:error, 'code') && l:error.lnum == a:line - let l:diff = abs(l:error.col - a:column) - - if l:nearest_error_diff == -1 || l:diff < l:nearest_error_diff - let l:nearest_error_diff = l:diff - let l:nearest_error = l:error - endif - endif - endfor - - let l:diagnostics = [] - - if l:nearest_error isnot v:null - let l:diagnostics = [{ - \ 'code': l:nearest_error.code, - \ 'message': l:nearest_error.text, - \ 'range': { - \ 'start': { 'line': l:nearest_error.lnum - 1, 'character': l:nearest_error.col - 1 }, - \ 'end': { 'line': l:nearest_error.end_lnum - 1, 'character': l:nearest_error.end_col - 1 } - \} - \}] - endif - - let l:message = ale#lsp#message#CodeAction( - \ l:buffer, - \ a:line, - \ a:column, - \ a:end_line, - \ a:end_column, - \ l:diagnostics, - \) - else - let l:message = ale#lsp#message#CodeAction( - \ l:buffer, - \ a:line, - \ a:column, - \ a:end_line, - \ a:end_column, - \ [], - \) + let l:diagnostics = [] + let l:nearest_error = + \ s:FindError(l:buffer, a:line, a:column, a:end_line, a:end_column) + + if l:nearest_error isnot v:null + let l:diagnostics = [ + \ { + \ 'code': l:nearest_error.code, + \ 'message': l:nearest_error.text, + \ 'range': { + \ 'start': { + \ 'line': l:nearest_error.lnum - 1, + \ 'character': l:nearest_error.col - 1, + \ }, + \ 'end': { + \ 'line': l:nearest_error.end_lnum - 1, + \ 'character': l:nearest_error.end_col - 1, + \ }, + \ }, + \ }, + \] endif + + let l:message = ale#lsp#message#CodeAction( + \ l:buffer, + \ a:line, + \ a:column, + \ a:end_line, + \ a:end_column, + \ l:diagnostics, + \) endif let l:Callback = a:linter.lsp is# 'tsserver' @@ -338,22 +405,40 @@ function! s:OnReady(line, column, end_line, end_column, linter, lsp_details) abo let l:request_id = ale#lsp#Send(l:id, l:message) let s:codefix_map[l:request_id] = { - \ 'connection_id': l:id, - \ 'buffer': l:buffer, - \ 'line': a:line, - \ 'column': a:column, - \ 'end_line': a:end_line, - \ 'end_column': a:end_column, + \ 'connection_id': l:id, + \ 'buffer': l:buffer, + \ 'line': a:line, + \ 'column': a:column, + \ 'end_line': a:end_line, + \ 'end_column': a:end_column, + \ 'menu_callback': a:MenuCallback, \} endfunction -function! s:ExecuteGetCodeFix(linter, range) abort +function! s:ExecuteGetCodeFix(linter, range, MenuCallback) abort let l:buffer = bufnr('') if a:range == 0 let [l:line, l:column] = getpos('.')[1:2] let l:end_line = l:line let l:end_column = l:column + + " Expand the range to cover the current word, if there is one. + let l:cword = expand('') + + if !empty(l:cword) + let l:search_pos = searchpos('\V' . l:cword, 'bn', l:line) + + if l:search_pos != [0, 0] + let l:column = l:search_pos[1] + let l:end_column = l:column + len(l:cword) - 1 + endif + endif + elseif mode() is# 'v' || mode() is# "\" + " You need to get the start and end in a different way when you're in + " visual mode. + let [l:line, l:column] = getpos('v')[1:2] + let [l:end_line, l:end_column] = getpos('.')[1:2] else let [l:line, l:column] = getpos("'<")[1:2] let [l:end_line, l:end_column] = getpos("'>")[1:2] @@ -363,11 +448,18 @@ function! s:ExecuteGetCodeFix(linter, range) abort let l:end_column = min([l:end_column, len(getline(l:end_line))]) let l:Callback = function( - \ 's:OnReady', [l:line, l:column, l:end_line, l:end_column]) + \ 's:OnReady', [l:line, l:column, l:end_line, l:end_column, a:MenuCallback] + \) + call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) endfunction -function! ale#codefix#Execute(range) abort +function! ale#codefix#Execute(range, ...) abort + if a:0 > 1 + throw 'Too many arguments' + endif + + let l:MenuCallback = get(a:000, 0, v:null) let l:lsp_linters = [] for l:linter in ale#linter#Get(&filetype) @@ -377,12 +469,16 @@ function! ale#codefix#Execute(range) abort endfor if empty(l:lsp_linters) - call s:message('No active LSPs') + if l:MenuCallback is v:null + call s:message('No active LSPs') + else + call l:MenuCallback({}, []) + endif return endif for l:lsp_linter in l:lsp_linters - call s:ExecuteGetCodeFix(l:lsp_linter, a:range) + call s:ExecuteGetCodeFix(l:lsp_linter, a:range, l:MenuCallback) endfor endfunction diff --git a/doc/ale.txt b/doc/ale.txt index 013b39f98d..20baf35556 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -673,13 +673,31 @@ for a full list of options. ------------------------------------------------------------------------------- 5.7 Refactoring: Rename, Actions *ale-refactor* -ALE supports renaming symbols in symbols in code such as variables or class -names with the |ALERename| command. +ALE supports renaming symbols in code such as variables or class names with +the |ALERename| command. |ALECodeAction| will execute actions on the cursor or applied to a visual range selection, such as automatically fixing errors. +Actions will appear in the right click mouse menu by default for GUI versions +of Vim, unless disabled by setting |g:ale_popup_menu_enabled| to `0`. +Make sure to set your Vim to move the cursor position whenever you right +click, and enable the mouse menu: > + + set mouse=a + set mousemodel=popup_setpos +< +You may wish to remove some other menu items you don't want to see: > + + silent! aunmenu PopUp.Select\ Word + silent! aunmenu PopUp.Select\ Sentence + silent! aunmenu PopUp.Select\ Paragraph + silent! aunmenu PopUp.Select\ Line + silent! aunmenu PopUp.Select\ Block + silent! aunmenu PopUp.Select\ Blockwise + silent! aunmenu PopUp.Select\ All +< =============================================================================== 6. Global Options *ale-options* @@ -1784,6 +1802,19 @@ g:ale_pattern_options_enabled *g:ale_pattern_options_enabled* will not set buffer variables per |g:ale_pattern_options|. +g:ale_popup_menu_enabled *g:ale_popup_menu_enabled* + + Type: |Number| + Default: `has('gui')` + + When this option is set to `1`, ALE will show code actions and rename + capabilities in the right click mouse menu when there's a LSP server or + tsserver available. See |ale-refactor|. + + This setting must be set to `1` before ALE is loaded for this behavior + to be enabled. See |ale-lint-settings-on-startup|. + + g:ale_rename_tsserver_find_in_comments *g:ale_rename_tsserver_find_in_comments* Type: |Number| diff --git a/plugin/ale.vim b/plugin/ale.vim index c5c1d3d3c1..2398956e6d 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -158,6 +158,9 @@ let g:ale_python_auto_pipenv = get(g:, 'ale_python_auto_pipenv', 0) " This variable can be overridden to set the GO111MODULE environment variable. let g:ale_go_go111module = get(g:, 'ale_go_go111module', '') +" If 1, enable a popup menu for commands. +let g:ale_popup_menu_enabled = get(g:, 'ale_popup_menu_enabled', has('gui')) + if g:ale_set_balloons call ale#balloon#Enable() endif @@ -166,6 +169,10 @@ if g:ale_completion_enabled call ale#completion#Enable() endif +if g:ale_popup_menu_enabled + call ale#code_action#EnablePopUpMenu() +endif + " Define commands for moving through warnings and errors. command! -bar -nargs=* ALEPrevious \ :call ale#loclist_jumping#WrapJump('before', ) diff --git a/test/test_codefix.vader b/test/test_codefix.vader index 63275d7f2e..deb9725647 100644 --- a/test/test_codefix.vader +++ b/test/test_codefix.vader @@ -105,11 +105,9 @@ Execute(Failed codefix responses should be handled correctly): \) AssertEqual g:handle_code_action_called, 0 - - Given typescript(Some typescript file): foo - somelongerline + somelongerline () bazxyzxyzxyz Execute(getCodeFixes from tsserver should be handled): @@ -283,7 +281,7 @@ Execute(tsserver codefix requests should be sent): runtime ale_linters/typescript/tsserver.vim let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5, 'code': 2304}]}} - call setpos('.', [bufnr(''), 2, 5, 0]) + call setpos('.', [bufnr(''), 2, 16, 0]) " ALECodeAction call ale#codefix#Execute(0) @@ -303,9 +301,9 @@ Execute(tsserver codefix requests should be sent): \ ale#lsp#tsserver_message#Change(bufnr('')), \ [0, 'ts@getCodeFixes', { \ 'startLine': 2, - \ 'startOffset': 5, + \ 'startOffset': 16, \ 'endLine': 2, - \ 'endOffset': 6, + \ 'endOffset': 17, \ 'file': expand('%:p'), \ 'errorCodes': [2304], \ }] @@ -316,8 +314,8 @@ Execute(tsserver codefix requests should be sent only for error with code): call ale#linter#Reset() runtime ale_linters/typescript/tsserver.vim - let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 5}, {'lnum': 2, 'col': 5, 'code': 2304}]}} - call setpos('.', [bufnr(''), 2, 5, 0]) + let g:ale_buffer_info = {bufnr(''): {'loclist': [{'lnum': 2, 'col': 16}, {'lnum': 2, 'col': 16, 'code': 2304}]}} + call setpos('.', [bufnr(''), 2, 16, 0]) " ALECodeAction call ale#codefix#Execute(0) @@ -337,9 +335,9 @@ Execute(tsserver codefix requests should be sent only for error with code): \ ale#lsp#tsserver_message#Change(bufnr('')), \ [0, 'ts@getCodeFixes', { \ 'startLine': 2, - \ 'startOffset': 5, + \ 'startOffset': 16, \ 'endLine': 2, - \ 'endOffset': 6, + \ 'endOffset': 17, \ 'file': expand('%:p'), \ 'errorCodes': [2304], \ }] @@ -424,43 +422,6 @@ Execute(getEditsForRefactor should print error on failure): AssertEqual ['echom ''Error while getting edits for refactor. Reason: oops'''], g:expr_list -" TODO: I can't figure out how to run ALECodeAction on range -" in test function. Therefore I can't write properly working -" test. If somebody knows how to do that help is appreciated. -" -" Execute(tsserver getApplicableRefactors requests should be sent): -" call ale#linter#Reset() -" -" runtime ale_linters/typescript/tsserver.vim -" let g:ale_buffer_info = {bufnr(''): {'loclist': []}} -" call setpos('.', [bufnr(''), 2, 5, 0]) -" normal "v$" -" -" execute "ALECodeAction" -" -" " We shouldn't register the callback yet. -" AssertEqual '''''', string(g:Callback) -" -" AssertEqual type(function('type')), type(g:InitCallback) -" call g:InitCallback() -" -" AssertEqual 'code_actions', g:capability_checked -" AssertEqual -" \ 'function(''ale#codefix#HandleTSServerResponse'')', -" \ string(g:Callback) -" AssertEqual -" \ [ -" \ ale#lsp#tsserver_message#Change(bufnr('')), -" \ [0, 'ts@getApplicableRefactors', { -" \ 'startLine': 2, -" \ 'startOffset': 5, -" \ 'endLine': 2, -" \ 'endOffset': 15, -" \ 'file': expand('%:p'), -" \ }] -" \ ], -" \ g:message_list - Execute(Failed LSP responses should be handled correctly): call ale#codefix#HandleLSPResponse( \ 1, @@ -545,14 +506,6 @@ Execute(LSP code action requests should be sent): \ string(g:Callback) AssertEqual \ [ - \ [1, 'workspace/didChangeConfiguration', {'settings': {'python': {}}}], - \ [1, 'textDocument/didChange', { - \ 'contentChanges': [{'text': "def main():\n a = 1\n b = a + 2\n"}], - \ 'textDocument': { - \ 'uri': ale#path#ToURI(expand('%:p')), - \ 'version': g:ale_lsp_next_version_id - 1, - \ }, - \ }], \ [0, 'textDocument/codeAction', { \ 'context': { \ 'diagnostics': [{'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, 'code': 2304, 'message': 'oops'}] @@ -561,7 +514,7 @@ Execute(LSP code action requests should be sent): \ 'textDocument': {'uri': ale#path#ToURI(expand('%:p'))} \ }] \ ], - \ g:message_list + \ g:message_list[-1:] Execute(LSP code action requests should be sent only for error with code): call ale#linter#Reset() @@ -585,14 +538,6 @@ Execute(LSP code action requests should be sent only for error with code): \ string(g:Callback) AssertEqual \ [ - \ [1, 'workspace/didChangeConfiguration', {'settings': {'python': {}}}], - \ [1, 'textDocument/didChange', { - \ 'contentChanges': [{'text': "def main():\n a = 1\n b = a + 2\n"}], - \ 'textDocument': { - \ 'uri': ale#path#ToURI(expand('%:p')), - \ 'version': g:ale_lsp_next_version_id - 1, - \ }, - \ }], \ [0, 'textDocument/codeAction', { \ 'context': { \ 'diagnostics': [{'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, 'code': 2304, 'message': 'oops'}] @@ -601,4 +546,4 @@ Execute(LSP code action requests should be sent only for error with code): \ 'textDocument': {'uri': ale#path#ToURI(expand('%:p'))} \ }] \ ], - \ g:message_list + \ g:message_list[-1:] From d0b5909fd8cb96fa65363af44d02eb0038c6112c Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Sat, 21 Nov 2020 01:26:16 +0000 Subject: [PATCH 27/40] #3442 Fix code fix clangd issue --- autoload/ale/codefix.vim | 2 +- test/test_codefix.vader | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/autoload/ale/codefix.vim b/autoload/ale/codefix.vim index 3120e7cb39..69bf36fa3e 100644 --- a/autoload/ale/codefix.vim +++ b/autoload/ale/codefix.vim @@ -379,7 +379,7 @@ function! s:OnReady( \ }, \ 'end': { \ 'line': l:nearest_error.end_lnum - 1, - \ 'character': l:nearest_error.end_col - 1, + \ 'character': l:nearest_error.end_col, \ }, \ }, \ }, diff --git a/test/test_codefix.vader b/test/test_codefix.vader index deb9725647..fc5470aa25 100644 --- a/test/test_codefix.vader +++ b/test/test_codefix.vader @@ -508,7 +508,7 @@ Execute(LSP code action requests should be sent): \ [ \ [0, 'textDocument/codeAction', { \ 'context': { - \ 'diagnostics': [{'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, 'code': 2304, 'message': 'oops'}] + \ 'diagnostics': [{'range': {'end': {'character': 6, 'line': 1}, 'start': {'character': 4, 'line': 1}}, 'code': 2304, 'message': 'oops'}] \ }, \ 'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, \ 'textDocument': {'uri': ale#path#ToURI(expand('%:p'))} @@ -540,7 +540,7 @@ Execute(LSP code action requests should be sent only for error with code): \ [ \ [0, 'textDocument/codeAction', { \ 'context': { - \ 'diagnostics': [{'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, 'code': 2304, 'message': 'oops'}] + \ 'diagnostics': [{'range': {'end': {'character': 6, 'line': 1}, 'start': {'character': 4, 'line': 1}}, 'code': 2304, 'message': 'oops'}] \ }, \ 'range': {'end': {'character': 5, 'line': 1}, 'start': {'character': 4, 'line': 1}}, \ 'textDocument': {'uri': ale#path#ToURI(expand('%:p'))} From e5d16caebe8a57074ee52d9bf0c0334fba4106a9 Mon Sep 17 00:00:00 2001 From: zandr <7629614+deathlyfrantic@users.noreply.github.com> Date: Sat, 21 Nov 2020 10:59:50 -0500 Subject: [PATCH 28/40] Add luafmt fixer (#3289) --- autoload/ale/fix/registry.vim | 5 +++ autoload/ale/fixers/luafmt.vim | 13 ++++++++ doc/ale-lua.txt | 16 +++++++++ doc/ale-supported-languages-and-tools.txt | 1 + doc/ale.txt | 1 + supported-tools.md | 1 + test/fixers/test_luafmt_fixer_callback.vader | 35 ++++++++++++++++++++ test/lua_files/testfile.lua | 0 8 files changed, 72 insertions(+) create mode 100644 autoload/ale/fixers/luafmt.vim create mode 100644 test/fixers/test_luafmt_fixer_callback.vader create mode 100644 test/lua_files/testfile.lua diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index d71668f2f3..9ea5331b58 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -375,6 +375,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['html', 'htmldjango'], \ 'description': 'Fix HTML files with html-beautify.', \ }, +\ 'luafmt': { +\ 'function': 'ale#fixers#luafmt#Fix', +\ 'suggested_filetypes': ['lua'], +\ 'description': 'Fix Lua files with luafmt.', +\ }, \ 'dhall': { \ 'function': 'ale#fixers#dhall#Fix', \ 'suggested_filetypes': ['dhall'], diff --git a/autoload/ale/fixers/luafmt.vim b/autoload/ale/fixers/luafmt.vim new file mode 100644 index 0000000000..6cb9ef4a4c --- /dev/null +++ b/autoload/ale/fixers/luafmt.vim @@ -0,0 +1,13 @@ +call ale#Set('lua_luafmt_executable', 'luafmt') +call ale#Set('lua_luafmt_options', '') + +function! ale#fixers#luafmt#Fix(buffer) abort + let l:executable = ale#Var(a:buffer, 'lua_luafmt_executable') + let l:options = ale#Var(a:buffer, 'lua_luafmt_options') + + return { + \ 'command': ale#Escape(l:executable) + \ . (empty(l:options) ? '' : ' ' . l:options) + \ . ' --stdin', + \} +endfunction diff --git a/doc/ale-lua.txt b/doc/ale-lua.txt index f1286f89cf..408f0c3c8c 100644 --- a/doc/ale-lua.txt +++ b/doc/ale-lua.txt @@ -30,5 +30,21 @@ g:ale_lua_luacheck_options *g:ale_lua_luacheck_options* This variable can be set to pass additional options to luacheck. +=============================================================================== +luafmt *ale-lua-luafmt* + +g:ale_lua_luafmt_executable *g:ale_lua_luafmt_executable* + *b:ale_lua_luafmt_executable* + Type: |String| + Default: `'luafmt'` + + This variable can be set to use a different executable for luafmt. + +g:ale_lua_luafmt_options *g:ale_lua_luafmt_options* + *b:ale_lua_luafmt_options* + Type: |String| + Default: `''` + + This variable can be set to pass additional options to the luafmt fixer. =============================================================================== vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index ba96c37c6a..b95c3a9b4c 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -266,6 +266,7 @@ Notes: * Lua * `luac` * `luacheck` + * `luafmt` * Mail * `alex`!! * `languagetool`!! diff --git a/doc/ale.txt b/doc/ale.txt index fdd95aceb0..1a2938eed6 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -2744,6 +2744,7 @@ documented in additional help files. lua.....................................|ale-lua-options| luac..................................|ale-lua-luac| luacheck..............................|ale-lua-luacheck| + luafmt................................|ale-lua-luafmt| markdown................................|ale-markdown-options| markdownlint..........................|ale-markdown-markdownlint| mdl...................................|ale-markdown-mdl| diff --git a/supported-tools.md b/supported-tools.md index cf7ee25ab8..4567bf101b 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -275,6 +275,7 @@ formatting. * Lua * [luac](https://www.lua.org/manual/5.1/luac.html) * [luacheck](https://github.com/mpeterv/luacheck) + * [luafmt](https://github.com/trixnz/lua-fmt) * Mail * [alex](https://github.com/wooorm/alex) :floppy_disk: * [languagetool](https://languagetool.org/) :floppy_disk: diff --git a/test/fixers/test_luafmt_fixer_callback.vader b/test/fixers/test_luafmt_fixer_callback.vader new file mode 100644 index 0000000000..362da11892 --- /dev/null +++ b/test/fixers/test_luafmt_fixer_callback.vader @@ -0,0 +1,35 @@ +Before: + Save g:ale_lua_luafmt_executable + Save g:ale_lua_luafmt_options + + " Use an invalid global executable, so we don't match it. + let g:ale_lua_luafmt_executable = 'xxxinvalid' + let g:ale_lua_luafmt_options = '' + + call ale#test#SetDirectory('/testplugin/test/fixers') + +After: + Restore + + call ale#test#RestoreDirectory() + +Execute(The luafmt callback should return the correct default values): + call ale#test#SetFilename('../lua_files/testfile.lua') + + AssertEqual + \ { + \ 'command': ale#Escape('xxxinvalid') . ' --stdin', + \ }, + \ ale#fixers#luafmt#Fix(bufnr('')) + +Execute(The luafmt callback should include custom luafmt options): + let g:ale_lua_luafmt_options = "--skip-children" + call ale#test#SetFilename('../lua_files/testfile.lua') + + AssertEqual + \ { + \ 'command': ale#Escape('xxxinvalid') + \ . ' ' . g:ale_lua_luafmt_options + \ . ' --stdin', + \ }, + \ ale#fixers#luafmt#Fix(bufnr('')) diff --git a/test/lua_files/testfile.lua b/test/lua_files/testfile.lua new file mode 100644 index 0000000000..e69de29bb2 From 342e5af4e367e225eb1c96cb0ecb4a211c2a4f8e Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 21 Nov 2020 16:19:56 +0000 Subject: [PATCH 29/40] Add a missing blank line in documentation --- doc/ale.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/ale.txt b/doc/ale.txt index 95634171ed..71caf0e5ac 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -1694,6 +1694,7 @@ g:ale_lsp_root *g:ale_lsp_root* If neither variable yields a result, a linter-specific function is invoked to detect a project root. If this, too, yields no result, the linter is disabled. + g:ale_max_buffer_history_size *g:ale_max_buffer_history_size* Type: |Number| From 06e7f2195ef6375be32a63f98b5e46070708a315 Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 21 Nov 2020 19:00:53 +0000 Subject: [PATCH 30/40] Fix #3332 - Modify everything for rename/actions ALE now just modifies every open buffer for rename and actions, and sets up a one-time use BufEnter event to reload buffers that are changed so you don't have to think about what to do with changed buffers. --- autoload/ale/code_action.vim | 42 ++++++++++++++++++++++-------------- autoload/ale/rename.vim | 5 +---- doc/ale.txt | 6 ------ plugin/ale.vim | 2 +- test/test_rename.vader | 6 +++--- 5 files changed, 31 insertions(+), 30 deletions(-) diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim index 6b808b348f..69d40933c0 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -1,28 +1,24 @@ " Author: Jerko Steiner " Description: Code action support for LSP / tsserver +function! ale#code_action#ReloadBuffer() abort + let l:buffer = bufnr('') + + execute 'augroup ALECodeActionReloadGroup' . l:buffer + autocmd! + augroup END + + silent! execute 'augroup! ALECodeActionReloadGroup' . l:buffer + + call ale#util#Execute(':e!') +endfunction + function! ale#code_action#HandleCodeAction(code_action, options) abort let l:current_buffer = bufnr('') let l:changes = a:code_action.changes let l:should_save = get(a:options, 'should_save') - let l:force_save = get(a:options, 'force_save') - let l:safe_changes = [] for l:file_code_edit in l:changes - let l:buf = bufnr(l:file_code_edit.fileName) - - if l:buf != -1 && l:buf != l:current_buffer && getbufvar(l:buf, '&mod') - if !l:force_save - call ale#util#Execute('echom ''Aborting action, file is unsaved''') - - return - endif - else - call add(l:safe_changes, l:file_code_edit) - endif - endfor - - for l:file_code_edit in l:safe_changes call ale#code_action#ApplyChanges( \ l:file_code_edit.fileName, \ l:file_code_edit.textChanges, @@ -161,6 +157,20 @@ function! ale#code_action#ApplyChanges(filename, changes, should_save) abort call setpos('.', [0, l:pos[0], l:pos[1], 0]) endif + + if a:should_save && l:buffer > 0 && !l:is_current_buffer + " Set up a one-time use event that will delete itself to reload the + " buffer next time it's entered to view the changes made to it. + execute 'augroup ALECodeActionReloadGroup' . l:buffer + autocmd! + + execute printf( + \ 'autocmd BufEnter ' + \ . ' call ale#code_action#ReloadBuffer()', + \ l:buffer + \) + augroup END + endif endfunction function! s:UpdateCursor(cursor, start, end, offset) abort diff --git a/autoload/ale/rename.vim b/autoload/ale/rename.vim index 0d074c245f..9030618e29 100644 --- a/autoload/ale/rename.vim +++ b/autoload/ale/rename.vim @@ -85,7 +85,6 @@ function! ale#rename#HandleTSServerResponse(conn_id, response) abort \ }, \ { \ 'should_save': 1, - \ 'force_save': get(l:options, 'force_save'), \ }, \) endfunction @@ -118,7 +117,6 @@ function! ale#rename#HandleLSPResponse(conn_id, response) abort \ }, \ { \ 'should_save': 1, - \ 'force_save': get(l:options, 'force_save'), \ }, \) endif @@ -177,7 +175,7 @@ function! s:ExecuteRename(linter, options) abort call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) endfunction -function! ale#rename#Execute(options) abort +function! ale#rename#Execute() abort let l:lsp_linters = [] for l:linter in ale#linter#Get(&filetype) @@ -205,7 +203,6 @@ function! ale#rename#Execute(options) abort call s:ExecuteRename(l:lsp_linter, { \ 'old_name': l:old_name, \ 'new_name': l:new_name, - \ 'force_save': get(a:options, 'force_save') is 1, \}) endfor endfunction diff --git a/doc/ale.txt b/doc/ale.txt index 71caf0e5ac..a64f86f00f 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -3145,12 +3145,6 @@ ALERename *ALERename* The symbol where the cursor is resting will be the symbol renamed, and a prompt will open to request a new name. - ALE will refuse to complete a rename operation if there are files to modify - which have not yet been saved in Vim. If the command is run with a bang - (`:ALERename!`), all warnings will be suppressed, and files that are still - open in Vim and not saved will be ignored and left in a state where symbols - in those files will not be updated. - ALECodeAction *ALECodeAction* diff --git a/plugin/ale.vim b/plugin/ale.vim index 2398956e6d..1fde9df1e8 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -245,7 +245,7 @@ command! -bar ALEComplete :call ale#completion#GetCompletions('ale-manual') command! -bar ALEImport :call ale#completion#Import() " Rename symbols using tsserver and LSP -command! -bar -bang ALERename :call ale#rename#Execute({'force_save': '' is# '!'}) +command! -bar -bang ALERename :call ale#rename#Execute() " Apply code actions to a range. command! -bar -range ALECodeAction :call ale#codefix#Execute() diff --git a/test/test_rename.vader b/test/test_rename.vader index 2e8b746e56..5bc655f475 100644 --- a/test/test_rename.vader +++ b/test/test_rename.vader @@ -269,7 +269,7 @@ Execute(tsserver rename requests should be sent): \ }] \ ], \ g:message_list - AssertEqual {'42': {'old_name': 'somelongerline', 'new_name': 'a-new-name', 'force_save': 0}}, + AssertEqual {'42': {'old_name': 'somelongerline', 'new_name': 'a-new-name'}}, \ ale#rename#GetMap() Given python(Some Python file): @@ -470,7 +470,7 @@ Execute(LSP rename requests should be sent): let b:ale_linters = ['pyls'] call setpos('.', [bufnr(''), 1, 5, 0]) - ALERename! + ALERename " We shouldn't register the callback yet. AssertEqual '''''', string(g:Callback) @@ -500,5 +500,5 @@ Execute(LSP rename requests should be sent): \ ], \ g:message_list - AssertEqual {'42': {'old_name': 'foo', 'new_name': 'a-new-name', 'force_save': 1}}, + AssertEqual {'42': {'old_name': 'foo', 'new_name': 'a-new-name'}}, \ ale#rename#GetMap() From a139599d3938b2f4fa5c8a97d3280b5f4f859321 Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 21 Nov 2020 20:12:09 +0000 Subject: [PATCH 31/40] Close #2727 - Add a hover-only setting for balloons --- autoload/ale/balloon.vim | 32 ++++++++++++++++++++++++-------- autoload/ale/hover.vim | 8 ++++++-- doc/ale.txt | 9 ++++++++- plugin/ale.vim | 2 +- test/test_hover.vader | 4 ++-- 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/autoload/ale/balloon.vim b/autoload/ale/balloon.vim index 72f6b91c6a..8678376f50 100644 --- a/autoload/ale/balloon.vim +++ b/autoload/ale/balloon.vim @@ -2,23 +2,39 @@ " Description: balloonexpr support for ALE. function! ale#balloon#MessageForPos(bufnr, lnum, col) abort + let l:set_balloons = ale#Var(a:bufnr, 'set_balloons') + let l:show_problems = 0 + let l:show_hover = 0 + + if l:set_balloons is 1 + let l:show_problems = 1 + let l:show_hover = 1 + elseif l:set_balloons is# 'hover' + let l:show_hover = 1 + endif + " Don't show balloons if they are disabled, or linting is disabled. - if !ale#Var(a:bufnr, 'set_balloons') + if !(l:show_problems || l:show_hover) \|| !g:ale_enabled \|| !getbufvar(a:bufnr, 'ale_enabled', 1) return '' endif - let l:loclist = get(g:ale_buffer_info, a:bufnr, {'loclist': []}).loclist - let l:index = ale#util#BinarySearch(l:loclist, a:bufnr, a:lnum, a:col) + if l:show_problems + let l:loclist = get(g:ale_buffer_info, a:bufnr, {'loclist': []}).loclist + let l:index = ale#util#BinarySearch(l:loclist, a:bufnr, a:lnum, a:col) + endif " Show the diagnostics message if found, 'Hover' output otherwise - if l:index >= 0 + if l:show_problems && l:index >= 0 return l:loclist[l:index].text - elseif exists('*balloon_show') || getbufvar( - \ a:bufnr, - \ 'ale_set_balloons_legacy_echo', - \ get(g:, 'ale_set_balloons_legacy_echo', 0) + elseif l:show_hover && ( + \ exists('*balloon_show') + \ || getbufvar( + \ a:bufnr, + \ 'ale_set_balloons_legacy_echo', + \ get(g:, 'ale_set_balloons_legacy_echo', 0) + \ ) \) " Request LSP/tsserver hover information, but only if this version of " Vim supports the balloon_show function, or if we turned a legacy diff --git a/autoload/ale/hover.vim b/autoload/ale/hover.vim index 38b4b86668..1d38f3b9cd 100644 --- a/autoload/ale/hover.vim +++ b/autoload/ale/hover.vim @@ -24,6 +24,8 @@ function! ale#hover#HandleTSServerResponse(conn_id, response) abort if get(a:response, 'success', v:false) is v:true \&& get(a:response, 'body', v:null) isnot v:null + let l:set_balloons = ale#Var(l:options.buffer, 'set_balloons') + " If we pass the show_documentation flag, we should show the full " documentation, and always in the preview window. if get(l:options, 'show_documentation', 0) @@ -40,7 +42,7 @@ function! ale#hover#HandleTSServerResponse(conn_id, response) abort endif elseif get(l:options, 'hover_from_balloonexpr', 0) \&& exists('*balloon_show') - \&& ale#Var(l:options.buffer, 'set_balloons') + \&& (l:set_balloons is 1 || l:set_balloons is# 'hover') call balloon_show(a:response.body.displayString) elseif get(l:options, 'truncated_echo', 0) call ale#cursor#TruncatedEcho(split(a:response.body.displayString, "\n")[0]) @@ -216,9 +218,11 @@ function! ale#hover#HandleLSPResponse(conn_id, response) abort let [l:commands, l:lines] = ale#hover#ParseLSPResult(l:result.contents) if !empty(l:lines) + let l:set_balloons = ale#Var(l:options.buffer, 'set_balloons') + if get(l:options, 'hover_from_balloonexpr', 0) \&& exists('*balloon_show') - \&& ale#Var(l:options.buffer, 'set_balloons') + \&& (l:set_balloons is 1 || l:set_balloons is# 'hover') call balloon_show(join(l:lines, "\n")) elseif get(l:options, 'truncated_echo', 0) call ale#cursor#TruncatedEcho(l:lines[0]) diff --git a/doc/ale.txt b/doc/ale.txt index a64f86f00f..195e6f132b 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -1840,7 +1840,7 @@ g:ale_rename_tsserver_find_in_strings *g:ale_rename_tsserver_find_in_strings* g:ale_set_balloons *g:ale_set_balloons* *b:ale_set_balloons* - Type: |Number| + Type: |Number| or |String| Default: `has('balloon_eval') && has('gui_running')` When this option is set to `1`, balloon messages will be displayed for @@ -1851,6 +1851,13 @@ g:ale_set_balloons *g:ale_set_balloons* supporting "Hover" information, per |ale-hover|, then brief information about the symbol under the cursor will be displayed in a balloon. + This option can be set to `'hover'` to only enable balloons for hover + message, so diagnostics are never shown in balloons. You may wish to + configure use this setting only in GUI Vim like so: > + + let g:ale_set_balloons = has('gui_running') ? 'hover' : 0 +< + Balloons can be enabled for terminal versions of Vim that support balloons, but some versions of Vim will produce strange mouse behavior when balloons are enabled. To configure balloons for your terminal, you should first diff --git a/plugin/ale.vim b/plugin/ale.vim index 1fde9df1e8..b1abb10a5f 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -161,7 +161,7 @@ let g:ale_go_go111module = get(g:, 'ale_go_go111module', '') " If 1, enable a popup menu for commands. let g:ale_popup_menu_enabled = get(g:, 'ale_popup_menu_enabled', has('gui')) -if g:ale_set_balloons +if g:ale_set_balloons is 1 || g:ale_set_balloons is# 'hover' call ale#balloon#Enable() endif diff --git a/test/test_hover.vader b/test/test_hover.vader index 9689cda2d0..ed7563968b 100644 --- a/test/test_hover.vader +++ b/test/test_hover.vader @@ -101,7 +101,7 @@ Execute(tsserver quickinfo responses will null missing bodies should be handled) AssertEqual {}, ale#hover#GetMap() Execute(tsserver quickinfo displayString values should be displayed): - call ale#hover#SetMap({3: {}}) + call ale#hover#SetMap({3: {'buffer': bufnr('')}}) call ale#hover#HandleTSServerResponse( \ 1, \ { @@ -169,7 +169,7 @@ Execute(LSP hover response with lists of strings and marked strings should be ha AssertEqual {}, ale#hover#GetMap() Execute(tsserver responses for documentation requests should be handled): - call ale#hover#SetMap({3: {'show_documentation': 1}}) + call ale#hover#SetMap({3: {'show_documentation': 1, 'buffer': bufnr('')}}) call ale#hover#HandleTSServerResponse( \ 1, From 2e91f0e689362e33ae3172ddf8153c7a7631cdcf Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 21 Nov 2020 20:16:32 +0000 Subject: [PATCH 32/40] Use has('gui_running') instead of has('gui') --- doc/ale.txt | 2 +- plugin/ale.vim | 2 +- test/test_lint_on_enter_when_file_changed.vader | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/ale.txt b/doc/ale.txt index 195e6f132b..e00d5f155f 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -1806,7 +1806,7 @@ g:ale_pattern_options_enabled *g:ale_pattern_options_enabled* g:ale_popup_menu_enabled *g:ale_popup_menu_enabled* Type: |Number| - Default: `has('gui')` + Default: `has('gui_running')` When this option is set to `1`, ALE will show code actions and rename capabilities in the right click mouse menu when there's a LSP server or diff --git a/plugin/ale.vim b/plugin/ale.vim index b1abb10a5f..5b7be1169d 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -159,7 +159,7 @@ let g:ale_python_auto_pipenv = get(g:, 'ale_python_auto_pipenv', 0) let g:ale_go_go111module = get(g:, 'ale_go_go111module', '') " If 1, enable a popup menu for commands. -let g:ale_popup_menu_enabled = get(g:, 'ale_popup_menu_enabled', has('gui')) +let g:ale_popup_menu_enabled = get(g:, 'ale_popup_menu_enabled', has('gui_running')) if g:ale_set_balloons is 1 || g:ale_set_balloons is# 'hover' call ale#balloon#Enable() diff --git a/test/test_lint_on_enter_when_file_changed.vader b/test/test_lint_on_enter_when_file_changed.vader index 8849300508..0d4c4af8c9 100644 --- a/test/test_lint_on_enter_when_file_changed.vader +++ b/test/test_lint_on_enter_when_file_changed.vader @@ -35,7 +35,7 @@ After: Execute(The file changed event function should set b:ale_file_changed): let g:ale_lint_on_enter = 0 - if has('gui') + if has('gui_running') new else e test From 2873be2d6a5e383edd1fead396a53a8a38eebd76 Mon Sep 17 00:00:00 2001 From: Dale Jung Date: Sat, 21 Nov 2020 15:19:02 -0500 Subject: [PATCH 33/40] Add php phpcbf options (#3383) * Taken from phpcs. add add_php_phpcbf_options #3382 * Updated docs for php_phpcbf_options #3382 * Added tests #3382 --- autoload/ale/fixers/phpcbf.vim | 3 ++- doc/ale-php.txt | 8 ++++++++ test/fixers/test_phpcbf_fixer_callback.vader | 11 +++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/autoload/ale/fixers/phpcbf.vim b/autoload/ale/fixers/phpcbf.vim index f14b8406b6..0a61c65792 100644 --- a/autoload/ale/fixers/phpcbf.vim +++ b/autoload/ale/fixers/phpcbf.vim @@ -2,6 +2,7 @@ " Description: Fixing files with phpcbf. call ale#Set('php_phpcbf_standard', '') +call ale#Set('php_phpcbf_options', '') call ale#Set('php_phpcbf_executable', 'phpcbf') call ale#Set('php_phpcbf_use_global', get(g:, 'ale_use_global_executables', 0)) @@ -20,6 +21,6 @@ function! ale#fixers#phpcbf#Fix(buffer) abort \ : '' return { - \ 'command': ale#Escape(l:executable) . ' --stdin-path=%s ' . l:standard_option . ' -' + \ 'command': ale#Escape(l:executable) . ' --stdin-path=%s ' . l:standard_option . ale#Pad(ale#Var(a:buffer, 'php_phpcbf_options')) . ' -' \} endfunction diff --git a/doc/ale-php.txt b/doc/ale-php.txt index 79d9a03307..6da21046df 100644 --- a/doc/ale-php.txt +++ b/doc/ale-php.txt @@ -85,6 +85,14 @@ g:ale_php_phpcbf_use_global *g:ale_php_phpcbf_use_global* See |ale-integrations-local-executables| +g:ale_php_phpcbf_options *g:ale_php_phpcbf_options* + *b:ale_php_phpcbf_options* + Type: |String| + Default: `''` + + This variable can be set to pass additional options to php-cbf + + =============================================================================== phpcs *ale-php-phpcs* diff --git a/test/fixers/test_phpcbf_fixer_callback.vader b/test/fixers/test_phpcbf_fixer_callback.vader index 1663c89c44..f7bcc2d8aa 100644 --- a/test/fixers/test_phpcbf_fixer_callback.vader +++ b/test/fixers/test_phpcbf_fixer_callback.vader @@ -5,6 +5,7 @@ Before: let g:ale_php_phpcbf_executable = 'phpcbf_test' let g:ale_php_phpcbf_standard = '' + let g:ale_php_phpcbf_options = '' let g:ale_php_phpcbf_use_global = 0 call ale#test#SetDirectory('/testplugin/test/fixers') @@ -54,6 +55,15 @@ Execute(The phpcbf callback should include the phpcbf_standard option): \ {'command': ale#Escape(ale#path#Simplify(g:dir . '/php_paths/project-with-phpcbf/vendor/bin/phpcbf')) . ' --stdin-path=%s ' . '--standard=phpcbf_ruleset.xml' . ' -'}, \ ale#fixers#phpcbf#Fix(bufnr('')) +Execute(User provided options should be used): + let g:ale_php_phpcbf_options = '--my-user-provided-option my-value' + call ale#test#SetFilename('php_paths/project-with-phpcbf/foo/test.php') + + AssertEqual + \ {'command': ale#Escape(ale#path#Simplify(g:dir . '/php_paths/project-with-phpcbf/vendor/bin/phpcbf')) . ' --stdin-path=%s ' . ale#Pad('--my-user-provided-option my-value') . ' -'}, + \ ale#fixers#phpcbf#Fix(bufnr('')) + + Before: Save g:ale_php_phpcbf_executable Save g:ale_php_phpcbf_standard @@ -61,6 +71,7 @@ Before: let g:ale_php_phpcbf_executable = 'phpcbf_test' let g:ale_php_phpcbf_standard = '' + let g:ale_php_phpcbf_options = '' let g:ale_php_phpcbf_use_global = 0 call ale#test#SetDirectory('/testplugin/test/fixers') From e1184e31f6ed7907b0fdc51eaf4507af6f22d7d9 Mon Sep 17 00:00:00 2001 From: Jose Soto Date: Sat, 21 Nov 2020 12:29:33 -0800 Subject: [PATCH 34/40] Adds support for Tlint - A Tighten Opinionated PHP Linter (#3291) Co-authored-by: w0rp --- ale_linters/php/tlint.vim | 80 +++++++++++++++++++++++ doc/ale-php.txt | 30 +++++++++ doc/ale-supported-languages-and-tools.txt | 1 + doc/ale.txt | 1 + supported-tools.md | 1 + test/handler/test_tlint_handler.vader | 34 ++++++++++ 6 files changed, 147 insertions(+) create mode 100644 ale_linters/php/tlint.vim create mode 100644 test/handler/test_tlint_handler.vader diff --git a/ale_linters/php/tlint.vim b/ale_linters/php/tlint.vim new file mode 100644 index 0000000000..6bba8defd6 --- /dev/null +++ b/ale_linters/php/tlint.vim @@ -0,0 +1,80 @@ +" Author: Jose Soto +" +" Description: Tighten Opinionated PHP Linting +" Website: https://github.com/tightenco/tlint + +call ale#Set('php_tlint_executable', 'tlint') +call ale#Set('php_tlint_use_global', get(g:, 'ale_use_global_executables', 0)) +call ale#Set('php_tlint_options', '') + +function! ale_linters#php#tlint#GetProjectRoot(buffer) abort + let l:composer_path = ale#path#FindNearestFile(a:buffer, 'composer.json') + + if !empty(l:composer_path) + return fnamemodify(l:composer_path, ':h') + endif + + let l:git_path = ale#path#FindNearestDirectory(a:buffer, '.git') + + return !empty(l:git_path) ? fnamemodify(l:git_path, ':h:h') : '' +endfunction + +function! ale_linters#php#tlint#GetExecutable(buffer) abort + return ale#node#FindExecutable(a:buffer, 'php_tlint', [ + \ 'vendor/bin/tlint', + \ 'tlint', + \]) +endfunction + +function! ale_linters#php#tlint#GetCommand(buffer) abort + let l:executable = ale_linters#php#tlint#GetExecutable(a:buffer) + let l:options = ale#Var(a:buffer, 'php_tlint_options') + + return ale#node#Executable(a:buffer, l:executable) + \ . (!empty(l:options) ? ' ' . l:options : '') + \ . ' lint %s' +endfunction + +function! ale_linters#php#tlint#Handle(buffer, lines) abort + " Matches against lines like the following: + " + " ! There should be 1 space around `.` concatenations, and additional lines should always start with a `.` + " 22 : ` $something = 'a'.'name';` + " + let l:loop_count = 0 + let l:messages_pattern = '^\! \(.*\)' + let l:output = [] + let l:pattern = '^\(\d\+\) \:' + let l:temp_messages = [] + + for l:message in ale#util#GetMatches(a:lines, l:messages_pattern) + call add(l:temp_messages, l:message) + endfor + + let l:loop_count = 0 + + for l:match in ale#util#GetMatches(a:lines, l:pattern) + let l:num = l:match[1] + let l:text = l:temp_messages[l:loop_count] + + call add(l:output, { + \ 'lnum': l:num, + \ 'col': 0, + \ 'text': l:text, + \ 'type': 'W', + \ 'sub_type': 'style', + \}) + + let l:loop_count += 1 + endfor + + return l:output +endfunction + +call ale#linter#Define('php', { +\ 'name': 'tlint', +\ 'executable': function('ale_linters#php#tlint#GetExecutable'), +\ 'command': function('ale_linters#php#tlint#GetCommand'), +\ 'callback': 'ale_linters#php#tlint#Handle', +\ 'project_root': function('ale_linters#php#tlint#GetProjectRoot'), +\}) diff --git a/doc/ale-php.txt b/doc/ale-php.txt index 6da21046df..4ee016fbce 100644 --- a/doc/ale-php.txt +++ b/doc/ale-php.txt @@ -251,6 +251,34 @@ g:ale_php_php_executable *g:ale_php_php_executable* This variable sets the executable used for php. + +=============================================================================== +tlint *ale-php-tlint* + +g:ale_php_tlint_executable *g:ale_php_tlint_executable* + *b:ale_php_tlint_executable* + Type: |String| + Default: `'tlint'` + + See |ale-integrations-local-executables| + + +g:ale_php_tlint_use_global *g:ale_php_tlint_use_global* + *b:ale_php_tlint_use_global* + Type: |Number| + Default: `get(g:, 'ale_use_global_executables', 0)` + + See |ale-integrations-local-executables| + + +g:ale_php_tlint_options *g:ale_php_tlint_options* + *b:ale_php_tlint_options* + Type: |String| + Default: `''` + + This variable can be set to pass additional options to tlint + + =============================================================================== intelephense *ale-php-intelephense* @@ -277,6 +305,7 @@ g:ale_php_intelephense_use_global *g:ale_php_intelephense_use_global* See: |ale-integrations-local-executables| + g:ale_php_intelephense_config *g:ale_php_intelephense_config* *b:ale_php_intelephense_config* Type: |Dictionary| @@ -286,5 +315,6 @@ g:ale_php_intelephense_config *g:ale_php_intelephense_config* installation docs provided by intelephense (github.com/bmewburn/intelephense -docs). + =============================================================================== vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index 60b53c0389..0ca822549a 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -337,6 +337,7 @@ Notes: * `phpmd` * `phpstan` * `psalm`!! + * `tlint` * PO * `alex`!! * `msgfmt` diff --git a/doc/ale.txt b/doc/ale.txt index c5de43331a..208bda4ab1 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -2805,6 +2805,7 @@ documented in additional help files. psalm.................................|ale-php-psalm| php-cs-fixer..........................|ale-php-php-cs-fixer| php...................................|ale-php-php| + tlint.................................|ale-php-tlint| intelephense..........................|ale-php-intelephense| po......................................|ale-po-options| write-good............................|ale-po-write-good| diff --git a/supported-tools.md b/supported-tools.md index ec49ad192f..6f467703b6 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -346,6 +346,7 @@ formatting. * [phpmd](https://phpmd.org) * [phpstan](https://github.com/phpstan/phpstan) * [psalm](https://getpsalm.org) :floppy_disk: + * [tlint](https://github.com/tightenco/tlint) * PO * [alex](https://github.com/wooorm/alex) :floppy_disk: * [msgfmt](https://www.gnu.org/software/gettext/manual/html_node/msgfmt-Invocation.html) diff --git a/test/handler/test_tlint_handler.vader b/test/handler/test_tlint_handler.vader new file mode 100644 index 0000000000..e146346c67 --- /dev/null +++ b/test/handler/test_tlint_handler.vader @@ -0,0 +1,34 @@ +Before: + runtime ale_linters/php/tlint.vim + +After: + call ale#linter#Reset() + +Execute(The tlint handler should calculate line numbers): + AssertEqual + \ [ + \ { + \ 'lnum': '5', + \ 'col': 0, + \ 'sub_type': + \ 'style', + \ 'type': 'W', + \ 'text': ['! There should be no unused imports.', 'There should be no unused imports.', '', '', '', '', '', '', '', ''] + \ }, + \ { + \ 'lnum': '15', + \ 'col': 0, + \ 'sub_type': + \ 'style', + \ 'type': 'W', + \ 'text': ['! There should be no method visibility in test methods.', 'There should be no method visibility in test methods.', '', '', '', '', '', '', '', ''] + \ }, + \ ], + \ ale_linters#php#tlint#Handle(347, [ + \ "Lints for /Users/jose/Code/Tighten/tester/tests/Unit/ExampleTest.php", + \ "============", + \ "! There should be no unused imports.", + \ "5 : `use Illuminate\Foundation\Testing\RefreshDatabase;`", + \ "! There should be no method visibility in test methods.", + \ "15 : ` public function testBasicTest()`", + \ ]) From b09ccc12c3001b4d551d35cb0c4195793d9b6d10 Mon Sep 17 00:00:00 2001 From: Gabriel Petrovay Date: Sat, 21 Nov 2020 21:32:58 +0100 Subject: [PATCH 35/40] Added the Vundle command in installation instructions (#3400) --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 69ff33ae1f..438af9b601 100644 --- a/README.md +++ b/README.md @@ -341,12 +341,14 @@ git clone https://github.com/dense-analysis/ale.git ### 3.iii. Installation with Vundle You can install this plugin using [Vundle](https://github.com/VundleVim/Vundle.vim) -by using the path on GitHub for this repository. +by adding the GitHub path for this repository to your `~/.vimrc`: ```vim Plugin 'dense-analysis/ale' ``` +Then run the command `:PluginInstall` in Vim. + See the Vundle documentation for more information. @@ -354,13 +356,16 @@ See the Vundle documentation for more information. ### 3.iiii. Installation with Vim-Plug You can install this plugin using [Vim-Plug](https://github.com/junegunn/vim-plug) -by adding the GitHub path for this repository to your `~/.vimrc` -and running `:PlugInstall`. +by adding the GitHub path for this repository to your `~/.vimrc`: ```vim Plug 'dense-analysis/ale' ``` +Then run the command `:PlugInstall` in Vim. + +See the Vim-Plug documentation for more information. + ## 4. Contributing From 5458a1b29167b1fefde5fb6545e371a9d77ca36e Mon Sep 17 00:00:00 2001 From: Horacio Sanson Date: Sun, 22 Nov 2020 05:49:31 +0900 Subject: [PATCH 36/40] Fix 3103 - add shellcheck shell directive detection. (#3216) * Fix 3103 - add shellcheck shell directive detection. Searches for shellcheck shell directive to detect dialects for scripts that do not have shebang. * Change order of detection of shellcheck dialect In a situation where the filetype can be wrong (example: something.sh which is written in bash dialect) and has no hash-bang (since it is meant to be sourced) then the override specified within the script will be ignored. It probably is the most right thing to do if the script author has added a specific directive; it should trump everything else. Co-authored-by: Horacio Sanson Co-authored-by: Dino Korah --- autoload/ale/handlers/shellcheck.vim | 26 ++++++++++++++- test/test_shell_detection.vader | 48 ++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/autoload/ale/handlers/shellcheck.vim b/autoload/ale/handlers/shellcheck.vim index 351d6d3fd3..701c43b29b 100644 --- a/autoload/ale/handlers/shellcheck.vim +++ b/autoload/ale/handlers/shellcheck.vim @@ -1,8 +1,32 @@ " Author: w0rp " Description: This file adds support for using the shellcheck linter +" Shellcheck supports shell directives to define the shell dialect for scripts +" that do not have a shebang for some reason. +" https://github.com/koalaman/shellcheck/wiki/Directive#shell +function! ale#handlers#shellcheck#GetShellcheckDialectDirective(buffer) abort + let l:linenr = 0 + let l:pattern = '\s\{-}#\s\{-}shellcheck\s\{-}shell=\(.*\)' + let l:possible_shell = ['bash', 'dash', 'ash', 'tcsh', 'csh', 'zsh', 'ksh', 'sh'] + + while l:linenr < min([50, line('$')]) + let l:linenr += 1 + let l:match = matchlist(getline(l:linenr), l:pattern) + + if len(l:match) > 1 && index(l:possible_shell, l:match[1]) >= 0 + return l:match[1] + endif + endwhile + + return '' +endfunction + function! ale#handlers#shellcheck#GetDialectArgument(buffer) abort - let l:shell_type = ale#handlers#sh#GetShellType(a:buffer) + let l:shell_type = ale#handlers#shellcheck#GetShellcheckDialectDirective(a:buffer) + + if empty(l:shell_type) + let l:shell_type = ale#handlers#sh#GetShellType(a:buffer) + endif if !empty(l:shell_type) " Use the dash dialect for /bin/ash, etc. diff --git a/test/test_shell_detection.vader b/test/test_shell_detection.vader index 697054d042..11d801c30e 100644 --- a/test/test_shell_detection.vader +++ b/test/test_shell_detection.vader @@ -127,3 +127,51 @@ Execute(The dash dialect should be used for the shell and the base function): Execute(dash should be used for shellcheck): AssertEqual 'dash', ale#handlers#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with a Bash shellcheck shell directive): + # shellcheck shell=bash + +Execute(bash dialect should be detected appropriately): + AssertEqual 'bash', ale#handlers#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with a sh shellcheck shell directive): + #shellcheck shell=sh + +Execute(sh dialect should be detected appropriately): + AssertEqual 'sh', ale#handlers#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with a tcsh shellcheck shell directive): + # shellcheck shell=tcsh + +Execute(tcsh dialect should be detected appropriately): + AssertEqual 'tcsh', ale#handlers#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with a zsh shellcheck shell directive): + # shellcheck shell=zsh + +Execute(zsh dialect should be detected appropriately): + AssertEqual 'zsh', ale#handlers#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with a csh shellcheck shell directive): + # shellcheck shell=csh + +Execute(zsh dialect should be detected appropriately): + AssertEqual 'csh', ale#handlers#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with a ksh shellcheck shell directive): + # shellcheck shell=ksh + +Execute(ksh dialect should be detected appropriately): + AssertEqual 'ksh', ale#handlers#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with a dash shellcheck shell directive): + # shellcheck shell=dash + +Execute(dash dialect should be detected appropriately): + AssertEqual 'dash', ale#handlers#shellcheck#GetDialectArgument(bufnr('')) + +Given(A file with a ash shellcheck shell directive): + # shellcheck shell=ash + +Execute(dash dialect should be detected for ash that shellcheck does not support): + AssertEqual 'dash', ale#handlers#shellcheck#GetDialectArgument(bufnr('')) From 9692c0c64c83bb8f67962cf35e7dc3f1757aa8cc Mon Sep 17 00:00:00 2001 From: Eric Zhao <21zhaoe@protonmail.com> Date: Sat, 21 Nov 2020 15:56:38 -0500 Subject: [PATCH 37/40] Add support for R languageserver (#3370) --- ale_linters/r/languageserver.vim | 26 +++++++++++++++++++ doc/ale-r.txt | 23 ++++++++++++++++ doc/ale-supported-languages-and-tools.txt | 1 + doc/ale.txt | 1 + supported-tools.md | 1 + test/command_callback/r_paths/.Rprofile | 0 .../test_r_languageserver_callbacks.vader | 22 ++++++++++++++++ 7 files changed, 74 insertions(+) create mode 100644 ale_linters/r/languageserver.vim create mode 100644 test/command_callback/r_paths/.Rprofile create mode 100644 test/command_callback/test_r_languageserver_callbacks.vader diff --git a/ale_linters/r/languageserver.vim b/ale_linters/r/languageserver.vim new file mode 100644 index 0000000000..c625d8b473 --- /dev/null +++ b/ale_linters/r/languageserver.vim @@ -0,0 +1,26 @@ +" Author: Eric Zhao <21zhaoe@protonmail.com> +" Description: Implementation of the Language Server Protocol for R. + +call ale#Set('r_languageserver_cmd', 'languageserver::run()') +call ale#Set('r_languageserver_options', {}) + +function! ale_linters#r#languageserver#GetCommand(buffer) abort + let l:cmd_string = ale#Var(a:buffer, 'r_languageserver_cmd') + + return 'Rscript --vanilla -e ' . ale#Escape(l:cmd_string) +endfunction + +function! ale_linters#r#languageserver#GetProjectRoot(buffer) abort + let l:project_root = ale#path#FindNearestFile(a:buffer, '.Rprofile') + + return !empty(l:project_root) ? fnamemodify(l:project_root, ':h') : fnamemodify(a:buffer, ':h') +endfunction + +call ale#linter#Define('r', { +\ 'name': 'languageserver', +\ 'lsp': 'stdio', +\ 'lsp_config': {b -> ale#Var(b, 'r_languageserver_options')}, +\ 'executable': 'Rscript', +\ 'command': function('ale_linters#r#languageserver#GetCommand'), +\ 'project_root': function('ale_linters#r#languageserver#GetProjectRoot') +\}) diff --git a/doc/ale-r.txt b/doc/ale-r.txt index b5ccebe5e2..2078b218c3 100644 --- a/doc/ale-r.txt +++ b/doc/ale-r.txt @@ -2,6 +2,29 @@ ALE R Integration *ale-r-options* +=============================================================================== +languageserver *ale-r-languageserver* + +g:ale_r_languageserver_cmd *g:ale_r_languageserver_cmd* + *b:ale_r_languageserver_cmd* + Type: |String| + Default: `'languageserver::run()'` + + This option can be configured to change the execution command for + languageserver. + + See the languageserver documentation for more options. + + +g:ale_r_languageserver_options *g:ale_r_languageserver_options* + *b:ale_r_languageserver_options* + Type: |Dictionary| + Default: `{}` + + This option can be configured to change settings for languageserver. See the + languageserver documentation for more information. + + =============================================================================== lintr *ale-r-lintr* diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index cc514649da..f4673ec0ca 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -389,6 +389,7 @@ Notes: * `qmlfmt` * `qmllint` * R + * `languageserver` * `lintr` * `styler` * Racket diff --git a/doc/ale.txt b/doc/ale.txt index e8c681f98e..00f43aa508 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -2854,6 +2854,7 @@ documented in additional help files. qml.....................................|ale-qml-options| qmlfmt................................|ale-qml-qmlfmt| r.......................................|ale-r-options| + languageserver........................|ale-r-languageserver| lintr.................................|ale-r-lintr| styler................................|ale-r-styler| reasonml................................|ale-reasonml-options| diff --git a/supported-tools.md b/supported-tools.md index 70c99a8cc3..e1ac887397 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -398,6 +398,7 @@ formatting. * [qmlfmt](https://github.com/jesperhh/qmlfmt) * [qmllint](https://github.com/qt/qtdeclarative/tree/5.11/tools/qmllint) * R + * [languageserver](https://github.com/REditorSupport/languageserver) * [lintr](https://github.com/jimhester/lintr) * [styler](https://github.com/r-lib/styler) * Racket diff --git a/test/command_callback/r_paths/.Rprofile b/test/command_callback/r_paths/.Rprofile new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/command_callback/test_r_languageserver_callbacks.vader b/test/command_callback/test_r_languageserver_callbacks.vader new file mode 100644 index 0000000000..7829165cb6 --- /dev/null +++ b/test/command_callback/test_r_languageserver_callbacks.vader @@ -0,0 +1,22 @@ +Before: + call ale#assert#SetUpLinterTest('r', 'languageserver') + +After: + call ale#assert#TearDownLinterTest() + +Execute(The default executable path should be correct): + AssertLinter 'Rscript', 'Rscript --vanilla -e ' . ale#Escape('languageserver::run()') + +Execute(The project root should be detected correctly): + AssertLSPProject '.' + + call ale#test#SetFilename('r_paths/dummy/test.R') + + AssertLSPProject ale#path#Simplify(g:dir . '/r_paths') + +Execute(Should accept configuration settings): + AssertLSPConfig {} + + let b:ale_r_languageserver_options = {'r': {'lsp': {'debug': 'true', 'diagnostics': 'true'}}} + + AssertLSPConfig {'r': {'lsp': {'debug': 'true', 'diagnostics': 'true'}}} From 681a6e371d02cce9c2414c19f5deeae61aa321fa Mon Sep 17 00:00:00 2001 From: w0rp Date: Sat, 21 Nov 2020 20:58:34 +0000 Subject: [PATCH 38/40] Use _config for LSP config options --- ale_linters/r/languageserver.vim | 4 ++-- doc/ale-r.txt | 10 +++++----- .../test_r_languageserver_callbacks.vader | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ale_linters/r/languageserver.vim b/ale_linters/r/languageserver.vim index c625d8b473..febe66bda6 100644 --- a/ale_linters/r/languageserver.vim +++ b/ale_linters/r/languageserver.vim @@ -2,7 +2,7 @@ " Description: Implementation of the Language Server Protocol for R. call ale#Set('r_languageserver_cmd', 'languageserver::run()') -call ale#Set('r_languageserver_options', {}) +call ale#Set('r_languageserver_config', {}) function! ale_linters#r#languageserver#GetCommand(buffer) abort let l:cmd_string = ale#Var(a:buffer, 'r_languageserver_cmd') @@ -19,7 +19,7 @@ endfunction call ale#linter#Define('r', { \ 'name': 'languageserver', \ 'lsp': 'stdio', -\ 'lsp_config': {b -> ale#Var(b, 'r_languageserver_options')}, +\ 'lsp_config': {b -> ale#Var(b, 'r_languageserver_config')}, \ 'executable': 'Rscript', \ 'command': function('ale_linters#r#languageserver#GetCommand'), \ 'project_root': function('ale_linters#r#languageserver#GetProjectRoot') diff --git a/doc/ale-r.txt b/doc/ale-r.txt index 2078b218c3..3fabf70280 100644 --- a/doc/ale-r.txt +++ b/doc/ale-r.txt @@ -16,8 +16,8 @@ g:ale_r_languageserver_cmd *g:ale_r_languageserver_cmd* See the languageserver documentation for more options. -g:ale_r_languageserver_options *g:ale_r_languageserver_options* - *b:ale_r_languageserver_options* +g:ale_r_languageserver_config *g:ale_r_languageserver_config* + *b:ale_r_languageserver_config* Type: |Dictionary| Default: `{}` @@ -45,7 +45,7 @@ g:ale_r_lintr_lint_package *g:ale_r_lintr_lint_package* Default: `0` When set to `1`, the file will be checked with `lintr::lint_package` instead - of `lintr::lint`. This prevents erroneous namespace warnings when linting + of `lintr::lint`. This prevents erroneous namespace warnings when linting package files. @@ -59,8 +59,8 @@ g:ale_r_styler_options *g:ale_r_styler_options* This option can be configured to change the options for styler. - The value of this option will be used as the `style` argument for the - `styler::style_file` options. Consult the styler documentation + The value of this option will be used as the `style` argument for the + `styler::style_file` options. Consult the styler documentation for more information. diff --git a/test/command_callback/test_r_languageserver_callbacks.vader b/test/command_callback/test_r_languageserver_callbacks.vader index 7829165cb6..9a4a1f87c2 100644 --- a/test/command_callback/test_r_languageserver_callbacks.vader +++ b/test/command_callback/test_r_languageserver_callbacks.vader @@ -17,6 +17,6 @@ Execute(The project root should be detected correctly): Execute(Should accept configuration settings): AssertLSPConfig {} - let b:ale_r_languageserver_options = {'r': {'lsp': {'debug': 'true', 'diagnostics': 'true'}}} + let b:ale_r_languageserver_config = {'r': {'lsp': {'debug': 'true', 'diagnostics': 'true'}}} AssertLSPConfig {'r': {'lsp': {'debug': 'true', 'diagnostics': 'true'}}} From c69d696e1b63c7797e8696f923856d131f02f5e9 Mon Sep 17 00:00:00 2001 From: Lyz Date: Tue, 24 Nov 2020 23:56:30 +0100 Subject: [PATCH 39/40] feat: add yamlfix fixer --- autoload/ale/fix/registry.vim | 5 ++ autoload/ale/fixers/yamlfix.vim | 25 ++++++++++ doc/ale-supported-languages-and-tools.txt | 1 + doc/ale-yaml.txt | 38 +++++++++++++- doc/ale.txt | 1 + supported-tools.md | 1 + .../with_virtualenv/env/Scripts/yamlfix.exe | 0 .../with_virtualenv/env/bin/yamlfix | 0 test/fixers/test_yamlfix_fixer_callback.vader | 50 +++++++++++++++++++ 9 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 autoload/ale/fixers/yamlfix.vim create mode 100644 test/command_callback/python_paths/with_virtualenv/env/Scripts/yamlfix.exe create mode 100755 test/command_callback/python_paths/with_virtualenv/env/bin/yamlfix create mode 100644 test/fixers/test_yamlfix_fixer_callback.vader diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index 3a88ede151..e7c8a6b5de 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -110,6 +110,11 @@ let s:default_registry = { \ 'suggested_filetypes': [], \ 'description': 'Remove all trailing whitespace characters at the end of every line.', \ }, +\ 'yamlfix': { +\ 'function': 'ale#fixers#yamlfix#Fix', +\ 'suggested_filetypes': ['python'], +\ 'description': 'Fix yaml files with yamlfix.', +\ }, \ 'yapf': { \ 'function': 'ale#fixers#yapf#Fix', \ 'suggested_filetypes': ['python'], diff --git a/autoload/ale/fixers/yamlfix.vim b/autoload/ale/fixers/yamlfix.vim new file mode 100644 index 0000000000..966556c9d8 --- /dev/null +++ b/autoload/ale/fixers/yamlfix.vim @@ -0,0 +1,25 @@ +" Author: lyz-code +" Description: Fixing yaml files with yamlfix. + +call ale#Set('yaml_yamlfix_executable', 'yamlfix') +call ale#Set('yaml_yamlfix_options', '') +call ale#Set('yaml_yamlfix_use_global', get(g:, 'ale_use_global_executables', 0)) + +function! ale#fixers#yamlfix#Fix(buffer) abort + let l:options = ale#Var(a:buffer, 'yaml_yamlfix_options') + + let l:executable = ale#python#FindExecutable( + \ a:buffer, + \ 'yaml_yamlfix', + \ ['yamlfix'], + \) + + if !executable(l:executable) + return 0 + endif + + return { + \ 'command': ale#path#BufferCdString(a:buffer) + \ . ale#Escape(l:executable) . (!empty(l:options) ? ' ' . l:options : '') . ' -', + \} +endfunction diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index f4673ec0ca..36e2793230 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -524,6 +524,7 @@ Notes: * YAML * `prettier` * `swaglint` + * `yamlfix` * `yamllint` * YANG * `yang-lsp` diff --git a/doc/ale-yaml.txt b/doc/ale-yaml.txt index c9a12ea1b6..61bfc13949 100644 --- a/doc/ale-yaml.txt +++ b/doc/ale-yaml.txt @@ -15,7 +15,6 @@ Install prettier either globally or locally: > npm install prettier -g # global npm install prettier # local < - =============================================================================== swaglint *ale-yaml-swaglint* @@ -49,6 +48,43 @@ g:ale_yaml_swaglint_use_global *g:ale_yaml_swaglint_use_global* See |ale-integrations-local-executables| +=============================================================================== +yamlfix *ale-yaml-yamlfix* + +Website: https://lyz-code.github.io/yamlfix + + +Installation +------------------------------------------------------------------------------- + +Install yamlfix: > + + pip install yamlfix +< + +Options +------------------------------------------------------------------------------- +g:ale_yaml_yamlfix_executable *g:ale_yaml_yamlfix_executable* + *b:ale_yaml_yamlfix_executable* + Type: |String| + Default: `'yamlfix'` + + See |ale-integrations-local-executables| + + +g:ale_yaml_yamlfix_options *g:ale_yaml_yamlfix_options* + *b:ale_yaml_yamlfix_options* + Type: |String| + Default: `''` + + This variable can be set to pass extra options to yamlfix. + +g:ale_yaml_yamlfix_use_global *g:ale_yaml_yamlfix_use_global* + *b:ale_yaml_yamlfix_use_global* + Type: |Number| + Default: `get(g:, 'ale_use_global_executables', 0)` + + See |ale-integrations-local-executables| =============================================================================== yamllint *ale-yaml-yamllint* diff --git a/doc/ale.txt b/doc/ale.txt index 00f43aa508..f9f40d12d9 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -2971,6 +2971,7 @@ documented in additional help files. yaml....................................|ale-yaml-options| prettier..............................|ale-yaml-prettier| swaglint..............................|ale-yaml-swaglint| + yamlfix...............................|ale-yaml-yamlfix| yamllint..............................|ale-yaml-yamllint| yang....................................|ale-yang-options| yang-lsp..............................|ale-yang-lsp| diff --git a/supported-tools.md b/supported-tools.md index e1ac887397..96ef273b36 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -533,6 +533,7 @@ formatting. * YAML * [prettier](https://github.com/prettier/prettier) * [swaglint](https://github.com/byCedric/swaglint) + * [yamlfix](https://lyz-code.github.io/yamlfix) * [yamllint](https://yamllint.readthedocs.io/) * YANG * [yang-lsp](https://github.com/theia-ide/yang-lsp) diff --git a/test/command_callback/python_paths/with_virtualenv/env/Scripts/yamlfix.exe b/test/command_callback/python_paths/with_virtualenv/env/Scripts/yamlfix.exe new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/command_callback/python_paths/with_virtualenv/env/bin/yamlfix b/test/command_callback/python_paths/with_virtualenv/env/bin/yamlfix new file mode 100755 index 0000000000..e69de29bb2 diff --git a/test/fixers/test_yamlfix_fixer_callback.vader b/test/fixers/test_yamlfix_fixer_callback.vader new file mode 100644 index 0000000000..3ffda91ecc --- /dev/null +++ b/test/fixers/test_yamlfix_fixer_callback.vader @@ -0,0 +1,50 @@ +Before: + Save g:ale_python_yamlfix_executable + Save g:ale_python_yamlfix_options + + " Use an invalid global executable, so we don't match it. + let g:ale_python_yamlfix_executable = 'xxxinvalid' + let g:ale_python_yamlfix_options = '' + + call ale#test#SetDirectory('/testplugin/test/fixers') + silent cd .. + silent cd command_callback + let g:dir = getcwd() + + let b:bin_dir = has('win32') ? 'Scripts' : 'bin' + +After: + Restore + + unlet! b:bin_dir + + call ale#test#RestoreDirectory() + +Execute(The yamlfix callback should return the correct default values): + AssertEqual + \ 0, + \ ale#fixers#yamlfix#Fix(bufnr('')) + + silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.yaml') + AssertEqual + \ { + \ 'command': ale#path#BufferCdString(bufnr('')) + \ . ale#Escape(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/env/' . b:bin_dir . '/yamlfix')) . ' -', + \ }, + \ ale#fixers#yamlfix#Fix(bufnr('')) + +Execute(The yamlfix callback should respect custom options): + let g:ale_yaml_yamlfix_options = '--multi-line=3 --trailing-comma' + + AssertEqual + \ 0, + \ ale#fixers#yamlfix#Fix(bufnr('')) + + silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.yaml') + AssertEqual + \ { + \ 'command': ale#path#BufferCdString(bufnr('')) + \ . ale#Escape(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/env/' . b:bin_dir . '/yamlfix')) + \ . ' --multi-line=3 --trailing-comma -', + \ }, + \ ale#fixers#yamlfix#Fix(bufnr('')) From 12eb8d15234e6f4e77672c29659c29a7c0dcf2fb Mon Sep 17 00:00:00 2001 From: Lyz Date: Thu, 26 Nov 2020 12:42:50 +0100 Subject: [PATCH 40/40] fix: correct suggested filetype for yamlfix --- autoload/ale/fix/registry.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index e7c8a6b5de..0f146faa7f 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -112,7 +112,7 @@ let s:default_registry = { \ }, \ 'yamlfix': { \ 'function': 'ale#fixers#yamlfix#Fix', -\ 'suggested_filetypes': ['python'], +\ 'suggested_filetypes': ['yaml'], \ 'description': 'Fix yaml files with yamlfix.', \ }, \ 'yapf': {