mirror of
https://github.com/dense-analysis/ale.git
synced 2026-08-12 19:06:53 +08:00
Read trigger characters from LSP initialize responses (#5121)
CI / Build (push) Has been cancelled
CI / Neovim 0.10 Windows (push) Has been cancelled
CI / Neovim 0.12 Windows (push) Has been cancelled
CI / Vim 8.2 Windows (push) Has been cancelled
CI / Vim 9.2 Windows (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Lua (push) Has been cancelled
CI / Neovim 0.10 Linux (push) Has been cancelled
CI / Neovim 0.12 Linux (push) Has been cancelled
CI / Vim 8.2 Linux (push) Has been cancelled
CI / Vim 9.2 Linux (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Neovim 0.10 Windows (push) Has been cancelled
CI / Neovim 0.12 Windows (push) Has been cancelled
CI / Vim 8.2 Windows (push) Has been cancelled
CI / Vim 9.2 Windows (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Lua (push) Has been cancelled
CI / Neovim 0.10 Linux (push) Has been cancelled
CI / Neovim 0.12 Linux (push) Has been cancelled
CI / Vim 8.2 Linux (push) Has been cancelled
CI / Vim 9.2 Linux (push) Has been cancelled
* Add ale#lsp#GetCompletionTriggerCharacters getter * Add s:GetTriggerCharacters helper with LSP support * Pass connection ID to GetTriggerCharacter in s:OnReady * Use LSP trigger characters in Filter function * Add tests for LSP completion trigger characters * Check LSP trigger characters in GetPrefix * Add tests for GetAllCompletionTriggerCharactersForBuffer GetTriggerCharacter now accepts optional conn_id parameter to prefer LSP-provided trigger characters over the hardcoded map. GetPrefix now checks if the line ends with any trigger character from LSPs active for the current buffer, enabling automatic completion for LSP-provided triggers like > for PHP.
This commit is contained in:
@@ -145,6 +145,7 @@ let s:omni_start_map = {
|
|||||||
|
|
||||||
" A map of exact characters for triggering LSP completions. Do not forget to
|
" A map of exact characters for triggering LSP completions. Do not forget to
|
||||||
" update self.input_patterns in ale.py in updating entries in this map.
|
" update self.input_patterns in ale.py in updating entries in this map.
|
||||||
|
" These are used as a fallback when LSP servers don't provide trigger chars.
|
||||||
let s:trigger_character_map = {
|
let s:trigger_character_map = {
|
||||||
\ '<default>': ['.'],
|
\ '<default>': ['.'],
|
||||||
\ 'typescript': ['.', '''', '"'],
|
\ 'typescript': ['.', '''', '"'],
|
||||||
@@ -153,6 +154,19 @@ let s:trigger_character_map = {
|
|||||||
\ 'c': ['.', '->'],
|
\ 'c': ['.', '->'],
|
||||||
\}
|
\}
|
||||||
|
|
||||||
|
" Get trigger characters, preferring LSP-provided ones over hardcoded.
|
||||||
|
function! s:GetTriggerCharacters(filetype, conn_id) abort
|
||||||
|
if !empty(a:conn_id)
|
||||||
|
let l:lsp_triggers = ale#lsp#GetCompletionTriggerCharacters(a:conn_id)
|
||||||
|
|
||||||
|
if !empty(l:lsp_triggers)
|
||||||
|
return l:lsp_triggers
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
return s:GetFiletypeValue(s:trigger_character_map, a:filetype)
|
||||||
|
endfunction
|
||||||
|
|
||||||
function! s:GetFiletypeValue(map, filetype) abort
|
function! s:GetFiletypeValue(map, filetype) abort
|
||||||
for l:part in reverse(split(a:filetype, '\.'))
|
for l:part in reverse(split(a:filetype, '\.'))
|
||||||
let l:regex = get(a:map, l:part, [])
|
let l:regex = get(a:map, l:part, [])
|
||||||
@@ -175,15 +189,32 @@ function! ale#completion#GetPrefix(filetype, line, column) abort
|
|||||||
" abc
|
" abc
|
||||||
" ^
|
" ^
|
||||||
" So we need check the text in the column before that position.
|
" So we need check the text in the column before that position.
|
||||||
return matchstr(getline(a:line)[: a:column - 2], l:regex)
|
let l:line_text = getline(a:line)[: a:column - 2]
|
||||||
|
let l:prefix = matchstr(l:line_text, l:regex)
|
||||||
|
|
||||||
|
if !empty(l:prefix)
|
||||||
|
return l:prefix
|
||||||
|
endif
|
||||||
|
|
||||||
|
" Check LSP trigger characters for active connections on this buffer.
|
||||||
|
let l:triggers = ale#lsp#GetAllCompletionTriggerCharactersForBuffer(bufnr(''))
|
||||||
|
|
||||||
|
for l:char in l:triggers
|
||||||
|
if l:line_text[-len(l:char):] is# l:char
|
||||||
|
return l:char
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
|
||||||
|
return ''
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! ale#completion#GetTriggerCharacter(filetype, prefix) abort
|
function! ale#completion#GetTriggerCharacter(filetype, prefix, ...) abort
|
||||||
if empty(a:prefix)
|
if empty(a:prefix)
|
||||||
return ''
|
return ''
|
||||||
endif
|
endif
|
||||||
|
|
||||||
let l:char_list = s:GetFiletypeValue(s:trigger_character_map, a:filetype)
|
let l:conn_id = get(a:, 1, '')
|
||||||
|
let l:char_list = s:GetTriggerCharacters(a:filetype, l:conn_id)
|
||||||
|
|
||||||
if index(l:char_list, a:prefix) >= 0
|
if index(l:char_list, a:prefix) >= 0
|
||||||
return a:prefix
|
return a:prefix
|
||||||
@@ -204,7 +235,8 @@ function! ale#completion#Filter(
|
|||||||
if empty(a:prefix)
|
if empty(a:prefix)
|
||||||
let l:filtered_suggestions = a:suggestions
|
let l:filtered_suggestions = a:suggestions
|
||||||
else
|
else
|
||||||
let l:triggers = s:GetFiletypeValue(s:trigger_character_map, a:filetype)
|
let l:conn_id = get(get(b:, 'ale_completion_info', {}), 'conn_id', '')
|
||||||
|
let l:triggers = s:GetTriggerCharacters(a:filetype, l:conn_id)
|
||||||
|
|
||||||
" For completing...
|
" For completing...
|
||||||
" foo.
|
" foo.
|
||||||
@@ -805,7 +837,7 @@ function! s:OnReady(linter, lsp_details) abort
|
|||||||
\ l:buffer,
|
\ l:buffer,
|
||||||
\ b:ale_completion_info.line,
|
\ b:ale_completion_info.line,
|
||||||
\ b:ale_completion_info.column,
|
\ b:ale_completion_info.column,
|
||||||
\ ale#completion#GetTriggerCharacter(&filetype, b:ale_completion_info.prefix),
|
\ ale#completion#GetTriggerCharacter(&filetype, b:ale_completion_info.prefix, l:id),
|
||||||
\)
|
\)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|||||||
@@ -1010,3 +1010,33 @@ function! ale#lsp#HasCapability(conn_id, capability) abort
|
|||||||
|
|
||||||
return l:conn.capabilities[a:capability]
|
return l:conn.capabilities[a:capability]
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
" Get the completion trigger characters for a connection.
|
||||||
|
function! ale#lsp#GetCompletionTriggerCharacters(conn_id) abort
|
||||||
|
let l:conn = get(s:connections, a:conn_id, {})
|
||||||
|
|
||||||
|
if empty(l:conn)
|
||||||
|
return []
|
||||||
|
endif
|
||||||
|
|
||||||
|
return get(l:conn.capabilities, 'completion_trigger_characters', [])
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" Get all completion trigger characters from LSPs active for a buffer.
|
||||||
|
function! ale#lsp#GetAllCompletionTriggerCharactersForBuffer(buffer) abort
|
||||||
|
let l:all_triggers = []
|
||||||
|
|
||||||
|
for l:conn in values(s:connections)
|
||||||
|
if has_key(l:conn.open_documents, a:buffer)
|
||||||
|
let l:triggers = get(l:conn.capabilities, 'completion_trigger_characters', [])
|
||||||
|
|
||||||
|
for l:char in l:triggers
|
||||||
|
if index(l:all_triggers, l:char) < 0
|
||||||
|
call add(l:all_triggers, l:char)
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
endif
|
||||||
|
endfor
|
||||||
|
|
||||||
|
return l:all_triggers
|
||||||
|
endfunction
|
||||||
|
|||||||
@@ -140,3 +140,61 @@ Execute(Filtering should respect filetype triggers):
|
|||||||
AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), '', b:suggestions, '.', 0)
|
AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), '', b:suggestions, '.', 0)
|
||||||
AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), 'rust', b:suggestions, '.', 0)
|
AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), 'rust', b:suggestions, '.', 0)
|
||||||
AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), 'rust', b:suggestions, '::', 0)
|
AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), 'rust', b:suggestions, '::', 0)
|
||||||
|
|
||||||
|
Execute(GetTriggerCharacter should return trigger characters from hardcoded map):
|
||||||
|
AssertEqual '.', ale#completion#GetTriggerCharacter('python', '.')
|
||||||
|
AssertEqual '::', ale#completion#GetTriggerCharacter('rust', '::')
|
||||||
|
AssertEqual '->', ale#completion#GetTriggerCharacter('c', '->')
|
||||||
|
AssertEqual '', ale#completion#GetTriggerCharacter('python', '@')
|
||||||
|
|
||||||
|
Execute(GetTriggerCharacter should return empty for empty prefix):
|
||||||
|
AssertEqual '', ale#completion#GetTriggerCharacter('python', '')
|
||||||
|
|
||||||
|
Execute(GetTriggerCharacter should use LSP triggers when conn_id provided):
|
||||||
|
call ale#lsp#Register('test-lsp', '/project', '', {})
|
||||||
|
let g:conn_id = 'test-lsp:/project'
|
||||||
|
call ale#lsp#UpdateCapabilities(g:conn_id, {
|
||||||
|
\ 'completionProvider': {'triggerCharacters': ['@', '#']},
|
||||||
|
\})
|
||||||
|
|
||||||
|
" '@' is in LSP triggers
|
||||||
|
AssertEqual '@', ale#completion#GetTriggerCharacter('python', '@', g:conn_id)
|
||||||
|
" '.' is NOT in LSP triggers (should not match even though it's in hardcoded)
|
||||||
|
AssertEqual '', ale#completion#GetTriggerCharacter('python', '.', g:conn_id)
|
||||||
|
" '#' is in LSP triggers
|
||||||
|
AssertEqual '#', ale#completion#GetTriggerCharacter('python', '#', g:conn_id)
|
||||||
|
|
||||||
|
call ale#lsp#RemoveConnectionWithID(g:conn_id)
|
||||||
|
unlet g:conn_id
|
||||||
|
|
||||||
|
Execute(GetTriggerCharacter should fall back to hardcoded when no LSP triggers):
|
||||||
|
call ale#lsp#Register('test-lsp-empty', '/project', '', {})
|
||||||
|
let g:conn_id = 'test-lsp-empty:/project'
|
||||||
|
call ale#lsp#UpdateCapabilities(g:conn_id, {})
|
||||||
|
|
||||||
|
" Falls back to hardcoded map
|
||||||
|
AssertEqual '.', ale#completion#GetTriggerCharacter('python', '.', g:conn_id)
|
||||||
|
AssertEqual '', ale#completion#GetTriggerCharacter('python', '@', g:conn_id)
|
||||||
|
|
||||||
|
call ale#lsp#RemoveConnectionWithID(g:conn_id)
|
||||||
|
unlet g:conn_id
|
||||||
|
|
||||||
|
Execute(Filtering should use LSP trigger characters):
|
||||||
|
call ale#lsp#Register('test-lsp-filter', '/project', '', {})
|
||||||
|
let g:conn_id = 'test-lsp-filter:/project'
|
||||||
|
call ale#lsp#UpdateCapabilities(g:conn_id, {
|
||||||
|
\ 'completionProvider': {'triggerCharacters': ['@']},
|
||||||
|
\})
|
||||||
|
|
||||||
|
" Set up completion info with conn_id
|
||||||
|
let b:ale_completion_info = {'conn_id': g:conn_id}
|
||||||
|
let b:suggestions = [{'word': 'foo'}, {'word': 'bar'}]
|
||||||
|
|
||||||
|
" '@' is LSP trigger - should return all suggestions
|
||||||
|
AssertEqual b:suggestions, ale#completion#Filter(bufnr(''), 'python', b:suggestions, '@', 0)
|
||||||
|
" '.' is NOT LSP trigger - should filter
|
||||||
|
AssertEqual [], ale#completion#Filter(bufnr(''), 'python', b:suggestions, '.', 0)
|
||||||
|
|
||||||
|
unlet b:ale_completion_info
|
||||||
|
call ale#lsp#RemoveConnectionWithID(g:conn_id)
|
||||||
|
unlet g:conn_id
|
||||||
|
|||||||
@@ -343,3 +343,51 @@ Execute(Results that are not dictionaries should be handled correctly):
|
|||||||
\ 'result': v:null,
|
\ 'result': v:null,
|
||||||
\})
|
\})
|
||||||
AssertEqual [], g:message_list
|
AssertEqual [], g:message_list
|
||||||
|
|
||||||
|
Execute(GetCompletionTriggerCharacters should return stored characters):
|
||||||
|
call ale#lsp#HandleInitResponse(b:conn, {
|
||||||
|
\ 'jsonrpc': '2.0',
|
||||||
|
\ 'id': 1,
|
||||||
|
\ 'result': {
|
||||||
|
\ 'capabilities': {
|
||||||
|
\ 'completionProvider': {
|
||||||
|
\ 'triggerCharacters': ['@', '#', '.'],
|
||||||
|
\ },
|
||||||
|
\ },
|
||||||
|
\ },
|
||||||
|
\})
|
||||||
|
|
||||||
|
AssertEqual ['@', '#', '.'], ale#lsp#GetCompletionTriggerCharacters(b:conn.id)
|
||||||
|
|
||||||
|
Execute(GetCompletionTriggerCharacters should return empty for missing connection):
|
||||||
|
AssertEqual [], ale#lsp#GetCompletionTriggerCharacters('nonexistent-connection')
|
||||||
|
|
||||||
|
Execute(GetCompletionTriggerCharacters should return empty when no triggers):
|
||||||
|
call ale#lsp#HandleInitResponse(b:conn, {
|
||||||
|
\ 'jsonrpc': '2.0',
|
||||||
|
\ 'id': 1,
|
||||||
|
\ 'result': {
|
||||||
|
\ 'capabilities': {},
|
||||||
|
\ },
|
||||||
|
\})
|
||||||
|
|
||||||
|
AssertEqual [], ale#lsp#GetCompletionTriggerCharacters(b:conn.id)
|
||||||
|
|
||||||
|
Execute(GetAllCompletionTriggerCharactersForBuffer should return triggers for open buffers):
|
||||||
|
call ale#lsp#HandleInitResponse(b:conn, {
|
||||||
|
\ 'jsonrpc': '2.0',
|
||||||
|
\ 'id': 1,
|
||||||
|
\ 'result': {
|
||||||
|
\ 'capabilities': {
|
||||||
|
\ 'completionProvider': {
|
||||||
|
\ 'triggerCharacters': ['>', '$'],
|
||||||
|
\ },
|
||||||
|
\ },
|
||||||
|
\ },
|
||||||
|
\})
|
||||||
|
|
||||||
|
call ale#lsp#MarkDocumentAsOpen(b:conn.id, 1)
|
||||||
|
AssertEqual sort(['>', '$']), sort(ale#lsp#GetAllCompletionTriggerCharactersForBuffer(1))
|
||||||
|
|
||||||
|
Execute(GetAllCompletionTriggerCharactersForBuffer should return empty for unknown buffer):
|
||||||
|
AssertEqual [], ale#lsp#GetAllCompletionTriggerCharactersForBuffer(99999)
|
||||||
|
|||||||
Reference in New Issue
Block a user