diff options
73 files changed, 2773 insertions, 244 deletions
diff --git a/.appveyor.yml b/.appveyor.yml index 54113953..aca83191 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -5,6 +5,11 @@ clone_depth: 10 # Use the directory C:\testplugin so test directories will mostly work. clone_folder: C:\testplugin +branches: + only: + - master + - /v\d+\.\d+\.(x|\d+)/ + # Cache the vim and vader directories between builds. cache: - C:\vim -> .appveyor.yml diff --git a/.travis.yml b/.travis.yml index d48c1e08..00baff1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,10 @@ sudo: required services: - docker language: generic +branches: + only: + - master + - /^v\d+\.\d+\.(x|\d+)$/ env: - OPTIONS=--vim-80-only - OPTIONS=--vim-81-only @@ -189,6 +189,14 @@ completion manually with `<C-x><C-o>`. set omnifunc=ale#completion#OmniFunc ``` +When working with TypeScript files, ALE supports automatic imports from +external modules. This behavior is disabled by default and can be enabled by +setting: + +```vim +let g:ale_completion_tsserver_autoimport = 1 +``` + See `:help ale-completion` for more information. <a name="usage-go-to-definition"></a> diff --git a/ale_linters/eruby/ruumba.vim b/ale_linters/eruby/ruumba.vim index e68bb51d..2e84acf7 100644 --- a/ale_linters/eruby/ruumba.vim +++ b/ale_linters/eruby/ruumba.vim @@ -8,7 +8,7 @@ call ale#Set('eruby_ruumba_options', '') function! ale_linters#eruby#ruumba#GetCommand(buffer) abort let l:executable = ale#Var(a:buffer, 'eruby_ruumba_executable') - return ale#handlers#ruby#EscapeExecutable(l:executable, 'ruumba') + return ale#ruby#EscapeExecutable(l:executable, 'ruumba') \ . ' --format json --force-exclusion ' \ . ale#Var(a:buffer, 'eruby_ruumba_options') \ . ' --stdin ' . ale#Escape(expand('#' . a:buffer . ':p')) diff --git a/ale_linters/python/mypy.vim b/ale_linters/python/mypy.vim index c4c6507f..dc4044e6 100644 --- a/ale_linters/python/mypy.vim +++ b/ale_linters/python/mypy.vim @@ -78,4 +78,5 @@ call ale#linter#Define('python', { \ 'executable': function('ale_linters#python#mypy#GetExecutable'), \ 'command': function('ale_linters#python#mypy#GetCommand'), \ 'callback': 'ale_linters#python#mypy#Handle', +\ 'output_stream': 'both' \}) diff --git a/ale_linters/ruby/brakeman.vim b/ale_linters/ruby/brakeman.vim index a8088080..2dc48740 100644 --- a/ale_linters/ruby/brakeman.vim +++ b/ale_linters/ruby/brakeman.vim @@ -36,7 +36,7 @@ function! ale_linters#ruby#brakeman#GetCommand(buffer) abort let l:executable = ale#Var(a:buffer, 'ruby_brakeman_executable') - return ale#handlers#ruby#EscapeExecutable(l:executable, 'brakeman') + return ale#ruby#EscapeExecutable(l:executable, 'brakeman') \ . ' -f json -q ' \ . ale#Var(a:buffer, 'ruby_brakeman_options') \ . ' -p ' . ale#Escape(l:rails_root) diff --git a/ale_linters/ruby/rails_best_practices.vim b/ale_linters/ruby/rails_best_practices.vim index a94fb671..36646647 100644 --- a/ale_linters/ruby/rails_best_practices.vim +++ b/ale_linters/ruby/rails_best_practices.vim @@ -33,7 +33,7 @@ function! ale_linters#ruby#rails_best_practices#GetCommand(buffer) abort let l:output_file = has('win32') ? '%t ' : '/dev/stdout ' let l:cat_file = has('win32') ? '; type %t' : '' - return ale#handlers#ruby#EscapeExecutable(l:executable, 'rails_best_practices') + return ale#ruby#EscapeExecutable(l:executable, 'rails_best_practices') \ . ' --silent -f json --output-file ' . l:output_file \ . ale#Var(a:buffer, 'ruby_rails_best_practices_options') \ . ale#Escape(l:rails_root) diff --git a/ale_linters/ruby/reek.vim b/ale_linters/ruby/reek.vim index e39e366f..226b452e 100644 --- a/ale_linters/ruby/reek.vim +++ b/ale_linters/ruby/reek.vim @@ -14,7 +14,7 @@ function! ale_linters#ruby#reek#GetCommand(buffer, version) abort \ ? ' --stdin-filename %s' \ : '' - return ale#handlers#ruby#EscapeExecutable(l:executable, 'reek') + return ale#ruby#EscapeExecutable(l:executable, 'reek') \ . ' -f json --no-progress --no-color --force-exclusion' \ . l:display_name_args endfunction diff --git a/ale_linters/ruby/rubocop.vim b/ale_linters/ruby/rubocop.vim index 8b9e9c84..410ed0ea 100644 --- a/ale_linters/ruby/rubocop.vim +++ b/ale_linters/ruby/rubocop.vim @@ -7,7 +7,7 @@ call ale#Set('ruby_rubocop_options', '') function! ale_linters#ruby#rubocop#GetCommand(buffer) abort let l:executable = ale#Var(a:buffer, 'ruby_rubocop_executable') - return ale#handlers#ruby#EscapeExecutable(l:executable, 'rubocop') + return ale#ruby#EscapeExecutable(l:executable, 'rubocop') \ . ' --format json --force-exclusion ' \ . ale#Var(a:buffer, 'ruby_rubocop_options') \ . ' --stdin ' . ale#Escape(expand('#' . a:buffer . ':p')) diff --git a/ale_linters/ruby/sorbet.vim b/ale_linters/ruby/sorbet.vim index ee765a6e..cae0683c 100644 --- a/ale_linters/ruby/sorbet.vim +++ b/ale_linters/ruby/sorbet.vim @@ -5,7 +5,7 @@ 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') - return ale#handlers#ruby#EscapeExecutable(l:executable, 'srb') + return ale#ruby#EscapeExecutable(l:executable, 'srb') \ . ' tc' \ . (!empty(l:options) ? ' ' . l:options : '') \ . ' --lsp --disable-watchman' diff --git a/ale_linters/ruby/standardrb.vim b/ale_linters/ruby/standardrb.vim index f075a7d5..f751e803 100644 --- a/ale_linters/ruby/standardrb.vim +++ b/ale_linters/ruby/standardrb.vim @@ -8,7 +8,7 @@ call ale#Set('ruby_standardrb_options', '') function! ale_linters#ruby#standardrb#GetCommand(buffer) abort let l:executable = ale#Var(a:buffer, 'ruby_standardrb_executable') - return ale#handlers#ruby#EscapeExecutable(l:executable, 'standardrb') + return ale#ruby#EscapeExecutable(l:executable, 'standardrb') \ . ' --format json --force-exclusion ' \ . ale#Var(a:buffer, 'ruby_standardrb_options') \ . ' --stdin ' . ale#Escape(expand('#' . a:buffer . ':p')) diff --git a/ale_linters/sh/shell.vim b/ale_linters/sh/shell.vim index 189dc21d..171fe64e 100644 --- a/ale_linters/sh/shell.vim +++ b/ale_linters/sh/shell.vim @@ -34,8 +34,10 @@ function! ale_linters#sh#shell#Handle(buffer, lines) abort " Matches patterns line the following: " " bash: line 13: syntax error near unexpected token `d' + " bash:行0: 未预期的符号“done”附近有语法错误 + " bash: 列 90: 尋找匹配的「"」時遇到了未預期的檔案結束符 " sh: 11: Syntax error: "(" unexpected - let l:pattern = '\v(line |: ?)(\d+): (.+)$' + let l:pattern = '\v([^:]+:\D*)(\d+): (.+)$' let l:output = [] for l:match in ale#util#GetMatches(a:lines, l:pattern) diff --git a/autoload/ale/c.vim b/autoload/ale/c.vim index 2d2083da..5540ec14 100644 --- a/autoload/ale/c.vim +++ b/autoload/ale/c.vim @@ -28,76 +28,107 @@ function! ale#c#GetBuildDirectory(buffer) abort return ale#path#Dirname(l:json_file) endfunction -function! ale#c#AreSpecialCharsBalanced(option) abort - " Escape \" - let l:option_escaped = substitute(a:option, '\\"', '', 'g') - - " Retain special chars only - let l:special_chars = substitute(l:option_escaped, '[^"''()`]', '', 'g') - let l:special_chars = split(l:special_chars, '\zs') - - " Check if they are balanced +function! ale#c#ShellSplit(line) abort let l:stack = [] + let l:args = [''] + let l:prev = '' - for l:char in l:special_chars - if l:char is# ')' - if len(l:stack) == 0 || get(l:stack, -1) isnot# '(' - return 0 + for l:char in split(a:line, '\zs') + if l:char is# '''' + if len(l:stack) > 0 && get(l:stack, -1) is# '''' + call remove(l:stack, -1) + elseif (len(l:stack) == 0 || get(l:stack, -1) isnot# '"') && l:prev isnot# '\' + call add(l:stack, l:char) endif - - call remove(l:stack, -1) - elseif l:char is# '(' - call add(l:stack, l:char) - else + elseif (l:char is# '"' || l:char is# '`') && l:prev isnot# '\' if len(l:stack) > 0 && get(l:stack, -1) is# l:char call remove(l:stack, -1) - else + elseif len(l:stack) == 0 || get(l:stack, -1) isnot# '''' call add(l:stack, l:char) endif + elseif (l:char is# '(' || l:char is# '[' || l:char is# '{') && l:prev isnot# '\' + if len(l:stack) == 0 || get(l:stack, -1) isnot# '''' + call add(l:stack, l:char) + endif + elseif (l:char is# ')' || l:char is# ']' || l:char is# '}') && l:prev isnot# '\' + if len(l:stack) > 0 && get(l:stack, -1) is# {')': '(', ']': '[', '}': '{'}[l:char] + call remove(l:stack, -1) + endif + elseif l:char is# ' ' && len(l:stack) == 0 + if len(get(l:args, -1)) > 0 + call add(l:args, '') + endif + + continue endif + + let l:args[-1] = get(l:args, -1) . l:char endfor - return len(l:stack) == 0 + return l:args endfunction function! ale#c#ParseCFlags(path_prefix, cflag_line) abort - let l:split_lines = split(a:cflag_line) + let l:cflags_list = [] + + let l:split_lines = ale#c#ShellSplit(a:cflag_line) let l:option_index = 0 while l:option_index < len(l:split_lines) - let l:next_option_index = l:option_index + 1 - - " Join space-separated option - while l:next_option_index < len(l:split_lines) - \&& stridx(l:split_lines[l:next_option_index], '-') != 0 - let l:next_option_index += 1 - endwhile - - let l:option = join(l:split_lines[l:option_index : l:next_option_index-1], ' ') - call remove(l:split_lines, l:option_index, l:next_option_index-1) - call insert(l:split_lines, l:option, l:option_index) - - " Ignore invalid or conflicting options - if stridx(l:option, '-') != 0 - \|| stridx(l:option, '-o') == 0 - \|| stridx(l:option, '-c') == 0 - call remove(l:split_lines, l:option_index) - let l:option_index = l:option_index - 1 - " Fix relative path - elseif stridx(l:option, '-I') == 0 - if !(stridx(l:option, ':') == 2+1 || stridx(l:option, '/') == 2+0) - let l:option = '-I' . a:path_prefix . s:sep . l:option[2:] - call remove(l:split_lines, l:option_index) - call insert(l:split_lines, l:option, l:option_index) + let l:option = l:split_lines[l:option_index] + let l:option_index = l:option_index + 1 + + " Include options, that may need relative path fix + if stridx(l:option, '-I') == 0 + \ || stridx(l:option, '-iquote') == 0 + \ || stridx(l:option, '-isystem') == 0 + \ || stridx(l:option, '-idirafter') == 0 + if stridx(l:option, '-I') == 0 && l:option isnot# '-I' + let l:arg = join(split(l:option, '\zs')[2:], '') + let l:option = '-I' + else + let l:arg = l:split_lines[l:option_index] + let l:option_index = l:option_index + 1 endif - endif - let l:option_index = l:option_index + 1 - endwhile + " Fix relative paths if needed + if stridx(l:arg, s:sep) != 0 && stridx(l:arg, '/') != 0 + let l:rel_path = substitute(l:arg, '"', '', 'g') + let l:rel_path = substitute(l:rel_path, '''', '', 'g') + let l:arg = ale#Escape(a:path_prefix . s:sep . l:rel_path) + endif + + call add(l:cflags_list, l:option) + call add(l:cflags_list, l:arg) + " Options with arg that can be grouped with the option or separate + elseif stridx(l:option, '-D') == 0 || stridx(l:option, '-B') == 0 + call add(l:cflags_list, l:option) - call uniq(l:split_lines) + if l:option is# '-D' || l:option is# '-B' + call add(l:cflags_list, l:split_lines[l:option_index]) + let l:option_index = l:option_index + 1 + endif + " Options that have an argument (always separate) + elseif l:option is# '-iprefix' || stridx(l:option, '-iwithprefix') == 0 + \ || l:option is# '-isysroot' || l:option is# '-imultilib' + call add(l:cflags_list, l:option) + call add(l:cflags_list, l:split_lines[l:option_index]) + let l:option_index = l:option_index + 1 + " Options without argument + elseif (stridx(l:option, '-W') == 0 && stridx(l:option, '-Wa,') != 0 && stridx(l:option, '-Wl,') != 0 && stridx(l:option, '-Wp,') != 0) + \ || l:option is# '-w' || stridx(l:option, '-pedantic') == 0 + \ || l:option is# '-ansi' || stridx(l:option, '-std=') == 0 + \ || (stridx(l:option, '-f') == 0 && stridx(l:option, '-fdump') != 0 && stridx(l:option, '-fdiagnostics') != 0 && stridx(l:option, '-fno-show-column') != 0) + \ || stridx(l:option, '-O') == 0 + \ || l:option is# '-C' || l:option is# '-CC' || l:option is# '-trigraphs' + \ || stridx(l:option, '-nostdinc') == 0 || stridx(l:option, '-iplugindir=') == 0 + \ || stridx(l:option, '--sysroot=') == 0 || l:option is# '--no-sysroot-suffix' + \ || stridx(l:option, '-m') == 0 + call add(l:cflags_list, l:option) + endif + endwhile - return join(l:split_lines, ' ') + return join(l:cflags_list, ' ') endfunction function! ale#c#ParseCFlagsFromMakeOutput(buffer, make_output) abort diff --git a/autoload/ale/code_action.vim b/autoload/ale/code_action.vim new file mode 100644 index 00000000..0af1bb70 --- /dev/null +++ b/autoload/ale/code_action.vim @@ -0,0 +1,163 @@ +" Author: Jerko Steiner <jerko.steiner@gmail.com> +" Description: Code action support for LSP / tsserver + +function! ale#code_action#HandleCodeAction(code_action) abort + let l:current_buffer = bufnr('') + let l:changes = a:code_action.changes + + for l:file_code_edit in l:changes + let l:buf = bufnr(l:file_code_edit.fileName) + + if l:buf != -1 && l:buf != l:current_buffer && getbufvar(l:buf, '&mod') + call ale#util#Execute('echom ''Aborting action, file is unsaved''') + + return + endif + endfor + + for l:file_code_edit in l:changes + call ale#code_action#ApplyChanges( + \ l:file_code_edit.fileName, l:file_code_edit.textChanges) + endfor +endfunction + +function! ale#code_action#ApplyChanges(filename, changes) abort + let l:current_buffer = bufnr('') + " The buffer is used to determine the fileformat, if available. + let l:buffer = bufnr(a:filename) + let l:is_current_buffer = l:buffer > 0 && l:buffer == l:current_buffer + + if l:buffer > 0 + let l:lines = getbufline(l:buffer, 1, '$') + else + let l:lines = readfile(a:filename, 'b') + endif + + if l:is_current_buffer + let l:pos = getpos('.')[1:2] + else + 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 + + for l:code_edit in a:changes + if l:code_edit.start.line isnot l:last_end_line + let l:column_offset = 0 + endif + + let l:line = l:code_edit.start.line + l:line_offset + let l:column = l:code_edit.start.offset + l:column_offset + let l:end_line = l:code_edit.end.line + l:line_offset + let l:end_column = l:code_edit.end.offset + l:column_offset + let l: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 + else + let l:end_line_len = len(l:lines[l:end_line - 1]) + endif + + let l:insertions = split(l:text, '\n', 1) + + if l:line is 1 + " Same logic as for column below. Vimscript's slice [:-1] will not + " be an empty list. + let l:start = [] + else + let l:start = l:lines[: l:line - 2] + 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. + let l:middle = [l:insertions[0]] + else + let l:middle = [l:lines[l:line - 1][: l:column - 2] . l:insertions[0]] + endif + + call extend(l:middle, l:insertions[1:]) + let l:middle[-1] .= l:lines[l:end_line - 1][l:end_column - 1 :] + + 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, + \ [l:line, l:column], + \ [l:end_line, l:end_column], + \ [l:current_line_offset, l:column_offset]) + endfor + + if l:lines[-1] is# '' + call remove(l:lines, -1) + endif + + call ale#util#Writefile(l:buffer, l:lines, a:filename) + + if l:is_current_buffer + call ale#util#Execute(':e!') + call setpos('.', [0, l:pos[0], l:pos[1], 0]) + endif +endfunction + +function! s:UpdateCursor(cursor, start, end, offset) abort + let l:cur_line = a:cursor[0] + let l:cur_column = a:cursor[1] + let l:line = a:start[0] + let l:column = a:start[1] + let l:end_line = a:end[0] + let l:end_column = a:end[1] + let l:line_offset = a:offset[0] + let l:column_offset = a:offset[1] + + if l:end_line < l:cur_line + " both start and end lines are before the cursor. only line offset + " needs to be updated + let l:cur_line += l:line_offset + elseif l:end_line == l:cur_line + " end line is at the same location as cursor, which means + " l:line <= l:cur_line + if l:line < l:cur_line || l:column <= l:cur_column + " updates are happening either before or around the cursor + if l:end_column < l:cur_column + " updates are happening before the cursor, update the + " column offset for cursor + let l:cur_line += l:line_offset + let l:cur_column += l:column_offset + else + " updates are happening around the cursor, move the cursor + " to the end of the changes + let l:cur_line += l:line_offset + let l:cur_column = l:end_column + l:column_offset + endif + " else is not necessary, it means modifications are happening + " after the cursor so no cursor updates need to be done + endif + else + " end line is after the cursor + if l:line < l:cur_line || l:line == l:cur_line && l:column <= l:cur_column + " changes are happening around the cursor, move the cursor + " to the end of the changes + let l:cur_line = l:end_line + l:line_offset + let l:cur_column = l:end_column + l:column_offset + " else is not necesary, it means modifications are happening + " after the cursor so no cursor updates need to be done + endif + endif + + return [l:cur_line, l:cur_column] +endfunction diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index c0e8abd9..94e85916 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -15,6 +15,7 @@ onoremap <silent> <Plug>(ale_show_completion_menu) <Nop> let g:ale_completion_delay = get(g:, 'ale_completion_delay', 100) let g:ale_completion_excluded_words = get(g:, 'ale_completion_excluded_words', []) let g:ale_completion_max_suggestions = get(g:, 'ale_completion_max_suggestions', 50) +let g:ale_completion_tsserver_autoimport = get(g:, 'ale_completion_tsserver_autoimport', 0) let s:timer_id = -1 let s:last_done_pos = [] @@ -267,6 +268,14 @@ function! ale#completion#Show(result) abort \ {-> ale#util#FeedKeys("\<Plug>(ale_show_completion_menu)")} \) endif + + if l:source is# 'ale-callback' + call b:CompleteCallback(b:ale_completion_result) + endif +endfunction + +function! ale#completion#GetAllTriggers() abort + return deepcopy(s:trigger_character_map) endfunction function! s:CompletionStillValid(request_id) abort @@ -280,6 +289,7 @@ function! s:CompletionStillValid(request_id) abort \ b:ale_completion_info.column == l:column \ || b:ale_completion_info.source is# 'deoplete' \ || b:ale_completion_info.source is# 'ale-omnifunc' + \ || b:ale_completion_info.source is# 'ale-callback' \) endfunction @@ -287,7 +297,10 @@ function! ale#completion#ParseTSServerCompletions(response) abort let l:names = [] for l:suggestion in a:response.body - call add(l:names, l:suggestion.name) + call add(l:names, { + \ 'word': l:suggestion.name, + \ 'source': get(l:suggestion, 'source', ''), + \}) endfor return l:names @@ -321,13 +334,22 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort endif " See :help complete-items - call add(l:results, { + let l:result = { \ 'word': l:suggestion.name, \ 'kind': l:kind, \ 'icase': 1, \ 'menu': join(l:displayParts, ''), + \ 'dup': g:ale_completion_tsserver_autoimport, \ 'info': join(l:documentationParts, ''), - \}) + \} + + if has_key(l:suggestion, 'codeActions') + let l:result.user_data = json_encode({ + \ 'codeActions': l:suggestion.codeActions, + \ }) + endif + + call add(l:results, l:result) endfor let l:names = getbufvar(l:buffer, 'ale_tsserver_completion_names', []) @@ -336,12 +358,12 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort let l:names_with_details = map(copy(l:results), 'v:val.word') let l:missing_names = filter( \ copy(l:names), - \ 'index(l:names_with_details, v:val) < 0', + \ 'index(l:names_with_details, v:val.word) < 0', \) for l:name in l:missing_names call add(l:results, { - \ 'word': l:name, + \ 'word': l:name.word, \ 'kind': 'v', \ 'icase': 1, \ 'menu': '', @@ -463,13 +485,22 @@ function! ale#completion#HandleTSServerResponse(conn_id, response) abort call setbufvar(l:buffer, 'ale_tsserver_completion_names', l:names) if !empty(l:names) + let l:identifiers = [] + + for l:name in l:names + call add(l:identifiers, { + \ 'name': l:name.word, + \ 'source': get(l:name, 'source', ''), + \}) + endfor + let b:ale_completion_info.request_id = ale#lsp#Send( \ b:ale_completion_info.conn_id, \ ale#lsp#tsserver_message#CompletionEntryDetails( \ l:buffer, \ b:ale_completion_info.line, \ b:ale_completion_info.column, - \ l:names, + \ l:identifiers, \ ), \) endif @@ -516,6 +547,7 @@ function! s:OnReady(linter, lsp_details) abort \ b:ale_completion_info.line, \ b:ale_completion_info.column, \ b:ale_completion_info.prefix, + \ g:ale_completion_tsserver_autoimport, \) else " Send a message saying the buffer has changed first, otherwise @@ -560,12 +592,25 @@ endfunction " This function can be used to manually trigger autocomplete, even when " g:ale_completion_enabled is set to false -function! ale#completion#GetCompletions(source) abort +function! ale#completion#GetCompletions(...) abort + let l:source = get(a:000, 0, '') + let l:options = get(a:000, 1, {}) + + if len(a:000) > 2 + throw 'Too many arguments!' + endif + + let l:CompleteCallback = get(l:options, 'callback', v:null) + + if l:CompleteCallback isnot v:null + let b:CompleteCallback = l:CompleteCallback + endif + let [l:line, l:column] = getpos('.')[1:2] let l:prefix = ale#completion#GetPrefix(&filetype, l:line, l:column) - if a:source is# 'ale-automatic' && empty(l:prefix) + if l:source is# 'ale-automatic' && empty(l:prefix) return 0 endif @@ -578,7 +623,7 @@ function! ale#completion#GetCompletions(source) abort \ 'prefix': l:prefix, \ 'conn_id': 0, \ 'request_id': 0, - \ 'source': a:source, + \ 'source': l:source, \} unlet! b:ale_completion_result @@ -670,6 +715,26 @@ function! ale#completion#Queue() abort let s:timer_id = timer_start(g:ale_completion_delay, function('s:TimerHandler')) endfunction +function! ale#completion#HandleUserData(completed_item) abort + let l:source = get(get(b:, 'ale_completion_info', {}), 'source', '') + + if l:source isnot# 'ale-automatic' && l:source isnot# 'ale-manual' + return + endif + + let l:user_data_json = get(a:completed_item, 'user_data', '') + + if empty(l:user_data_json) + return + endif + + let l:user_data = json_decode(l:user_data_json) + + for l:code_action in get(l:user_data, 'codeActions', []) + call ale#code_action#HandleCodeAction(l:code_action) + endfor +endfunction + function! ale#completion#Done() abort silent! pclose @@ -678,6 +743,10 @@ function! ale#completion#Done() abort let s:last_done_pos = getpos('.')[1:2] endfunction +augroup ALECompletionActions + autocmd CompleteDone * call ale#completion#HandleUserData(v:completed_item) +augroup END + function! s:Setup(enabled) abort augroup ALECompletionGroup autocmd! diff --git a/autoload/ale/debugging.vim b/autoload/ale/debugging.vim index 379c0d73..4e134f8c 100644 --- a/autoload/ale/debugging.vim +++ b/autoload/ale/debugging.vim @@ -50,6 +50,7 @@ let s:global_variable_list = [ \ 'ale_sign_style_error', \ 'ale_sign_style_warning', \ 'ale_sign_warning', +\ 'ale_sign_highlight_linenrs', \ 'ale_statusline_format', \ 'ale_type_map', \ 'ale_use_global_executables', diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index c5ef1544..ace37207 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -27,6 +27,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['python'], \ 'description': 'Fix PEP8 issues with black.', \ }, +\ 'dfmt': { +\ 'function': 'ale#fixers#dfmt#Fix', +\ 'suggested_filetypes': ['d'], +\ 'description': 'Fix D files with dfmt.', +\ }, \ 'fecs': { \ 'function': 'ale#fixers#fecs#Fix', \ 'suggested_filetypes': ['javascript', 'css', 'html'], @@ -215,6 +220,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['haskell'], \ 'description': 'Fix Haskell files with brittany.', \ }, +\ 'hindent': { +\ 'function': 'ale#fixers#hindent#Fix', +\ 'suggested_filetypes': ['haskell'], +\ 'description': 'Fix Haskell files with hindent.', +\ }, \ 'hlint': { \ 'function': 'ale#fixers#hlint#Fix', \ 'suggested_filetypes': ['haskell'], @@ -250,6 +260,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['sql'], \ 'description': 'Fix SQL files with sqlfmt.', \ }, +\ 'sqlformat': { +\ 'function': 'ale#fixers#sqlformat#Fix', +\ 'suggested_filetypes': ['sql'], +\ 'description': 'Fix SQL files with sqlformat.', +\ }, \ 'google_java_format': { \ 'function': 'ale#fixers#google_java_format#Fix', \ 'suggested_filetypes': ['java'], @@ -325,6 +340,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['python'], \ 'description': 'Sort Python imports with reorder-python-imports.', \ }, +\ 'gnatpp': { +\ 'function': 'ale#fixers#gnatpp#Fix', +\ 'suggested_filetypes': ['ada'], +\ 'description': 'Format Ada files with gnatpp.', +\ }, \} " Reset the function registry to the default entries. diff --git a/autoload/ale/fixers/black.vim b/autoload/ale/fixers/black.vim index 367b8d52..fba6c3b4 100644 --- a/autoload/ale/fixers/black.vim +++ b/autoload/ale/fixers/black.vim @@ -29,6 +29,10 @@ function! ale#fixers#black#Fix(buffer) abort let l:options = ale#Var(a:buffer, 'python_black_options') + if expand('#' . a:buffer . ':e') is? 'pyi' + let l:options .= '--pyi' + endif + return { \ 'command': l:cd_string . ale#Escape(l:executable) . l:exec_args \ . (!empty(l:options) ? ' ' . l:options : '') diff --git a/autoload/ale/fixers/dfmt.vim b/autoload/ale/fixers/dfmt.vim new file mode 100644 index 00000000..0072e045 --- /dev/null +++ b/autoload/ale/fixers/dfmt.vim @@ -0,0 +1,18 @@ +" Author: theoldmoon0602 +" Description: Integration of dfmt with ALE. + +call ale#Set('d_dfmt_executable', 'dfmt') +call ale#Set('d_dfmt_options', '') + +function! ale#fixers#dfmt#Fix(buffer) abort + let l:executable = ale#Var(a:buffer, 'd_dfmt_executable') + let l:options = ale#Var(a:buffer, 'd_dfmt_options') + + return { + \ 'command': ale#Escape(l:executable) + \ . ' -i' + \ . (empty(l:options) ? '' : ' ' . l:options) + \ . ' %t', + \ 'read_temporary_file': 1, + \} +endfunction diff --git a/autoload/ale/fixers/gnatpp.vim b/autoload/ale/fixers/gnatpp.vim new file mode 100644 index 00000000..bf3d484e --- /dev/null +++ b/autoload/ale/fixers/gnatpp.vim @@ -0,0 +1,17 @@ +" Author: tim <tim@inept.tech> +" Description: Fix files with gnatpp. + +call ale#Set('ada_gnatpp_executable', 'gnatpp') +call ale#Set('ada_gnatpp_options', '') + +function! ale#fixers#gnatpp#Fix(buffer) abort + let l:executable = ale#Var(a:buffer, 'ada_gnatpp_executable') + let l:options = ale#Var(a:buffer, 'ada_gnatpp_options') + + return { + \ 'command': ale#Escape(l:executable) + \ . (!empty(l:options) ? ' ' . l:options : '') + \ . ' %t', + \ 'read_temporary_file': 1, + \} +endfunction diff --git a/autoload/ale/fixers/hindent.vim b/autoload/ale/fixers/hindent.vim new file mode 100644 index 00000000..b6009a2c --- /dev/null +++ b/autoload/ale/fixers/hindent.vim @@ -0,0 +1,20 @@ +" Author: AlexeiDrake <drake.alexei@gmail.com> +" Description: Integration of hindent formatting with ALE. +" +call ale#Set('haskell_hindent_executable', 'hindent') + +function! ale#fixers#hindent#GetExecutable(buffer) abort + let l:executable = ale#Var(a:buffer, 'haskell_hindent_executable') + + return ale#handlers#haskell_stack#EscapeExecutable(l:executable, 'hindent') +endfunction + +function! ale#fixers#hindent#Fix(buffer) abort + let l:executable = ale#fixers#hindent#GetExecutable(a:buffer) + + return { + \ 'command': l:executable + \ . ' %t', + \ 'read_temporary_file': 1, + \} +endfunction diff --git a/autoload/ale/fixers/rubocop.vim b/autoload/ale/fixers/rubocop.vim index 33ba6887..0c7441e4 100644 --- a/autoload/ale/fixers/rubocop.vim +++ b/autoload/ale/fixers/rubocop.vim @@ -6,7 +6,7 @@ function! ale#fixers#rubocop#GetCommand(buffer) abort let l:config = ale#path#FindNearestFile(a:buffer, '.rubocop.yml') let l:options = ale#Var(a:buffer, 'ruby_rubocop_options') - return ale#handlers#ruby#EscapeExecutable(l:executable, 'rubocop') + return ale#ruby#EscapeExecutable(l:executable, 'rubocop') \ . (!empty(l:config) ? ' --config ' . ale#Escape(l:config) : '') \ . (!empty(l:options) ? ' ' . l:options : '') \ . ' --auto-correct --force-exclusion %t' diff --git a/autoload/ale/fixers/sorbet.vim b/autoload/ale/fixers/sorbet.vim index 182f7300..7c12fa1e 100644 --- a/autoload/ale/fixers/sorbet.vim +++ b/autoload/ale/fixers/sorbet.vim @@ -5,7 +5,7 @@ function! ale#fixers#sorbet#GetCommand(buffer) abort let l:executable = ale#Var(a:buffer, 'ruby_sorbet_executable') let l:options = ale#Var(a:buffer, 'ruby_sorbet_options') - return ale#handlers#ruby#EscapeExecutable(l:executable, 'srb') + return ale#ruby#EscapeExecutable(l:executable, 'srb') \ . ' tc' \ . (!empty(l:options) ? ' ' . l:options : '') \ . ' --autocorrect --file %t' diff --git a/autoload/ale/fixers/sqlformat.vim b/autoload/ale/fixers/sqlformat.vim new file mode 100644 index 00000000..6319c1ac --- /dev/null +++ b/autoload/ale/fixers/sqlformat.vim @@ -0,0 +1,16 @@ +" Author: Cluas <Cluas@live.cn> +" Description: Fixing files with sqlformat. + +call ale#Set('sql_sqlformat_executable', 'sqlformat') +call ale#Set('sql_sqlformat_options', '') + +function! ale#fixers#sqlformat#Fix(buffer) abort + let l:executable = ale#Var(a:buffer, 'sql_sqlformat_executable') + let l:options = ale#Var(a:buffer, 'sql_sqlformat_options') + + return { + \ 'command': ale#Escape(l:executable) + \ . (!empty(l:options) ? ' ' . l:options : '') + \ . ' -' + \} +endfunction diff --git a/autoload/ale/fixers/standardrb.vim b/autoload/ale/fixers/standardrb.vim index fab1e2bc..54330a37 100644 --- a/autoload/ale/fixers/standardrb.vim +++ b/autoload/ale/fixers/standardrb.vim @@ -9,7 +9,7 @@ function! ale#fixers#standardrb#GetCommand(buffer) abort let l:config = ale#path#FindNearestFile(a:buffer, '.standard.yml') let l:options = ale#Var(a:buffer, 'ruby_standardrb_options') - return ale#handlers#ruby#EscapeExecutable(l:executable, 'standardrb') + return ale#ruby#EscapeExecutable(l:executable, 'standardrb') \ . (!empty(l:config) ? ' --config ' . ale#Escape(l:config) : '') \ . (!empty(l:options) ? ' ' . l:options : '') \ . ' --fix --force-exclusion %t' diff --git a/autoload/ale/handlers/languagetool.vim b/autoload/ale/handlers/languagetool.vim index 10e049df..73974ceb 100644 --- a/autoload/ale/handlers/languagetool.vim +++ b/autoload/ale/handlers/languagetool.vim @@ -2,6 +2,7 @@ " Description: languagetool for markdown files " call ale#Set('languagetool_executable', 'languagetool') +call ale#Set('languagetool_options', '--autoDetect') function! ale#handlers#languagetool#GetExecutable(buffer) abort return ale#Var(a:buffer, 'languagetool_executable') @@ -9,8 +10,10 @@ endfunction function! ale#handlers#languagetool#GetCommand(buffer) abort let l:executable = ale#handlers#languagetool#GetExecutable(a:buffer) + let l:options = ale#Var(a:buffer, 'languagetool_options') - return ale#Escape(l:executable) . ' --autoDetect %s' + return ale#Escape(l:executable) + \ . (empty(l:options) ? '' : ' ' . l:options) . ' %s' endfunction function! ale#handlers#languagetool#HandleOutput(buffer, lines) abort diff --git a/autoload/ale/handlers/ruby.vim b/autoload/ale/handlers/ruby.vim index c28b8b75..7a1c5765 100644 --- a/autoload/ale/handlers/ruby.vim +++ b/autoload/ale/handlers/ruby.vim @@ -36,11 +36,3 @@ endfunction function! ale#handlers#ruby#HandleSyntaxErrors(buffer, lines) abort return s:HandleSyntaxError(a:buffer, a:lines) endfunction - -function! ale#handlers#ruby#EscapeExecutable(executable, bundle_exec) abort - let l:exec_args = a:executable =~? 'bundle' - \ ? ' exec ' . a:bundle_exec - \ : '' - - return ale#Escape(a:executable) . l:exec_args -endfunction diff --git a/autoload/ale/highlight.vim b/autoload/ale/highlight.vim index cb7911e1..82ad57e0 100644 --- a/autoload/ale/highlight.vim +++ b/autoload/ale/highlight.vim @@ -26,6 +26,25 @@ endif let s:MAX_POS_VALUES = 8 let s:MAX_COL_SIZE = 1073741824 " pow(2, 30) +let s:has_nvim_highlight = exists('*nvim_buf_add_highlight') && exists('*nvim_buf_clear_namespace') + +if s:has_nvim_highlight + let s:ns_id = nvim_create_namespace('ale_highlight') +endif + +" Wrappers are necessary to test this functionality by faking the calls in tests. +function! ale#highlight#nvim_buf_add_highlight(buffer, ns_id, hl_group, line, col_start, col_end) abort + " Ignore all errors for adding highlights. + try + call nvim_buf_add_highlight(a:buffer, a:ns_id, a:hl_group, a:line, a:col_start, a:col_end) + catch + endtry +endfunction + +function! ale#highlight#nvim_buf_clear_namespace(buffer, ns_id, line_start, line_end) abort + call nvim_buf_clear_namespace(a:buffer, a:ns_id, a:line_start, a:line_end) +endfunction + function! ale#highlight#CreatePositions(line, col, end_line, end_col) abort if a:line >= a:end_line " For single lines, just return the one position. @@ -51,15 +70,53 @@ endfunction " except these which have matching loclist item entries. function! ale#highlight#RemoveHighlights() abort - for l:match in getmatches() - if l:match.group =~? '\v^ALE(Style)?(Error|Warning|Info)(Line)?$' - call matchdelete(l:match.id) - endif - endfor + if s:has_nvim_highlight + call ale#highlight#nvim_buf_clear_namespace(bufnr(''), s:ns_id, 0, -1) + else + for l:match in getmatches() + if l:match.group =~? '\v^ALE(Style)?(Error|Warning|Info)(Line)?$' + call matchdelete(l:match.id) + endif + endfor + endif +endfunction + +" Same semantics of matchaddpos but will use nvim_buf_add_highlight if +" available. This involves iterating over the position list, switching from +" 1-based indexing to 0-based indexing, and translating the multiple ways +" that position can be specified for matchaddpos into line + col_start + +" col_end. +function! s:matchaddpos(group, pos_list) abort + if s:has_nvim_highlight + for l:pos in a:pos_list + let l:line = type(l:pos) == v:t_number + \ ? l:pos - 1 + \ : l:pos[0] - 1 + + if type(l:pos) == v:t_number || len(l:pos) == 1 + let l:col_start = 0 + let l:col_end = s:MAX_COL_SIZE + else + let l:col_start = l:pos[1] - 1 + let l:col_end = l:col_start + get(l:pos, 2, 1) + endif + + call ale#highlight#nvim_buf_add_highlight( + \ bufnr(''), + \ s:ns_id, + \ a:group, + \ l:line, + \ l:col_start, + \ l:col_end, + \) + endfor + else + call matchaddpos(a:group, a:pos_list) + endif endfunction function! s:highlight_line(bufnr, lnum, group) abort - call matchaddpos(a:group, [a:lnum]) + call s:matchaddpos(a:group, [a:lnum]) endfunction function! s:highlight_range(bufnr, range, group) abort @@ -72,7 +129,7 @@ function! s:highlight_range(bufnr, range, group) abort \ a:range.end_lnum, \ a:range.end_col \ ), - \ 'matchaddpos(a:group, v:val)' + \ 's:matchaddpos(a:group, v:val)' \) endfunction diff --git a/autoload/ale/list.vim b/autoload/ale/list.vim index e9f3f4ec..4bfe2a7b 100644 --- a/autoload/ale/list.vim +++ b/autoload/ale/list.vim @@ -103,6 +103,9 @@ function! s:SetListsImpl(timer_id, buffer, loclist) abort endfor endif + " Save the current view before opening/closing any window + call setbufvar(a:buffer, 'ale_winview', winsaveview()) + " Open a window to show the problems if we need to. " " We'll check if the current buffer's List is not empty here, so the @@ -141,6 +144,8 @@ function! s:SetListsImpl(timer_id, buffer, loclist) abort normal! "\<c-g>" endif endif + + call s:RestoreViewIfNeeded(a:buffer) endif " If ALE isn't currently checking for more problems, close the window if @@ -151,6 +156,30 @@ function! s:SetListsImpl(timer_id, buffer, loclist) abort endif endfunction +" Try to restore the window view after closing any of the lists to avoid making +" the it moving around, especially useful when on insert mode +function! s:RestoreViewIfNeeded(buffer) abort + let l:saved_view = getbufvar(a:buffer, 'ale_winview', {}) + + " Saved view is empty, can't do anything + if empty(l:saved_view) + return + endif + + " Check wether the cursor has moved since linting was actually requested. If + " the user has indeed moved lines, do nothing + let l:current_view = winsaveview() + + if l:current_view['lnum'] != l:saved_view['lnum'] + return + endif + + " Anchor view by topline if the list is set to open horizontally + if ale#Var(a:buffer, 'list_vertical') == 0 + call winrestview({'topline': l:saved_view['topline']}) + endif +endfunction + function! ale#list#SetLists(buffer, loclist) abort if get(g:, 'ale_set_lists_synchronously') == 1 \|| getbufvar(a:buffer, 'ale_save_event_fired', 0) @@ -174,12 +203,15 @@ function! s:CloseWindowIfNeeded(buffer) abort return endif + let l:did_close_any_list = 0 + try " Only close windows if the quickfix list or loclist is completely empty, " including errors set through other means. if g:ale_set_quickfix if empty(getqflist()) cclose + let l:did_close_any_list = 1 endif else let l:win_ids = s:WinFindBuf(a:buffer) @@ -187,10 +219,15 @@ function! s:CloseWindowIfNeeded(buffer) abort for l:win_id in l:win_ids if g:ale_set_loclist && empty(getloclist(l:win_id)) lclose + let l:did_close_any_list = 1 endif endfor endif " Ignore 'Cannot close last window' errors. catch /E444/ endtry + + if l:did_close_any_list + call s:RestoreViewIfNeeded(a:buffer) + endif endfunction diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index 017096cd..2509174e 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -37,6 +37,7 @@ function! ale#lsp#Register(executable_or_address, project, init_options) abort \ 'init_queue': [], \ 'capabilities': { \ 'hover': 0, + \ 'rename': 0, \ 'references': 0, \ 'completion': 0, \ 'completion_trigger_characters': [], @@ -199,6 +200,10 @@ function! s:UpdateCapabilities(conn, capabilities) abort let a:conn.capabilities.references = 1 endif + if get(a:capabilities, 'renameProvider') is v:true + let a:conn.capabilities.rename = 1 + endif + if !empty(get(a:capabilities, 'completionProvider')) let a:conn.capabilities.completion = 1 endif @@ -317,6 +322,7 @@ function! ale#lsp#MarkConnectionAsTsserver(conn_id) abort let l:conn.capabilities.completion_trigger_characters = ['.'] let l:conn.capabilities.definition = 1 let l:conn.capabilities.symbol_search = 1 + let l:conn.capabilities.rename = 1 endfunction function! s:SendInitMessage(conn) abort diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim index b6b14a22..5b0cb8b7 100644 --- a/autoload/ale/lsp/message.vim +++ b/autoload/ale/lsp/message.vim @@ -162,3 +162,13 @@ function! ale#lsp#message#DidChangeConfiguration(buffer, config) abort \ 'settings': a:config, \}] endfunction + +function! ale#lsp#message#Rename(buffer, line, column, new_name) abort + return [0, 'textDocument/rename', { + \ 'textDocument': { + \ 'uri': ale#path#ToURI(expand('#' . a:buffer . ':p')), + \ }, + \ 'position': {'line': a:line - 1, 'character': a:column - 1}, + \ 'newName': a:new_name, + \}] +endfunction diff --git a/autoload/ale/lsp/tsserver_message.vim b/autoload/ale/lsp/tsserver_message.vim index d6919516..b9fafaa0 100644 --- a/autoload/ale/lsp/tsserver_message.vim +++ b/autoload/ale/lsp/tsserver_message.vim @@ -36,12 +36,14 @@ function! ale#lsp#tsserver_message#Geterr(buffer) abort return [1, 'ts@geterr', {'files': [expand('#' . a:buffer . ':p')]}] endfunction -function! ale#lsp#tsserver_message#Completions(buffer, line, column, prefix) abort +function! ale#lsp#tsserver_message#Completions( +\ buffer, line, column, prefix, include_external) abort return [0, 'ts@completions', { \ 'line': a:line, \ 'offset': a:column, \ 'file': expand('#' . a:buffer . ':p'), \ 'prefix': a:prefix, + \ 'includeExternalModuleExports': a:include_external, \}] endfunction @@ -77,3 +79,27 @@ function! ale#lsp#tsserver_message#Quickinfo(buffer, line, column) abort \ 'file': expand('#' . a:buffer . ':p'), \}] endfunction + +function! ale#lsp#tsserver_message#Rename( +\ buffer, line, column, find_in_comments, find_in_strings) abort + return [0, 'ts@rename', { + \ 'line': a:line, + \ 'offset': a:column, + \ 'file': expand('#' . a:buffer . ':p'), + \ 'arguments': { + \ 'findInComments': a:find_in_comments, + \ 'findInStrings': a:find_in_strings, + \ } + \}] +endfunction + +function! ale#lsp#tsserver_message#OrganizeImports(buffer) abort + return [0, 'ts@organizeImports', { + \ 'scope': { + \ 'type': 'file', + \ 'args': { + \ 'file': expand('#' . a:buffer . ':p'), + \ }, + \ }, + \}] +endfunction diff --git a/autoload/ale/organize_imports.vim b/autoload/ale/organize_imports.vim new file mode 100644 index 00000000..bc9b829e --- /dev/null +++ b/autoload/ale/organize_imports.vim @@ -0,0 +1,59 @@ +" Author: Jerko Steiner <jerko.steiner@gmail.com> +" Description: Organize imports support for tsserver +" +function! ale#organize_imports#HandleTSServerResponse(conn_id, response) abort + if get(a:response, 'command', '') isnot# 'organizeImports' + return + endif + + if get(a:response, 'success', v:false) isnot v:true + return + endif + + let l:file_code_edits = a:response.body + + call ale#code_action#HandleCodeAction({ + \ 'description': 'Organize Imports', + \ 'changes': l:file_code_edits, + \}) +endfunction + +function! s:OnReady(linter, lsp_details) abort + let l:id = a:lsp_details.connection_id + + if a:linter.lsp isnot# 'tsserver' + call ale#util#Execute('echom ''OrganizeImports currently only works with tsserver''') + + return + endif + + let l:buffer = a:lsp_details.buffer + + let l:Callback = function('ale#organize_imports#HandleTSServerResponse') + + call ale#lsp#RegisterCallback(l:id, l:Callback) + + let l:message = ale#lsp#tsserver_message#OrganizeImports(l:buffer) + + let l:request_id = ale#lsp#Send(l:id, l:message) +endfunction + +function! s:OrganizeImports(linter) abort + let l:buffer = bufnr('') + let [l:line, l:column] = getpos('.')[1:2] + + if a:linter.lsp isnot# 'tsserver' + let l:column = min([l:column, len(getline(l:line))]) + endif + + let l:Callback = function('s:OnReady') + call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) +endfunction + +function! ale#organize_imports#Execute() abort + for l:linter in ale#linter#Get(&filetype) + if !empty(l:linter.lsp) + call s:OrganizeImports(l:linter) + endif + endfor +endfunction diff --git a/autoload/ale/rename.vim b/autoload/ale/rename.vim new file mode 100644 index 00000000..02b7b579 --- /dev/null +++ b/autoload/ale/rename.vim @@ -0,0 +1,225 @@ +" Author: Jerko Steiner <jerko.steiner@gmail.com> +" Description: Rename symbol support for LSP / tsserver + +let s:rename_map = {} + +" Used to get the rename map in tests. +function! ale#rename#GetMap() abort + return deepcopy(s:rename_map) +endfunction + +" Used to set the rename map in tests. +function! ale#rename#SetMap(map) abort + let s:rename_map = a:map +endfunction + +function! ale#rename#ClearLSPData() abort + let s:rename_map = {} +endfunction + +let g:ale_rename_tsserver_find_in_comments = get(g:, 'ale_rename_tsserver_find_in_comments') +let g:ale_rename_tsserver_find_in_strings = get(g:, 'ale_rename_tsserver_find_in_strings') + +function! s:message(message) abort + call ale#util#Execute('echom ' . string(a:message)) +endfunction + +function! ale#rename#HandleTSServerResponse(conn_id, response) abort + if get(a:response, 'command', '') isnot# 'rename' + return + endif + + if !has_key(s:rename_map, a:response.request_seq) + return + endif + + let l:old_name = s:rename_map[a:response.request_seq].old_name + let l:new_name = s:rename_map[a:response.request_seq].new_name + call remove(s:rename_map, a:response.request_seq) + + if get(a:response, 'success', v:false) is v:false + let l:message = get(a:response, 'message', 'unknown') + call s:message('Error renaming "' . l:old_name . '" to: "' . l:new_name + \ . '". Reason: ' . l:message) + + return + endif + + let l:changes = [] + + for l:response_item in a:response.body.locs + let l:filename = l:response_item.file + let l:text_changes = [] + + for l:loc in l:response_item.locs + call add(l:text_changes, { + \ 'start': { + \ 'line': l:loc.start.line, + \ 'offset': l:loc.start.offset, + \ }, + \ 'end': { + \ 'line': l:loc.end.line, + \ 'offset': l:loc.end.offset, + \ }, + \ 'newText': l:new_name, + \}) + endfor + + call add(l:changes, { + \ 'fileName': l:filename, + \ 'textChanges': l:text_changes, + \}) + endfor + + if empty(l:changes) + call s:message('Error renaming "' . l:old_name . '" to: "' . l:new_name . '"') + + return + endif + + call ale#code_action#HandleCodeAction({ + \ 'description': 'rename', + \ 'changes': 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) + call remove(s:rename_map, a:response.id) + + if !has_key(a:response, 'result') + call s:message('No rename result received from server') + + return + endif + + let l:workspace_edit = a:response.result + + if !has_key(l:workspace_edit, 'changes') || empty(l:workspace_edit.changes) + call s:message('No changes received from server') + + return + endif + + let l:changes = [] + + for l:file_name in keys(l:workspace_edit.changes) + let l:text_edits = l:workspace_edit.changes[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 + + call ale#code_action#HandleCodeAction({ + \ 'description': 'rename', + \ 'changes': l:changes, + \}) + endif +endfunction + +function! s:OnReady(line, column, old_name, new_name, linter, lsp_details) abort + let l:id = a:lsp_details.connection_id + + if !ale#lsp#HasCapability(l:id, 'rename') + return + endif + + let l:buffer = a:lsp_details.buffer + + let l:Callback = a:linter.lsp is# 'tsserver' + \ ? function('ale#rename#HandleTSServerResponse') + \ : function('ale#rename#HandleLSPResponse') + + call ale#lsp#RegisterCallback(l:id, l:Callback) + + if a:linter.lsp is# 'tsserver' + let l:message = ale#lsp#tsserver_message#Rename( + \ l:buffer, + \ a:line, + \ a:column, + \ g:ale_rename_tsserver_find_in_comments, + \ g:ale_rename_tsserver_find_in_strings, + \) + else + " Send a message saying the buffer has changed first, or the + " rename position probably won't make sense. + call ale#lsp#NotifyForChanges(l:id, l:buffer) + + let l:message = ale#lsp#message#Rename( + \ l:buffer, + \ a:line, + \ a:column, + \ a:new_name + \) + endif + + let l:request_id = ale#lsp#Send(l:id, l:message) + + let s:rename_map[l:request_id] = { + \ 'new_name': a:new_name, + \ 'old_name': a:old_name, + \} +endfunction + +function! s:ExecuteRename(linter, old_name, new_name) abort + let l:buffer = bufnr('') + let [l:line, l:column] = getpos('.')[1:2] + + if a:linter.lsp isnot# 'tsserver' + let l:column = min([l:column, len(getline(l:line))]) + endif + + let l:Callback = function( + \ 's:OnReady', [l:line, l:column, a:old_name, a:new_name]) + call ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) +endfunction + +function! ale#rename#Execute() abort + let l:lsp_linters = [] + + for l:linter in ale#linter#Get(&filetype) + if !empty(l:linter.lsp) + call add(l:lsp_linters, l:linter) + endif + endfor + + if empty(l:lsp_linters) + call s:message('No active LSPs') + + return + endif + + let l:old_name = expand('<cword>') + let l:new_name = ale#util#Input('New name: ', l:old_name) + + if empty(l:new_name) + call s:message('New name cannot be empty!') + + return + endif + + for l:lsp_linter in l:lsp_linters + call s:ExecuteRename(l:lsp_linter, l:old_name, l:new_name) + endfor +endfunction diff --git a/autoload/ale/ruby.vim b/autoload/ale/ruby.vim index 15e835c9..d941bb2c 100644 --- a/autoload/ale/ruby.vim +++ b/autoload/ale/ruby.vim @@ -74,3 +74,10 @@ function! ale#ruby#HandleRubocopOutput(buffer, lines) abort return l:output endfunction +function! ale#ruby#EscapeExecutable(executable, bundle_exec) abort + let l:exec_args = a:executable =~? 'bundle' + \ ? ' exec ' . a:bundle_exec + \ : '' + + return ale#Escape(a:executable) . l:exec_args +endfunction diff --git a/autoload/ale/sign.vim b/autoload/ale/sign.vim index eb0dd1cd..829de118 100644 --- a/autoload/ale/sign.vim +++ b/autoload/ale/sign.vim @@ -20,6 +20,7 @@ let g:ale_sign_info = get(g:, 'ale_sign_info', g:ale_sign_warning) let g:ale_sign_offset = get(g:, 'ale_sign_offset', 1000000) " This flag can be set to 1 to keep sign gutter always open let g:ale_sign_column_always = get(g:, 'ale_sign_column_always', 0) +let g:ale_sign_highlight_linenrs = get(g:, 'ale_sign_highlight_linenrs', 0) if !hlexists('ALEErrorSign') highlight link ALEErrorSign error @@ -82,7 +83,7 @@ execute 'sign define ALEInfoSign text=' . s:EscapeSignText(g:ale_sign_info) \ . ' texthl=ALEInfoSign linehl=ALEInfoLine' sign define ALEDummySign -if has('nvim-0.3.2') +if g:ale_sign_highlight_linenrs && has('nvim-0.3.2') if !hlexists('ALEErrorSignLineNr') highlight link ALEErrorSignLineNr CursorLineNr endif diff --git a/autoload/ale/util.vim b/autoload/ale/util.vim index e7563608..99cd856a 100644 --- a/autoload/ale/util.vim +++ b/autoload/ale/util.vim @@ -477,3 +477,6 @@ function! ale#util#FindItemAtCursor(buffer) abort return [l:info, l:loc] endfunction +function! ale#util#Input(message, value) abort + return input(a:message, a:value) +endfunction diff --git a/autoload/asyncomplete/sources/ale.vim b/autoload/asyncomplete/sources/ale.vim new file mode 100644 index 00000000..ce793773 --- /dev/null +++ b/autoload/asyncomplete/sources/ale.vim @@ -0,0 +1,26 @@ +function! asyncomplete#sources#ale#get_source_options(...) abort + let l:default = extend({ + \ 'name': 'ale', + \ 'completor': function('asyncomplete#sources#ale#completor'), + \ 'whitelist': ['*'], + \ 'triggers': asyncomplete#sources#ale#get_triggers(), + \ }, a:0 >= 1 ? a:1 : {}) + + return extend(l:default, {'refresh_pattern': '\k\+$'}) +endfunction + +function! asyncomplete#sources#ale#get_triggers() abort + let l:triggers = ale#completion#GetAllTriggers() + let l:triggers['*'] = l:triggers['<default>'] + + return l:triggers +endfunction + +function! asyncomplete#sources#ale#completor(options, context) abort + let l:keyword = matchstr(a:context.typed, '\w\+$') + let l:startcol = a:context.col - len(l:keyword) + + call ale#completion#GetCompletions('ale-callback', { 'callback': {completions -> + \ asyncomplete#complete(a:options.name, a:context, l:startcol, completions) + \ }}) +endfunction diff --git a/doc/ale-ada.txt b/doc/ale-ada.txt index 93e3261a..2ac30c0a 100644 --- a/doc/ale-ada.txt +++ b/doc/ale-ada.txt @@ -22,4 +22,15 @@ g:ale_ada_gcc_options *g:ale_ada_gcc_options* =============================================================================== +gnatpp *ale-ada-gnatpp* + +g:ale_ada_gnatpp_options *g:ale_ada_gnatpp_options* + *b:ale_ada_gnatpp_options* + Type: |String| + Default: `''` + + This variable can be set to pass extra options to the gnatpp fixer. + + +=============================================================================== vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: diff --git a/doc/ale-d.txt b/doc/ale-d.txt index 55596062..72349a20 100644 --- a/doc/ale-d.txt +++ b/doc/ale-d.txt @@ -1,6 +1,15 @@ =============================================================================== ALE D Integration *ale-d-options* +=============================================================================== +dfmt *ale-d-dfmt* + +g:ale_d_dfmt_options *g:ale_d_dfmt_options* + *b:ale_d_dfmt_options* + Type: |String| + Default: `''` + +This variable can be set to pass additional options to the dfmt fixer. =============================================================================== dls *ale-d-dls* diff --git a/doc/ale-development.txt b/doc/ale-development.txt index 16b16483..faa570c1 100644 --- a/doc/ale-development.txt +++ b/doc/ale-development.txt @@ -184,13 +184,12 @@ tests: https://github.com/junegunn/vader.vim See |ale-development-linter-tests| for more information on how to write linter tests. -When you add new linters or fixers, make sure to add them into the table in -the README, and also into the |ale-support| list in the main help file. If you -forget to keep them both in sync, you should see an error like the following -in Travis CI. > - +When you add new linters or fixers, make sure to add them into the tables in +supported-tools.md and |ale-supported-languages-and-tools.txt|. If you forget to +keep them both in sync, you should see an error like the following in Travis CI. +> ======================================== - diff README.md and doc/ale.txt tables + diff supported-tools.md and doc/ale-supported-languages-and-tools.txt tables ======================================== Differences follow: diff --git a/doc/ale-haskell.txt b/doc/ale-haskell.txt index e2073e37..5dd3ec15 100644 --- a/doc/ale-haskell.txt +++ b/doc/ale-haskell.txt @@ -12,6 +12,7 @@ g:ale_haskell_brittany_executable *g:ale_haskell_brittany_executable* This variable can be changed to use a different executable for brittany. + =============================================================================== floskell *ale-haskell-floskell* @@ -22,6 +23,7 @@ g:ale_haskell_floskell_executable *g:ale_haskell_floskell_executable* This variable can be changed to use a different executable for floskell. + =============================================================================== ghc *ale-haskell-ghc* @@ -32,6 +34,7 @@ g:ale_haskell_ghc_options *g:ale_haskell_ghc_options* This variable can be changed to modify flags given to ghc. + =============================================================================== ghc-mod *ale-haskell-ghc-mod* @@ -42,6 +45,7 @@ g:ale_haskell_ghc_mod_executable *g:ale_haskell_ghc_mod_executable* This variable can be changed to use a different executable for ghc-mod. + =============================================================================== cabal-ghc *ale-haskell-cabal-ghc* @@ -53,6 +57,7 @@ g:ale_haskell_cabal_ghc_options *g:ale_haskell_cabal_ghc_options* This variable can be changed to modify flags given to ghc through cabal exec. + =============================================================================== hdevtools *ale-haskell-hdevtools* @@ -87,6 +92,18 @@ g:ale_haskell_hfmt_executable *g:ale_haskell_hfmt_executable* This variable can be changed to use a different executable for hfmt. + +=============================================================================== +hindent *ale-haskell-hindent* + +g:ale_haskell_hindent_executable *g:ale_haskell_hindent_executable* + *b:ale_haskell_hindent_executable* + Type: |String| + Default: `'hindent'` + + This variable can be changed to use a different executable for hindent. + + =============================================================================== hlint *ale-haskell-hlint* @@ -106,6 +123,7 @@ g:ale_haskell_hlint_options g:ale_haskell_hlint_options This variable can be used to pass extra options to the underlying hlint executable. + =============================================================================== stack-build *ale-haskell-stack-build* @@ -117,6 +135,7 @@ g:ale_haskell_stack_build_options *g:ale_haskell_stack_build_options* We default to using `'--fast'`. Since Stack generates binaries, your programs will be slower unless you separately rebuild them outside of ALE. + =============================================================================== stack-ghc *ale-haskell-stack-ghc* @@ -128,6 +147,7 @@ g:ale_haskell_stack_ghc_options *g:ale_haskell_stack_ghc_options* This variable can be changed to modify flags given to ghc through `stack ghc` + =============================================================================== stylish-haskell *ale-haskell-stylish-haskell* @@ -139,6 +159,7 @@ g:ale_haskell_stylish_haskell_executable This variable can be changed to use a different executable for stylish-haskell. + =============================================================================== hie *ale-haskell-hie* @@ -150,5 +171,6 @@ g:ale_haskell_hie_executable *g:ale_haskell_hie_executable* This variable can be changed to use a different executable for the haskell ide engine. i.e. `'hie-wrapper'` + =============================================================================== vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: diff --git a/doc/ale-sql.txt b/doc/ale-sql.txt index f9bc6ac2..2807271b 100644 --- a/doc/ale-sql.txt +++ b/doc/ale-sql.txt @@ -40,4 +40,22 @@ g:ale_sql_sqlfmt_options *g:ale_sql_sqlfmt_options* =============================================================================== +sqlformat *ale-sql-sqlformat* + +g:ale_sql_sqlformat_executable *g:ale_sql_sqlformat_executable* + *b:ale_sql_sqlformat_executable* + Type: |String| + Default: `'sqlformat'` + + This variable sets executable used for sqlformat. + +g:ale_sql_sqlformat_options *g:ale_sql_sqlformat_options* + *b:ale_sql_sqlformat_options* + Type: |String| + Default: `''` + + This variable can be set to pass additional options to the sqlformat fixer. + + +=============================================================================== vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl: diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index d39aaf7e..98c6f354 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -14,6 +14,7 @@ Notes: * Ada * `gcc` + * `gnatpp` * Ansible * `ansible-lint` * API Blueprint @@ -102,6 +103,7 @@ Notes: * Cython (pyrex filetype) * `cython` * D + * `dfmt` * `dls` * `dmd` * `uncrustify` @@ -182,6 +184,7 @@ Notes: * `hdevtools` * `hfmt` * `hie` + * `hindent` * `hlint` * `stack-build`!! * `stack-ghc` @@ -423,6 +426,7 @@ Notes: * SQL * `pgformatter` * `sqlfmt` + * `sqlformat` * `sqlint` * Stylus * `stylelint` diff --git a/doc/ale.txt b/doc/ale.txt index 5541236f..5a123921 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -341,6 +341,17 @@ completion source for Deoplete is named `'ale'`, and should enabled automatically if Deoplete is enabled and configured correctly. Deoplete integration should not be combined with ALE's own implementation. + *ale-asyncomplete-integration* + +ALE additionally integrates with asyncomplete.vim for offering automatic +completion data. ALE's asyncomplete source requires registration and should +use the defaults provided by the|asyncomplete#sources#ale#get_source_options| function > + + " Use ALE's function for asyncomplete defaults + au User asyncomplete_setup call asyncomplete#register_source(asyncomplete#sources#ale#get_source_options({ + \ 'priority': 10, " Provide your own overrides here + \ })) +> ALE also offers its own completion implementation, which does not require any other plugins. Suggestions will be made while you type after completion is enabled. ALE's own completion implementation can be enabled by setting @@ -365,6 +376,10 @@ The |ALEComplete| command can be used to show completion suggestions manually, even when |g:ale_completion_enabled| is set to `0`. For manually requesting completion information with Deoplete, consult Deoplete's documentation. +When working with TypeScript files, ALE by can support automatic imports +from external modules. This behavior can be enabled by setting the +|g:ale_completion_tsserver_autoimport| variable to `1`. + *ale-completion-completeopt-bug* ALE Automatic completion implementation replaces |completeopt| before opening @@ -586,6 +601,16 @@ b:ale_completion_enabled *b:ale_completion_enabled* See |ale-completion| +g:ale_completion_tsserver_autoimport *g:ale_completion_tsserver_autoimport* + + Type: Number + Default: `0` + + When this option is set to `0`, ALE will not try to automatically import + completion results from external modules. It can be enabled by setting it + to `1`. + + g:ale_completion_excluded_words *g:ale_completion_excluded_words* *b:ale_completion_excluded_words* Type: |List| @@ -1306,6 +1331,27 @@ g:ale_pattern_options_enabled *g:ale_pattern_options_enabled* will not set buffer variables per |g:ale_pattern_options|. +g:ale_rename_tsserver_find_in_comments *g:ale_rename_tsserver_find_in_comments* + + Type: |Number| + Default: `0` + + If enabled, this option will tell tsserver to find and replace text in + comments when calling |ALERename|. It can be enabled by settings the value + to `1`. + + +g:ale_rename_tsserver_find_in_strings *g:ale_rename_tsserver_find_in_strings* + + + Type: |Number| + Default: `0` + + If enabled, this option will tell tsserver to find and replace text in + strings when calling |ALERename|. It can be enabled by settings the value to + `1`. + + g:ale_set_balloons *g:ale_set_balloons* *b:ale_set_balloons* @@ -1426,8 +1472,8 @@ g:ale_set_signs *g:ale_set_signs* |ALEWarningLine| - All items with `'type': 'W'` |ALEInfoLine| - All items with `'type': 'I'` - With Neovim 0.3.2 or higher, ALE uses `numhl` option to highlight 'number' - column. It uses the following highlight groups. + With Neovim 0.3.2 or higher, ALE can use the `numhl` option to highlight the + 'number' column. It uses the following highlight groups. |ALEErrorSignLineNr| - Items with `'type': 'E'` |ALEWarningSignLineNr| - Items with `'type': 'W'` @@ -1435,6 +1481,9 @@ g:ale_set_signs *g:ale_set_signs* |ALEStyleErrorSignLineNr| - Items with `'type': 'E'` and `'sub_type': 'style'` |ALEStyleWarningSignLineNr| - Items with `'type': 'W'` and `'sub_type': 'style'` + To enable line number highlighting |g:ale_sign_highlight_linenrs| must be + set to `1` before ALE is loaded. + The markers for the highlights can be customized with the following options: |g:ale_sign_error| @@ -1541,6 +1590,16 @@ g:ale_sign_warning *g:ale_sign_warning* The sign for warnings in the sign gutter. +g:ale_sign_highlight_linenrs *g:ale_sign_highlight_linenrs* + + Type: |Number| + Default: `0` + + When set to `1`, this option enables highlighting problems on the 'number' + column in Vim versions that support `numhl` highlights. This option must be + configured before ALE is loaded. + + g:ale_update_tagstack *g:ale_update_tagstack* *b:ale_update_tagstack* Type: |Number| @@ -1943,6 +2002,14 @@ g:ale_languagetool_executable *g:ale_languagetool_executable* The executable to run for languagetool. +g:ale_languagetool_options *g:ale_languagetool_options* + *b:ale_languagetool_options* + Type: |String| + Default: `'--autoDetect'` + + This variable can be set to pass additional options to languagetool. + + ------------------------------------------------------------------------------- 7.3. Options for write-good *ale-write-good-options* @@ -1981,6 +2048,7 @@ documented in additional help files. ada.....................................|ale-ada-options| gcc...................................|ale-ada-gcc| + gnatpp................................|ale-ada-gnatpp| ansible.................................|ale-ansible-options| ansible-lint..........................|ale-ansible-ansible-lint| asciidoc................................|ale-asciidoc-options| @@ -2041,6 +2109,7 @@ documented in additional help files. nvcc..................................|ale-cuda-nvcc| clang-format..........................|ale-cuda-clangformat| d.......................................|ale-d-options| + dfmt..................................|ale-d-dfmt| dls...................................|ale-d-dls| uncrustify............................|ale-d-uncrustify| dart....................................|ale-dart-options| @@ -2107,6 +2176,7 @@ documented in additional help files. cabal-ghc.............................|ale-haskell-cabal-ghc| hdevtools.............................|ale-haskell-hdevtools| hfmt..................................|ale-haskell-hfmt| + hindent...............................|ale-haskell-hindent| hlint.................................|ale-haskell-hlint| stack-build...........................|ale-haskell-stack-build| stack-ghc.............................|ale-haskell-stack-ghc| @@ -2307,6 +2377,7 @@ documented in additional help files. sql.....................................|ale-sql-options| pgformatter...........................|ale-sql-pgformatter| sqlfmt................................|ale-sql-sqlfmt| + sqlformat.............................|ale-sql-sqlformat| stylus..................................|ale-stylus-options| stylelint.............................|ale-stylus-stylelint| sugarss.................................|ale-sugarss-options| @@ -2508,6 +2579,18 @@ ALEHover *ALEHover* A plug mapping `<Plug>(ale_hover)` is defined for this command. +ALEOrganizeImports *ALEOrganizeImports* + + Organize imports using tsserver. Currently not implemented for LSPs. + + +ALERename *ALERename* + + Rename a symbol using TypeScript server or Language Server. + + The user will be prompted for a new name. + + ALESymbolSearch `<query>` *ALESymbolSearch* Search for symbols in the workspace, taken from any available LSP linters. diff --git a/plugin/ale.vim b/plugin/ale.vim index 6262a7c4..1912a9c0 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -221,6 +221,12 @@ command! -nargs=1 ALESymbolSearch :call ale#symbol#Search(<q-args>) command! -bar ALEComplete :call ale#completion#GetCompletions('ale-manual') +" Rename symbols using tsserver and LSP +command! -bar ALERename :call ale#rename#Execute() + +" Organize import statements using tsserver +command! -bar ALEOrganizeImports :call ale#organize_imports#Execute() + " <Plug> mappings for commands nnoremap <silent> <Plug>(ale_previous) :ALEPrevious<Return> nnoremap <silent> <Plug>(ale_previous_wrap) :ALEPreviousWrap<Return> @@ -259,6 +265,7 @@ nnoremap <silent> <Plug>(ale_find_references) :ALEFindReferences<Return> nnoremap <silent> <Plug>(ale_hover) :ALEHover<Return> nnoremap <silent> <Plug>(ale_documentation) :ALEDocumentation<Return> inoremap <silent> <Plug>(ale_complete) <C-\><C-O>:ALEComplete<Return> +nnoremap <silent> <Plug>(ale_rename) :ALERename<Return> " Set up autocmd groups now. call ale#events#Init() diff --git a/supported-tools.md b/supported-tools.md index 6cb9f418..6cbc2f2c 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -23,6 +23,7 @@ formatting. * Ada * [gcc](https://gcc.gnu.org) + * [gnatpp](https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/gnat_utility_programs.html#the-gnat-pretty-printer-gnatpp) :floppy_disk: * Ansible * [ansible-lint](https://github.com/willthames/ansible-lint) * API Blueprint @@ -111,6 +112,7 @@ formatting. * Cython (pyrex filetype) * [cython](http://cython.org/) * D + * [dfmt](https://github.com/dlang-community/dfmt) * [dls](https://github.com/d-language-server/dls) * [dmd](https://dlang.org/dmd-linux.html) * [uncrustify](https://github.com/uncrustify/uncrustify) @@ -125,8 +127,8 @@ formatting. * [hadolint](https://github.com/hadolint/hadolint) * Elixir * [credo](https://github.com/rrrene/credo) - * [dialyxir](https://github.com/jeremyjh/dialyxir) - * [dogma](https://github.com/lpil/dogma) + * [dialyxir](https://github.com/jeremyjh/dialyxir) :floppy_disk: + * [dogma](https://github.com/lpil/dogma) :floppy_disk: * [elixir-ls](https://github.com/JakeBecker/elixir-ls) :warning: * [mix](https://hexdocs.pm/mix/Mix.html) :warning: :floppy_disk: * Elm @@ -191,6 +193,7 @@ formatting. * [hdevtools](https://hackage.haskell.org/package/hdevtools) * [hfmt](https://github.com/danstiner/hfmt) * [hie](https://github.com/haskell/haskell-ide-engine) + * [hindent](https://hackage.haskell.org/package/hindent) * [hlint](https://hackage.haskell.org/package/hlint) * [stack-build](https://haskellstack.org/) :floppy_disk: * [stack-ghc](https://haskellstack.org/) @@ -221,7 +224,7 @@ formatting. * [eslint](http://eslint.org/) * [fecs](http://fecs.baidu.com/) * [flow](https://flowtype.org/) - * [jscs](http://jscs.info/) + * [jscs](https://jscs-dev.github.io/) * [jshint](http://jshint.com/) * [prettier](https://github.com/prettier/prettier) * [prettier-eslint](https://github.com/prettier/prettier-eslint-cli) @@ -432,6 +435,7 @@ formatting. * SQL * [pgformatter](https://github.com/darold/pgFormatter) * [sqlfmt](https://github.com/jackc/sqlfmt) + * [sqlformat](https://github.com/andialbrecht/sqlparse) * [sqlint](https://github.com/purcell/sqlint) * Stylus * [stylelint](https://github.com/stylelint/stylelint) diff --git a/test/ada_files/testfile.adb b/test/ada_files/testfile.adb new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/ada_files/testfile.adb diff --git a/test/command_callback/python_paths/with_virtualenv/subdir/foo/bar.pyi b/test/command_callback/python_paths/with_virtualenv/subdir/foo/bar.pyi new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/command_callback/python_paths/with_virtualenv/subdir/foo/bar.pyi diff --git a/test/command_callback/test_languagetool_command_callback.vader b/test/command_callback/test_languagetool_command_callback.vader index a79662b9..ff6b2064 100644 --- a/test/command_callback/test_languagetool_command_callback.vader +++ b/test/command_callback/test_languagetool_command_callback.vader @@ -12,4 +12,11 @@ Execute(Should be able to set a custom executable): let g:ale_languagetool_executable = 'foobar' AssertLinter 'foobar' , ale#Escape('foobar') - \ . ' --autoDetect %s' + \ . ' --autoDetect %s' + +Execute(Should be able to include custom languagetool options): + let g:ale_languagetool_options = '--language en' + + " is now 'foobar' based on above global + AssertLinter 'foobar', ale#Escape('foobar') + \ . ' --language en %s' diff --git a/test/completion/test_completion_events.vader b/test/completion/test_completion_events.vader index 6bc0035e..e06ac98b 100644 --- a/test/completion/test_completion_events.vader +++ b/test/completion/test_completion_events.vader @@ -47,6 +47,14 @@ Before: AssertEqual a:expect_success, g:get_completions_called endfunction + let g:handle_code_action_called = 0 + function! MockHandleCodeAction() abort + " delfunction! ale#code_action#HandleCodeAction + function! ale#code_action#HandleCodeAction(action) abort + let g:handle_code_action_called += 1 + endfunction + endfunction + After: Restore @@ -54,6 +62,7 @@ After: unlet! g:output unlet! g:fake_mode unlet! g:get_completions_called + unlet! g:handle_code_action_called unlet! b:ale_old_omnifunc unlet! b:ale_old_completeopt unlet! b:ale_completion_info @@ -61,6 +70,12 @@ After: unlet! b:ale_complete_done_time delfunction CheckCompletionCalled + delfunction ale#code_action#HandleCodeAction + delfunction MockHandleCodeAction + + if exists('*CompleteCallback') + delfunction CompleteCallback + endif " Stop any timers we left behind. " This stops the tests from failing randomly. @@ -73,6 +88,7 @@ After: endfunction runtime autoload/ale/completion.vim + runtime autoload/ale/code_action.vim runtime autoload/ale/util.vim Execute(ale#completion#GetCompletions should be called when the cursor position stays the same): @@ -333,6 +349,30 @@ Execute(b:ale_completion_info should be set up correctly for other sources): \ b:ale_completion_info Assert !exists('b:ale_completion_result') +Execute(b:ale_completion_info should be set up correctly when requesting completions via callback): + let b:ale_completion_result = [] + call setpos('.', [bufnr(''), 3, 14, 0]) + + function! CompleteCallback() abort + echo 'Called' + endfunction + + + call ale#completion#GetCompletions('ale-callback', {'callback': funcref('CompleteCallback')}) + + AssertEqual + \ { + \ 'request_id': 0, + \ 'conn_id': 0, + \ 'column': 14, + \ 'line_length': 14, + \ 'line': 3, + \ 'prefix': 'ab', + \ 'source': 'ale-callback', + \ }, + \ b:ale_completion_info + Assert !exists('b:ale_completion_result') + Execute(The correct keybinds should be configured): redir => g:output silent map <Plug>(ale_show_completion_menu) @@ -357,3 +397,44 @@ Execute(Running the normal mode <Plug> keybind should reset the settings): AssertEqual 'menu', &l:completeopt Assert !has_key(b:, 'ale_old_omnifunc') Assert !has_key(b:, 'ale_old_completeopt') + +Execute(HandleUserData should call ale#code_action#HandleCodeAction): + let b:ale_completion_info = {'source': 'ale-manual'} + call MockHandleCodeAction() + + call ale#completion#HandleUserData({}) + AssertEqual g:handle_code_action_called, 0 + + call ale#completion#HandleUserData({ + \ 'user_data': '' + \}) + AssertEqual g:handle_code_action_called, 0 + + call ale#completion#HandleUserData({ + \ 'user_data': '{}' + \}) + AssertEqual g:handle_code_action_called, 0 + + call ale#completion#HandleUserData({ + \ 'user_data': '{"codeActions": []}' + \}) + AssertEqual g:handle_code_action_called, 0 + + call ale#completion#HandleUserData({ + \ 'user_data': '{"codeActions": [{"description":"", "changes": []}]}' + \}) + AssertEqual g:handle_code_action_called, 1 + + let b:ale_completion_info = {'source': 'ale-automatic'} + call ale#completion#HandleUserData({ + \ 'user_data': '{"codeActions": [{"description":"", "changes": []}]}' + \}) + AssertEqual g:handle_code_action_called, 2 + +Execute(ale#code_action#HandleCodeAction should not be called when when source is not ALE): + call MockHandleCodeAction() + let b:ale_completion_info = {'source': 'syntastic'} + call ale#completion#HandleUserData({ + \ 'user_data': '{"codeActions": [{"description":"", "changes": []}]}' + \}) + AssertEqual g:handle_code_action_called, 0 diff --git a/test/completion/test_lsp_completion_messages.vader b/test/completion/test_lsp_completion_messages.vader index 6bd241a8..b997ac86 100644 --- a/test/completion/test_lsp_completion_messages.vader +++ b/test/completion/test_lsp_completion_messages.vader @@ -116,7 +116,13 @@ Execute(The right message should be sent for the initial tsserver request): \ string(g:Callback) " We should send the right message. AssertEqual - \ [[0, 'ts@completions', {'file': expand('%:p'), 'line': 1, 'offset': 3, 'prefix': 'fo'}]], + \ [[0, 'ts@completions', { + \ 'file': expand('%:p'), + \ 'line': 1, + \ 'offset': 3, + \ 'prefix': 'fo', + \ 'includeExternalModuleExports': g:ale_completion_tsserver_autoimport, + \ }]], \ g:message_list " We should set up the completion info correctly. AssertEqual @@ -151,7 +157,7 @@ Execute(The right message sent to the tsserver LSP when the first completion mes \ 'body': [ \ {'name': 'Baz'}, \ {'name': 'dingDong'}, - \ {'name': 'Foo'}, + \ {'name': 'Foo', 'source': '/path/to/foo.ts'}, \ {'name': 'FooBar'}, \ {'name': 'frazzle'}, \ {'name': 'FFS'}, @@ -160,8 +166,16 @@ Execute(The right message sent to the tsserver LSP when the first completion mes " We should save the names we got in the buffer, as TSServer doesn't return " details for every name. - AssertEqual - \ ['Foo', 'FooBar', 'frazzle'], + AssertEqual [{ + \ 'word': 'Foo', + \ 'source': '/path/to/foo.ts', + \ }, { + \ 'word': 'FooBar', + \ 'source': '', + \ }, { + \ 'word': 'frazzle', + \ 'source': '', + \}], \ get(b:, 'ale_tsserver_completion_names', []) " The entry details messages should have been sent. @@ -171,7 +185,16 @@ Execute(The right message sent to the tsserver LSP when the first completion mes \ 'ts@completionEntryDetails', \ { \ 'file': expand('%:p'), - \ 'entryNames': ['Foo', 'FooBar', 'frazzle'], + \ 'entryNames': [{ + \ 'name': 'Foo', + \ 'source': '/path/to/foo.ts', + \ }, { + \ 'name': 'FooBar', + \ 'source': '', + \ }, { + \ 'name': 'frazzle', + \ 'source': '', + \ }], \ 'offset': 1, \ 'line': 1, \ }, diff --git a/test/completion/test_tsserver_completion_parsing.vader b/test/completion/test_tsserver_completion_parsing.vader index dbc4f9e2..02f287a9 100644 --- a/test/completion/test_tsserver_completion_parsing.vader +++ b/test/completion/test_tsserver_completion_parsing.vader @@ -6,10 +6,24 @@ Execute(TypeScript completions responses should be parsed correctly): \ ale#completion#ParseTSServerCompletions({ \ 'body': [], \}) - AssertEqual ['foo', 'bar', 'baz'], + AssertEqual + \ [ + \ { + \ 'word': 'foo', + \ 'source': '/path/to/foo.ts', + \ }, + \ { + \ 'word': 'bar', + \ 'source': '', + \ }, + \ { + \ 'word': 'baz', + \ 'source': '', + \ } + \ ], \ ale#completion#ParseTSServerCompletions({ \ 'body': [ - \ {'name': 'foo'}, + \ {'name': 'foo', 'source': '/path/to/foo.ts'}, \ {'name': 'bar'}, \ {'name': 'baz'}, \ ], @@ -24,6 +38,7 @@ Execute(TypeScript completion details responses should be parsed correctly): \ 'info': '', \ 'kind': 'f', \ 'icase': 1, + \ 'dup': g:ale_completion_tsserver_autoimport, \ }, \ { \ 'word': 'def', @@ -31,6 +46,7 @@ Execute(TypeScript completion details responses should be parsed correctly): \ 'info': 'foo bar baz', \ 'kind': 'f', \ 'icase': 1, + \ 'dup': g:ale_completion_tsserver_autoimport, \ }, \ { \ 'word': 'ghi', @@ -38,6 +54,7 @@ Execute(TypeScript completion details responses should be parsed correctly): \ 'info': '', \ 'kind': 'f', \ 'icase': 1, + \ 'dup': g:ale_completion_tsserver_autoimport, \ }, \ ], \ ale#completion#ParseTSServerCompletionEntryDetails({ @@ -96,7 +113,10 @@ Execute(TypeScript completion details responses should be parsed correctly): \}) Execute(Entries without details should be included in the responses): - let b:ale_tsserver_completion_names = ['xyz'] + let b:ale_tsserver_completion_names = [{ + \ 'word': 'xyz', + \ 'source': '/path/to/xyz.ts', + \ }] AssertEqual \ [ @@ -106,6 +126,13 @@ Execute(Entries without details should be included in the responses): \ 'info': '', \ 'kind': 'f', \ 'icase': 1, + \ 'user_data': json_encode({ + \ 'codeActions': [{ + \ 'description': 'abc action', + \ 'changes': [], + \ }], + \ }), + \ 'dup': g:ale_completion_tsserver_autoimport, \ }, \ { \ 'word': 'def', @@ -113,6 +140,7 @@ Execute(Entries without details should be included in the responses): \ 'info': 'foo bar baz', \ 'kind': 'f', \ 'icase': 1, + \ 'dup': g:ale_completion_tsserver_autoimport, \ }, \ { \ 'word': 'xyz', @@ -139,6 +167,10 @@ Execute(Entries without details should be included in the responses): \ {'text': ' '}, \ {'text': 'number'}, \ ], + \ 'codeActions': [{ + \ 'description': 'abc action', + \ 'changes': [], + \ }], \ }, \ { \ 'name': 'def', diff --git a/test/d_files/test.d b/test/d_files/test.d new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/d_files/test.d diff --git a/test/fixers/test_black_fixer_callback.vader b/test/fixers/test_black_fixer_callback.vader index 25ad05db..75864479 100644 --- a/test/fixers/test_black_fixer_callback.vader +++ b/test/fixers/test_black_fixer_callback.vader @@ -36,6 +36,14 @@ Execute(The black callback should include options): \ {'command': ale#Escape(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/env/' . b:bin_dir . '/black')) . ' --some-option -' }, \ ale#fixers#black#Fix(bufnr('')) +Execute(The black callback should include --pyi for .pyi files): + let g:ale_python_black_change_directory = 0 + + silent execute 'file ' . fnameescape(g:dir . '/python_paths/with_virtualenv/subdir/foo/bar.pyi') + AssertEqual + \ {'command': ale#Escape(ale#path#Simplify(g:dir . '/python_paths/with_virtualenv/env/' . b:bin_dir . '/black')) . ' --pyi -' }, + \ ale#fixers#black#Fix(bufnr('')) + Execute(Pipenv is detected when python_black_auto_pipenv is set): let g:ale_python_black_auto_pipenv = 1 let g:ale_python_black_change_directory = 0 diff --git a/test/fixers/test_dfmt_fixer_callback.vader b/test/fixers/test_dfmt_fixer_callback.vader new file mode 100644 index 00000000..5ecb56e6 --- /dev/null +++ b/test/fixers/test_dfmt_fixer_callback.vader @@ -0,0 +1,40 @@ +Before: + Save g:ale_d_dfmt_executable + Save g:ale_d_dfmt_options + + " Use an invalid global executable, so we don't match it. + let g:ale_d_dfmt_executable = 'xxxinvalid' + let g:ale_d_dfmt_options = '' + + call ale#test#SetDirectory('/testplugin/test/fixers') + +After: + Restore + + call ale#test#RestoreDirectory() + +Execute(The dfmt callback should return the correct default values): + call ale#test#SetFilename('../d_files/test.d') + + AssertEqual + \ { + \ 'read_temporary_file': 1, + \ 'command': ale#Escape('xxxinvalid') + \ . ' -i' + \ . ' %t', + \ }, + \ ale#fixers#dfmt#Fix(bufnr('')) + +Execute(The dfmt callback should include custom dfmt options): + let g:ale_d_dfmt_options = "--space-after-cast" + call ale#test#SetFilename('../d_files/test.d') + + AssertEqual + \ { + \ 'read_temporary_file': 1, + \ 'command': ale#Escape('xxxinvalid') + \ . ' -i' + \ . ' ' . g:ale_d_dfmt_options + \ . ' %t', + \ }, + \ ale#fixers#dfmt#Fix(bufnr('')) diff --git a/test/fixers/test_gnatpp_fixer_callback.vader b/test/fixers/test_gnatpp_fixer_callback.vader new file mode 100644 index 00000000..a2bf898e --- /dev/null +++ b/test/fixers/test_gnatpp_fixer_callback.vader @@ -0,0 +1,28 @@ +Before: + call ale#assert#SetUpFixerTest('ada', 'gnatpp') + +After: + " Reset fixers, variables, etc. + " + " Vader's 'Restore' command will be called here. + call ale#assert#TearDownFixerTest() + +Execute(The default command should be correct): + call ale#test#SetFilename('../ada_files/testfile.adb') + + AssertFixer + \ { + \ 'command': ale#Escape(g:ale_ada_gnatpp_executable) .' %t', + \ 'read_temporary_file': 1, + \ } + +Execute(The version check should be correct): + call ale#test#SetFilename('../ada_files/testfile.adb') + let g:ale_ada_gnatpp_options = '--no-alignment' + + AssertFixer + \ { + \ 'command': ale#Escape(g:ale_ada_gnatpp_executable) + \ . ' --no-alignment %t', + \ 'read_temporary_file': 1, + \ } diff --git a/test/fixers/test_hindent_fixer_callback.vader b/test/fixers/test_hindent_fixer_callback.vader new file mode 100644 index 00000000..2e5a8b9f --- /dev/null +++ b/test/fixers/test_hindent_fixer_callback.vader @@ -0,0 +1,18 @@ +Before: + call ale#test#SetDirectory('/testplugin/test/fixers') + +After: + Restore + + call ale#test#RestoreDirectory() + +Execute(The hindent callback should return the correct default values): + call ale#test#SetFilename('../haskell_files/testfile.hs') + + AssertEqual + \ { + \ 'read_temporary_file': 1, + \ 'command': ale#Escape('hindent') + \ . ' %t', + \ }, + \ ale#fixers#hindent#Fix(bufnr('')) diff --git a/test/fixers/test_sqlformat_fixer_callback.vader b/test/fixers/test_sqlformat_fixer_callback.vader new file mode 100644 index 00000000..4bace089 --- /dev/null +++ b/test/fixers/test_sqlformat_fixer_callback.vader @@ -0,0 +1,24 @@ +Before: + Save g:ale_sql_sqlformat_executable + Save g:ale_sql_sqlformat_options + +After: + Restore + +Execute(The sqlformat callback should return the correct default values): + AssertEqual + \ { + \ 'command': ale#Escape('sqlformat') . ' -' + \ }, + \ ale#fixers#sqlformat#Fix(bufnr('')) + +Execute(The sqlformat executable and options should be configurable): + let g:ale_sql_sqlformat_executable = '/path/to/sqlformat' + let g:ale_sql_sqlformat_options = '-a' + + AssertEqual + \ { + \ 'command': ale#Escape('/path/to/sqlformat') + \ . ' -a -' + \ }, + \ ale#fixers#sqlformat#Fix(bufnr('')) diff --git a/test/handler/test_shell_handler.vader b/test/handler/test_shell_handler.vader index 2465f179..c61cf37d 100644 --- a/test/handler/test_shell_handler.vader +++ b/test/handler/test_shell_handler.vader @@ -40,3 +40,138 @@ Execute(The shell handler should parse lines correctly): \ 'qfm:22: :11: :33: :44:', \ 'foo.sh: syntax error at line 9: `done'' unexpected', \ ]) + +Execute(The shell handler should parse Simplified Chinese lines correctly): + AssertEqual + \ [ + \ { + \ 'lnum': 0, + \ 'text': '未预期的符号“done”附近有语法错误', + \ }, + \ { + \ 'lnum': 90, + \ 'text': '寻找匹配的“"”时遇到了未预期的文件结束符', + \ }, + \ { + \ 'lnum': 111, + \ 'text': '语法错误: 未预期的文件结尾', + \ }, + \ { + \ 'lnum': 22, + \ 'text': ':11: :33: :44:', + \ }, + \ ], + \ ale_linters#sh#shell#Handle(347, [ + \ '/tmp/nvimWL5sOL/2/a.sh:行0: 未预期的符号“done”附近有语法错误', + \ '/tmp/nvimWL5sOL/2/a.sh:行90: 寻找匹配的“"”时遇到了未预期的文件结束符', + \ '/tmp/nvimWL5sOL/2/a.sh:行111: 语法错误: 未预期的文件结尾', + \ '/tmp/nvimWL5sOL/2/a.sh:行22: :11: :33: :44:', + \ ]) + +Execute(The shell handler should parse Traditional Chinese lines correctly): + AssertEqual + \ [ + \ { + \ 'lnum': 0, + \ 'text': '未預期的字組「(」附近有語法錯誤', + \ }, + \ { + \ 'lnum': 90, + \ 'text': '尋找匹配的「"」時遇到了未預期的檔案結束符', + \ }, + \ { + \ 'lnum': 111, + \ 'text': '語法錯誤: 未預期的檔案結尾', + \ }, + \ { + \ 'lnum': 22, + \ 'text': ':11: :33: :44:', + \ }, + \ ], + \ ale_linters#sh#shell#Handle(347, [ + \ '/tmp/nvimWL5sOL/2/a.sh: 列 0: 未預期的字組「(」附近有語法錯誤', + \ '/tmp/nvimWL5sOL/2/a.sh: 列 90: 尋找匹配的「"」時遇到了未預期的檔案結束符', + \ '/tmp/nvimWL5sOL/2/a.sh: 列 111: 語法錯誤: 未預期的檔案結尾', + \ '/tmp/nvimWL5sOL/2/a.sh: 列 22: :11: :33: :44:', + \ ]) + +Execute(The shell handler should parse Japanese lines correctly): + AssertEqual + \ [ + \ { + \ 'lnum': 0, + \ 'text': "予期しないトークン `(' 周辺に構文エラーがあります", + \ }, + \ { + \ 'lnum': 90, + \ 'text': "予期しないトークン `done' 周辺に構文エラーがあります", + \ }, + \ { + \ 'lnum': 111, + \ 'text': "対応する `\"' を探索中に予期しないファイル終了 (EOF) です", + \ }, + \ { + \ 'lnum': 22, + \ 'text': ':11: :33: :44:', + \ }, + \ ], + \ ale_linters#sh#shell#Handle(347, [ + \ "/tmp/nvimWL5sOL/2/a.sh: 行 0: 予期しないトークン `(' 周辺に構文エラーがあります", + \ "/tmp/nvimWL5sOL/2/a.sh: 行 90: 予期しないトークン `done' 周辺に構文エラーがあります", + \ "/tmp/nvimWL5sOL/2/a.sh: 行 111: 対応する `\"' を探索中に予期しないファイル終了 (EOF) です", + \ "/tmp/nvimWL5sOL/2/a.sh: 行 22: :11: :33: :44:", + \ ]) + +Execute(The shell handler should parse Greek lines correctly): + AssertEqual + \ [ + \ { + \ 'lnum': 0, + \ 'text': 'συντακτικό σφάλμα κοντά στο μη αναμενόμενο σύμβολο «done»', + \ }, + \ { + \ 'lnum': 90, + \ 'text': 'syntax error: μη αναμενόμενο τέλος αρχείου', + \ }, + \ { + \ 'lnum': 111, + \ 'text': 'μη αναμενόμενο EOF κατά την αναζήτηση «"»', + \ }, + \ { + \ 'lnum': 22, + \ 'text': ':11: :33: :44:', + \ }, + \ ], + \ ale_linters#sh#shell#Handle(347, [ + \ '/tmp/nvimWL5sOL/2/a.sh: γραμμή 0: συντακτικό σφάλμα κοντά στο μη αναμενόμενο σύμβολο «done»', + \ '/tmp/nvimWL5sOL/2/a.sh: γραμμή 90: syntax error: μη αναμενόμενο τέλος αρχείου', + \ '/tmp/nvimWL5sOL/2/a.sh: γραμμή 111: μη αναμενόμενο EOF κατά την αναζήτηση «"»', + \ "/tmp/nvimWL5sOL/2/a.sh: γραμμή 22: :11: :33: :44:", + \ ]) + +Execute(The shell handler should parse Russian lines correctly): + AssertEqual + \ [ + \ { + \ 'lnum': 0, + \ 'text': 'синтаксическая ошибка рядом с неожиданным маркером «done»', + \ }, + \ { + \ 'lnum': 90, + \ 'text': 'синтаксическая ошибка: неожиданный конец файла', + \ }, + \ { + \ 'lnum': 111, + \ 'text': 'неожиданный конец файла во время поиска «"»', + \ }, + \ { + \ 'lnum': 22, + \ 'text': ':11: :33: :44:', + \ }, + \ ], + \ ale_linters#sh#shell#Handle(347, [ + \ '/tmp/nvimWL5sOL/2/a.sh: строка 0: синтаксическая ошибка рядом с неожиданным маркером «done»', + \ '/tmp/nvimWL5sOL/2/a.sh: строка 90: синтаксическая ошибка: неожиданный конец файла', + \ '/tmp/nvimWL5sOL/2/a.sh: строка 111: неожиданный конец файла во время поиска «"»', + \ '/tmp/nvimWL5sOL/2/a.sh: строка 22: :11: :33: :44:', + \ ]) diff --git a/test/lsp/test_lsp_client_messages.vader b/test/lsp/test_lsp_client_messages.vader index 90a20832..bc91bf68 100644 --- a/test/lsp/test_lsp_client_messages.vader +++ b/test/lsp/test_lsp_client_messages.vader @@ -275,9 +275,10 @@ Execute(ale#lsp#tsserver_message#Completions() should return correct messages): \ 'line': 347, \ 'offset': 12, \ 'prefix': 'abc', + \ 'includeExternalModuleExports': 1, \ } \ ], - \ ale#lsp#tsserver_message#Completions(bufnr(''), 347, 12, 'abc') + \ ale#lsp#tsserver_message#Completions(bufnr(''), 347, 12, 'abc', 1) Execute(ale#lsp#tsserver_message#CompletionEntryDetails() should return correct messages): AssertEqual diff --git a/test/lsp/test_other_initialize_message_handling.vader b/test/lsp/test_other_initialize_message_handling.vader index 0372765d..6473e283 100644 --- a/test/lsp/test_other_initialize_message_handling.vader +++ b/test/lsp/test_other_initialize_message_handling.vader @@ -17,6 +17,7 @@ Before: \ 'init_queue': [], \ 'capabilities': { \ 'hover': 0, + \ 'rename': 0, \ 'references': 0, \ 'completion': 0, \ 'completion_trigger_characters': [], @@ -100,6 +101,7 @@ Execute(Capabilities should bet set up correctly): \ 'hover': 1, \ 'definition': 1, \ 'symbol_search': 1, + \ 'rename': 1, \ }, \ b:conn.capabilities AssertEqual [[1, 'initialized', {}]], g:message_list @@ -110,7 +112,7 @@ Execute(Disabled capabilities should be recognised correctly): \ 'id': 1, \ 'result': { \ 'capabilities': { - \ 'renameProvider': v:true, + \ 'renameProvider': v:false, \ 'executeCommandProvider': { \ 'commands': [], \ }, @@ -143,6 +145,7 @@ Execute(Disabled capabilities should be recognised correctly): \ 'hover': 0, \ 'definition': 0, \ 'symbol_search': 0, + \ 'rename': 0, \ }, \ b:conn.capabilities AssertEqual [[1, 'initialized', {}]], g:message_list diff --git a/test/script/check-supported-tools-tables b/test/script/check-supported-tools-tables index 65270029..f4305707 100755 --- a/test/script/check-supported-tools-tables +++ b/test/script/check-supported-tools-tables @@ -3,8 +3,9 @@ set -e set -u -# This script compares the table of supported tools in both the README file -# and the doc/ale.txt file, so we can complain if they don't match up. +# This script compares the table of supported tools in both supported-tools.md +# (for GitHub) and doc/ale-supported-languages-and-tools.txt (for vim), so we +# can complain if they don't match up. doc_file="$(mktemp -t doc.XXXXXXXX)" doc_sorted_file="$(mktemp -t doc-sorted.XXXXXXXX)" diff --git a/test/script/check-toc b/test/script/check-toc index f3a57ed2..87a61262 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="$(/bin/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 20dbfb80..ca9069e4 100755 --- a/test/script/custom-checks +++ b/test/script/custom-checks @@ -36,12 +36,12 @@ 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 --exclude=tags -roh "|$tag_regex|" doc | sort -u | sed 's/|//g') \ | grep '^+[^+]' && exit_code=1 echo '========================================' -echo 'diff README.md and doc/ale.txt tables' +echo 'diff supported-tools.md and doc/ale-supported-languages-and-tools.txt tables' echo '========================================' echo 'Differences follow:' echo diff --git a/test/test_ale_info.vader b/test/test_ale_info.vader index e1208679..2bc8c635 100644 --- a/test/test_ale_info.vader +++ b/test/test_ale_info.vader @@ -47,6 +47,7 @@ Before: Save g:ale_sign_style_error Save g:ale_sign_style_warning Save g:ale_sign_warning + Save g:ale_sign_highlight_linenrs Save g:ale_statusline_format Save g:ale_type_map Save g:ale_use_global_executables @@ -102,6 +103,7 @@ Before: let g:ale_sign_style_error = '>>' let g:ale_sign_style_warning = '--' let g:ale_sign_warning = '--' + let g:ale_sign_highlight_linenrs = 0 let g:ale_statusline_format = ['%d error(s)', '%d warning(s)', 'OK'] let g:ale_type_map = {} let g:ale_use_global_executables = v:null @@ -178,6 +180,7 @@ Before: \ 'let g:ale_sign_style_error = ''>>''', \ 'let g:ale_sign_style_warning = ''--''', \ 'let g:ale_sign_warning = ''--''', + \ 'let g:ale_sign_highlight_linenrs = 0', \ 'let g:ale_statusline_format = [''%d error(s)'', ''%d warning(s)'', ''OK'']', \ 'let g:ale_type_map = {}', \ 'let g:ale_use_global_executables = v:null', diff --git a/test/test_ale_toggle.vader b/test/test_ale_toggle.vader index d0bca329..a0763cb1 100644 --- a/test/test_ale_toggle.vader +++ b/test/test_ale_toggle.vader @@ -36,6 +36,7 @@ Before: \ 'ALEEvents', \ 'ALEHighlightBufferGroup', \] + let g:has_nvim_highlight = exists('*nvim_buf_add_highlight') && exists('*nvim_buf_clear_namespace') function! ToggleTestCallback(buffer, output) return [{ @@ -91,6 +92,7 @@ After: unlet! g:expected_groups unlet! b:ale_enabled unlet! g:output + unlet! g:has_nvim_highlight call ale#linter#Reset() @@ -120,9 +122,14 @@ Execute(ALEToggle should reset everything and then run again): " First check that everything is there... AssertEqual g:expected_loclist, ale#test#GetLoclistWithoutModule() AssertEqual [0, [[2, 1000001, 'ALEErrorSign']]], ale#sign#FindCurrentSigns(bufnr('%')) - AssertEqual - \ [{'group': 'ALEError', 'pos1': [2, 3, 1]}], - \ map(getmatches(), '{''group'': v:val.group, ''pos1'': v:val.pos1}') + + " Only check the legacy matches if not using the new NeoVIM API. + if !g:has_nvim_highlight + AssertEqual + \ [{'group': 'ALEError', 'pos1': [2, 3, 1]}], + \ map(getmatches(), '{''group'': v:val.group, ''pos1'': v:val.pos1}') + endif + AssertEqual g:expected_groups, ParseAuGroups() AssertEqual [{'lnum': 2, 'bufnr': bufnr(''), 'col': 3, 'linter_name': 'testlinter', 'vcol': 0, 'nr': -1, 'type': 'E', 'text': 'foo bar', 'sign_id': 1000001}], g:ale_buffer_info[bufnr('')].loclist @@ -133,7 +140,11 @@ Execute(ALEToggle should reset everything and then run again): Assert !has_key(g:ale_buffer_info, bufnr('')), 'The g:ale_buffer_info Dictionary was not removed' AssertEqual [], ale#test#GetLoclistWithoutModule(), 'The loclist was not cleared' AssertEqual [0, []], ale#sign#FindCurrentSigns(bufnr('%')), 'The signs were not cleared' - AssertEqual [], getmatches(), 'The highlights were not cleared' + + if !g:has_nvim_highlight + AssertEqual [], getmatches(), 'The highlights were not cleared' + endif + AssertEqual g:expected_groups, ParseAuGroups() " Toggle ALE on, everything should be set up and run again. @@ -142,9 +153,13 @@ Execute(ALEToggle should reset everything and then run again): AssertEqual g:expected_loclist, ale#test#GetLoclistWithoutModule() AssertEqual [0, [[2, 1000001, 'ALEErrorSign']]], ale#sign#FindCurrentSigns(bufnr('%')) - AssertEqual - \ [{'group': 'ALEError', 'pos1': [2, 3, 1]}], - \ map(getmatches(), '{''group'': v:val.group, ''pos1'': v:val.pos1}') + + if !g:has_nvim_highlight + AssertEqual + \ [{'group': 'ALEError', 'pos1': [2, 3, 1]}], + \ map(getmatches(), '{''group'': v:val.group, ''pos1'': v:val.pos1}') + endif + AssertEqual g:expected_groups, ParseAuGroups() AssertEqual [{'lnum': 2, 'bufnr': bufnr(''), 'col': 3, 'linter_name': 'testlinter', 'vcol': 0, 'nr': -1, 'type': 'E', 'text': 'foo bar', 'sign_id': 1000001}], g:ale_buffer_info[bufnr('')].loclist @@ -228,9 +243,13 @@ Execute(ALEReset should reset everything for a buffer): " First check that everything is there... AssertEqual g:expected_loclist, ale#test#GetLoclistWithoutModule() AssertEqual [0, [[2, 1000001, 'ALEErrorSign']]], ale#sign#FindCurrentSigns(bufnr('%')) - AssertEqual - \ [{'group': 'ALEError', 'pos1': [2, 3, 1]}], - \ map(getmatches(), '{''group'': v:val.group, ''pos1'': v:val.pos1}') + + if !g:has_nvim_highlight + AssertEqual + \ [{'group': 'ALEError', 'pos1': [2, 3, 1]}], + \ map(getmatches(), '{''group'': v:val.group, ''pos1'': v:val.pos1}') + endif + AssertEqual [{'lnum': 2, 'bufnr': bufnr(''), 'col': 3, 'linter_name': 'testlinter', 'vcol': 0, 'nr': -1, 'type': 'E', 'text': 'foo bar', 'sign_id': 1000001}], g:ale_buffer_info[bufnr('')].loclist " Now Toggle ALE off. @@ -241,7 +260,10 @@ Execute(ALEReset should reset everything for a buffer): Assert !has_key(g:ale_buffer_info, bufnr('')), 'The g:ale_buffer_info Dictionary was not removed' AssertEqual [], ale#test#GetLoclistWithoutModule(), 'The loclist was not cleared' AssertEqual [0, []], ale#sign#FindCurrentSigns(bufnr('%')), 'The signs were not cleared' - AssertEqual [], getmatches(), 'The highlights were not cleared' + + if !g:has_nvim_highlight + AssertEqual [], getmatches(), 'The highlights were not cleared' + endif AssertEqual 1, g:ale_enabled @@ -254,9 +276,13 @@ Execute(ALEToggleBuffer should reset everything and then run again): " First check that everything is there... AssertEqual g:expected_loclist, ale#test#GetLoclistWithoutModule() AssertEqual [0, [[2, 1000001, 'ALEErrorSign']]], ale#sign#FindCurrentSigns(bufnr('%')) - AssertEqual - \ [{'group': 'ALEError', 'pos1': [2, 3, 1]}], - \ map(getmatches(), '{''group'': v:val.group, ''pos1'': v:val.pos1}') + + if !g:has_nvim_highlight + AssertEqual + \ [{'group': 'ALEError', 'pos1': [2, 3, 1]}], + \ map(getmatches(), '{''group'': v:val.group, ''pos1'': v:val.pos1}') + endif + AssertEqual [{'lnum': 2, 'bufnr': bufnr(''), 'col': 3, 'linter_name': 'testlinter', 'vcol': 0, 'nr': -1, 'type': 'E', 'text': 'foo bar', 'sign_id': 1000001}], g:ale_buffer_info[bufnr('')].loclist " Now Toggle ALE off. @@ -266,7 +292,10 @@ Execute(ALEToggleBuffer should reset everything and then run again): Assert !has_key(g:ale_buffer_info, bufnr('')), 'The g:ale_buffer_info Dictionary was not removed' AssertEqual [], ale#test#GetLoclistWithoutModule(), 'The loclist was not cleared' AssertEqual [0, []], ale#sign#FindCurrentSigns(bufnr('%')), 'The signs were not cleared' - AssertEqual [], getmatches(), 'The highlights were not cleared' + + if !g:has_nvim_highlight + AssertEqual [], getmatches(), 'The highlights were not cleared' + endif " Toggle ALE on, everything should be set up and run again. ALEToggleBuffer @@ -274,9 +303,13 @@ Execute(ALEToggleBuffer should reset everything and then run again): AssertEqual g:expected_loclist, ale#test#GetLoclistWithoutModule() AssertEqual [0, [[2, 1000001, 'ALEErrorSign']]], ale#sign#FindCurrentSigns(bufnr('%')) - AssertEqual - \ [{'group': 'ALEError', 'pos1': [2, 3, 1]}], - \ map(getmatches(), '{''group'': v:val.group, ''pos1'': v:val.pos1}') + + if !g:has_nvim_highlight + AssertEqual + \ [{'group': 'ALEError', 'pos1': [2, 3, 1]}], + \ map(getmatches(), '{''group'': v:val.group, ''pos1'': v:val.pos1}') + endif + AssertEqual g:expected_groups, ParseAuGroups() AssertEqual [{'lnum': 2, 'bufnr': bufnr(''), 'col': 3, 'linter_name': 'testlinter', 'vcol': 0, 'nr': -1, 'type': 'E', 'text': 'foo bar', 'sign_id': 1000001}], g:ale_buffer_info[bufnr('')].loclist @@ -325,9 +358,13 @@ Execute(ALEResetBuffer should reset everything for a buffer): " First check that everything is there... AssertEqual g:expected_loclist, ale#test#GetLoclistWithoutModule() AssertEqual [0, [[2, 1000001, 'ALEErrorSign']]], ale#sign#FindCurrentSigns(bufnr('%')) - AssertEqual - \ [{'group': 'ALEError', 'pos1': [2, 3, 1]}], - \ map(getmatches(), '{''group'': v:val.group, ''pos1'': v:val.pos1}') + + if !g:has_nvim_highlight + AssertEqual + \ [{'group': 'ALEError', 'pos1': [2, 3, 1]}], + \ map(getmatches(), '{''group'': v:val.group, ''pos1'': v:val.pos1}') + endif + AssertEqual [{'lnum': 2, 'bufnr': bufnr(''), 'col': 3, 'linter_name': 'testlinter', 'vcol': 0, 'nr': -1, 'type': 'E', 'text': 'foo bar', 'sign_id': 1000001}], g:ale_buffer_info[bufnr('')].loclist " Now Toggle ALE off. @@ -338,7 +375,10 @@ Execute(ALEResetBuffer should reset everything for a buffer): Assert !has_key(g:ale_buffer_info, bufnr('')), 'The g:ale_buffer_info Dictionary was not removed' AssertEqual [], ale#test#GetLoclistWithoutModule(), 'The loclist was not cleared' AssertEqual [0, []], ale#sign#FindCurrentSigns(bufnr('%')), 'The signs were not cleared' - AssertEqual [], getmatches(), 'The highlights were not cleared' + + if !g:has_nvim_highlight + AssertEqual [], getmatches(), 'The highlights were not cleared' + endif AssertEqual 1, g:ale_enabled AssertEqual 1, get(b:, 'ale_enabled', 1) diff --git a/test/test_autocmd_commands.vader b/test/test_autocmd_commands.vader index 241e7d3e..355b4c77 100644 --- a/test/test_autocmd_commands.vader +++ b/test/test_autocmd_commands.vader @@ -188,6 +188,10 @@ Execute (ALECleanupGroup should include the right commands): \], CheckAutocmd('ALECleanupGroup') endif +Execute(ALECompletionActions should always be set up): + AssertEqual [ + \ 'CompleteDone * call ale#completion#HandleUserData(v:completed_item)', + \], CheckAutocmd('ALECompletionActions') Execute(Enabling completion should set up autocmd events correctly): let g:ale_completion_enabled = 0 diff --git a/test/test_c_flag_parsing.vader b/test/test_c_flag_parsing.vader index 045554a3..8ae6f9dc 100644 --- a/test/test_c_flag_parsing.vader +++ b/test/test_c_flag_parsing.vader @@ -14,7 +14,7 @@ Execute(The CFlags parser should be able to parse include directives): call ale#test#SetFilename('test_c_projects/makefile_project/subdir/file.c') AssertEqual - \ '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir'), + \ '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')), \ ale#c#ParseCFlagsFromMakeOutput(bufnr(''), ['gcc -Isubdir -c file.c']) AssertEqual @@ -25,14 +25,14 @@ Execute(ParseCFlags should ignore -c and -o): call ale#test#SetFilename('test_c_projects/makefile_project/subdir/file.c') AssertEqual - \ '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir'), + \ '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')), \ ale#c#ParseCFlagsFromMakeOutput(bufnr(''), ['gcc -Isubdir -c file.c -o a.out']) Execute(The CFlags parser should be able to parse macro directives): call ale#test#SetFilename('test_c_projects/makefile_project/subdir/file.c') AssertEqual - \ '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir') + \ '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')) \ . ' -DTEST=1', \ ale#c#ParseCFlagsFromMakeOutput(bufnr(''), ['gcc -Isubdir -DTEST=1 -c file.c']) @@ -40,7 +40,7 @@ Execute(The CFlags parser should be able to parse macro directives with spaces): call ale#test#SetFilename('test_c_projects/makefile_project/subdir/file.c') AssertEqual - \ '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir') + \ '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')) \ . ' -DTEST=$(( 2 * 4 ))', \ ale#c#ParseCFlagsFromMakeOutput(bufnr(''), ['gcc -Isubdir -DTEST=$(( 2 * 4 )) -c file.c']) @@ -48,14 +48,14 @@ Execute(The CFlags parser should be able to parse shell directives with spaces): call ale#test#SetFilename('test_c_projects/makefile_project/subdir/file.c') AssertEqual - \ '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir') + \ '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')) \ . ' -DTEST=`date +%s`', \ ale#c#ParseCFlagsFromMakeOutput(bufnr(''), ['gcc -Isubdir -DTEST=`date +%s` -c file.c']) Execute(ParseCFlags should be able to parse flags with relative paths): AssertEqual - \ '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir') - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include') + \ '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include')) \ . ' -DTEST=`date +%s`', \ ale#c#ParseCFlags( \ ale#path#Simplify(g:dir. '/test_c_projects/makefile_project'), @@ -67,8 +67,8 @@ Execute(ParseCFlags should be able to parse flags with relative paths): Execute(ParseCFlags should be able to parse -Dgoal): AssertEqual \ '-Dgoal=9' - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir') - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include') + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include')) \ . ' -DTEST=`date +%s`', \ ale#c#ParseCFlags( \ ale#path#Simplify(g:dir. '/test_c_projects/makefile_project'), @@ -77,16 +77,30 @@ Execute(ParseCFlags should be able to parse -Dgoal): \ . ' -DTEST=`date +%s` -c file.c' \ ) +Execute(ParseCFlags should ignore -T and other arguments): + AssertEqual + \ '-Dgoal=9' + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')) + \ . ' ' . '--sysroot=subdir' + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include')) + \ . ' -DTEST=`date +%s`', + \ ale#c#ParseCFlags( + \ ale#path#Simplify(g:dir. '/test_c_projects/makefile_project'), + \ 'gcc -Dgoal=9 -Tlinkerfile.ld blabla -Isubdir --sysroot=subdir ' + \ . '-I'. ale#path#Simplify('kernel/include') + \ . ' -DTEST=`date +%s` -c file.c' + \ ) + Execute(ParseCFlags should handle paths with spaces in double quotes): AssertEqual \ '-Dgoal=9' - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir') - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/"dir with spaces"') - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include') + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/dir with spaces')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include')) \ . ' -DTEST=`date +%s`', \ ale#c#ParseCFlags( \ ale#path#Simplify(g:dir. '/test_c_projects/makefile_project'), - \ 'gcc -Dgoal=9 -Isubdir ' + \ 'gcc -Dgoal=9 -Tlinkerfile.ld blabla -Isubdir ' \ . '-I"dir with spaces"' . ' -I'. ale#path#Simplify('kernel/include') \ . ' -DTEST=`date +%s` -c file.c' \ ) @@ -94,28 +108,29 @@ Execute(ParseCFlags should handle paths with spaces in double quotes): Execute(ParseCFlags should handle paths with spaces in single quotes): AssertEqual \ '-Dgoal=9' - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir') - \ . ' ' . '-I' . ale#path#Simplify(g:dir. "/test_c_projects/makefile_project/'dir with spaces'") - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include') + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/dir with spaces')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include')) \ . ' -DTEST=`date +%s`', \ ale#c#ParseCFlags( \ ale#path#Simplify(g:dir. '/test_c_projects/makefile_project'), - \ 'gcc -Dgoal=9 -Isubdir ' - \ . "-I'dir with spaces'" . ' -I'. ale#path#Simplify('kernel/include') + \ 'gcc -Dgoal=9 -Tlinkerfile.ld blabla -Isubdir ' + \ . '-I''dir with spaces''' . ' -I'. ale#path#Simplify('kernel/include') \ . ' -DTEST=`date +%s` -c file.c' \ ) Execute(ParseCFlags should handle paths with minuses): AssertEqual \ '-Dgoal=9' - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir') - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/dir-with-dash') - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include') + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/dir with spaces')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/dir-with-dash')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include')) \ . ' -DTEST=`date +%s`', \ ale#c#ParseCFlags( \ ale#path#Simplify(g:dir. '/test_c_projects/makefile_project'), - \ 'gcc -Dgoal=9 -Isubdir ' - \ . ' -Idir-with-dash' + \ 'gcc -Dgoal=9 -Tlinkerfile.ld blabla -Isubdir ' + \ . '-I''dir with spaces''' . ' -Idir-with-dash' \ . ' -I'. ale#path#Simplify('kernel/include') \ . ' -DTEST=`date +%s` -c file.c' \ ) @@ -123,14 +138,17 @@ Execute(ParseCFlags should handle paths with minuses): Execute(ParseCFlags should handle -D with minuses): AssertEqual \ '-Dgoal=9' - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir') + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')) \ . ' -Dmacro-with-dash' - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include') + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/dir with spaces')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/dir-with-dash')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include')) \ . ' -DTEST=`date +%s`', \ ale#c#ParseCFlags( \ ale#path#Simplify(g:dir. '/test_c_projects/makefile_project'), - \ 'gcc -Dgoal=9 -Isubdir ' + \ 'gcc -Dgoal=9 -Tlinkerfile.ld blabla -Isubdir ' \ . '-Dmacro-with-dash ' + \ . '-I''dir with spaces''' . ' -Idir-with-dash' \ . ' -I'. ale#path#Simplify('kernel/include') \ . ' -DTEST=`date +%s` -c file.c' \ ) @@ -138,11 +156,16 @@ Execute(ParseCFlags should handle -D with minuses): Execute(ParseCFlags should handle flags at the end of the line): AssertEqual \ '-Dgoal=9' - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir') - \ . ' ' . '-I' . ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include'), + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')) + \ . ' -Dmacro-with-dash' + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/dir with spaces')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/dir-with-dash')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include')), \ ale#c#ParseCFlags( \ ale#path#Simplify(g:dir. '/test_c_projects/makefile_project'), - \ 'gcc -Dgoal=9 -Isubdir ' + \ 'gcc -Dgoal=9 -Tlinkerfile.ld blabla -Isubdir ' + \ . '-Dmacro-with-dash ' + \ . '-I''dir with spaces''' . ' -Idir-with-dash' \ . ' -I'. ale#path#Simplify('kernel/include') \ ) @@ -156,14 +179,14 @@ Execute(ParseCompileCommandsFlags should parse some basic flags): silent noautocmd execute 'file! ' . fnameescape(ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c')) AssertEqual - \ '-I/usr/include/xmms2', + \ '-I ' . ale#path#Simplify('/usr/include/xmms2'), \ ale#c#ParseCompileCommandsFlags(bufnr(''), { "xmms2-mpris.c": [ \ { - \ 'directory': '/foo/bar/xmms2-mpris', - \ 'command': '/usr/bin/cc -I' . '/usr/include/xmms2' + \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris'), + \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/xmms2') \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' - \ . ' -c ' . '/foo/bar/xmms2-mpris/src/xmms2-mpris.c', - \ 'file': '/foo/bar/xmms2-mpris/src/xmms2-mpris.c', + \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ 'file': ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), \ }, \ ] }, {}) @@ -183,14 +206,14 @@ Execute(ParseCompileCommandsFlags should fall back to files in the same director silent noautocmd execute 'file! ' . fnameescape(ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c')) AssertEqual - \ '-I/usr/include/xmms2', + \ '-I ' . ale#path#Simplify('/usr/include/xmms2'), \ ale#c#ParseCompileCommandsFlags(bufnr(''), {}, { "src": [ \ { - \ 'directory': '/foo/bar/xmms2-mpris', - \ 'command': '/usr/bin/cc -I' . '/usr/include/xmms2' + \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris'), + \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/xmms2') \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' - \ . ' -c ' . '/foo/bar/xmms2-mpris/src/xmms2-mpris.c', - \ 'file': (has('win32') ? 'C:' : '') . '/foo/bar/xmms2-mpris/src/xmms2-other.c', + \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ 'file': ale#path#Simplify((has('win32') ? 'C:' : '') . '/foo/bar/xmms2-mpris/src/xmms2-other.c'), \ }, \ ] }) @@ -198,7 +221,7 @@ Execute(ParseCompileCommandsFlags should take commands from matching .c files fo silent noautocmd execute 'file! ' . fnameescape(ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.h')) AssertEqual - \ '-I/usr/include/xmms2', + \ '-I /usr/include/xmms2', \ ale#c#ParseCompileCommandsFlags( \ bufnr(''), \ { @@ -220,7 +243,7 @@ Execute(ParseCompileCommandsFlags should take commands from matching .cpp files silent noautocmd execute 'file! ' . fnameescape(ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.hpp')) AssertEqual - \ '-I/usr/include/xmms2', + \ '-I /usr/include/xmms2', \ ale#c#ParseCompileCommandsFlags( \ bufnr(''), \ { @@ -242,7 +265,7 @@ Execute(ParseCompileCommandsFlags should take commands from matching .cpp files silent noautocmd execute 'file! ' . fnameescape(ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.h')) AssertEqual - \ '-I/usr/include/xmms2', + \ '-I /usr/include/xmms2', \ ale#c#ParseCompileCommandsFlags( \ bufnr(''), \ { @@ -282,13 +305,64 @@ Execute(ParseCompileCommandsFlags should not take commands from .c files for .h \ }, \ ) +Execute(ParseCFlags should not merge flags): + AssertEqual + \ '-Dgoal=9' + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/subdir')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/dir with spaces')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/dir-with-dash')) + \ . ' ' . '-I' . ' ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/kernel/include')), + \ ale#c#ParseCFlags( + \ ale#path#Simplify(g:dir. '/test_c_projects/makefile_project'), + \ 'gcc -Dgoal=9 -Tlinkerfile.ld blabla -Isubdir ' + \ . 'subdir/somedep1.o ' . 'subdir/somedep2.o ' + \ . '-I''dir with spaces''' . ' -Idir-with-dash ' + \ . 'subdir/somedep3.o ' . 'subdir/somedep4.o ' + \ . ' -I'. ale#path#Simplify('kernel/include') . ' ' + \ . 'subdir/somedep5.o ' . 'subdir/somedep6.o ' + \ ) + Execute(ParseCFlags should handle parenthesis and quotes): AssertEqual - \ '-Dgoal=9 -Dtest1="('' '')" file1.o -Dtest2=''(` `)'' file2.o -Dtest3=`(" ")` file3.o', + \ '-Dgoal=9 -Dtest1="('' '')" -Dtest2=''(` `)'' -Dtest3=`(" ")`', \ ale#c#ParseCFlags( \ ale#path#Simplify(g:dir. '/test_c_projects/makefile_project'), - \ 'gcc -Dgoal=9 ' + \ 'gcc -Dgoal=9 -Tlinkerfile.ld blabla ' \ . '-Dtest1="('' '')" file1.o ' \ . '-Dtest2=''(` `)'' file2.o ' \ . '-Dtest3=`(" ")` file3.o ' \ ) + +Execute(CFlags we want to pass): + AssertEqual + \ '-I ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/inc')) + \ . ' -I ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/include')) + \ . ' -iquote ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/incquote')) + \ . ' -isystem ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/incsystem')) + \ . ' -idirafter ' . ale#Escape(ale#path#Simplify(g:dir. '/test_c_projects/makefile_project/incafter')) + \ . ' -Dmacro=value -D macro2 -Bbdir -B bdir2' + \ . ' -iprefix prefix -iwithprefix prefix2 -iwithprefixbefore prefix3' + \ . ' -isysroot sysroot --sysroot=test --no-sysroot-suffix -imultilib multidir' + \ . ' -Wsome-warning -std=c89 -pedantic -pedantic-errors -ansi' + \ . ' -foption -O2 -C -CC -trigraphs -nostdinc -nostdinc++' + \ . ' -iplugindir=dir -march=native -w', + \ ale#c#ParseCFlags( + \ ale#path#Simplify(g:dir. '/test_c_projects/makefile_project'), + \ 'gcc' + \ . ' -Iinc -I include -iquote incquote -isystem incsystem -idirafter incafter' + \ . ' -Dmacro=value -D macro2 -Bbdir -B bdir2' + \ . ' -iprefix prefix -iwithprefix prefix2 -iwithprefixbefore prefix3' + \ . ' -isysroot sysroot --sysroot=test --no-sysroot-suffix -imultilib multidir' + \ . ' -Wsome-warning -std=c89 -pedantic -pedantic-errors -ansi' + \ . ' -foption -O2 -C -CC -trigraphs -nostdinc -nostdinc++' + \ . ' -iplugindir=dir -march=native -w' + \ ) + +Execute(CFlags we dont want to pass): + AssertEqual + \ '', + \ ale#c#ParseCFlags( + \ ale#path#Simplify(g:dir. '/test_c_projects/makefile_project'), + \ 'gcc -Wl,option -Wa,option -Wp,option filename.c somelib.a ' + \ . '-fdump-file=name -fdiagnostics-arg -fno-show-column' + \ ) diff --git a/test/test_code_action.vader b/test/test_code_action.vader new file mode 100644 index 00000000..ffaca630 --- /dev/null +++ b/test/test_code_action.vader @@ -0,0 +1,334 @@ +Before: + runtime autoload/ale/code_action.vim + runtime autoload/ale/util.vim + + let g:file1 = tempname() + let g:file2 = tempname() + let g:test = {} + + let g:test.create_change = {line, offset, end_line, end_offset, value -> + \{ + \ 'changes': [{ + \ 'fileName': g:file1, + \ 'textChanges': [{ + \ 'start': { + \ 'line': line, + \ 'offset': offset, + \ }, + \ 'end': { + \ 'line': end_line, + \ 'offset': end_offset, + \ }, + \ 'newText': value, + \ }], + \ }] + \}} + + function! WriteFileAndEdit() abort + let g:test.text = [ + \ 'class Name {', + \ ' value: string', + \ '}', + \] + call writefile(g:test.text, g:file1, 'S') + execute 'edit ' . g:file1 + endfunction! + +After: + " Close the extra buffers if we opened it. + if bufnr(g:file1) != -1 + execute ':bp | :bd ' . bufnr(g:file1) + endif + if bufnr(g:file2) != -1 + execute ':bp | :bd ' . bufnr(g:file2) + endif + + if filereadable(g:file1) + call delete(g:file1) + endif + if filereadable(g:file2) + call delete(g:file2) + endif + + unlet g:file1 + unlet g:file2 + unlet g:test + delfunction WriteFileAndEdit + + runtime autoload/ale/code_action.vim + runtime autoload/ale/util.vim + + +Execute(It should modify and save multiple files): + call writefile([ + \ 'class Name {', + \ ' value: string', + \ '}', + \ '', + \ 'class B {', + \ ' constructor(readonly a: Name) {}', + \ '}' + \], g:file1, 'S') + call writefile([ + \ 'import A from "A"', + \ 'import {', + \ ' B,', + \ ' C,', + \ '} from "module"', + \ 'import D from "D"', + \], g:file2, 'S') + + call ale#code_action#HandleCodeAction({ + \ 'changes': [{ + \ 'fileName': g:file1, + \ 'textChanges': [{ + \ 'start': { + \ 'line': 1, + \ 'offset': 7, + \ }, + \ 'end': { + \ 'line': 1, + \ 'offset': 11, + \ }, + \ 'newText': 'Value', + \ }, { + \ 'start': { + \ 'line': 6, + \ 'offset': 27, + \ }, + \ 'end': { + \ 'line': 6, + \ 'offset': 31, + \ }, + \ 'newText': 'Value', + \ }], + \ }, { + \ 'fileName': g:file2, + \ 'textChanges': [{ + \ 'start': { + \ 'line': 2, + \ 'offset': 1, + \ }, + \ 'end': { + \ 'line': 6, + \ 'offset': 1, + \ }, + \ 'newText': "import {A, B} from 'module'\n\n", + \ }] + \ }], + \}) + + AssertEqual [ + \ 'class Value {', + \ ' value: string', + \ '}', + \ '', + \ 'class B {', + \ ' constructor(readonly a: Value) {}', + \ '}', + \ '', + \], readfile(g:file1, 'b') + + AssertEqual [ + \ 'import A from "A"', + \ 'import {A, B} from ''module''', + \ '', + \ 'import D from "D"', + \ '', + \], readfile(g:file2, 'b') + + +Execute(Beginning of file can be modified): + let g:test.text = [ + \ 'class Name {', + \ ' value: string', + \ '}', + \] + call writefile(g:test.text, g:file1, 'S') + + call ale#code_action#HandleCodeAction({ + \ 'changes': [{ + \ 'fileName': g:file1, + \ 'textChanges': [{ + \ 'start': { + \ 'line': 1, + \ 'offset': 1, + \ }, + \ 'end': { + \ 'line': 1, + \ 'offset': 1, + \ }, + \ 'newText': "type A: string\ntype B: number\n", + \ }], + \ }] + \}) + + AssertEqual [ + \ 'type A: string', + \ 'type B: number', + \] + g:test.text + [''], readfile(g:file1, 'b') + + +Execute(End of file can be modified): + let g:test.text = [ + \ 'class Name {', + \ ' value: string', + \ '}', + \] + call writefile(g:test.text, g:file1, 'S') + + call ale#code_action#HandleCodeAction({ + \ 'changes': [{ + \ 'fileName': g:file1, + \ 'textChanges': [{ + \ 'start': { + \ 'line': 4, + \ 'offset': 1, + \ }, + \ 'end': { + \ 'line': 4, + \ 'offset': 1, + \ }, + \ 'newText': "type A: string\ntype B: number\n", + \ }], + \ }] + \}) + + AssertEqual g:test.text + [ + \ 'type A: string', + \ 'type B: number', + \ '', + \], readfile(g:file1, 'b') + + +Execute(Current buffer contents will be reloaded): + let g:test.text = [ + \ 'class Name {', + \ ' value: string', + \ '}', + \] + call writefile(g:test.text, g:file1, 'S') + + execute 'edit ' . g:file1 + let g:test.buffer = bufnr(g:file1) + + call ale#code_action#HandleCodeAction({ + \ 'changes': [{ + \ 'fileName': g:file1, + \ 'textChanges': [{ + \ 'start': { + \ 'line': 1, + \ 'offset': 1, + \ }, + \ 'end': { + \ 'line': 1, + \ 'offset': 1, + \ }, + \ 'newText': "type A: string\ntype B: number\n", + \ }], + \ }] + \}) + + AssertEqual [ + \ 'type A: string', + \ 'type B: number', + \] + g:test.text + [''], readfile(g:file1, 'b') + + AssertEqual [ + \ 'type A: string', + \ 'type B: number', + \] + g:test.text, getbufline(g:test.buffer, 1, '$') + + +# Tests for cursor repositioning. In comments `=` designates change range, and +# `C` cursor position + +# C === +Execute(Cursor will not move when it is before text change): + call WriteFileAndEdit() + let g:test.changes = g:test.create_change(2, 3, 2, 8, 'value2') + + call setpos('.', [0, 1, 1, 0]) + call ale#code_action#HandleCodeAction(g:test.changes) + AssertEqual [1, 1], getpos('.')[1:2] + + call setpos('.', [0, 2, 2, 0]) + call ale#code_action#HandleCodeAction(g:test.changes) + AssertEqual [2, 2], getpos('.')[1:2] + +# ====C==== +Execute(Cursor column will move to the change end when cursor between start/end): + let g:test.changes = g:test.create_change(2, 3, 2, 8, 'value2') + + for r in range(3, 8) + call WriteFileAndEdit() + call setpos('.', [0, 2, r, 0]) + AssertEqual ' value: string', getline('.') + call ale#code_action#HandleCodeAction(g:test.changes) + AssertEqual ' value2: string', getline('.') + AssertEqual [2, 9], getpos('.')[1:2] + endfor + + +# ====C +Execute(Cursor column will move back when new text is shorter): + call WriteFileAndEdit() + call setpos('.', [0, 2, 8, 0]) + AssertEqual ' value: string', getline('.') + call ale#code_action#HandleCodeAction(g:test.create_change(2, 3, 2, 8, 'val')) + AssertEqual ' val: string', getline('.') + AssertEqual [2, 6], getpos('.')[1:2] + + +# ==== C +Execute(Cursor column will move forward when new text is longer): + call WriteFileAndEdit() + + call setpos('.', [0, 2, 8, 0]) + AssertEqual ' value: string', getline('.') + call ale#code_action#HandleCodeAction(g:test.create_change(2, 3, 2, 8, 'longValue')) + AssertEqual ' longValue: string', getline('.') + AssertEqual [2, 12], getpos('.')[1:2] + +# ========= +# = +# C +Execute(Cursor line will move when updates are happening on lines above): + call WriteFileAndEdit() + call setpos('.', [0, 3, 1, 0]) + AssertEqual '}', getline('.') + call ale#code_action#HandleCodeAction(g:test.create_change(1, 1, 2, 1, "test\ntest\n")) + AssertEqual '}', getline('.') + AssertEqual [4, 1], getpos('.')[1:2] + + +# ========= +# =C +Execute(Cursor line and column will move when change on lines above and just before cursor column): + call WriteFileAndEdit() + call setpos('.', [0, 2, 2, 0]) + AssertEqual ' value: string', getline('.') + call ale#code_action#HandleCodeAction(g:test.create_change(1, 1, 2, 1, "test\ntest\n123")) + AssertEqual '123 value: string', getline('.') + AssertEqual [3, 5], getpos('.')[1:2] + +# ========= +# ======C== +# = +Execute(Cursor line and column will move at the end of changes): + call WriteFileAndEdit() + call setpos('.', [0, 2, 10, 0]) + AssertEqual ' value: string', getline('.') + call ale#code_action#HandleCodeAction(g:test.create_change(1, 1, 3, 1, "test\n")) + AssertEqual '}', getline('.') + AssertEqual [2, 1], getpos('.')[1:2] + +# C == +# === +Execute(Cursor will not move when changes happening on lines >= cursor, but after cursor): + call WriteFileAndEdit() + call setpos('.', [0, 2, 3, 0]) + AssertEqual ' value: string', getline('.') + call ale#code_action#HandleCodeAction(g:test.create_change(2, 10, 3, 1, "number\n")) + AssertEqual ' value: number', getline('.') + AssertEqual [2, 3], getpos('.')[1:2] diff --git a/test/test_highlight_placement.vader b/test/test_highlight_placement.vader index 87ac6073..c062018b 100644 --- a/test/test_highlight_placement.vader +++ b/test/test_highlight_placement.vader @@ -8,6 +8,8 @@ Before: Save g:ale_set_quickfix Save g:ale_set_signs + runtime autoload/ale/highlight.vim + let g:ale_run_synchronously = 1 unlet! g:ale_run_synchronously_callbacks let g:ale_set_highlights = 1 @@ -42,16 +44,54 @@ Before: \] endfunction + let g:has_nvim_highlight = exists('*nvim_buf_add_highlight') && exists('*nvim_buf_clear_namespace') + let g:nvim_highlight_matches = {} + + function! ale#highlight#nvim_buf_clear_namespace(buffer, ns_id, line_start, line_end) abort + if a:line_end != -1 + throw 'nvim api behavior not supported' + endif + + let l:matches = get(g:nvim_highlight_matches, a:buffer, []) + call filter( + \ l:matches, + \ {_, val -> val.pos1[0] < (a:line_start + 1) }, + \) + endfunction + + function! ale#highlight#nvim_buf_add_highlight(buffer, ns_id, hl_group, line, col_start, col_end) abort + if a:col_end == -1 + throw 'nvim api behavior not supported' + endif + + let l:matches = get(g:nvim_highlight_matches, a:buffer, []) + let g:nvim_highlight_matches[a:buffer] = l:matches + + let l:new_match = { + \ 'group': a:hl_group, + \ 'priority': 10, + \ 'pos1': [a:line + 1, a:col_start + 1, a:col_end - a:col_start], + \} + + call add(l:matches, l:new_match) + " sort by line number to emulate getmatches faithfully + call sort(l:matches, {m1, m2 -> m1.pos1[0] - m2.pos1[0]}) + endfunction + " We don't care what the IDs are, just that we have some matches. " The IDs are generated. function! GetMatchesWithoutIDs() abort - let l:list = getmatches() + if g:has_nvim_highlight + return get(g:nvim_highlight_matches, bufnr(''), []) + else + let l:list = getmatches() - for l:item in l:list - call remove(l:item, 'id') - endfor + for l:item in l:list + call remove(l:item, 'id') + endfor - return l:list + return l:list + endif endfunction call ale#linter#Define('testft', { @@ -68,6 +108,8 @@ After: unlet! g:ale_run_synchronously_callbacks unlet! g:items unlet! b:ale_enabled + unlet! g:has_nvim_highlight + unlet! g:nvim_highlight_matches delfunction GenerateResults call ale#linter#Reset() @@ -75,6 +117,8 @@ After: sign unplace * highlight clear SomeOtherGroup + runtime autoload/ale/highlight.vim + Given testft(A Javscript file with warnings/errors): foo bar @@ -102,9 +146,12 @@ Execute(Highlights set by ALE should be removed when buffer cleanup is done): \ {'bufnr': bufnr('%'), 'type': 'E', 'lnum': 3, 'col': 2}, \]) - AssertEqual - \ [{'group': 'ALEError', 'priority': 10, 'pos1': [3, 2, 1]}], - \ GetMatchesWithoutIDs() + if !g:has_nvim_highlight + " This check doesn't work with the new API, for some reason. + AssertEqual + \ [{'group': 'ALEError', 'priority': 10, 'pos1': [3, 2, 1]}], + \ GetMatchesWithoutIDs() + endif call ale#engine#Cleanup(bufnr('%')) @@ -145,31 +192,53 @@ Execute(Only ALE highlights should be restored when buffers are restored): call matchaddpos('SomeOtherGroup', [[1, 1, 1]]) " We should have both highlights. - AssertEqual - \ [ - \ {'group': 'ALEError', 'priority': 10, 'pos1': [3, 2, 1]}, - \ {'group': 'SomeOtherGroup', 'priority': 10, 'pos1': [1, 1, 1]}, - \ ], - \ GetMatchesWithoutIDs() + if g:has_nvim_highlight + " When the newer NeoVim API is used, we don't have to worry about + " other highlights, namespacing is available. + AssertEqual + \ [ + \ {'group': 'ALEError', 'priority': 10, 'pos1': [3, 2, 1]}, + \ ], + \ GetMatchesWithoutIDs() + else + AssertEqual + \ [ + \ {'group': 'ALEError', 'priority': 10, 'pos1': [3, 2, 1]}, + \ {'group': 'SomeOtherGroup', 'priority': 10, 'pos1': [1, 1, 1]}, + \ ], + \ sort(GetMatchesWithoutIDs(), {m1, m2 -> m1.group < m2.group ? -1 : 1}) + endif call ale#highlight#BufferHidden(bufnr('%')) " We should remove our highlight, but not the other one. - AssertEqual - \ [ - \ {'group': 'SomeOtherGroup', 'priority': 10, 'pos1': [1, 1, 1]} - \ ], - \ GetMatchesWithoutIDs() + if g:has_nvim_highlight + AssertEqual [], GetMatchesWithoutIDs() + else + AssertEqual + \ [ + \ {'group': 'SomeOtherGroup', 'priority': 10, 'pos1': [1, 1, 1]} + \ ], + \ GetMatchesWithoutIDs() + endif call ale#highlight#UpdateHighlights() " Our highlight should apper again. - AssertEqual - \ [ - \ {'group': 'SomeOtherGroup', 'priority': 10, 'pos1': [1, 1, 1]}, - \ {'group': 'ALEError', 'priority': 10, 'pos1': [3, 2, 1]}, - \ ], - \ GetMatchesWithoutIDs() + if g:has_nvim_highlight + AssertEqual + \ [ + \ {'group': 'ALEError', 'priority': 10, 'pos1': [3, 2, 1]}, + \ ], + \ GetMatchesWithoutIDs() + else + AssertEqual + \ [ + \ {'group': 'ALEError', 'priority': 10, 'pos1': [3, 2, 1]}, + \ {'group': 'SomeOtherGroup', 'priority': 10, 'pos1': [1, 1, 1]}, + \ ], + \ sort(GetMatchesWithoutIDs(), {m1, m2 -> m1.group < m2.group ? -1 : 1}) + endif Execute(Higlight end columns should set an appropriate size): call ale#highlight#SetHighlights(bufnr('%'), [ @@ -216,32 +285,67 @@ Execute(Highlighting should support errors spanning many lines): call ale#highlight#SetHighlights(bufnr(''), g:items) - " We should set 2 highlights for the item, as we can only add 8 at a time. - AssertEqual - \ [ - \ { - \ 'group': 'ALEError', 'priority': 10, 'pos1': [1, 1, 1073741824], - \ 'pos2': [2], 'pos3': [3], 'pos4': [4], 'pos5': [5], 'pos6': [6], - \ 'pos7': [7], 'pos8': [8], - \ }, - \ { - \ 'group': 'ALEError', 'priority': 10, - \ 'pos1': [9], 'pos2': [10, 1, 3] - \ }, - \ ], - \ GetMatchesWithoutIDs() + if g:has_nvim_highlight + " The newer NeoVim highlight API produces different output. + AssertEqual + \ [ + \ {'group': 'ALEError', 'priority': 10, 'pos1': [1, 1, 1073741824]}, + \ {'group': 'ALEError', 'priority': 10, 'pos1': [2, 1, 1073741824]}, + \ {'group': 'ALEError', 'priority': 10, 'pos1': [3, 1, 1073741824]}, + \ {'group': 'ALEError', 'priority': 10, 'pos1': [4, 1, 1073741824]}, + \ {'group': 'ALEError', 'priority': 10, 'pos1': [5, 1, 1073741824]}, + \ {'group': 'ALEError', 'priority': 10, 'pos1': [6, 1, 1073741824]}, + \ {'group': 'ALEError', 'priority': 10, 'pos1': [7, 1, 1073741824]}, + \ {'group': 'ALEError', 'priority': 10, 'pos1': [8, 1, 1073741824]}, + \ {'group': 'ALEError', 'priority': 10, 'pos1': [9, 1, 1073741824]}, + \ {'group': 'ALEError', 'priority': 10, 'pos1': [10, 1, 3]}, + \ ], + \ GetMatchesWithoutIDs() + else + " We should set 2 highlights for the item, as we can only add 8 at a time. + AssertEqual + \ [ + \ { + \ 'group': 'ALEError', 'priority': 10, 'pos1': [1, 1, 1073741824], + \ 'pos2': [2], 'pos3': [3], 'pos4': [4], 'pos5': [5], 'pos6': [6], + \ 'pos7': [7], 'pos8': [8], + \ }, + \ { + \ 'group': 'ALEError', 'priority': 10, + \ 'pos1': [9], 'pos2': [10, 1, 3] + \ }, + \ ], + \ GetMatchesWithoutIDs() + endif Execute(Highlights should always be cleared when the buffer highlight list is empty): - " Add our highlights and something else. - call matchaddpos('ALEError', [[1, 1, 1]]) - call matchaddpos('SomeOtherGroup', [[1, 1, 1]]) + if g:has_nvim_highlight + " The newer API uses namespacing. We'll emulate it here. + call ale#highlight#nvim_buf_add_highlight( + \ bufnr(''), + \ 1, + \ 'ALEError', + \ 0, + \ 0, + \ 1, + \) + + AssertEqual + \ [{'group': 'ALEError', 'priority': 10, 'pos1': [1, 1, 1]}], + \ GetMatchesWithoutIDs() + else + " Add our highlights and something else. + call matchaddpos('ALEError', [[1, 1, 1]]) + call matchaddpos('SomeOtherGroup', [[1, 1, 1]]) + + AssertEqual + \ [ + \ {'group': 'ALEError', 'priority': 10, 'pos1': [1, 1, 1]}, + \ {'group': 'SomeOtherGroup', 'priority': 10, 'pos1': [1, 1, 1]}, + \ ], + \ GetMatchesWithoutIDs() + endif - AssertEqual - \ [ - \ {'group': 'ALEError', 'priority': 10, 'pos1': [1, 1, 1]}, - \ {'group': 'SomeOtherGroup', 'priority': 10, 'pos1': [1, 1, 1]}, - \ ], - \ GetMatchesWithoutIDs() " Set the List we use for holding highlights for buffers. let b:ale_highlight_items = [] @@ -251,11 +355,13 @@ Execute(Highlights should always be cleared when the buffer highlight list is em call ale#highlight#UpdateHighlights() " Check that we remove our highlights. - AssertEqual - \ [ - \ {'group': 'SomeOtherGroup', 'priority': 10, 'pos1': [1, 1, 1]}, - \ ], - \ GetMatchesWithoutIDs() + if g:has_nvim_highlight + AssertEqual [], GetMatchesWithoutIDs() + else + AssertEqual + \ [{'group': 'SomeOtherGroup', 'priority': 10, 'pos1': [1, 1, 1]}], + \ GetMatchesWithoutIDs() + endif Execute(Highlights should be cleared when ALE is disabled): let g:ale_enabled = 1 @@ -291,16 +397,30 @@ Execute(Line highlights should be set when signs are disabled): \ {'bufnr': bufnr(''), 'type': 'I', 'lnum': 3, 'col': 1}, \]) - AssertEqual - \ [ - \ {'group': 'ALEError', 'priority': 10, 'pos1': [1, 1, 1]}, - \ {'group': 'ALEWarning', 'priority': 10, 'pos1': [2, 1, 1]}, - \ {'group': 'ALEInfo', 'priority': 10, 'pos1': [3, 1, 1]}, - \ {'group': 'aleerrorline', 'priority': 10, 'pos1': [1]}, - \ {'group': 'ALEWarningLine', 'priority': 10, 'pos1': [2]}, - \ {'group': 'ALEInfoLine', 'priority': 10, 'pos1': [3]}, - \ ], - \ GetMatchesWithoutIDs() + if g:has_nvim_highlight + " The output is different with the newer NeoVIM highlight API. + AssertEqual + \ [ + \ {'group': 'ALEError', 'priority': 10, 'pos1': [1, 1, 1]}, + \ {'group': 'ALEErrorLine', 'priority': 10, 'pos1': [1, 1, 1073741824]}, + \ {'group': 'ALEWarning', 'priority': 10, 'pos1': [2, 1, 1]}, + \ {'group': 'ALEWarningLine', 'priority': 10, 'pos1': [2, 1, 1073741824]}, + \ {'group': 'ALEInfo', 'priority': 10, 'pos1': [3, 1, 1]}, + \ {'group': 'ALEInfoLine', 'priority': 10, 'pos1': [3, 1, 1073741824]} + \ ], + \ GetMatchesWithoutIDs() + else + AssertEqual + \ [ + \ {'group': 'ALEError', 'priority': 10, 'pos1': [1, 1, 1]}, + \ {'group': 'ALEWarning', 'priority': 10, 'pos1': [2, 1, 1]}, + \ {'group': 'ALEInfo', 'priority': 10, 'pos1': [3, 1, 1]}, + \ {'group': 'aleerrorline', 'priority': 10, 'pos1': [1]}, + \ {'group': 'ALEWarningLine', 'priority': 10, 'pos1': [2]}, + \ {'group': 'ALEInfoLine', 'priority': 10, 'pos1': [3]}, + \ ], + \ GetMatchesWithoutIDs() + endif " All of the highlights should be removed. call ale#highlight#RemoveHighlights() diff --git a/test/test_organize_imports.vader b/test/test_organize_imports.vader new file mode 100644 index 00000000..137326a9 --- /dev/null +++ b/test/test_organize_imports.vader @@ -0,0 +1,171 @@ +Before: + call ale#test#SetDirectory('/testplugin/test') + call ale#test#SetFilename('dummy.txt') + + 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/organize_imports.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) abort + let g:handle_code_action_called = 1 + call add(g:code_actions, a:code_action) + endfunction + +After: + if g:conn_id isnot v:null + call ale#lsp#RemoveConnectionWithID(g:conn_id) + endif + + call ale#references#SetMap({}) + 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/organize_imports.vim + runtime autoload/ale/code_action.vim + +Execute(Other messages for the tsserver handler should be ignored): + call ale#organize_imports#HandleTSServerResponse(1, {'command': 'foo'}) + AssertEqual g:handle_code_action_called, 0 + +Execute(Failed organizeImports responses should be handled correctly): + call ale#organize_imports#HandleTSServerResponse( + \ 1, + \ {'command': 'organizeImports', 'request_seq': 3} + \) + AssertEqual g:handle_code_action_called, 0 + +Execute(Code actions from tsserver should be handled): + call ale#organize_imports#HandleTSServerResponse(1, { + \ 'command': 'organizeImports', + \ 'request_seq': 3, + \ 'success': v:true, + \ 'body': [], + \}) + AssertEqual g:handle_code_action_called, 1 + AssertEqual [{ + \ 'description': 'Organize Imports', + \ 'changes': [], + \}], g:code_actions + +Given typescript(Some typescript file): + foo + somelongerline + bazxyzxyzxyz + +Execute(tsserver organize imports requests should be sent): + call ale#linter#Reset() + runtime ale_linters/typescript/tsserver.vim + + ALEOrganizeImports + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual + \ 'function(''ale#organize_imports#HandleTSServerResponse'')', + \ string(g:Callback) + + AssertEqual + \ [ + \ ale#lsp#tsserver_message#Change(bufnr('')), + \ [0, 'ts@organizeImports', { + \ 'scope': { + \ 'type': 'file', + \ 'args': { + \ 'file': expand('%:p'), + \ }, + \ }, + \ }] + \ ], + \ g:message_list + +Given python(Some Python file): + foo + somelongerline + bazxyzxyzxyz + +Execute(Should result in error message): + call ale#linter#Reset() + runtime ale_linters/python/pyls.vim + let b:ale_linters = ['pyls'] + + ALEOrganizeImports + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual [ + \ 'echom ''OrganizeImports currently only works with tsserver''', + \], g:expr_list diff --git a/test/test_rename.vader b/test/test_rename.vader new file mode 100644 index 00000000..98e3ef30 --- /dev/null +++ b/test/test_rename.vader @@ -0,0 +1,394 @@ +Before: + call ale#test#SetDirectory('/testplugin/test') + call ale#test#SetFilename('dummy.txt') + + 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/rename.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) abort + let g:handle_code_action_called = 1 + call add(g:code_actions, a:code_action) + endfunction + + function! ale#util#Input(message, value) abort + return 'a-new-name' + endfunction + + call ale#rename#SetMap({ + \ 3: { + \ 'old_name': 'oldName', + \ 'new_name': 'aNewName', + \ }, + \}) + +After: + if g:conn_id isnot v:null + call ale#lsp#RemoveConnectionWithID(g:conn_id) + endif + + call ale#rename#SetMap({}) + 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/rename.vim + runtime autoload/ale/code_action.vim + +Execute(Other messages for the tsserver handler should be ignored): + call ale#rename#HandleTSServerResponse(1, {'command': 'foo'}) + AssertEqual g:handle_code_action_called, 0 + +Execute(Failed rename responses should be handled correctly): + call ale#rename#SetMap({3: {'old_name': 'oldName', 'new_name': 'a-test'}}) + call ale#rename#HandleTSServerResponse( + \ 1, + \ {'command': 'rename', 'request_seq': 3} + \) + AssertEqual g:handle_code_action_called, 0 + +Given typescript(Some typescript file): + foo + somelongerline + bazxyzxyzxyz + +Execute(Code actions from tsserver should be handled): + call ale#rename#HandleTSServerResponse(1, { + \ 'command': 'rename', + \ 'request_seq': 3, + \ 'success': v:true, + \ 'body': { + \ 'locs': [ + \ { + \ 'file': '/foo/bar/file1.ts', + \ 'locs': [ + \ { + \ 'start': { + \ 'line': 1, + \ 'offset': 2, + \ }, + \ 'end': { + \ 'line': 3, + \ 'offset': 4, + \ }, + \ }, + \ ], + \ }, + \ { + \ 'file': '/foo/bar/file2.ts', + \ 'locs': [ + \ { + \ 'start': { + \ 'line': 10, + \ 'offset': 20, + \ }, + \ 'end': { + \ 'line': 30, + \ 'offset': 40, + \ }, + \ }, + \ ], + \ }, + \ ] + \ }, + \}) + + AssertEqual + \ [ + \ { + \ 'description': 'rename', + \ 'changes': [ + \ { + \ 'fileName': '/foo/bar/file1.ts', + \ 'textChanges': [ + \ { + \ 'start': { + \ 'line': 1, + \ 'offset': 2, + \ }, + \ 'end': { + \ 'line': 3, + \ 'offset': 4, + \ }, + \ 'newText': 'aNewName', + \ }, + \ ], + \ }, + \ { + \ 'fileName': '/foo/bar/file2.ts', + \ 'textChanges': [ + \ { + \ 'start': { + \ 'line': 10, + \ 'offset': 20, + \ }, + \ 'end': { + \ 'line': 30, + \ 'offset': 40, + \ }, + \ 'newText': 'aNewName', + \ }, + \ ], + \ }, + \ ], + \ } + \ ], + \ g:code_actions + +Execute(HandleTSServerResponse does nothing when no data in rename_map): + call ale#rename#HandleTSServerResponse(1, { + \ 'command': 'rename', + \ 'request_seq': -9, + \ 'success': v:true, + \ 'body': {} + \}) + + AssertEqual g:handle_code_action_called, 0 + +Execute(Prints a tsserver error message when unsuccessful): + call ale#rename#HandleTSServerResponse(1, { + \ 'command': 'rename', + \ 'request_seq': 3, + \ 'success': v:false, + \ 'message': 'This symbol cannot be renamed', + \}) + + AssertEqual g:handle_code_action_called, 0 + AssertEqual ['echom ''Error renaming "oldName" to: "aNewName". ' . + \ 'Reason: This symbol cannot be renamed'''], g:expr_list + +Execute(Does nothing when no changes): + call ale#rename#HandleTSServerResponse(1, { + \ 'command': 'rename', + \ 'request_seq': 3, + \ 'success': v:true, + \ 'body': { + \ 'locs': [] + \ } + \}) + + AssertEqual g:handle_code_action_called, 0 + AssertEqual ['echom ''Error renaming "oldName" to: "aNewName"'''], g:expr_list + +Execute(tsserver rename requests should be sent): + call ale#rename#SetMap({}) + call ale#linter#Reset() + + runtime ale_linters/typescript/tsserver.vim + call setpos('.', [bufnr(''), 2, 5, 0]) + + ALERename + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'rename', g:capability_checked + AssertEqual + \ 'function(''ale#rename#HandleTSServerResponse'')', + \ string(g:Callback) + AssertEqual + \ [ + \ ale#lsp#tsserver_message#Change(bufnr('')), + \ [0, 'ts@rename', { + \ 'file': expand('%:p'), + \ 'line': 2, + \ 'offset': 5, + \ 'arguments': { + \ 'findInComments': g:ale_rename_tsserver_find_in_comments, + \ 'findInStrings': g:ale_rename_tsserver_find_in_strings, + \ }, + \ }] + \ ], + \ g:message_list + AssertEqual {'42': {'old_name': 'somelongerline', 'new_name': 'a-new-name'}}, + \ ale#rename#GetMap() + +Given python(Some Python file): + foo + somelongerline + bazxyzxyzxyz + +Execute(Code actions from LSP should be handled): + call ale#rename#HandleLSPResponse(1, { + \ 'id': 3, + \ 'result': { + \ 'changes': { + \ 'file:///foo/bar/file1.ts': [ + \ { + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 2, + \ }, + \ 'end': { + \ 'line': 3, + \ 'character': 4, + \ }, + \ }, + \ 'newText': 'bla123' + \ }, + \ ], + \ }, + \ }, + \}) + + AssertEqual + \ [ + \ { + \ 'description': 'rename', + \ 'changes': [ + \ { + \ 'fileName': '/foo/bar/file1.ts', + \ 'textChanges': [ + \ { + \ 'start': { + \ 'line': 2, + \ 'offset': 3, + \ }, + \ 'end': { + \ 'line': 4, + \ 'offset': 5, + \ }, + \ 'newText': 'bla123', + \ }, + \ ], + \ }, + \ ], + \ } + \ ], + \ g:code_actions + +Execute(LSP should perform no action when no result): + call ale#rename#HandleLSPResponse(1, { + \ 'id': 3, + \}) + + AssertEqual g:handle_code_action_called, 0 + AssertEqual ['echom ''No rename result received from server'''], g:expr_list + +Execute(LSP should perform no action when no changes): + call ale#rename#HandleLSPResponse(1, { + \ 'id': 3, + \ 'result': {}, + \}) + + AssertEqual g:handle_code_action_called, 0 + AssertEqual ['echom ''No changes received from server'''], g:expr_list + +Execute(LSP should perform no action when changes is empty): + call ale#rename#HandleLSPResponse(1, { + \ 'id': 3, + \ 'result': { + \ 'changes': [], + \ }, + \}) + + AssertEqual g:handle_code_action_called, 0 + AssertEqual ['echom ''No changes received from server'''], g:expr_list + +Execute(LSP rename requests should be sent): + call ale#rename#SetMap({}) + runtime ale_linters/python/pyls.vim + let b:ale_linters = ['pyls'] + call setpos('.', [bufnr(''), 1, 5, 0]) + + ALERename + + " We shouldn't register the callback yet. + AssertEqual '''''', string(g:Callback) + + AssertEqual type(function('type')), type(g:InitCallback) + call g:InitCallback() + + AssertEqual 'rename', g:capability_checked + AssertEqual + \ 'function(''ale#rename#HandleLSPResponse'')', + \ string(g:Callback) + + AssertEqual + \ [ + \ [1, 'textDocument/didChange', { + \ 'textDocument': { + \ 'uri': ale#path#ToURI(expand('%:p')), + \ 'version': g:ale_lsp_next_version_id - 1, + \ }, + \ 'contentChanges': [{'text': join(getline(1, '$'), "\n") . "\n"}] + \ }], + \ [0, 'textDocument/rename', { + \ 'textDocument': {'uri': ale#path#ToURI(expand('%:p'))}, + \ 'position': {'line': 0, 'character': 2}, + \ 'newName': 'a-new-name', + \ }], + \ ], + \ g:message_list + + AssertEqual {'42': {'old_name': 'foo', 'new_name': 'a-new-name'}}, + \ ale#rename#GetMap() |