diff options
110 files changed, 3413 insertions, 252 deletions
@@ -10,6 +10,8 @@ ENV PACKAGES="\ git \ python \ py-pip \ + grep \ + sed \ " RUN apk --update add $PACKAGES && \ rm -rf /var/cache/apk/* /tmp/* /var/tmp/* @@ -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. +<a name="usage-refactoring"></a> + +### 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. + <a name="installation"></a> ## 3. Installation @@ -328,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. <a name="installation-with-vim-plug"></a> @@ -341,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. + <a name="contributing"></a> ## 4. Contributing diff --git a/ale_linters/dafny/dafny.vim b/ale_linters/dafny/dafny.vim index b5b90675..e6021d99 100644 --- a/ale_linters/dafny/dafny.vim +++ b/ale_linters/dafny/dafny.vim @@ -6,7 +6,7 @@ function! ale_linters#dafny#dafny#Handle(buffer, lines) abort for l:match in ale#util#GetMatches(a:lines, l:pattern) call add(l:output, { - \ 'bufnr': a:buffer, + \ 'filename': l:match[1], \ 'col': l:match[3] + 0, \ 'lnum': l:match[2] + 0, \ 'text': l:match[5], diff --git a/ale_linters/elixir/credo.vim b/ale_linters/elixir/credo.vim index 7c298502..892d47b9 100644 --- a/ale_linters/elixir/credo.vim +++ b/ale_linters/elixir/credo.vim @@ -45,6 +45,16 @@ function! ale_linters#elixir#credo#GetMode() abort endif endfunction +function! ale_linters#elixir#credo#GetConfigFile() abort + let l:config_file = get(g:, 'ale_elixir_credo_config_file', '') + + if empty(l:config_file) + return '' + endif + + return ' --config-file ' . l:config_file +endfunction + function! ale_linters#elixir#credo#GetCommand(buffer) abort let l:project_root = ale#handlers#elixir#FindMixUmbrellaRoot(a:buffer) let l:mode = ale_linters#elixir#credo#GetMode() @@ -52,6 +62,7 @@ function! ale_linters#elixir#credo#GetCommand(buffer) abort return ale#path#CdString(l:project_root) \ . 'mix help credo && ' \ . 'mix credo ' . ale_linters#elixir#credo#GetMode() + \ . ale_linters#elixir#credo#GetConfigFile() \ . ' --format=flycheck --read-from-stdin %s' endfunction diff --git a/ale_linters/erlang/elvis.vim b/ale_linters/erlang/elvis.vim new file mode 100644 index 00000000..31dea3dd --- /dev/null +++ b/ale_linters/erlang/elvis.vim @@ -0,0 +1,39 @@ +" Author: Dmitri Vereshchagin <dmitri.vereshchagin@gmail.com> +" 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/ale_linters/erlang/erlc.vim b/ale_linters/erlang/erlc.vim index a83bacc3..e78dc341 100644 --- a/ale_linters/erlang/erlc.vim +++ b/ale_linters/erlang/erlc.vim @@ -1,14 +1,22 @@ " Author: Magnus Ottenklinger - https://github.com/evnu +let g:ale_erlang_erlc_executable = get(g:, 'ale_erlang_erlc_executable', 'erlc') let g:ale_erlang_erlc_options = get(g:, 'ale_erlang_erlc_options', '') +function! ale_linters#erlang#erlc#GetExecutable(buffer) abort + return ale#Var(a:buffer, 'erlang_erlc_executable') +endfunction + function! ale_linters#erlang#erlc#GetCommand(buffer) abort let l:output_file = ale#util#Tempname() call ale#command#ManageFile(a:buffer, l:output_file) - return 'erlc -o ' . ale#Escape(l:output_file) - \ . ' ' . ale#Var(a:buffer, 'erlang_erlc_options') - \ . ' %t' + let l:command = ale#Escape(ale_linters#erlang#erlc#GetExecutable(a:buffer)) + \ . ' -o ' . ale#Escape(l:output_file) + \ . ' ' . ale#Var(a:buffer, 'erlang_erlc_options') + \ . ' %t' + + return l:command endfunction function! ale_linters#erlang#erlc#Handle(buffer, lines) abort @@ -90,7 +98,7 @@ endfunction call ale#linter#Define('erlang', { \ 'name': 'erlc', -\ 'executable': 'erlc', +\ 'executable': function('ale_linters#erlang#erlc#GetExecutable'), \ 'command': function('ale_linters#erlang#erlc#GetCommand'), \ 'callback': 'ale_linters#erlang#erlc#Handle', \}) diff --git a/ale_linters/inko/inko.vim b/ale_linters/inko/inko.vim new file mode 100644 index 00000000..11558897 --- /dev/null +++ b/ale_linters/inko/inko.vim @@ -0,0 +1,33 @@ +" Author: Yorick Peterse <yorick@yorickpeterse.com> +" Description: linting of Inko source code using the Inko compiler + +call ale#Set('inko_inko_executable', 'inko') + +function! ale_linters#inko#inko#GetCommand(buffer) abort + let l:include = '' + + " Include the tests source directory, but only for test files. + if expand('#' . a:buffer . ':p') =~? '\vtests[/\\]test[/\\]' + let l:test_dir = ale#path#FindNearestDirectory(a:buffer, 'tests') + + if isdirectory(l:test_dir) + let l:include = '--include ' . ale#Escape(l:test_dir) + endif + endif + + " We use %s instead of %t so the compiler determines the correct module + " names for the file being edited. Not doing so may lead to errors in + " certain cases. + return '%e build --check --format=json' + \ . ale#Pad(l:include) + \ . ' %s' +endfunction + +call ale#linter#Define('inko', { +\ 'name': 'inko', +\ 'executable': {b -> ale#Var(b, 'inko_inko_executable')}, +\ 'command': function('ale_linters#inko#inko#GetCommand'), +\ 'callback': 'ale#handlers#inko#Handle', +\ 'output_stream': 'stderr', +\ 'lint_file': 1 +\}) diff --git a/ale_linters/java/checkstyle.vim b/ale_linters/java/checkstyle.vim index ec7339d1..f00734e0 100644 --- a/ale_linters/java/checkstyle.vim +++ b/ale_linters/java/checkstyle.vim @@ -9,7 +9,7 @@ function! ale_linters#java#checkstyle#Handle(buffer, lines) abort let l:output = [] " modern checkstyle versions - let l:pattern = '\v\[(WARN|ERROR)\] [a-zA-Z]?:?[^:]+:(\d+):(\d+)?:? (.*) \[(.+)\]$' + let l:pattern = '\v\[(WARN|ERROR)\] [a-zA-Z]?:?[^:]+:(\d+):(\d+)?:? (.*) \[(.+)\]' for l:match in ale#util#GetMatches(a:lines, l:pattern) call add(l:output, { diff --git a/ale_linters/julia/languageserver.vim b/ale_linters/julia/languageserver.vim index 564bec39..999ad815 100644 --- a/ale_linters/julia/languageserver.vim +++ b/ale_linters/julia/languageserver.vim @@ -6,9 +6,9 @@ call ale#Set('julia_executable', 'julia') function! ale_linters#julia#languageserver#GetCommand(buffer) abort let l:julia_executable = ale#Var(a:buffer, 'julia_executable') - let l:cmd_string = 'using LanguageServer; server = LanguageServer.LanguageServerInstance(isdefined(Base, :stdin) ? stdin : STDIN, isdefined(Base, :stdout) ? stdout : STDOUT, false); server.runlinter = true; run(server);' + let l:cmd_string = 'using LanguageServer; using Pkg; import StaticLint; import SymbolServer; server = LanguageServer.LanguageServerInstance(isdefined(Base, :stdin) ? stdin : STDIN, isdefined(Base, :stdout) ? stdout : STDOUT, dirname(Pkg.Types.Context().env.project_file)); server.runlinter = true; run(server);' - return ale#Escape(l:julia_executable) . ' --startup-file=no --history-file=no -e ' . ale#Escape(l:cmd_string) + return ale#Escape(l:julia_executable) . ' --project=@. --startup-file=no --history-file=no -e ' . ale#Escape(l:cmd_string) endfunction call ale#linter#Define('julia', { diff --git a/ale_linters/php/intelephense.vim b/ale_linters/php/intelephense.vim new file mode 100755 index 00000000..aca619e3 --- /dev/null +++ b/ale_linters/php/intelephense.vim @@ -0,0 +1,32 @@ +" Author: Eric Stern <eric@ericstern.com>, +" Arnold Chand <creativenull@outlook.com> +" 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(buffer) abort + return ale#Var(a:buffer, '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/ale_linters/php/phpcs.vim b/ale_linters/php/phpcs.vim index 11b81e84..c5a3faa9 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/ale_linters/php/tlint.vim b/ale_linters/php/tlint.vim new file mode 100644 index 00000000..6bba8def --- /dev/null +++ b/ale_linters/php/tlint.vim @@ -0,0 +1,80 @@ +" Author: Jose Soto <jose@tighten.co> +" +" 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/ale_linters/python/jedils.vim b/ale_linters/python/jedils.vim new file mode 100644 index 00000000..eae5fb07 --- /dev/null +++ b/ale_linters/python/jedils.vim @@ -0,0 +1,34 @@ +" Author: Dalius Dobravolskas <dalius.dobravolskas@gmail.com> +" 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/ale_linters/r/languageserver.vim b/ale_linters/r/languageserver.vim new file mode 100644 index 00000000..febe66bd --- /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_config', {}) + +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_config')}, +\ 'executable': 'Rscript', +\ 'command': function('ale_linters#r#languageserver#GetCommand'), +\ 'project_root': function('ale_linters#r#languageserver#GetProjectRoot') +\}) diff --git a/ale_linters/ruby/sorbet.vim b/ale_linters/ruby/sorbet.vim index cae0683c..c67e20cc 100644 --- a/ale_linters/ruby/sorbet.vim +++ b/ale_linters/ruby/sorbet.vim @@ -1,14 +1,17 @@ call ale#Set('ruby_sorbet_executable', 'srb') call ale#Set('ruby_sorbet_options', '') +call ale#Set('ruby_sorbet_enable_watchman', 0) function! ale_linters#ruby#sorbet#GetCommand(buffer) abort let l:executable = ale#Var(a:buffer, 'ruby_sorbet_executable') let l:options = ale#Var(a:buffer, 'ruby_sorbet_options') + let l:enable_watchman = ale#Var(a:buffer, 'ruby_sorbet_enable_watchman') return ale#ruby#EscapeExecutable(l:executable, 'srb') \ . ' tc' \ . (!empty(l:options) ? ' ' . l:options : '') - \ . ' --lsp --disable-watchman' + \ . ' --lsp' + \ . (l:enable_watchman ? '' : ' --disable-watchman') endfunction call ale#linter#Define('ruby', { diff --git a/ale_linters/salt/salt_lint.vim b/ale_linters/salt/salt_lint.vim new file mode 100644 index 00000000..47f66d83 --- /dev/null +++ b/ale_linters/salt/salt_lint.vim @@ -0,0 +1,33 @@ +" Author: Benjamin BINIER <poulpatine@gmail.com> +" Description: salt-lint, saltstack linter + +call ale#Set('salt_salt_lint_executable', 'salt-lint') +call ale#Set('salt_salt_lint_options', '') + +function! ale_linters#salt#salt_lint#GetCommand(buffer) abort + return '%e' . ale#Pad(ale#Var(a:buffer, 'salt_salt_lint_options')) + \ . ' --json' +endfunction + +function! ale_linters#salt#salt_lint#Handle(buffer, lines) abort + let l:output = [] + + for l:error in ale#util#FuzzyJSONDecode(a:lines, []) + call add(l:output, { + \ 'lnum': l:error.linenumber + 0, + \ 'code': l:error.id + 0, + \ 'text': l:error.message, + \ 'type': l:error.severity is# 'HIGH' ? 'E' : 'W', + \}) + endfor + + return l:output +endfunction + +call ale#linter#Define('salt', { +\ 'name': 'salt_lint', +\ 'aliases': ['salt-lint'], +\ 'executable': {b -> ale#Var(b, 'salt_salt_lint_executable')}, +\ 'command': function('ale_linters#salt#salt_lint#GetCommand'), +\ 'callback': 'ale_linters#salt#salt_lint#Handle' +\}) diff --git a/ale_linters/typescript/tsserver.vim b/ale_linters/typescript/tsserver.vim index 840889f3..4726e40d 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/balloon.vim b/autoload/ale/balloon.vim index 72f6b91c..8678376f 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/code_action.vim b/autoload/ale/code_action.vim index 42f4f265..69d40933 100644 --- a/autoload/ale/code_action.vim +++ b/autoload/ale/code_action.vim @@ -1,28 +1,24 @@ " Author: Jerko Steiner <jerko.steiner@gmail.com> " 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, @@ -85,29 +81,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. - 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 + " Changes have to be sorted so we apply them from bottom-to-top + 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 + 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 +106,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. @@ -134,13 +121,17 @@ 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 :] 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, @@ -166,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 <buffer=%d>' + \ . ' call ale#code_action#ReloadBuffer()', + \ l:buffer + \) + augroup END + endif endfunction function! s:UpdateCursor(cursor, start, end, offset) abort @@ -215,3 +220,163 @@ 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 + +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 <silent> PopUp.&Refactor\.\.\..%s' + \ . ' :call %s(%s, %s)<CR>', + \ 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('<cword>')) + silent! anoremenu <silent> PopUp.Rename :ALERename<CR> + endif + + silent! anoremenu <silent> PopUp.Refactor\.\.\..(None) :silent<CR> + + call ale#codefix#Execute( + \ mode() is# 'v' || mode() is# "\<C-V>", + \ 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 new file mode 100644 index 00000000..4a78063b --- /dev/null +++ b/autoload/ale/codefix.vim @@ -0,0 +1,487 @@ +" Author: Dalius Dobravolskas <dalius.dobravolskas@gmail.com> +" Description: Code Fix support for tsserver and LSP servers + +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#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: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 + + 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 + endif + + let l:code_fix_to_apply = 0 + + 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 l:result + let l:codefixstring .= l:codefix_no . ') ' + \ . l:codefix.description . "\n" + let l:codefix_no += 1 + endfor + + let l:codefixstring .= 'Type number and <Enter> (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 + + 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 + + let l:result = get(a:response, 'body', []) + + if len(l:result) == 0 + call s:message('No applicable refactors available.') + + return + endif + + let l:refactors = [] + + for l:item in l:result + for l:action in l:item.actions + call add(l:refactors, { + \ 'name': l:action.description, + \ 'id': [l:item.name, l:action.name], + \}) + 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:refactor_no += 1 + endfor + + let l:refactorstring .= 'Type number and <Enter> (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 + + call ale#codefix#ApplyTSServerCodeAction( + \ l:data, + \ l:refactors[l:refactor_to_apply - 1], + \) + 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#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 + +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: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 + + " Send the results to the menu callback, if set. + if l:MenuCallback isnot v:null + call l:MenuCallback( + \ l:data, + \ map(copy(l:result), '[''lsp'', v:val]') + \) + + return + endif + + if len(l: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 l:result + let l:codeactionstring .= l:codeaction_no . ') ' + \ . l:codeaction.title . "\n" + let l:codeaction_no += 1 + endfor + + let l:codeactionstring .= 'Type number and <Enter> (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 = l:result[l:codeaction_to_apply - 1] + + call ale#codefix#ApplyLSPCodeAction(l:data, l:item) + endif +endfunction + +function! s:FindError(buffer, line, column, end_line, end_column) abort + let l:nearest_error = v:null + + 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 + + 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) + + 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 + + return l:nearest_error +endfunction + +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') + return + endif + + let l:buffer = a:lsp_details.buffer + + if a:linter.lsp is# 'tsserver' + 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.code], + \) + 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) + + 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, + \ }, + \ }, + \ }, + \] + 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' + \ ? 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, + \ 'menu_callback': a:MenuCallback, + \} +endfunction + +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('<cword>') + + 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# "\<C-V>" + " 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] + 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, a:MenuCallback] + \) + + call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) +endfunction + +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) + if !empty(l:linter.lsp) + call add(l:lsp_linters, l:linter) + endif + endfor + + if empty(l:lsp_linters) + 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, l:MenuCallback) + endfor +endfunction diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index efbf0fd5..39bfc094 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -606,17 +606,21 @@ 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. 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/cursor.vim b/autoload/ale/cursor.vim index 9ca6fb15..e8478e93 100644 --- a/autoload/ale/cursor.vim +++ b/autoload/ale/cursor.vim @@ -9,7 +9,6 @@ let g:ale_echo_delay = get(g:, 'ale_echo_delay', 10) let g:ale_echo_msg_format = get(g:, 'ale_echo_msg_format', '%code: %%s') let s:cursor_timer = -1 -let s:last_pos = [0, 0, 0] function! ale#cursor#TruncatedEcho(original_message) abort let l:message = a:original_message @@ -118,14 +117,18 @@ function! ale#cursor#EchoCursorWarningWithDelay() abort let l:pos = getpos('.')[0:2] + if !exists('w:last_pos') + let w:last_pos = [0, 0, 0] + endif + " Check the current buffer, line, and column number against the last " recorded position. If the position has actually changed, *then* " we should echo something. Otherwise we can end up doing processing " the echo message far too frequently. - if l:pos != s:last_pos + if l:pos != w:last_pos let l:delay = ale#Var(l:buffer, 'echo_delay') - let s:last_pos = l:pos + let w:last_pos = l:pos let s:cursor_timer = timer_start( \ l:delay, \ function('ale#cursor#EchoCursorWarning') @@ -139,11 +142,16 @@ function! s:ShowCursorDetailForItem(loc, options) abort let s:last_detailed_line = line('.') let l:message = get(a:loc, 'detail', a:loc.text) let l:lines = split(l:message, "\n") - call ale#preview#Show(l:lines, {'stay_here': l:stay_here}) - " Clear the echo message if we manually displayed details. - if !l:stay_here - execute 'echo' + if g:ale_floating_preview || g:ale_detail_to_floating_preview + call ale#floating_preview#Show(l:lines) + else + call ale#preview#Show(l:lines, {'stay_here': l:stay_here}) + + " Clear the echo message if we manually displayed details. + if !l:stay_here + execute 'echo' + endif endif endfunction diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index d71668f2..f28e84fd 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'], @@ -105,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': ['yaml'], +\ 'description': 'Fix yaml files with yamlfix.', +\ }, \ 'yapf': { \ 'function': 'ale#fixers#yapf#Fix', \ 'suggested_filetypes': ['python'], @@ -122,7 +132,7 @@ let s:default_registry = { \ }, \ 'scalafmt': { \ 'function': 'ale#fixers#scalafmt#Fix', -\ 'suggested_filetypes': ['scala'], +\ 'suggested_filetypes': ['sbt', 'scala'], \ 'description': 'Fix Scala files using scalafmt', \ }, \ 'sorbet': { @@ -332,7 +342,7 @@ let s:default_registry = { \ }, \ 'ktlint': { \ 'function': 'ale#fixers#ktlint#Fix', -\ 'suggested_filetypes': ['kt'], +\ 'suggested_filetypes': ['kt', 'kotlin'], \ 'description': 'Fix Kotlin files with ktlint.', \ }, \ 'styler': { @@ -375,11 +385,21 @@ 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'], \ '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/autoimport.vim b/autoload/ale/fixers/autoimport.vim new file mode 100644 index 00000000..37a52db8 --- /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/autoload/ale/fixers/gofmt.vim b/autoload/ale/fixers/gofmt.vim index d5a539b9..b9cfbb58 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/autoload/ale/fixers/isort.vim b/autoload/ale/fixers/isort.vim index 9070fb27..55bb550e 100644 --- a/autoload/ale/fixers/isort.vim +++ b/autoload/ale/fixers/isort.vim @@ -2,24 +2,35 @@ " Description: Fixing Python imports with isort. call ale#Set('python_isort_executable', 'isort') -call ale#Set('python_isort_options', '') call ale#Set('python_isort_use_global', get(g:, 'ale_use_global_executables', 0)) +call ale#Set('python_isort_options', '') +call ale#Set('python_isort_auto_pipenv', 0) + +function! ale#fixers#isort#GetExecutable(buffer) abort + if (ale#Var(a:buffer, 'python_auto_pipenv') || ale#Var(a:buffer, 'python_isort_auto_pipenv')) + \ && ale#python#PipenvPresent(a:buffer) + return 'pipenv' + endif + + return ale#python#FindExecutable(a:buffer, 'python_isort', ['isort']) +endfunction function! ale#fixers#isort#Fix(buffer) abort let l:options = ale#Var(a:buffer, 'python_isort_options') - let l:executable = ale#python#FindExecutable( - \ a:buffer, - \ 'python_isort', - \ ['isort'], - \) + let l:executable = ale#fixers#isort#GetExecutable(a:buffer) + + let l:exec_args = l:executable =~? 'pipenv$' + \ ? ' run isort' + \ : '' - if !executable(l:executable) + if !executable(l:executable) && l:executable isnot# 'pipenv' return 0 endif return { \ 'command': ale#path#BufferCdString(a:buffer) - \ . ale#Escape(l:executable) . (!empty(l:options) ? ' ' . l:options : '') . ' -', + \ . ale#Escape(l:executable) . l:exec_args + \ . (!empty(l:options) ? ' ' . l:options : '') . ' -', \} endfunction diff --git a/autoload/ale/fixers/luafmt.vim b/autoload/ale/fixers/luafmt.vim new file mode 100644 index 00000000..6cb9ef4a --- /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/autoload/ale/fixers/ormolu.vim b/autoload/ale/fixers/ormolu.vim new file mode 100644 index 00000000..69b55c1f --- /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/autoload/ale/fixers/phpcbf.vim b/autoload/ale/fixers/phpcbf.vim index f14b8406..0a61c657 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/autoload/ale/fixers/yamlfix.vim b/autoload/ale/fixers/yamlfix.vim new file mode 100644 index 00000000..966556c9 --- /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/autoload/ale/floating_preview.vim b/autoload/ale/floating_preview.vim new file mode 100644 index 00000000..e6a75689 --- /dev/null +++ b/autoload/ale/floating_preview.vim @@ -0,0 +1,91 @@ +" Author: Jan-Grimo Sobez <jan-grimo.sobez@phys.chem.ethz.ch> +" Author: Kevin Clark <kevin.clark@gmail.com> +" Description: Floating preview window for showing whatever information in. + +" Precondition: exists('*nvim_open_win') + +function! ale#floating_preview#Show(lines, ...) abort + if !exists('*nvim_open_win') + execute 'echom ''Floating windows not supported in this vim instance.''' + + return + endif + + " Remove the close autocmd so it doesn't happen mid update + augroup ale_floating_preview_window + autocmd! + augroup END + + let l:options = get(a:000, 0, {}) + + " Only create a new window if we need it + if !exists('w:preview') || index(nvim_list_wins(), w:preview['id']) is# -1 + call s:Create(l:options) + else + call nvim_buf_set_option(w:preview['buffer'], 'modifiable', v:true) + endif + + " Execute commands in window context + let l:parent_window = nvim_get_current_win() + + call nvim_set_current_win(w:preview['id']) + + for l:command in get(l:options, 'commands', []) + call execute(l:command) + endfor + + call nvim_set_current_win(l:parent_window) + + " Return to parent context on move + augroup ale_floating_preview_window + autocmd! + + if g:ale_close_preview_on_insert + autocmd CursorMoved,TabLeave,WinLeave,InsertEnter <buffer> ++once call s:Close() + else + autocmd CursorMoved,TabLeave,WinLeave <buffer> ++once call s:Close() + endif + augroup END + + let l:width = max(map(copy(a:lines), 'strdisplaywidth(v:val)')) + let l:height = min([len(a:lines), 10]) + call nvim_win_set_width(w:preview['id'], l:width) + call nvim_win_set_height(w:preview['id'], l:height) + + call nvim_buf_set_lines(w:preview['buffer'], 0, -1, v:false, a:lines) + call nvim_buf_set_option(w:preview['buffer'], 'modified', v:false) + call nvim_buf_set_option(w:preview['buffer'], 'modifiable', v:false) +endfunction + +function! s:Create(options) abort + let l:buffer = nvim_create_buf(v:false, v:false) + let l:winid = nvim_open_win(l:buffer, v:false, { + \ 'relative': 'cursor', + \ 'row': 1, + \ 'col': 0, + \ 'width': 42, + \ 'height': 4, + \ 'style': 'minimal' + \ }) + call nvim_buf_set_option(l:buffer, 'buftype', 'acwrite') + call nvim_buf_set_option(l:buffer, 'bufhidden', 'delete') + call nvim_buf_set_option(l:buffer, 'swapfile', v:false) + call nvim_buf_set_option(l:buffer, 'filetype', get(a:options, 'filetype', 'ale-preview')) + + let w:preview = {'id': l:winid, 'buffer': l:buffer} +endfunction + +function! s:Close() abort + if !exists('w:preview') + return + endif + + call setbufvar(w:preview['buffer'], '&modified', 0) + + if win_id2win(w:preview['id']) > 0 + execute win_id2win(w:preview['id']).'wincmd c' + endif + + unlet w:preview +endfunction + diff --git a/autoload/ale/handlers/eslint.vim b/autoload/ale/handlers/eslint.vim index e37d6902..b8610612 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') ? '\' : '/' diff --git a/autoload/ale/handlers/inko.vim b/autoload/ale/handlers/inko.vim new file mode 100644 index 00000000..73f06871 --- /dev/null +++ b/autoload/ale/handlers/inko.vim @@ -0,0 +1,37 @@ +" Author: Yorick Peterse <yorick@yorickpeterse.com> +" Description: output handlers for the Inko JSON format + +function! ale#handlers#inko#GetType(severity) abort + if a:severity is? 'warning' + return 'W' + endif + + return 'E' +endfunction + +function! ale#handlers#inko#Handle(buffer, lines) abort + try + let l:errors = json_decode(join(a:lines, '')) + catch + return [] + endtry + + if empty(l:errors) + return [] + endif + + let l:output = [] + let l:dir = expand('#' . a:buffer . ':p:h') + + for l:error in l:errors + call add(l:output, { + \ 'filename': ale#path#GetAbsPath(l:dir, l:error['file']), + \ 'lnum': l:error['line'], + \ 'col': l:error['column'], + \ 'text': l:error['message'], + \ 'type': ale#handlers#inko#GetType(l:error['level']), + \}) + endfor + + return l:output +endfunction diff --git a/autoload/ale/handlers/sh.vim b/autoload/ale/handlers/sh.vim index 1e50cb89..6ed9fea3 100644 --- a/autoload/ale/handlers/sh.vim +++ b/autoload/ale/handlers/sh.vim @@ -1,18 +1,28 @@ " Author: w0rp <devw0rp@gmail.com> -" 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, '') + 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 + " 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 endif diff --git a/autoload/ale/handlers/shellcheck.vim b/autoload/ale/handlers/shellcheck.vim index b16280f0..701c43b2 100644 --- a/autoload/ale/handlers/shellcheck.vim +++ b/autoload/ale/handlers/shellcheck.vim @@ -1,8 +1,32 @@ " Author: w0rp <devw0rp@gmail.com> " 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. @@ -13,15 +37,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 diff --git a/autoload/ale/hover.vim b/autoload/ale/hover.vim index 38b4b866..cb0379fd 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,10 +42,14 @@ 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]) + elseif g:ale_hover_to_floating_preview || g:ale_floating_preview + call ale#floating_preview#Show(split(a:response.body.displayString, "\n"), { + \ 'filetype': 'ale-preview.message', + \}) elseif g:ale_hover_to_preview call ale#preview#Show(split(a:response.body.displayString, "\n"), { \ 'filetype': 'ale-preview.message', @@ -216,12 +222,19 @@ 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]) + elseif g:ale_hover_to_floating_preview || g:ale_floating_preview + call ale#floating_preview#Show(l:lines, { + \ 'filetype': 'ale-preview.message', + \ 'commands': l:commands, + \}) elseif g:ale_hover_to_preview call ale#preview#Show(l:lines, { \ 'filetype': 'ale-preview.message', diff --git a/autoload/ale/linter.vim b/autoload/ale/linter.vim index 645c25f9..ba11e1eb 100644 --- a/autoload/ale/linter.vim +++ b/autoload/ale/linter.vim @@ -43,6 +43,7 @@ let s:default_ale_linters = { \ 'go': ['gofmt', 'golint', 'go vet'], \ 'hack': ['hack'], \ 'help': [], +\ 'inko': ['inko'], \ 'perl': ['perlcritic'], \ 'perl6': [], \ 'python': ['flake8', 'mypy', 'pylint', 'pyright'], diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index 7d99e9d2..cb0573aa 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 5b0cb8b7..38be4da6 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 30da77e1..a4f80980 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 b9fafaa0..3c1b47ed 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/lsp_linter.vim b/autoload/ale/lsp_linter.vim index dcd76e8f..628dde78 100644 --- a/autoload/ale/lsp_linter.vim +++ b/autoload/ale/lsp_linter.vim @@ -85,12 +85,18 @@ function! s:HandleTSServerDiagnostics(response, error_type) abort endif let l:info.syntax_loclist = l:thislist - else + elseif a:error_type is# 'semantic' if len(l:thislist) is 0 && len(get(l:info, 'semantic_loclist', [])) is 0 let l:no_changes = 1 endif let l:info.semantic_loclist = l:thislist + else + if len(l:thislist) is 0 && len(get(l:info, 'suggestion_loclist', [])) is 0 + let l:no_changes = 1 + endif + + let l:info.suggestion_loclist = l:thislist endif if l:no_changes @@ -98,6 +104,7 @@ function! s:HandleTSServerDiagnostics(response, error_type) abort endif let l:loclist = get(l:info, 'semantic_loclist', []) + \ + get(l:info, 'suggestion_loclist', []) \ + get(l:info, 'syntax_loclist', []) call ale#engine#HandleLoclist(l:linter_name, l:buffer, l:loclist, 0) @@ -150,6 +157,10 @@ function! ale#lsp_linter#HandleLSPResponse(conn_id, response) abort elseif get(a:response, 'type', '') is# 'event' \&& get(a:response, 'event', '') is# 'syntaxDiag' call s:HandleTSServerDiagnostics(a:response, 'syntax') + elseif get(a:response, 'type', '') is# 'event' + \&& get(a:response, 'event', '') is# 'suggestionDiag' + \&& get(g:, 'ale_lsp_suggestions', '1') == 1 + call s:HandleTSServerDiagnostics(a:response, 'suggestion') endif endfunction diff --git a/autoload/ale/python.vim b/autoload/ale/python.vim index 7ed22367..fc6c1130 100644 --- a/autoload/ale/python.vim +++ b/autoload/ale/python.vim @@ -32,6 +32,8 @@ function! ale#python#FindProjectRootIni(buffer) abort \|| filereadable(l:path . '/.pylintrc') \|| filereadable(l:path . '/Pipfile') \|| filereadable(l:path . '/Pipfile.lock') + \|| filereadable(l:path . '/poetry.lock') + \|| filereadable(l:path . '/pyproject.toml') return l:path endif endfor diff --git a/autoload/ale/rename.vim b/autoload/ale/rename.vim index 8190411d..9030618e 100644 --- a/autoload/ale/rename.vim +++ b/autoload/ale/rename.vim @@ -85,36 +85,10 @@ function! ale#rename#HandleTSServerResponse(conn_id, response) abort \ }, \ { \ 'should_save': 1, - \ 'force_save': get(l:options, 'force_save'), \ }, \) 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 +100,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 +108,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( \ { @@ -170,7 +117,6 @@ function! ale#rename#HandleLSPResponse(conn_id, response) abort \ }, \ { \ 'should_save': 1, - \ 'force_save': get(l:options, 'force_save'), \ }, \) endif @@ -229,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) @@ -257,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-elixir.txt b/doc/ale-elixir.txt index de9daacf..a4e5c2c6 100644 --- a/doc/ale-elixir.txt +++ b/doc/ale-elixir.txt @@ -85,5 +85,12 @@ g:ale_elixir_credo_strict *g:ale_elixir_credo_strict* Tells credo to run in strict mode or suggest mode. Set variable to 1 to enable --strict mode. +g:ale_elixir_credo_config_file g:ale_elixir_credo_config_file + + Type: String + Default: '' + + Tells credo to use a custom configuration file. + =============================================================================== vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: diff --git a/doc/ale-erlang.txt b/doc/ale-erlang.txt index 59993a99..9ee9a51d 100644 --- a/doc/ale-erlang.txt +++ b/doc/ale-erlang.txt @@ -31,9 +31,29 @@ 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* +g:ale_erlang_erlc_executable *g:ale_erlang_erlc_executable* + *b:ale_erlang_erlc_executable* + Type: |String| + Default: `'erlc'` + + This variable can be changed to specify the erlc executable. + + g:ale_erlang_erlc_options *g:ale_erlang_erlc_options* *b:ale_erlang_erlc_options* Type: |String| diff --git a/doc/ale-haskell.txt b/doc/ale-haskell.txt index 5dd3ec15..fde439fe 100644 --- a/doc/ale-haskell.txt +++ b/doc/ale-haskell.txt @@ -173,4 +173,24 @@ g:ale_haskell_hie_executable *g:ale_haskell_hie_executable* =============================================================================== +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-inko.txt b/doc/ale-inko.txt new file mode 100644 index 00000000..5ca14af6 --- /dev/null +++ b/doc/ale-inko.txt @@ -0,0 +1,22 @@ +=============================================================================== +ALE Inko Integration *ale-inko-options* + *ale-integration-inko* + +=============================================================================== +Integration Information + + Currently, the only supported linter for Inko is the Inko compiler itself. + +=============================================================================== +inko *ale-inko-inko* + +g:ale_inko_inko_executable *g:ale_inko_inko_executable* + *b:ale_inko_inko_executable* + Type: |String| + Default: `'inko'` + + This variable can be modified to change the executable path for `inko`. + + +=============================================================================== + vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: diff --git a/doc/ale-lua.txt b/doc/ale-lua.txt index f1286f89..408f0c3c 100644 --- a/doc/ale-lua.txt +++ b/doc/ale-lua.txt @@ -31,4 +31,20 @@ g:ale_lua_luacheck_options *g:ale_lua_luacheck_options* =============================================================================== +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-php.txt b/doc/ale-php.txt index 9fe868f8..4ee016fb 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* @@ -243,5 +251,70 @@ 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* + +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-python.txt b/doc/ale-python.txt index 6b1a6d33..1f263e84 100644 --- a/doc/ale-python.txt +++ b/doc/ale-python.txt @@ -36,12 +36,40 @@ ALE will look for configuration files with the following filenames. > .pylintrc Pipfile Pipfile.lock + poetry.lock + pyproject.toml < 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* g:ale_python_autopep8_executable *g:ale_python_autopep8_executable* @@ -254,6 +282,15 @@ g:ale_python_isort_use_global *g:ale_python_isort_use_global* See |ale-integrations-local-executables| +g:ale_python_isort_auto_pipenv *g:ale_python_isort_auto_pipenv* + *b:ale_python_isort_auto_pipenv* + Type: |Number| + Default: `0` + + Detect whether the file is inside a pipenv, and set the executable to `pipenv` + if true. This is overridden by a manually-set executable. + + =============================================================================== mypy *ale-python-mypy* @@ -687,7 +724,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. diff --git a/doc/ale-r.txt b/doc/ale-r.txt index b5ccebe5..3fabf702 100644 --- a/doc/ale-r.txt +++ b/doc/ale-r.txt @@ -3,6 +3,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_config *g:ale_r_languageserver_config* + *b:ale_r_languageserver_config* + Type: |Dictionary| + Default: `{}` + + This option can be configured to change settings for languageserver. See the + languageserver documentation for more information. + + +=============================================================================== lintr *ale-r-lintr* g:ale_r_lintr_options *g:ale_r_lintr_options* @@ -22,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. @@ -36,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/doc/ale-ruby.txt b/doc/ale-ruby.txt index 8815404a..edc6144a 100644 --- a/doc/ale-ruby.txt +++ b/doc/ale-ruby.txt @@ -177,6 +177,16 @@ g:ale_ruby_sorbet_options *g:ale_ruby_sorbet_options* This variable can be changed to modify flags given to sorbet. +g:ale_ruby_sorbet_enable_watchman *g:ale_ruby_sorbet_enable_watchman* + *b:ale_ruby_sorbet_enable_watchman* + Type: |Number| + Default: `0` + + Whether or not to use watchman to let the LSP server to know about changes + to files from outside of vim. Defaults to disable watchman because it + requires watchman to be installed separately from sorbet. + + =============================================================================== standardrb *ale-ruby-standardrb* diff --git a/doc/ale-rust.txt b/doc/ale-rust.txt index f4b4e7b7..3aa63673 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. diff --git a/doc/ale-salt.tmt b/doc/ale-salt.tmt new file mode 100644 index 00000000..ac500d37 --- /dev/null +++ b/doc/ale-salt.tmt @@ -0,0 +1,43 @@ +=============================================================================== +ALE SALT Integration *ale-salt-options* + +=============================================================================== +salt-lint *ale-salt-salt-lint* + +Website: https://github.com/warpnet/salt-lint + + +Installation +------------------------------------------------------------------------------- + +Install salt-lint in your a virtualenv directory, locally, or globally: > + + pip install salt-lint # After activating virtualenv + pip install --user salt-lint # Install to ~/.local/bin + sudo pip install salt-lint # Install globally + +See |g:ale_virtualenv_dir_names| for configuring how ALE searches for +virtualenv directories. + + +Options +------------------------------------------------------------------------------- + +g:ale_salt_salt-lint_executable *g:ale_salt_salt_lint_executable* + *b:ale_salt_salt_lint_executable* + Type: |String| + Default: `'salt-lint'` + + This variable can be set to change the path to salt-lint. + + +g:ale_salt_salt-lint_options *g:ale_salt_salt-lint_options* + *b:ale_salt_salt-lint_options* + Type: |String| + Default: `''` + + This variable can be set to pass additional options to salt-lint. + + +=============================================================================== + 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 4f3afd85..ccedd2ad 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -49,8 +49,8 @@ Notes: * `astyle` * `ccls` * `clang` (`cc`) - * `clangd` * `clang-format` + * `clangd` * `clangtidy`!! * `cppcheck` * `cpplint`!! @@ -67,9 +67,9 @@ Notes: * `astyle` * `ccls` * `clang` (`cc`) + * `clang-format` * `clangcheck`!! * `clangd` - * `clang-format` * `clangtidy`!! * `clazy`!! * `cppcheck` @@ -140,8 +140,9 @@ Notes: * `erubis` * `ruumba` * Erlang - * `erlc` * `SyntaxErl` + * `elvis`!! + * `erlc` * Fish * `fish` (-n flag) * Fortran @@ -159,17 +160,17 @@ Notes: * Go * `bingo` * `go build`!! + * `go mod`!! + * `go vet`!! * `gofmt` * `goimports` * `golangci-lint`!! * `golangserver` * `golint` * `gometalinter`!! - * `go mod`!! * `gopls` * `gosimple`!! * `gotype`!! - * `go vet`!! * `revive`!! * `staticcheck`!! * GraphQL @@ -195,16 +196,17 @@ Notes: * `hie` * `hindent` * `hlint` + * `ormolu` * `stack-build`!! * `stack-ghc` * `stylish-haskell` * HCL * `terraform-fmt` * HTML + * `HTMLHint` * `alex`!! * `fecs` * `html-beautify` - * `HTMLHint` * `prettier` * `proselint` * `tidy` @@ -213,15 +215,17 @@ Notes: * `idris` * Ink * `ink-language-server` +* Inko + * `inko` !! * ISPC * `ispc`!! * Java + * `PMD` * `checkstyle` * `eclipselsp` * `google-java-format` * `javac` * `javalsp` - * `PMD` * `uncrustify` * JavaScript * `eslint` @@ -265,6 +269,7 @@ Notes: * Lua * `luac` * `luacheck` + * `luafmt` * Mail * `alex`!! * `languagetool`!! @@ -324,15 +329,17 @@ Notes: * Perl6 * `perl6 -c` * PHP + * `intelephense` * `langserver` * `phan` + * `php -l` + * `php-cs-fixer` * `phpcbf` * `phpcs` - * `php-cs-fixer` - * `php -l` * `phpmd` * `phpstan` * `psalm`!! + * `tlint` * PO * `alex`!! * `msgfmt` @@ -361,6 +368,7 @@ Notes: * `purescript-language-server` * `purty` * Python + * `autoimport` * `autopep8` * `bandit` * `black` @@ -383,10 +391,13 @@ Notes: * `qmlfmt` * `qmllint` * R + * `languageserver` * `lintr` * `styler` * Racket * `raco` +* Re:VIEW + * `redpen` * ReasonML * `merlin` * `ols` @@ -400,8 +411,6 @@ Notes: * `textlint` * `vale` * `write-good` -* Re:VIEW - * `redpen` * RPM spec * `rpmlint` * Ruby @@ -421,6 +430,8 @@ Notes: * `rust-analyzer` * `rustc` (see |ale-integration-rust|) * `rustfmt` +* Salt + * `salt-lint` * Sass * `sass-lint` * `stylelint` @@ -446,10 +457,10 @@ Notes: * `solium` * SQL * `pgformatter` + * `sql-lint` * `sqlfmt` * `sqlformat` * `sqlint` - * `sql-lint` * Stylus * `stylelint` * SugarSS @@ -517,6 +528,7 @@ Notes: * YAML * `prettier` * `swaglint` + * `yamlfix` * `yamllint` * YANG * `yang-lsp` diff --git a/doc/ale-yaml.txt b/doc/ale-yaml.txt index c9a12ea1..61bfc139 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 eb8f0275..09a95b25 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| @@ -645,6 +646,9 @@ problem will be displayed in a balloon instead of hover information. Hover information can be displayed in the preview window instead by setting |g:ale_hover_to_preview| to `1`. +When using Neovim, if |g:ale_hover_to_floating_preview| or |g:ale_floating_preview| +is set to 1, the hover information will show in a floating window. + For Vim 8.1+ terminals, mouse hovering is disabled by default. Enabling |balloonexpr| commands in terminals can cause scrolling issues in terminals, so ALE will not attempt to show balloons unless |g:ale_set_balloons| is set to @@ -669,6 +673,34 @@ 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 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* @@ -925,6 +957,15 @@ g:ale_default_navigation *g:ale_default_navigation* buffer, such as for |ALEFindReferences|, or |ALEGoToDefinition|. +g:ale_detail_to_floating_preview *g:ale_detail_to_floating_preview* + *b:ale_detail_to_floating_preview* + Type: |Number| + Default: `0` + + When this option is set to `1`, Neovim will use a floating window for + ALEDetail output. + + g:ale_disable_lsp *g:ale_disable_lsp* *b:ale_disable_lsp* @@ -1148,6 +1189,16 @@ g:ale_fix_on_save_ignore *g:ale_fix_on_save_ignore* let g:ale_fix_on_save_ignore = [g:AddBar] < +g:ale_floating_preview *g:ale_floating_preview* + + Type: |Number| + Default: `0` + + When set to `1`, Neovim will use a floating window for ale's preview window. + This is equivalent to setting |g:ale_hover_to_floating_preview| and + |g:ale_detail_to_floating_preview| to `1`. + + g:ale_history_enabled *g:ale_history_enabled* Type: |Number| @@ -1206,6 +1257,14 @@ g:ale_hover_to_preview *g:ale_hover_to_preview* instead of in balloons or the message line. +g:ale_hover_to_floating_preview *g:ale_hover_to_floating_preview* + *b:ale_hover_to_floating_preview* + Type: |Number| + Default: `0` + + If set to `1`, Neovim will use floating windows for hover messages. + + g:ale_keep_list_window_open *g:ale_keep_list_window_open* *b:ale_keep_list_window_open* Type: |Number| @@ -1502,6 +1561,7 @@ g:ale_linters *g:ale_linters* \ 'go': ['gofmt', 'golint', 'go vet'], \ 'hack': ['hack'], \ 'help': [], + \ 'inko': ['inko'], \ 'perl': ['perlcritic'], \ 'perl6': [], \ 'python': ['flake8', 'mypy', 'pylint', 'pyright'], @@ -1648,6 +1708,15 @@ g:ale_lsp_show_message_severity *g:ale_lsp_show_message_severity* `'disabled'` - Doesn't display any information at all. +g:ale_lsp_suggestions *g:ale_lsp_suggestions* + + Type: |Number| + Default: 1 + + This variable defines if suggestions must be collected from LSP or tsserver + and shown. + + g:ale_lsp_root *g:ale_lsp_root* *b:ale_lsp_root* @@ -1665,6 +1734,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| @@ -1773,6 +1843,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_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 + 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| @@ -1797,7 +1880,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 @@ -1808,6 +1891,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 @@ -2589,6 +2679,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| @@ -2642,6 +2733,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| @@ -2656,6 +2748,8 @@ documented in additional help files. idris.................................|ale-idris-idris| ink.....................................|ale-ink-options| ink-language-server...................|ale-ink-language-server| + inko....................................|ale-inko-options| + inko..................................|ale-inko-inko| ispc....................................|ale-ispc-options| ispc..................................|ale-ispc-ispc| java....................................|ale-java-options| @@ -2701,6 +2795,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| @@ -2752,6 +2847,8 @@ 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| pod.....................................|ale-pod-options| @@ -2777,6 +2874,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| @@ -2798,6 +2896,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| @@ -2825,6 +2924,8 @@ documented in additional help files. rls...................................|ale-rust-rls| rustc.................................|ale-rust-rustc| rustfmt...............................|ale-rust-rustfmt| + salt....................................|ale-salt-options| + salt-lint.............................|ale-salt-salt-lint| sass....................................|ale-sass-options| sasslint..............................|ale-sass-sasslint| stylelint.............................|ale-sass-stylelint| @@ -2899,11 +3000,11 @@ documented in additional help files. hdl-checker...........................|ale-vhdl-hdl-checker| vcom..................................|ale-vhdl-vcom| xvhdl.................................|ale-vhdl-xvhdl| + vim help................................|ale-vim-help-options| + write-good............................|ale-vim-help-write-good| vim.....................................|ale-vim-options| vimls.................................|ale-vim-vimls| vint..................................|ale-vim-vint| - vim help................................|ale-vim-help-options| - write-good............................|ale-vim-help-write-good| vue.....................................|ale-vue-options| prettier..............................|ale-vue-prettier| vls...................................|ale-vue-vls| @@ -2914,6 +3015,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| @@ -3099,11 +3201,17 @@ 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* + + 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. + + 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 32ec14ac..1735715d 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -138,6 +138,15 @@ let g:ale_set_balloons = get(g:, 'ale_set_balloons', has('balloon_eval') && has( " Use preview window for hover messages. let g:ale_hover_to_preview = get(g:, 'ale_hover_to_preview', 0) +" Float preview windows in Neovim +let g:ale_floating_preview = get(g:, 'ale_floating_preview', 0) + +" Hovers use floating windows in Neovim +let g:ale_hover_to_floating_preview = get(g:, 'ale_hover_to_floating_preview', 0) + +" Detail uses floating windows in Neovim +let g:ale_detail_to_floating_preview = get(g:, 'ale_detail_to_floating_preview', 0) + " This flag can be set to 0 to disable warnings for trailing whitespace let g:ale_warn_about_trailing_whitespace = get(g:, 'ale_warn_about_trailing_whitespace', 1) " This flag can be set to 0 to disable warnings for trailing blank lines @@ -158,7 +167,10 @@ 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 g:ale_set_balloons +" If 1, enable a popup menu for commands. +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() endif @@ -166,6 +178,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', <q-args>) @@ -238,7 +254,10 @@ 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': '<bang>' is# '!'}) +command! -bar -bang ALERename :call ale#rename#Execute() + +" Apply code actions to a range. +command! -bar -range ALECodeAction :call ale#codefix#Execute(<range>) " Organize import statements using tsserver command! -bar ALEOrganizeImports :call ale#organize_imports#Execute() @@ -283,6 +302,7 @@ nnoremap <silent> <Plug>(ale_documentation) :ALEDocumentation<Return> inoremap <silent> <Plug>(ale_complete) <C-\><C-O>:ALEComplete<Return> nnoremap <silent> <Plug>(ale_import) :ALEImport<Return> nnoremap <silent> <Plug>(ale_rename) :ALERename<Return> +nnoremap <silent> <Plug>(ale_code_action) :ALECodeAction<Return> nnoremap <silent> <Plug>(ale_repeat_selection) :ALERepeatSelection<Return> " Set up autocmd groups now. @@ -10,7 +10,7 @@ set -u # image=w0rp/ale -current_image_id=f58c7bf8900f +current_image_id=1c04720f5d17 # Used in all test scripts for running the selected Docker image. DOCKER_RUN_IMAGE="$image" diff --git a/supported-tools.md b/supported-tools.md index 49460892..7590e9a4 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -58,8 +58,8 @@ formatting. * [astyle](http://astyle.sourceforge.net/) * [ccls](https://github.com/MaskRay/ccls) * [clang](http://clang.llvm.org/) - * [clangd](https://clang.llvm.org/extra/clangd.html) * [clang-format](https://clang.llvm.org/docs/ClangFormat.html) + * [clangd](https://clang.llvm.org/extra/clangd.html) * [clangtidy](http://clang.llvm.org/extra/clang-tidy/) :floppy_disk: * [cppcheck](http://cppcheck.sourceforge.net) * [cpplint](https://github.com/google/styleguide/tree/gh-pages/cpplint) @@ -76,9 +76,9 @@ formatting. * [astyle](http://astyle.sourceforge.net/) * [ccls](https://github.com/MaskRay/ccls) * [clang](http://clang.llvm.org/) + * [clang-format](https://clang.llvm.org/docs/ClangFormat.html) * [clangcheck](http://clang.llvm.org/docs/ClangCheck.html) :floppy_disk: * [clangd](https://clang.llvm.org/extra/clangd.html) - * [clang-format](https://clang.llvm.org/docs/ClangFormat.html) * [clangtidy](http://clang.llvm.org/extra/clang-tidy/) :floppy_disk: * [clazy](https://github.com/KDE/clazy) :floppy_disk: * [cppcheck](http://cppcheck.sourceforge.net) @@ -149,8 +149,9 @@ formatting. * [erubis](https://github.com/kwatch/erubis) * [ruumba](https://github.com/ericqweinstein/ruumba) * Erlang - * [erlc](http://erlang.org/doc/man/erlc.html) * [SyntaxErl](https://github.com/ten0s/syntaxerl) + * [elvis](https://github.com/inaka/elvis) :floppy_disk: + * [erlc](http://erlang.org/doc/man/erlc.html) * Fish * fish [-n flag](https://linux.die.net/man/1/fish) * Fortran @@ -168,17 +169,17 @@ formatting. * Go * [bingo](https://github.com/saibing/bingo) :warning: * [go build](https://golang.org/cmd/go/) :warning: :floppy_disk: + * [go mod](https://golang.org/cmd/go/) :warning: :floppy_disk: + * [go vet](https://golang.org/cmd/vet/) :floppy_disk: * [gofmt](https://golang.org/cmd/gofmt/) * [goimports](https://godoc.org/golang.org/x/tools/cmd/goimports) :warning: * [golangci-lint](https://github.com/golangci/golangci-lint) :warning: :floppy_disk: * [golangserver](https://github.com/sourcegraph/go-langserver) :warning: * [golint](https://godoc.org/github.com/golang/lint) * [gometalinter](https://github.com/alecthomas/gometalinter) :warning: :floppy_disk: - * [go mod](https://golang.org/cmd/go/) :warning: :floppy_disk: * [gopls](https://github.com/golang/go/wiki/gopls) :warning: * [gosimple](https://github.com/dominikh/go-tools/tree/master/cmd/gosimple) :warning: :floppy_disk: * [gotype](https://godoc.org/golang.org/x/tools/cmd/gotype) :warning: :floppy_disk: - * [go vet](https://golang.org/cmd/vet/) :floppy_disk: * [revive](https://github.com/mgechev/revive) :warning: :floppy_disk: * [staticcheck](https://github.com/dominikh/go-tools/tree/master/cmd/staticcheck) :warning: :floppy_disk: * GraphQL @@ -204,16 +205,17 @@ 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) * HCL * [terraform-fmt](https://github.com/hashicorp/terraform) * HTML + * [HTMLHint](http://htmlhint.com/) * [alex](https://github.com/wooorm/alex) :floppy_disk: * [fecs](http://fecs.baidu.com/) * [html-beautify](https://beautifier.io/) - * [HTMLHint](http://htmlhint.com/) * [prettier](https://github.com/prettier/prettier) * [proselint](http://proselint.com/) * [tidy](http://www.html-tidy.org/) @@ -222,15 +224,17 @@ formatting. * [idris](http://www.idris-lang.org/) * Ink * [ink-language-server](https://github.com/ephread/ink-language-server) +* Inko + * [inko](https://inko-lang.org/) :floppy_disk: * ISPC * [ispc](https://ispc.github.io/) :floppy_disk: * Java + * [PMD](https://pmd.github.io/) * [checkstyle](http://checkstyle.sourceforge.net) * [eclipselsp](https://github.com/eclipse/eclipse.jdt.ls) * [google-java-format](https://github.com/google/google-java-format) * [javac](http://www.oracle.com/technetwork/java/javase/downloads/index.html) * [javalsp](https://github.com/georgewfraser/vscode-javac) - * [PMD](https://pmd.github.io/) * [uncrustify](https://github.com/uncrustify/uncrustify) * JavaScript * [eslint](http://eslint.org/) @@ -274,6 +278,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: @@ -333,15 +338,17 @@ 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 + * [php -l](https://secure.php.net/) + * [php-cs-fixer](http://cs.sensiolabs.org/) * [phpcbf](https://github.com/squizlabs/PHP_CodeSniffer) * [phpcs](https://github.com/squizlabs/PHP_CodeSniffer) - * [php-cs-fixer](http://cs.sensiolabs.org/) - * [php -l](https://secure.php.net/) * [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) @@ -370,6 +377,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) @@ -392,10 +400,13 @@ 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 * [raco](https://docs.racket-lang.org/raco/) +* Re:VIEW + * [redpen](http://redpen.cc/) * ReasonML * [merlin](https://github.com/the-lambda-church/merlin) see `:help ale-reasonml-ols` for configuration instructions * [ols](https://github.com/freebroccolo/ocaml-language-server) @@ -409,8 +420,6 @@ formatting. * [textlint](https://textlint.github.io/) * [vale](https://github.com/ValeLint/vale) * [write-good](https://github.com/btford/write-good) -* Re:VIEW - * [redpen](http://redpen.cc/) * RPM spec * [rpmlint](https://github.com/rpm-software-management/rpmlint) :warning: (see `:help ale-integration-spec`) * Ruby @@ -430,6 +439,8 @@ formatting. * [rust-analyzer](https://github.com/rust-analyzer/rust-analyzer) :warning: * [rustc](https://www.rust-lang.org/) :warning: * [rustfmt](https://github.com/rust-lang-nursery/rustfmt) +* Salt + * [salt-lint](https://github.com/warpnet/salt-lint) * Sass * [sass-lint](https://www.npmjs.com/package/sass-lint) * [stylelint](https://github.com/stylelint/stylelint) @@ -455,10 +466,10 @@ formatting. * [solium](https://github.com/duaraghav8/Solium) * SQL * [pgformatter](https://github.com/darold/pgFormatter) + * [sql-lint](https://github.com/joereynolds/sql-lint) * [sqlfmt](https://github.com/jackc/sqlfmt) * [sqlformat](https://github.com/andialbrecht/sqlparse) * [sqlint](https://github.com/purcell/sqlint) - * [sql-lint](https://github.com/joereynolds/sql-lint) * Stylus * [stylelint](https://github.com/stylelint/stylelint) * SugarSS @@ -526,6 +537,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/inko_paths/test.inko b/test/command_callback/inko_paths/test.inko new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/command_callback/inko_paths/test.inko diff --git a/test/command_callback/inko_paths/tests/test/test_foo.inko b/test/command_callback/inko_paths/tests/test/test_foo.inko new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/command_callback/inko_paths/tests/test/test_foo.inko 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 00000000..e69de29b --- /dev/null +++ b/test/command_callback/php-intelephense-project/with-composer/composer.json 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 00000000..e69de29b --- /dev/null +++ b/test/command_callback/python_paths/with_virtualenv/env/Scripts/autoimport.exe 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 00000000..e69de29b --- /dev/null +++ b/test/command_callback/python_paths/with_virtualenv/env/Scripts/yamlfix.exe 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 00000000..e69de29b --- /dev/null +++ b/test/command_callback/python_paths/with_virtualenv/env/bin/autoimport 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 00000000..e69de29b --- /dev/null +++ b/test/command_callback/python_paths/with_virtualenv/env/bin/yamlfix diff --git a/test/command_callback/r_paths/.Rprofile b/test/command_callback/r_paths/.Rprofile new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/command_callback/r_paths/.Rprofile diff --git a/test/command_callback/test_elixir_credo.vader b/test/command_callback/test_elixir_credo.vader index 3eb88846..b14444c6 100644 --- a/test/command_callback/test_elixir_credo.vader +++ b/test/command_callback/test_elixir_credo.vader @@ -38,3 +38,10 @@ Execute(Builds credo command with suggest mode when set to 0): AssertLinter 'mix', \ ale#path#CdString(ale#path#Simplify(g:dir . '/elixir_paths/mix_project')) \ . 'mix help credo && mix credo suggest --format=flycheck --read-from-stdin %s' + +Execute(Builds credo command with a custom config file): + let g:ale_elixir_credo_config_file = '/home/user/custom_credo.exs' + + AssertLinter 'mix', + \ ale#path#CdString(ale#path#Simplify(g:dir . '/elixir_paths/mix_project')) + \ . 'mix help credo && mix credo suggest --config-file /home/user/custom_credo.exs --format=flycheck --read-from-stdin %s' 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 00000000..4aab49d6 --- /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/command_callback/test_erlang_erlc_command_callback.vader b/test/command_callback/test_erlang_erlc_command_callback.vader new file mode 100644 index 00000000..7d659a07 --- /dev/null +++ b/test/command_callback/test_erlang_erlc_command_callback.vader @@ -0,0 +1,40 @@ +Before: + call ale#assert#SetUpLinterTest('erlang', 'erlc') + +After: + call ale#assert#TearDownLinterTest() + +Execute(The default command should be correct.): + let g:cmd = ale_linters#erlang#erlc#GetCommand(bufnr('')) + let g:regex = 'erlc.\+-o.\+%t' + let g:matched = match(g:cmd, g:regex) + + " match returns -1 if not found + AssertNotEqual + \ g:matched, + \ -1, + \ 'Command error: expected [' . g:cmd . '] to match [' . g:regex . ']' + +Execute(The command should accept configured executable.): + let b:ale_erlang_erlc_executable = '/usr/bin/erlc' + let g:cmd = ale_linters#erlang#erlc#GetCommand(bufnr('')) + let g:regex = '/usr/bin/erlc.\+-o.\+%t' + let g:matched = match(g:cmd, g:regex) + + " match returns -1 if not found + AssertNotEqual + \ g:matched, + \ -1, + \ 'Command error: expected [' . g:cmd . '] to match [' . g:regex . ']' + +Execute(The command should accept configured options.): + let b:ale_erlang_erlc_options = '-I include' + let g:cmd = ale_linters#erlang#erlc#GetCommand(bufnr('')) + let g:regex = 'erlc.\+-o.\+-I include.\+%t' + let g:matched = match(g:cmd, g:regex) + + " match returns -1 if not found + AssertNotEqual + \ g:matched, + \ -1, + \ 'Command error: expected [' . g:cmd . '] to match [' . g:regex . ']' diff --git a/test/command_callback/test_fecs_command_callback.vader b/test/command_callback/test_fecs_command_callback.vader index f70ad084..4287d324 100644 --- a/test/command_callback/test_fecs_command_callback.vader +++ b/test/command_callback/test_fecs_command_callback.vader @@ -1,5 +1,6 @@ Before: call ale#assert#SetUpLinterTest('javascript', 'fecs') + runtime autoload/ale/handlers/fecs.vim After: call ale#assert#TearDownLinterTest() diff --git a/test/command_callback/test_inko_inko_callbacks.vader b/test/command_callback/test_inko_inko_callbacks.vader new file mode 100644 index 00000000..93295c91 --- /dev/null +++ b/test/command_callback/test_inko_inko_callbacks.vader @@ -0,0 +1,20 @@ +Before: + call ale#assert#SetUpLinterTest('inko', 'inko') + call ale#test#SetFilename('inko_paths/test.inko') + +After: + call ale#assert#TearDownLinterTest() + +Execute(The default executable path should be correct): + AssertLinter 'inko', ale#Escape('inko') . ' build --check --format=json %s' + +Execute(The inko callback should include tests/ for test paths): + call ale#engine#Cleanup(bufnr('')) + noautocmd e! inko_paths/tests/test/test_foo.inko + call ale#engine#InitBufferInfo(bufnr('')) + + AssertLinter 'inko', + \ ale#Escape('inko') + \ . ' build --check --format=json --include ' + \ . ale#Escape(ale#path#Simplify(g:dir . '/inko_paths/tests/')) + \ . ' %s' diff --git a/test/command_callback/test_julia_languageserver_callbacks.vader b/test/command_callback/test_julia_languageserver_callbacks.vader index 3bc46e3d..96df81f1 100644 --- a/test/command_callback/test_julia_languageserver_callbacks.vader +++ b/test/command_callback/test_julia_languageserver_callbacks.vader @@ -11,16 +11,16 @@ After: Execute(The default executable path should be correct): AssertLinter 'julia', \ ale#Escape('julia') . - \' --startup-file=no --history-file=no -e ' . - \ ale#Escape('using LanguageServer; server = LanguageServer.LanguageServerInstance(isdefined(Base, :stdin) ? stdin : STDIN, isdefined(Base, :stdout) ? stdout : STDOUT, false); server.runlinter = true; run(server);') + \' --project=@. --startup-file=no --history-file=no -e ' . + \ ale#Escape('using LanguageServer; using Pkg; import StaticLint; import SymbolServer; server = LanguageServer.LanguageServerInstance(isdefined(Base, :stdin) ? stdin : STDIN, isdefined(Base, :stdout) ? stdout : STDOUT, dirname(Pkg.Types.Context().env.project_file)); server.runlinter = true; run(server);') Execute(The executable should be configurable): let g:ale_julia_executable = 'julia-new' AssertLinter 'julia-new', \ ale#Escape('julia-new') . - \' --startup-file=no --history-file=no -e ' . - \ ale#Escape('using LanguageServer; server = LanguageServer.LanguageServerInstance(isdefined(Base, :stdin) ? stdin : STDIN, isdefined(Base, :stdout) ? stdout : STDOUT, false); server.runlinter = true; run(server);') + \' --project=@. --startup-file=no --history-file=no -e ' . + \ ale#Escape('using LanguageServer; using Pkg; import StaticLint; import SymbolServer; server = LanguageServer.LanguageServerInstance(isdefined(Base, :stdin) ? stdin : STDIN, isdefined(Base, :stdout) ? stdout : STDOUT, dirname(Pkg.Types.Context().env.project_file)); server.runlinter = true; run(server);') Execute(The project root should be detected correctly): AssertLSPProject '' 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 00000000..dd6adb3d --- /dev/null +++ b/test/command_callback/test_php_intelephense_command_callback.vader @@ -0,0 +1,26 @@ +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-intelephense-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 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') 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 00000000..9a4a1f87 --- /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_config = {'r': {'lsp': {'debug': 'true', 'diagnostics': 'true'}}} + + AssertLSPConfig {'r': {'lsp': {'debug': 'true', 'diagnostics': 'true'}}} diff --git a/test/command_callback/test_sorbet_command_callback.vader b/test/command_callback/test_sorbet_command_callback.vader index b46e90a4..fe758635 100644 --- a/test/command_callback/test_sorbet_command_callback.vader +++ b/test/command_callback/test_sorbet_command_callback.vader @@ -5,6 +5,7 @@ Before: let g:ale_ruby_sorbet_executable = 'srb' let g:ale_ruby_sorbet_options = '' + let g:ale_ruby_sorbet_enable_watchman = 0 After: call ale#assert#TearDownLinterTest() @@ -13,6 +14,12 @@ Execute(Executable should default to srb): AssertLinter 'srb', ale#Escape('srb') \ . ' tc --lsp --disable-watchman' +Execute(Able to enable watchman): + let g:ale_ruby_sorbet_enable_watchman = 1 + + AssertLinter 'srb', ale#Escape('srb') + \ . ' tc --lsp' + Execute(Should be able to set a custom executable): let g:ale_ruby_sorbet_executable = 'bin/srb' diff --git a/test/completion/test_lsp_completion_parsing.vader b/test/completion/test_lsp_completion_parsing.vader index d989aefe..36228c10 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<str>) -> 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<T> 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<T>\nwhere\n T: Clone," \ } \ ] \ }) diff --git a/test/fixers/test_autoimport_fixer_callback.vader b/test/fixers/test_autoimport_fixer_callback.vader new file mode 100644 index 00000000..6952cbb8 --- /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('')) diff --git a/test/fixers/test_gofmt_fixer_callback.vader b/test/fixers/test_gofmt_fixer_callback.vader index 16659655..99407173 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('')) diff --git a/test/fixers/test_isort_fixer_callback.vader b/test/fixers/test_isort_fixer_callback.vader index 7f389dcf..3941f6dd 100644 --- a/test/fixers/test_isort_fixer_callback.vader +++ b/test/fixers/test_isort_fixer_callback.vader @@ -5,6 +5,7 @@ Before: " Use an invalid global executable, so we don't match it. let g:ale_python_isort_executable = 'xxxinvalid' let g:ale_python_isort_options = '' + let g:ale_python_isort_auto_pipenv = 0 call ale#test#SetDirectory('/testplugin/test/fixers') silent cd .. @@ -48,3 +49,12 @@ Execute(The isort callback should respect custom options): \ . ' --multi-line=3 --trailing-comma -', \ }, \ ale#fixers#isort#Fix(bufnr('')) + +Execute(Pipenv is detected when python_isort_auto_pipenv is set): + let g:ale_python_isort_auto_pipenv = 1 + + call ale#test#SetFilename('/testplugin/test/python_fixtures/pipenv/whatever.py') + + AssertEqual + \ {'command': ale#path#BufferCdString(bufnr('')) . ale#Escape('pipenv') . ' run isort -'}, + \ ale#fixers#isort#Fix(bufnr('')) diff --git a/test/fixers/test_luafmt_fixer_callback.vader b/test/fixers/test_luafmt_fixer_callback.vader new file mode 100644 index 00000000..362da118 --- /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/fixers/test_ormolu_fixer_callback.vader b/test/fixers/test_ormolu_fixer_callback.vader new file mode 100644 index 00000000..8df3fca9 --- /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('')) diff --git a/test/fixers/test_phpcbf_fixer_callback.vader b/test/fixers/test_phpcbf_fixer_callback.vader index 1663c89c..f7bcc2d8 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') diff --git a/test/fixers/test_yamlfix_fixer_callback.vader b/test/fixers/test_yamlfix_fixer_callback.vader new file mode 100644 index 00000000..3ffda91e --- /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('')) diff --git a/test/handler/test_dafny_handler.vader b/test/handler/test_dafny_handler.vader index 674f691d..797d348e 100644 --- a/test/handler/test_dafny_handler.vader +++ b/test/handler/test_dafny_handler.vader @@ -8,14 +8,14 @@ Execute(The Dafny handler should parse output correctly): AssertEqual \ [ \ { - \ 'bufnr': 0, + \ 'filename': 'File.dfy', \ 'col': 45, \ 'lnum': 123, \ 'text': 'A precondition for this call might not hold.', \ 'type': 'E' \ }, \ { - \ 'bufnr': 0, + \ 'filename': 'File.dfy', \ 'col': 90, \ 'lnum': 678, \ 'text': 'This is the precondition that might not hold.', diff --git a/test/handler/test_erlang_elvis_handler.vader b/test/handler/test_erlang_elvis_handler.vader new file mode 100644 index 00000000..365376c8 --- /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!"),.', + \ ]) diff --git a/test/handler/test_inko_handler.vader b/test/handler/test_inko_handler.vader new file mode 100644 index 00000000..6621d2d6 --- /dev/null +++ b/test/handler/test_inko_handler.vader @@ -0,0 +1,54 @@ +Before: + runtime ale_linters/inko/inko.vim + +After: + call ale#linter#Reset() + +Execute(The inko handler should parse errors correctly): + AssertEqual + \ [ + \ { + \ 'filename': ale#path#Simplify('/tmp/foo.inko'), + \ 'lnum': 4, + \ 'col': 5, + \ 'text': 'this is an error', + \ 'type': 'E', + \ } + \ ], + \ ale#handlers#inko#Handle(bufnr(''), [ + \ '[', + \ ' {', + \ ' "file": "/tmp/foo.inko",', + \ ' "line": 4,', + \ ' "column": 5,', + \ ' "message": "this is an error",', + \ ' "level": "error"', + \ ' }', + \ ']' + \ ]) + +Execute(The inko handler should parse warnings correctly): + AssertEqual + \ [ + \ { + \ 'filename': ale#path#Simplify('/tmp/foo.inko'), + \ 'lnum': 4, + \ 'col': 5, + \ 'text': 'this is a warning', + \ 'type': 'W', + \ } + \ ], + \ ale#handlers#inko#Handle(bufnr(''), [ + \ '[', + \ ' {', + \ ' "file": "/tmp/foo.inko",', + \ ' "line": 4,', + \ ' "column": 5,', + \ ' "message": "this is a warning",', + \ ' "level": "warning"', + \ ' }', + \ ']' + \ ]) + +Execute(The inko handler should handle empty output): + AssertEqual [], ale#handlers#inko#Handle(bufnr(''), []) diff --git a/test/handler/test_phpcs_handler.vader b/test/handler/test_phpcs_handler.vader index 18accece..26d35cb8 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'.", \ ]) diff --git a/test/handler/test_salt_salt_lint.vader b/test/handler/test_salt_salt_lint.vader new file mode 100644 index 00000000..7e234785 --- /dev/null +++ b/test/handler/test_salt_salt_lint.vader @@ -0,0 +1,34 @@ +Before: + runtime ale_linters/salt/salt_lint.vim + +After: + call ale#linter#Reset() + +Execute(The salt handler should parse lines correctly and show error in severity HIGH): + AssertEqual + \ [ + \ { + \ 'lnum': 5, + \ 'code': 207, + \ 'text': 'File modes should always be encapsulated in quotation marks', + \ 'type': 'E' + \ } + \ ], + \ ale_linters#salt#salt_lint#Handle(255, [ + \ '[{"id": "207", "message": "File modes should always be encapsulated in quotation marks", "filename": "test.sls", "linenumber": 5, "line": " - mode: 0755", "severity": "HIGH"}]' + \ ]) + + +Execute(The salt handler should parse lines correctly and show error in severity not HIGH): + AssertEqual + \ [ + \ { + \ 'lnum': 27, + \ 'code': 204, + \ 'text': 'Lines should be no longer that 160 chars', + \ 'type': 'W' + \ } + \ ], + \ ale_linters#salt#salt_lint#Handle(255, [ + \ '[{"id": "204", "message": "Lines should be no longer that 160 chars", "filename": "test2.sls", "linenumber": 27, "line": "this line is definitely longer than 160 chars, this line is definitely longer than 160 chars, this line is definitely longer than 160 chars", "severity": "VERY_LOW"}]' + \ ]) diff --git a/test/handler/test_tlint_handler.vader b/test/handler/test_tlint_handler.vader new file mode 100644 index 00000000..e146346c --- /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()`", + \ ]) diff --git a/test/lsp/test_other_initialize_message_handling.vader b/test/lsp/test_other_initialize_message_handling.vader index b6ef852a..f3b53843 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/lua_files/testfile.lua b/test/lua_files/testfile.lua new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/lua_files/testfile.lua diff --git a/test/script/check-duplicate-tags b/test/script/check-duplicate-tags new file mode 100755 index 00000000..ec1de788 --- /dev/null +++ b/test/script/check-duplicate-tags @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +grep --exclude=tags -roh '\*.*\*$' doc | sort | uniq -d diff --git a/test/script/check-supported-tools-tables b/test/script/check-supported-tools-tables index f4305707..d238e77f 100755 --- a/test/script/check-supported-tools-tables +++ b/test/script/check-supported-tools-tables @@ -15,14 +15,13 @@ while read -r; do if [[ "$REPLY" =~ ^! ]]; then language="${REPLY/!/}" else - # shellcheck disable=SC2001 echo "$language - $REPLY" fi done < <( grep '^\*\|^ *\*' doc/ale-supported-languages-and-tools.txt \ | sed -e '1,2d' \ | sed 's/^\* */!/' \ - | sed 's/^ *\* *\|!!\|\^\|(.*)\|`//g' \ + | sed -E 's/^ *\* *|!!|\^|\(.*\)|`//g' \ | sed 's/ *$//' ) > "$doc_file" @@ -30,13 +29,12 @@ while read -r; do if [[ "$REPLY" =~ ^! ]]; then language="${REPLY/!/}" else - # shellcheck disable=SC2001 echo "$language - $REPLY" fi done < <( grep '^\*\|^ *\*' supported-tools.md \ | sed 's/^\* */!/' \ - | sed 's/^ *\* *\|:floppy_disk:\|:warning:\|(.*)\|\[\|\].*\|-n flag//g' \ + | sed -E 's/^ *\* *|:floppy_disk:|:warning:|\(.*\)|\[|\].*|-n flag//g' \ | sed 's/ *$//' ) > "$readme_file" diff --git a/test/script/check-tag-alignment b/test/script/check-tag-alignment new file mode 100755 index 00000000..d41db160 --- /dev/null +++ b/test/script/check-tag-alignment @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +exit_code=0 + +# Documentation tags need to be aligned to the right margin, so look for +# tags which aren't at the right margin. +grep ' \*[^*]\+\*$' doc/ -r \ + | awk '{ sep = index($0, ":"); if (length(substr($0, sep + 1 )) < 79) { print } }' \ + | grep . && exit_code=1 + +exit $exit_code diff --git a/test/script/check-tag-references b/test/script/check-tag-references new file mode 100755 index 00000000..45e741fb --- /dev/null +++ b/test/script/check-tag-references @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -e + +exit_code=0 +tag_regex='[gb]\?:\?\(ale\|ALE\)[a-zA-Z_\-]\+' + +tags="$(mktemp -t tags.XXXXXXXX)" +refs="$(mktemp -t refs.XXXXXXXX)" +# Grep for tags and references, and complain if we find a reference without +# a tag for the reference. Only our tags will be included. +grep --exclude=tags -roh "\\*$tag_regex\\*" doc | sed 's/*//g' | sort -u > "$tags" +grep --exclude=tags -roh "|$tag_regex|" doc | sed 's/|//g' | sort -u > "$refs" + +exit_code=0 + +if ! [[ $(comm -23 $refs $tags | wc -l) -eq 0 ]]; then + exit_code=1 +fi + +rm "$tags" +rm "$refs" diff --git a/test/script/check-toc b/test/script/check-toc index 87a61262..f3f8a9ea 100755 --- a/test/script/check-toc +++ b/test/script/check-toc @@ -35,7 +35,7 @@ sed -n "$toc_start_line,$toc_end_line"p doc/ale.txt \ > "$toc_file" # Get all of the doc files in a natural sorted order. -doc_files="$(/usr/bin/env ls -1v doc | grep ^ale- | sed 's/^/doc\//' | paste -sd ' ' -)" +doc_files="$(/usr/bin/env ls -1v doc | grep '^ale-' | sed 's/^/doc\//' | paste -sd ' ' -)" # shellcheck disable=SC2086 grep -h '\*ale-.*-options\|^[a-z].*\*ale-.*\*$' $doc_files \ diff --git a/test/script/custom-checks b/test/script/custom-checks index ca9069e4..83afb28c 100755 --- a/test/script/custom-checks +++ b/test/script/custom-checks @@ -13,7 +13,7 @@ echo 'Custom warnings/errors follow:' echo set -o pipefail -docker run -a stdout "${docker_flags[@]}" test/script/custom-linting-rules . || exit_code=$? +docker run "${docker_flags[@]}" test/script/custom-linting-rules . || exit_code=$? set +o pipefail echo @@ -23,7 +23,10 @@ echo '========================================' echo 'Duplicate tags follow:' echo -grep --exclude=tags -roh '\*.*\*$' doc | sort | uniq -d || exit_code=$? +set -o pipefail +docker run "${docker_flags[@]}" test/script/check-duplicate-tags . || exit_code=$? +set +o pipefail +echo echo '========================================' echo 'Checking for invalid tag references' @@ -31,14 +34,9 @@ echo '========================================' echo 'Invalid tag references tags follow:' echo -tag_regex='[gb]\?:\?\(ale\|ALE\)[a-zA-Z_\-]\+' - -# Grep for tags and references, and complain if we find a reference without -# a tag for the reference. Only our tags will be included. -diff -u \ - <(grep --exclude=tags -roh "\\*$tag_regex\\*" doc | sort -u | sed 's/*//g') \ - <(grep --exclude=tags -roh "|$tag_regex|" doc | sort -u | sed 's/|//g') \ - | grep '^+[^+]' && exit_code=1 +set -o pipefail +docker run "${docker_flags[@]}" test/script/check-tag-references || exit_code=$? +set +o pipefail echo '========================================' echo 'diff supported-tools.md and doc/ale-supported-languages-and-tools.txt tables' @@ -46,7 +44,9 @@ echo '========================================' echo 'Differences follow:' echo -test/script/check-supported-tools-tables || exit_code=$? +set -o pipefail +docker run "${docker_flags[@]}" test/script/check-supported-tools-tables || exit_code=$? +set +o pipefail echo '========================================' echo 'Look for badly aligned doc tags' @@ -54,18 +54,18 @@ echo '========================================' echo 'Badly aligned tags follow:' echo -# Documentation tags need to be aligned to the right margin, so look for -# tags which aren't at the right margin. -grep ' \*[^*]\+\*$' doc/ -r \ - | awk '{ sep = index($0, ":"); if (length(substr($0, sep + 1 )) < 79) { print } }' \ - | grep . && exit_code=1 +set -o pipefail +docker run "${docker_flags[@]}" test/script/check-tag-alignment || exit_code=$? +set +o pipefail echo '========================================' echo 'Look for table of contents issues' echo '========================================' echo -test/script/check-toc || exit_code=$? +set -o pipefail +docker run "${docker_flags[@]}" test/script/check-toc || exit_code=$? +set +o pipefail echo '========================================' echo 'Check Python code' diff --git a/test/script/custom-linting-rules b/test/script/custom-linting-rules index 981a9459..86294f71 100755 --- a/test/script/custom-linting-rules +++ b/test/script/custom-linting-rules @@ -53,17 +53,29 @@ check_errors() { regex="$1" message="$2" include_arg='' + exclude_arg='' if [ $# -gt 2 ]; then include_arg="--include $3" fi + if [ $# -gt 3 ]; then + shift + shift + shift + + while (( "$#" )); do + exclude_arg="$exclude_arg --exclude $1" + shift + done + fi + for directory in "${directories[@]}"; do # shellcheck disable=SC2086 while read -r; do RETURN_CODE=1 echo "$REPLY $message" - done < <(grep -H -n "$regex" $include_arg "$directory"/**/*.vim \ + done < <(grep -H -n "$regex" $include_arg $exclude_arg "$directory"/**/*.vim \ | grep -v 'no-custom-checks' \ | grep -o '^[^:]\+:[0-9]\+' \ | sed 's:^\./::') @@ -92,7 +104,7 @@ if (( FIX_ERRORS )); then done fi -# The arguments are: regex, explanation, [filename_filter] +# The arguments are: regex, explanation, [filename_filter], [list, of, exclusions] check_errors \ '^function.*) *$' \ 'Function without abort keyword (See :help except-compat)' @@ -114,7 +126,10 @@ check_errors '==?' "Use 'is?' instead of '==?'. 0 ==? 'foobar' is true" check_errors '!=#' "Use 'isnot#' instead of '!=#'. 0 !=# 'foobar' is false" check_errors '!=?' "Use 'isnot?' instead of '!=?'. 0 !=? 'foobar' is false" check_errors '^ *:\?echo' "Stray echo line. Use \`execute echo\` if you want to echo something" -check_errors $'name.:.*\'[a-z_]*[^a-z_0-9][a-z_0-9]*\',$' 'Use snake_case names for linters' '*/ale_linters/*' +# Exclusions for grandfathered-in exceptions +exclusions="clojure/clj_kondo.vim elixir/elixir_ls.vim go/golangci_lint.vim swift/swiftformat.vim" +# shellcheck disable=SC2086 +check_errors $'name.:.*\'[a-z_]*[^a-z_0-9][a-z_0-9]*\',$' 'Use snake_case names for linters' '*/ale_linters/*' $exclusions # Checks for improving type checks. check_errors $'\\(==.\\?\\|is\\) type([\'"]\+)' "Use 'is v:t_string' instead" check_errors '\(==.\?\|is\) type([0-9]\+)' "Use 'is v:t_number' instead" diff --git a/test/test_code_action.vader b/test/test_code_action.vader index 2e5d1381..7eabb759 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 00000000..fd30633d --- /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 00000000..fc5470aa --- /dev/null +++ b/test/test_codefix.vader @@ -0,0 +1,549 @@ +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, 16, 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': 16, + \ 'endLine': 2, + \ 'endOffset': 17, + \ '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': 16}, {'lnum': 2, 'col': 16, 'code': 2304}]}} + call setpos('.', [bufnr(''), 2, 16, 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': 16, + \ 'endLine': 2, + \ 'endOffset': 17, + \ '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 + +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 + \ [ + \ [0, 'textDocument/codeAction', { + \ 'context': { + \ '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'))} + \ }] + \ ], + \ g:message_list[-1:] + +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 + \ [ + \ [0, 'textDocument/codeAction', { + \ 'context': { + \ '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'))} + \ }] + \ ], + \ g:message_list[-1:] diff --git a/test/test_floating_preview.vader b/test/test_floating_preview.vader new file mode 100644 index 00000000..43415556 --- /dev/null +++ b/test/test_floating_preview.vader @@ -0,0 +1,92 @@ +Before: + let g:ale_floating_preview = 0 + let g:ale_hover_to_floating_preview = 0 + let g:ale_detail_to_floating_preview = 0 + + runtime autoload/ale/floating_preview.vim + + let g:floated_lines = [] + let g:floating_preview_show_called = 0 + + " Stub out so we can track the call + function! ale#floating_preview#Show(lines, ...) abort + let g:floating_preview_show_called = 1 + let g:floated_lines = a:lines + endfunction + + let g:ale_buffer_info = { + \ bufnr('%'): { + \ 'loclist': [ + \ { + \ 'lnum': 1, + \ 'col': 10, + \ 'bufnr': bufnr('%'), + \ 'vcol': 0, + \ 'linter_name': 'notalinter', + \ 'nr': -1, + \ 'type': 'E', + \ 'code': 'semi', + \ 'text': "Missing semicolon.\r", + \ 'detail': "Every statement should end with a semicolon\nsecond line", + \ }, + \ ], + \ } + \} + + call ale#linter#Reset() + call ale#linter#PreventLoading('javascript') + +After: + Restore + + let g:ale_floating_preview = 0 + let g:ale_hover_to_floating_preview = 0 + let g:ale_detail_to_floating_preview = 0 + + call cursor(1, 1) + + let g:ale_buffer_info = {} + + " Close the preview window if it's open. + if &filetype is# 'ale-preview' + noautocmd :q! + endif + + call ale#linter#Reset() + + +Given javascript(A file with warnings/errors): + var x = 3 + 12345678 + var x = 5*2 + parseInt("10"); + // comment + +Execute(Floating preview is used with ALEDetail when g:ale_floating_preview set): + let g:ale_floating_preview = 1 + + call cursor(1, 10) + + ALEDetail + + let expected = ["Every statement should end with a semicolon", "second line"] + + AssertEqual 1, g:floating_preview_show_called + AssertEqual expected, g:floated_lines + +Execute(Floating preview is used with ALEDetail when g:ale_detail_to_floating_preview set): + let g:ale_detail_to_floating_preview = 1 + + call cursor(1, 10) + + ALEDetail + + let expected = ["Every statement should end with a semicolon", "second line"] + + AssertEqual 1, g:floating_preview_show_called + AssertEqual expected, g:floated_lines + +Execute(Floating preview is not used with ALEDetail by default): + call cursor(1, 10) + + ALEDetail + + AssertEqual 0, g:floating_preview_show_called diff --git a/test/test_hover.vader b/test/test_hover.vader index 9689cda2..7a9c8d91 100644 --- a/test/test_hover.vader +++ b/test/test_hover.vader @@ -7,9 +7,25 @@ Before: let g:item_list = [] let g:show_message_arg_list = [] + let g:ale_floating_preview = 0 + let g:ale_hover_to_floating_preview = 0 + let g:ale_detail_to_floating_preview = 0 + runtime autoload/ale/linter.vim runtime autoload/ale/lsp.vim + runtime autoload/ale/lsp_linter.vim runtime autoload/ale/util.vim + runtime autoload/ale/floating_preview.vim + runtime autoload/ale/hover.vim + + let g:floated_lines = [] + let g:floating_preview_show_called = 0 + + " Stub out so we can track the call + function! ale#floating_preview#Show(lines, ...) abort + let g:floating_preview_show_called = 1 + let g:floated_lines = a:lines + endfunction function! ale#lsp_linter#StartLSP(buffer, linter, callback) abort let g:Callback = a:callback @@ -50,6 +66,7 @@ Before: \) endfunction + After: call ale#hover#SetMap({}) call ale#test#RestoreDirectory() @@ -65,6 +82,7 @@ After: runtime autoload/ale/lsp_linter.vim runtime autoload/ale/lsp.vim runtime autoload/ale/util.vim + runtime autoload/ale/floating_preview.vim Given python(Some Python file): foo @@ -101,7 +119,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, \ { @@ -168,8 +186,30 @@ Execute(LSP hover response with lists of strings and marked strings should be ha \], g:show_message_arg_list AssertEqual {}, ale#hover#GetMap() +Execute(LSP hover with ale_floating_preview should float): + let g:ale_floating_preview = 1 + + call HandleValidLSPResult({'contents': "the message\ncontinuing"}) + + AssertEqual 1, g:floating_preview_show_called + AssertEqual ["the message", "continuing"], g:floated_lines + +Execute(LSP hover ale_hover_to_floating_preview should float): + let g:ale_hover_to_floating_preview = 1 + + call HandleValidLSPResult({'contents': "the message\ncontinuing"}) + + AssertEqual 1, g:floating_preview_show_called + AssertEqual ["the message", "continuing"], g:floated_lines + + +Execute(LSP hover by default should not float): + call HandleValidLSPResult({'contents': "the message\ncontinuing"}) + + AssertEqual 0, g:floating_preview_show_called + 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, @@ -187,3 +227,46 @@ Execute(tsserver responses for documentation requests should be handled): " The preview window should show the text. AssertEqual ['foo is a very good method'], ale#test#GetPreviewWindowText() silent! pclose + +Execute(hover with show_documentation should be in the preview window, not floating): + let g:ale_hover_to_floating_preview = 1 + let g:ale_floating_preview = 1 + + call ale#hover#SetMap({3: {'show_documentation': 1, 'buffer': bufnr('')}}) + + call ale#hover#HandleTSServerResponse( + \ 1, + \ { + \ 'command': 'quickinfo', + \ 'request_seq': 3, + \ 'success': v:true, + \ 'body': { + \ 'documentation': 'foo is a very good method', + \ 'displayString': 'foo bar ', + \ }, + \ } + \) + + let expected = ["Every statement should end with a semicolon", "second line"] + + AssertEqual 0, g:floating_preview_show_called + +Execute(TSServer hover without show_documentation and ale_floating_preview should float): + let g:ale_floating_preview = 1 + + call ale#hover#SetMap({3: {'buffer': bufnr('')}}) + + call ale#hover#HandleTSServerResponse( + \ 1, + \ { + \ 'command': 'quickinfo', + \ 'request_seq': 3, + \ 'success': v:true, + \ 'body': { + \ 'displayString': "the message\ncontinuing", + \ }, + \ } + \) + + AssertEqual 1, g:floating_preview_show_called + AssertEqual ["the message", "continuing"], g:floated_lines diff --git a/test/test_lint_on_enter_when_file_changed.vader b/test/test_lint_on_enter_when_file_changed.vader index 88493005..0d4c4af8 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 diff --git a/test/test_redundant_tsserver_rendering_avoided.vader b/test/test_redundant_tsserver_rendering_avoided.vader index 6125ebc2..bde5d152 100644 --- a/test/test_redundant_tsserver_rendering_avoided.vader +++ b/test/test_redundant_tsserver_rendering_avoided.vader @@ -111,7 +111,7 @@ Execute(An initial list of semantic errors should be handled): Assert g:ale_handle_loclist_called -Execute(Subsequent empty lists should be ignored): +Execute(Subsequent empty lists should be ignored - semantic): let g:ale_buffer_info[bufnr('')].semantic_loclist = [] call ale#lsp_linter#HandleLSPResponse(1, CreateError('semanticDiag', '')) @@ -138,3 +138,31 @@ Execute(Non-empty then non-empty semantic errors should be handled): call ale#lsp_linter#HandleLSPResponse(1, CreateError('semanticDiag', 'x')) Assert g:ale_handle_loclist_called + +Execute(Subsequent empty lists should be ignored - suggestion): + let g:ale_buffer_info[bufnr('')].suggestion_loclist = [] + + call ale#lsp_linter#HandleLSPResponse(1, CreateError('suggestionDiag', '')) + + Assert !g:ale_handle_loclist_called + +Execute(Empty then non-empty suggestion messages should be handled): + let g:ale_buffer_info[bufnr('')].suggestion_loclist = [] + + call ale#lsp_linter#HandleLSPResponse(1, CreateError('suggestionDiag', 'x')) + + Assert g:ale_handle_loclist_called + +Execute(Non-empty then empt suggestion messages should be handled): + let g:ale_buffer_info[bufnr('')].suggestion_loclist = CreateLoclist('x') + + call ale#lsp_linter#HandleLSPResponse(1, CreateError('suggestionDiag', '')) + + Assert g:ale_handle_loclist_called + +Execute(Non-empty then non-empty suggestion messages should be handled): + let g:ale_buffer_info[bufnr('')].suggestion_loclist = CreateLoclist('x') + + call ale#lsp_linter#HandleLSPResponse(1, CreateError('suggestionDiag', 'x')) + + Assert g:ale_handle_loclist_called diff --git a/test/test_rename.vader b/test/test_rename.vader index 2e8b746e..5bc655f4 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() diff --git a/test/test_shell_detection.vader b/test/test_shell_detection.vader index 697054d0..11d801c3 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('')) |