diff options
| author | Eric Stern | 2026-05-31 09:59:23 -0700 |
|---|---|---|
| committer | GitHub | 2026-05-31 17:59:23 +0100 |
| commit | 36d541facbcaa3c947bd19ce85a7b854f9257598 (patch) | |
| tree | ab7ad218a956c4a2f717254eca0deca8aa7fd298 | |
| parent | 7a7fc85e519ebe3eae0246445592e9636a0791ad (diff) | |
| download | ale-36d541facbcaa3c947bd19ce85a7b854f9257598.tar.gz | |
Read trigger characters from LSP initialize responses (#5121)
- 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.
| -rw-r--r-- | autoload/ale/completion.vim | 42 | ||||
| -rw-r--r-- | autoload/ale/lsp.vim | 30 | ||||
| -rw-r--r-- | test/completion/test_completion_filtering.vader | 58 | ||||
| -rw-r--r-- | test/lsp/test_other_initialize_message_handling.vader | 48 |
4 files changed, 173 insertions, 5 deletions
diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index a435eb13f..bd49f96eb 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -145,6 +145,7 @@ let s:omni_start_map = { " 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. +" These are used as a fallback when LSP servers don't provide trigger chars. let s:trigger_character_map = { \ '<default>': ['.'], \ 'typescript': ['.', '''', '"'], @@ -153,6 +154,19 @@ let s:trigger_character_map = { \ '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 for l:part in reverse(split(a:filetype, '\.')) let l:regex = get(a:map, l:part, []) @@ -175,15 +189,32 @@ function! ale#completion#GetPrefix(filetype, line, column) abort " abc " ^ " 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 -function! ale#completion#GetTriggerCharacter(filetype, prefix) abort +function! ale#completion#GetTriggerCharacter(filetype, prefix, ...) abort if empty(a:prefix) return '' 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 return a:prefix @@ -204,7 +235,8 @@ function! ale#completion#Filter( if empty(a:prefix) let l:filtered_suggestions = a:suggestions 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... " foo. @@ -805,7 +837,7 @@ function! s:OnReady(linter, lsp_details) abort \ l:buffer, \ b:ale_completion_info.line, \ 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 diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index 7103879ba..5b961d30a 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -1010,3 +1010,33 @@ function! ale#lsp#HasCapability(conn_id, capability) abort return l:conn.capabilities[a:capability] 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 diff --git a/test/completion/test_completion_filtering.vader b/test/completion/test_completion_filtering.vader index 172203a48..de73787d4 100644 --- a/test/completion/test_completion_filtering.vader +++ b/test/completion/test_completion_filtering.vader @@ -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(''), '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 diff --git a/test/lsp/test_other_initialize_message_handling.vader b/test/lsp/test_other_initialize_message_handling.vader index 2d1ffbe67..05392275b 100644 --- a/test/lsp/test_other_initialize_message_handling.vader +++ b/test/lsp/test_other_initialize_message_handling.vader @@ -343,3 +343,51 @@ Execute(Results that are not dictionaries should be handled correctly): \ 'result': v:null, \}) 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) |