diff --git a/README.md b/README.md index 5a416ac5..13631186 100644 --- a/README.md +++ b/README.md @@ -222,9 +222,7 @@ too. See `:help ale-fix` for detailed information. ALE offers some support for completion via hijacking of omnicompletion while you type. All of ALE's completion information must come from Language Server -Protocol linters, or similar protocols. At the moment, completion is only -supported for TypeScript code with `tsserver`, when `tsserver` is enabled. You -can enable completion like so: +Protocol linters, or from `tsserver` for TypeSript. ```vim " Enable completion where available. diff --git a/ale_linters/python/pyls.vim b/ale_linters/python/pyls.vim index 42c60431..09f31ec6 100644 --- a/ale_linters/python/pyls.vim +++ b/ale_linters/python/pyls.vim @@ -25,4 +25,5 @@ call ale#linter#Define('python', { \ 'command_callback': 'ale_linters#python#pyls#GetCommand', \ 'language_callback': 'ale_linters#python#pyls#GetLanguage', \ 'project_root_callback': 'ale#python#FindProjectRoot', +\ 'completion_filter': 'ale#completion#python#CompletionItemFilter', \}) diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index 7ad7f9da..29b1df1f 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -28,6 +28,7 @@ let s:LSP_COMPLETION_REFERENCE_KIND = 18 " the insert cursor is. If one of these matches, we'll check for completions. let s:should_complete_map = { \ '': '\v[a-zA-Z$_][a-zA-Z$_0-9]*$|\.$', +\ 'rust': '\v[a-zA-Z$_][a-zA-Z$_0-9]*$|\.$|::$', \} " Regular expressions for finding the start column to replace with completion. @@ -38,6 +39,7 @@ let s:omni_start_map = { " A map of exact characters for triggering LSP completions. let s:trigger_character_map = { \ '': ['.'], +\ 'rust': ['.', '::'], \} function! s:GetFiletypeValue(map, filetype) abort @@ -215,7 +217,21 @@ function! ale#completion#ParseTSServerCompletionEntryDetails(response) abort return l:results endfunction +function! ale#completion#NullFilter(buffer, item) abort + return 1 +endfunction + function! ale#completion#ParseLSPCompletions(response) abort + let l:buffer = bufnr('') + let l:info = get(b:, 'ale_completion_info', {}) + let l:Filter = get(l:info, 'completion_filter', v:null) + + if l:Filter is v:null + let l:Filter = function('ale#completion#NullFilter') + else + let l:Filter = ale#util#GetFunction(l:Filter) + endif + let l:item_list = [] if type(get(a:response, 'result')) is type([]) @@ -228,6 +244,16 @@ function! ale#completion#ParseLSPCompletions(response) abort let l:results = [] for l:item in l:item_list + if !call(l:Filter, [l:buffer, l:item]) + continue + endif + + let l:word = matchstr(l:item.label, '\v^[^(]+') + + if empty(l:word) + continue + endif + " See :help complete-items for Vim completion kinds if l:item.kind is s:LSP_COMPLETION_METHOD_KIND let l:kind = 'm' @@ -244,11 +270,11 @@ function! ale#completion#ParseLSPCompletions(response) abort endif call add(l:results, { - \ 'word': l:item.label, + \ 'word': l:word, \ 'kind': l:kind, \ 'icase': 1, \ 'menu': l:item.detail, - \ 'info': l:item.documentation, + \ 'info': get(l:item, 'documentation', ''), \}) endfor @@ -349,6 +375,10 @@ function! s:GetLSPCompletions(linter) abort if l:request_id let b:ale_completion_info.conn_id = l:id let b:ale_completion_info.request_id = l:request_id + + if has_key(a:linter, 'completion_filter') + let b:ale_completion_info.completion_filter = a:linter.completion_filter + endif endif endfunction @@ -378,10 +408,7 @@ function! ale#completion#GetCompletions() abort for l:linter in ale#linter#Get(&filetype) if !empty(l:linter.lsp) - if l:linter.lsp is# 'tsserver' - \|| get(g:, 'ale_completion_experimental_lsp_support', 0) - call s:GetLSPCompletions(l:linter) - endif + call s:GetLSPCompletions(l:linter) endif endfor endfunction diff --git a/autoload/ale/completion/python.vim b/autoload/ale/completion/python.vim new file mode 100644 index 00000000..6b65c5b5 --- /dev/null +++ b/autoload/ale/completion/python.vim @@ -0,0 +1,3 @@ +function! ale#completion#python#CompletionItemFilter(buffer, item) abort + return a:item.label !~# '\v^__[a-z_]+__' +endfunction diff --git a/autoload/ale/linter.vim b/autoload/ale/linter.vim index d059a12d..7e32b7bc 100644 --- a/autoload/ale/linter.vim +++ b/autoload/ale/linter.vim @@ -194,6 +194,14 @@ function! ale#linter#PreProcess(linter) abort if !s:IsCallback(l:obj.project_root_callback) throw '`project_root_callback` must be a callback for LSP linters' endif + + if has_key(a:linter, 'completion_filter') + let l:obj.completion_filter = a:linter.completion_filter + + if !s:IsCallback(l:obj.completion_filter) + throw '`completion_filter` must be a callback' + endif + endif endif let l:obj.output_stream = get(a:linter, 'output_stream', 'stdout') diff --git a/test/completion/test_completion_events.vader b/test/completion/test_completion_events.vader index 49d485f6..fbe18bb6 100644 --- a/test/completion/test_completion_events.vader +++ b/test/completion/test_completion_events.vader @@ -2,12 +2,9 @@ Before: Save g:ale_completion_enabled Save g:ale_completion_delay Save g:ale_completion_max_suggestions - Save g:ale_completion_experimental_lsp_support Save &l:omnifunc Save &l:completeopt - unlet! g:ale_completion_experimental_lsp_support - let g:ale_completion_enabled = 1 let g:get_completions_called = 0 let g:feedkeys_calls = [] @@ -43,7 +40,6 @@ After: unlet! b:ale_completion_response unlet! b:ale_completion_parser unlet! b:ale_complete_done_time - unlet! g:ale_completion_experimental_lsp_support delfunction CheckCompletionCalled diff --git a/test/completion/test_completion_prefixes.vader b/test/completion/test_completion_prefixes.vader index 8ac29326..a88e978d 100644 --- a/test/completion/test_completion_prefixes.vader +++ b/test/completion/test_completion_prefixes.vader @@ -17,3 +17,20 @@ Execute(Completion should not be done after parens in TypeScript): Execute(Completion prefixes should work for other filetypes): AssertEqual 'ab', ale#completion#GetPrefix('xxxyyyzzz', 3, 14) + +Given rust(): + let abc = y. + let abc = String:: + let foo = (ab) + +Execute(Completion should be done after dots in Rust): + AssertEqual '.', ale#completion#GetPrefix(&filetype, 1, 13) + +Execute(Completion should be done after colons in Rust): + AssertEqual '::', ale#completion#GetPrefix(&filetype, 2, 19) + +Execute(Completion should be done after words in parens in Rust): + AssertEqual 'ab', ale#completion#GetPrefix(&filetype, 3, 14) + +Execute(Completion should not be done after parens in Rust): + AssertEqual '', ale#completion#GetPrefix(&filetype, 3, 15) diff --git a/test/completion/test_lsp_completion_messages.vader b/test/completion/test_lsp_completion_messages.vader index f21acfb9..af3aa8c0 100644 --- a/test/completion/test_lsp_completion_messages.vader +++ b/test/completion/test_lsp_completion_messages.vader @@ -2,12 +2,9 @@ Before: Save g:ale_completion_delay Save g:ale_completion_max_suggestions Save g:ale_completion_info - Save g:ale_completion_experimental_lsp_support Save &l:omnifunc Save &l:completeopt - unlet! g:ale_completion_experimental_lsp_support - let g:ale_completion_enabled = 1 call ale#test#SetDirectory('/testplugin/test/completion') @@ -44,7 +41,6 @@ After: unlet! b:ale_completion_parser unlet! b:ale_complete_done_time unlet! b:ale_linters - unlet! g:ale_completion_experimental_lsp_support call ale#test#RestoreDirectory() call ale#linter#Reset() @@ -136,8 +132,6 @@ Given python(Some Python file): bazxyzxyzxyz Execute(The right message should be sent for the initial LSP request): - let g:ale_completion_experimental_lsp_support = 1 - runtime ale_linters/python/pyls.vim let b:ale_linters = ['pyls'] " The cursor position needs to match what was saved before. diff --git a/test/completion/test_lsp_completion_parsing.vader b/test/completion/test_lsp_completion_parsing.vader new file mode 100644 index 00000000..c9c51a66 --- /dev/null +++ b/test/completion/test_lsp_completion_parsing.vader @@ -0,0 +1,392 @@ +After: + unlet! b:ale_completion_info + +Execute(Should handle Rust completion results correctly): + AssertEqual + \ [ + \ {'word': 'new', 'menu': 'pub fn new() -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'with_capacity', 'menu': 'pub fn with_capacity(capacity: usize) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from_utf8', 'menu': 'pub fn from_utf8(vec: Vec) -> Result', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from_utf8_lossy', 'menu': 'pub fn from_utf8_lossy<''a>(v: &''a [u8]) -> Cow<''a, str>', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from_utf16', 'menu': 'pub fn from_utf16(v: &[u16]) -> Result', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from_utf16_lossy', 'menu': 'pub fn from_utf16_lossy(v: &[u16]) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from_raw_parts', 'menu': 'pub unsafe fn from_raw_parts(buf: *mut u8, length: usize, capacity: usize) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from_utf8_unchecked', 'menu': 'pub unsafe fn from_utf8_unchecked(bytes: Vec) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from_iter', 'menu': 'fn from_iter>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from_iter', 'menu': 'fn from_iter>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from_iter', 'menu': 'fn from_iter>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from_iter', 'menu': 'fn from_iter>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from_iter', 'menu': 'fn from_iter>>(iter: I) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'Searcher', 'menu': 'type Searcher = <&''b str as Pattern<''a>>::Searcher;', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'default', 'menu': 'fn default() -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'Output', 'menu': 'type Output = String;', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'Output', 'menu': 'type Output = str;', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'Target', 'menu': 'type Target = str;', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'Err', 'menu': 'type Err = ParseError;', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from_str', 'menu': 'fn from_str(s: &str) -> Result', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from', 'menu': 'fn from(s: &''a str) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from', 'menu': 'fn from(s: Box) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \ {'word': 'from', 'menu': 'fn from(s: Cow<''a, str>) -> String', 'info': '', 'kind': 'f', 'icase': 1}, + \], + \ ale#completion#ParseLSPCompletions({ + \ "jsonrpc":"2.0", + \ "id":65, + \ "result":[ + \ { + \ "label":"new", + \ "kind":3, + \ "detail":"pub fn new() -> String" + \ }, + \ { + \ "label":"with_capacity", + \ "kind":3, + \ "detail":"pub fn with_capacity(capacity: usize) -> String" + \ }, + \ { + \ "label":"from_utf8", + \ "kind":3, + \ "detail":"pub fn from_utf8(vec: Vec) -> Result" + \ }, + \ { + \ "label":"from_utf8_lossy", + \ "kind":3, + \ "detail":"pub fn from_utf8_lossy<'a>(v: &'a [u8]) -> Cow<'a, str>" + \ }, + \ { + \ "label":"from_utf16", + \ "kind":3, + \ "detail":"pub fn from_utf16(v: &[u16]) -> Result" + \ }, + \ { + \ "label":"from_utf16_lossy", + \ "kind":3, + \ "detail":"pub fn from_utf16_lossy(v: &[u16]) -> String" + \ }, + \ { + \ "label":"from_raw_parts", + \ "kind":3, + \ "detail":"pub unsafe fn from_raw_parts(buf: *mut u8, length: usize, capacity: usize) -> String" + \ }, + \ { + \ "label":"from_utf8_unchecked", + \ "kind":3, + \ "detail":"pub unsafe fn from_utf8_unchecked(bytes: Vec) -> String" + \ }, + \ { + \ "label":"from_iter", + \ "kind":3, + \ "detail":"fn from_iter>(iter: I) -> String" + \ }, + \ { + \ "label":"from_iter", + \ "kind":3, + \ "detail":"fn from_iter>(iter: I) -> String" + \ }, + \ { + \ "label":"from_iter", + \ "kind":3, + \ "detail":"fn from_iter>(iter: I) -> String" + \ }, + \ { + \ "label":"from_iter", + \ "kind":3, + \ "detail":"fn from_iter>(iter: I) -> String" + \ }, + \ { + \ "label":"from_iter", + \ "kind":3, + \ "detail":"fn from_iter>>(iter: I) -> String" + \ }, + \ { + \ "label":"Searcher", + \ "kind":8, + \ "detail":"type Searcher = <&'b str as Pattern<'a>>::Searcher;" + \ }, + \ { + \ "label":"default", + \ "kind":3, + \ "detail":"fn default() -> String" + \ }, + \ { + \ "label":"Output", + \ "kind":8, + \ "detail":"type Output = String;" + \ }, + \ { + \ "label":"Output", + \ "kind":8, + \ "detail":"type Output = str;" + \ }, + \ { + \ "label":"Output", + \ "kind":8, + \ "detail":"type Output = str;" + \ }, + \ { + \ "label":"Output", + \ "kind":8, + \ "detail":"type Output = str;" + \ }, + \ { + \ "label":"Output", + \ "kind":8, + \ "detail":"type Output = str;" + \ }, + \ { + \ "label":"Output", + \ "kind":8, + \ "detail":"type Output = str;" + \ }, + \ { + \ "label":"Output", + \ "kind":8, + \ "detail":"type Output = str;" + \ }, + \ { + \ "label":"Target", + \ "kind":8, + \ "detail":"type Target = str;" + \ }, + \ { + \ "label":"Err", + \ "kind":8, + \ "detail":"type Err = ParseError;" + \ }, + \ { + \ "label":"from_str", + \ "kind":3, + \ "detail":"fn from_str(s: &str) -> Result" + \ }, + \ { + \ "label":"from", + \ "kind":3, + \ "detail":"fn from(s: &'a str) -> String" + \ }, + \ { + \ "label":"from", + \ "kind":3, + \ "detail":"fn from(s: Box) -> String" + \ }, + \ { + \ "label":"from", + \ "kind":3, + \ "detail":"fn from(s: Cow<'a, str>) -> String" + \ } + \ ] + \ }) + +Execute(Should handle Python completion results correctly): + let b:ale_completion_info = { + \ 'completion_filter': 'ale#completion#python#CompletionItemFilter', + \} + + AssertEqual + \ [ + \ {'word': 'what', 'menu': 'example-python-project.bar.Bar', 'info': "what()\n\n", 'kind': 'f', 'icase': 1}, + \ ], + \ ale#completion#ParseLSPCompletions({ + \ "jsonrpc":"2.0", + \ "id":6, + \ "result":{ + \ "isIncomplete":v:false, + \ "items":[ + \ { + \ "label":"what()", + \ "kind":3, + \ "detail":"example-python-project.bar.Bar", + \ "documentation":"what()\n\n", + \ "sortText":"awhat", + \ "insertText":"what" + \ }, + \ { + \ "label":"__class__", + \ "kind":7, + \ "detail":"object", + \ "documentation":"type(object_or_name, bases, dict)\ntype(object) -> the object's type\ntype(name, bases, dict) -> a new type", + \ "sortText":"z__class__", + \ "insertText":"__class__" + \ }, + \ { + \ "label":"__delattr__(name)", + \ "kind":3, + \ "detail":"object", + \ "documentation":"Implement delattr(self, name).", + \ "sortText":"z__delattr__", + \ "insertText":"__delattr__" + \ }, + \ { + \ "label":"__dir__()", + \ "kind":3, + \ "detail":"object", + \ "documentation":"__dir__() -> list\ndefault dir() implementation", + \ "sortText":"z__dir__", + \ "insertText":"__dir__" + \ }, + \ { + \ "label":"__doc__", + \ "kind":18, + \ "detail":"object", + \ "documentation":"str(object='') -> str\nstr(bytes_or_buffer[, encoding[, errors]]) -> str\n\nCreate a new string object from the given object. If encoding or\nerrors is specified, then the object must expose a data buffer\nthat will be decoded using the given encoding and error handler.\nOtherwise, returns the result of object.__str__() (if defined)\nor repr(object).\nencoding defaults to sys.getdefaultencoding().\nerrors defaults to 'strict'.", + \ "sortText":"z__doc__", + \ "insertText":"__doc__" + \ }, + \ { + \ "label":"__eq__(value)", + \ "kind":3, + \ "detail":"object", + \ "documentation":"Return self==value.", + \ "sortText":"z__eq__", + \ "insertText":"__eq__" + \ }, + \ { + \ "label":"__format__()", + \ "kind":3, + \ "detail":"object", + \ "documentation":"default object formatter", + \ "sortText":"z__format__", + \ "insertText":"__format__" + \ }, + \ { + \ "label":"__ge__(value)", + \ "kind":3, + \ "detail":"object", + \ "documentation":"Return self>=value.", + \ "sortText":"z__ge__", + \ "insertText":"__ge__" + \ }, + \ { + \ "label":"__getattribute__(name)", + \ "kind":3, + \ "detail":"object", + \ "documentation":"Return getattr(self, name).", + \ "sortText":"z__getattribute__", + \ "insertText":"__getattribute__" + \ }, + \ { + \ "label":"__gt__(value)", + \ "kind":3, + \ "detail":"object", + \ "documentation":"Return self>value.", + \ "sortText":"z__gt__", + \ "insertText":"__gt__" + \ }, + \ { + \ "label":"__hash__()", + \ "kind":3, + \ "detail":"object", + \ "documentation":"Return hash(self).", + \ "sortText":"z__hash__", + \ "insertText":"__hash__" + \ }, + \ { + \ "label":"__init__(args, kwargs)", + \ "kind":3, + \ "detail":"object", + \ "documentation":"Initialize self.\u00a0\u00a0See help(type(self)) for accurate signature.", + \ "sortText":"z__init__", + \ "insertText":"__init__" + \ }, + \ { + \ "label":"__init_subclass__()", + \ "kind":3, + \ "detail":"object", + \ "documentation":"This method is called when a class is subclassed.\n\nThe default implementation does nothing. It may be\noverridden to extend subclasses.", + \ "sortText":"z__init_subclass__", + \ "insertText":"__init_subclass__" + \ }, + \ { + \ "label":"__le__(value)", + \ "kind":3, + \ "detail":"object", + \ "documentation":"Return self<=value.", + \ "sortText":"z__le__", + \ "insertText":"__le__" + \ }, + \ { + \ "label":"__lt__(value)", + \ "kind":3, + \ "detail":"object", + \ "documentation":"Return self int\nsize of object in memory, in bytes", + \ "sortText":"z__sizeof__", + \ "insertText":"__sizeof__" + \ }, + \ { + \ "label":"__str__()", + \ "kind":3, + \ "detail":"object", + \ "documentation":"Return str(self).", + \ "sortText":"z__str__", + \ "insertText":"__str__" + \ }, + \ { + \ "label":"__subclasshook__()", + \ "kind":3, + \ "detail":"object", + \ "documentation":"Abstract classes can override this to customize issubclass().\n\nThis is invoked early on by abc.ABCMeta.__subclasscheck__().\nIt should return True, False or NotImplemented.\u00a0\u00a0If it returns\nNotImplemented, the normal algorithm is used.\u00a0\u00a0Otherwise, it\noverrides the normal algorithm (and the outcome is cached).", + \ "sortText":"z__subclasshook__", + \ "insertText":"__subclasshook__" + \ } + \ ] + \ } + \ })