Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ALERename (tsserver & LSP), ALEOrganizeImports (tsserver) and auto import support (tsserver) #2709

Merged
merged 56 commits into from
Sep 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
5eb8d39
Add preliminary support for renaming symbols
mwilliammyers Sep 4, 2018
4487725
Merge branch 'master' into feature/renaming-symbols
jeremija Aug 14, 2019
2e6e77a
Add ability to rename variables in TypeScript files
jeremija Aug 14, 2019
83b48a5
Rename: Automatically save (and close newly opened) files
jeremija Aug 14, 2019
075418d
Fix a:new_name variable does not change on 2nd run
jeremija Aug 14, 2019
f022358
Add support for automatic imports
jeremija Aug 15, 2019
f3ba394
Fix rename stops working after VISUAL LINE
jeremija Aug 15, 2019
702618b
Add documentation for ALERename and tsserver autoimport
jeremija Aug 15, 2019
0b3de88
Fix broken test
jeremija Aug 15, 2019
bb8048b
Add right completion message
jeremija Aug 15, 2019
b5305f0
Add tests for tsserver completion modifications
jeremija Aug 16, 2019
a7f776d
Add augroup ALECompletionActions
jeremija Aug 16, 2019
e01fead
Add test for HandleCodeAction
jeremija Aug 16, 2019
15f0e0f
Fix linting errors
jeremija Aug 16, 2019
1eef4c5
Call right method for LSP Rename
jeremija Aug 16, 2019
a4327cc
Use ale#util#Execute in code_action
jeremija Aug 16, 2019
a8d5139
Add first test for autoload/ale/rename.vim
jeremija Aug 16, 2019
070a749
Rename g:ale_tsserver_include_external to g:ale_tsserver_autoimport
jeremija Aug 16, 2019
2badf51
Rename g:ale_tsserver_automiport to g:ale_completion_tsserver_autoimport
jeremija Aug 16, 2019
6d3516a
Add more tests for rename
jeremija Aug 16, 2019
c96eb1b
Fix broken test_completion_events.vader in vim 8.0
jeremija Aug 17, 2019
200a29a
Add test/test_code_action.vader
jeremija Aug 17, 2019
8143f35
Add support for TypeScript organizeImports
jeremija Aug 17, 2019
c4f8a80
Add ale#util#IsModified(buffer)
jeremija Aug 17, 2019
e213d75
Fix linting errors in autoload/ale/organize_imports.vim
jeremija Aug 17, 2019
f3a9fd8
Add test for organize_imports
jeremija Aug 17, 2019
5d47840
Add ALEOrganizeImports command
jeremija Aug 17, 2019
0871cca
Turn off tsserver autoimport by default
jeremija Aug 19, 2019
ddd62df
Add two blank lines in doc/ale.txt
jeremija Aug 19, 2019
9905b6a
Call s:OrganizeImports when iterating over linters
jeremija Aug 19, 2019
ed93090
Disable findInComments and findInStrings by default
jeremija Aug 19, 2019
fd8e2d0
Print rename symbol no changes received
jeremija Aug 19, 2019
726d9b5
Remove unneccessary l:completions variable
jeremija Aug 19, 2019
4b746ea
Use empty when checking for user_data_json
jeremija Aug 19, 2019
65fe1fb
Use default value when checking for codeActions
jeremija Aug 19, 2019
c8dd766
Remove commented-out echom test code
jeremija Aug 19, 2019
193765a
Add back autocmd! to beginning of ALECompletionGroup
jeremija Aug 19, 2019
74bb384
Add Author and Description
jeremija Aug 19, 2019
b3247df
Fix linting error
jeremija Aug 19, 2019
d6321d7
Do not use visual block in code_action.vim
jeremija Aug 20, 2019
9bb19b7
Add two more cursor tests
jeremija Aug 20, 2019
a7fb8c6
Add more cursor tests to test_code_action.vader
jeremija Aug 21, 2019
17c8ec1
Prompt user for new name when using ALERename
jeremija Aug 21, 2019
2e1727a
Fix linting rules
jeremija Aug 21, 2019
dc35bcd
Remove drop command from test
jeremija Aug 21, 2019
5e750e3
Make rename errors more descriptive
jeremija Aug 21, 2019
8fce6c4
Fix doc about tsserver autoimport in README.md
jeremija Aug 21, 2019
8a192cd
Echo message when {"success": false} for ALERename
jeremija Aug 21, 2019
6169226
Move mock of ale#util#Input to test_rename.vader
jeremija Aug 21, 2019
36dcbee
Do not run e! multiple times for same buffer
jeremija Aug 21, 2019
75ee313
Use <cword> instead of <cWORD> for ALERename
jeremija Aug 21, 2019
8845d6f
Parse filename URI when processing LSP rename response
jeremija Aug 22, 2019
643682a
Test for line contents in test_code_action.vader
jeremija Aug 22, 2019
723809f
Enable duplicates in autocompletion with tsserver autoimport
jeremija Sep 5, 2019
499b0be
Apply cursor line offset only once when moving cursor
jeremija Sep 6, 2019
d776758
Fix broken test
jeremija Sep 6, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
163 changes: 163 additions & 0 deletions autoload/ale/code_action.vim
Original file line number Diff line number Diff line change
@@ -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
59 changes: 53 additions & 6 deletions autoload/ale/completion.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -287,7 +288,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
Expand Down Expand Up @@ -321,13 +325,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({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try removing json_encode here and json_decode below. I think you can store Dictionary values too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought so too, but I get E731: using Dictionary as a String when I remove the json_encode/json_decode functions :(

\ 'codeActions': l:suggestion.codeActions,
\ })
endif

call add(l:results, l:result)
endfor

let l:names = getbufvar(l:buffer, 'ale_tsserver_completion_names', [])
Expand All @@ -336,12 +349,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': '',
Expand Down Expand Up @@ -463,13 +476,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
Expand Down Expand Up @@ -516,6 +538,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
Expand Down Expand Up @@ -670,6 +693,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

Expand All @@ -678,6 +721,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this logic into the existing ale#completion#Done() function instead. You can start passing v:completed_item to that.

Copy link
Contributor Author

@jeremija jeremija Aug 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that ale#completion#Done() will not be called when automatic completion is disabled, and the completion is triggered manually (via a hotkey). That's why I created a new group. Perhaps I can modify the s:Setup to look like:

function! s:Setup(enabled) abort
    augroup ALECompletionGroup
        autocmd!
        if a:enabled
            autocmd TextChangedI * call ale#completion#Queue()
            autocmd CompleteDone * call ale#completion#Done(v:completed_item)
        elseif g:ale_completion_tsserver_autoimport
            autocmd CompleteDone * call ale#completion#HandleUserData(v:completed_item)
        endif
    augroup END

    if !a:enabled && !g:ale_completion_tsserver_autoimport
        augroup! ALECompletionGroup
    endif
endfunction

What do you think?

augroup END

function! s:Setup(enabled) abort
augroup ALECompletionGroup
autocmd!
jeremija marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
6 changes: 6 additions & 0 deletions autoload/ale/lsp.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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': [],
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions autoload/ale/lsp/message.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 27 additions & 1 deletion autoload/ale/lsp/tsserver_message.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Loading