From 6fc016ad052725e0d6a474e80f817cb1853e98e2 Mon Sep 17 00:00:00 2001 From: "brian m. carlson" Date: Sat, 26 Jan 2019 04:45:12 +0000 Subject: [PATCH] Add additional ways to detect LSP project root Currently, we detect the linter root based on a variety of techniques. However, these techniques are not foolproof. For example, clangd works fine for many things without a compile_commands.json file, and Go projects may be built outside of the GOPATH to take advantage of Go 1.11's automatic module support. Add global and buffer-specific variables to allow the user to specify the root, either as a string or a funcref. Make the funcrefs accept the buffer number as an argument to make sure that they can function easily in an asynchronous environment. We define the global variable in the main plugin, since the LSP linter code is not loaded unless required, and we want the variable to be able to be read correctly by :ALEInfo regardless. --- autoload/ale/assert.vim | 13 ++++++ autoload/ale/debugging.vim | 1 + autoload/ale/lsp_linter.vim | 35 +++++++++++++- doc/ale.txt | 16 +++++++ plugin/ale.vim | 3 ++ test/lsp/test_lsp_root_detection.vader | 63 ++++++++++++++++++++++++++ test/test_ale_info.vader | 1 + 7 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 test/lsp/test_lsp_root_detection.vader diff --git a/autoload/ale/assert.vim b/autoload/ale/assert.vim index ed08ed09..7139f05c 100644 --- a/autoload/ale/assert.vim +++ b/autoload/ale/assert.vim @@ -109,6 +109,14 @@ function! ale#assert#LSPProject(expected_root) abort AssertEqual a:expected_root, l:root endfunction +function! ale#assert#LSPProjectFull(expected_root) abort + let l:buffer = bufnr('') + let l:linter = s:GetLinter() + let l:root = ale#lsp_linter#FindProjectRoot(l:buffer, l:linter) + + AssertEqual a:expected_root, l:root +endfunction + function! ale#assert#LSPAddress(expected_address) abort let l:buffer = bufnr('') let l:linter = s:GetLinter() @@ -158,6 +166,7 @@ function! ale#assert#SetUpLinterTest(filetype, name) abort command! -nargs=+ AssertLSPConfig :call ale#assert#LSPConfig() command! -nargs=+ AssertLSPLanguage :call ale#assert#LSPLanguage() command! -nargs=+ AssertLSPProject :call ale#assert#LSPProject() + command! -nargs=+ AssertLSPProjectFull :call ale#assert#LSPProjectFull() command! -nargs=+ AssertLSPAddress :call ale#assert#LSPAddress() endfunction @@ -193,6 +202,10 @@ function! ale#assert#TearDownLinterTest() abort delcommand AssertLSPProject endif + if exists(':AssertLSPProjectFull') + delcommand AssertLSPProjectFull + endif + if exists(':AssertLSPAddress') delcommand AssertLSPAddress endif diff --git a/autoload/ale/debugging.vim b/autoload/ale/debugging.vim index 3aed38fe..e4bf5e7e 100644 --- a/autoload/ale/debugging.vim +++ b/autoload/ale/debugging.vim @@ -31,6 +31,7 @@ let s:global_variable_list = [ \ 'ale_list_vertical', \ 'ale_list_window_size', \ 'ale_loclist_msg_format', +\ 'ale_lsp_root', \ 'ale_max_buffer_history_size', \ 'ale_max_signs', \ 'ale_maximum_file_size', diff --git a/autoload/ale/lsp_linter.vim b/autoload/ale/lsp_linter.vim index d92dae7e..2c31ea09 100644 --- a/autoload/ale/lsp_linter.vim +++ b/autoload/ale/lsp_linter.vim @@ -152,12 +152,45 @@ function! ale#lsp_linter#GetConfig(buffer, linter) abort return l:config endfunction +function! ale#lsp_linter#FindProjectRoot(buffer, linter) abort + let l:buffer_ale_root = getbufvar(a:buffer, 'ale_lsp_root', {}) + + if type(l:buffer_ale_root) is v:t_string + return l:buffer_ale_root + endif + + " Try to get a buffer-local setting for the root + if has_key(l:buffer_ale_root, a:linter.name) + let l:Root = l:buffer_ale_root[a:linter.name] + + if type(l:Root) is v:t_func + return l:Root(a:buffer) + else + return l:Root + endif + endif + + " Try to get a global setting for the root + if has_key(g:ale_lsp_root, a:linter.name) + let l:Root = g:ale_lsp_root[a:linter.name] + + if type(l:Root) is v:t_func + return l:Root(a:buffer) + else + return l:Root + endif + endif + + " Fall back to the linter-specific configuration + return ale#util#GetFunction(a:linter.project_root_callback)(a:buffer) +endfunction + " Given a buffer, an LSP linter, start up an LSP linter and get ready to " receive messages for the document. function! ale#lsp_linter#StartLSP(buffer, linter) abort let l:command = '' let l:address = '' - let l:root = ale#util#GetFunction(a:linter.project_root_callback)(a:buffer) + let l:root = ale#lsp_linter#FindProjectRoot(a:buffer, a:linter) if empty(l:root) && a:linter.lsp isnot# 'tsserver' " If there's no project root, then we can't check files with LSP, diff --git a/doc/ale.txt b/doc/ale.txt index ac71ab83..a3884c5b 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -1572,6 +1572,22 @@ b:ale_loclist_msg_format *b:ale_loclist_msg_format* The strings for configuring `%severity%` are also used for this option. +g:ale_lsp_root *g:ale_lsp_root* +b:ale_lsp_root *b:ale_lsp_root* + + Type: |Dictionary| or |String| + Default: {} + + This option is used to determine the project root for the LSP linter. If the + value is a |Dictionary|, it maps a linter to either a string containing the + project root or a |Funcref| to call to look up the root. The funcref is + provided the buffer number as its argument. + + The buffer-specific variable may additionally be a string containing the + project root itself. + + If neither variable yields a result, a linter-specific function is invoked to + detect a project root. If this, too, yields no result, the linter is disabled. g:ale_max_buffer_history_size *g:ale_max_buffer_history_size* diff --git a/plugin/ale.vim b/plugin/ale.vim index 7d6a48f0..416324bc 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -87,6 +87,9 @@ let g:ale_lint_on_save = get(g:, 'ale_lint_on_save', 1) " This flag can be set to 1 to enable linting when the filetype is changed. let g:ale_lint_on_filetype_changed = get(g:, 'ale_lint_on_filetype_changed', 1) +" This Dictionary configures the default LSP roots for various linters. +let g:ale_lsp_root = get(g:, 'ale_lsp_root', {}) + " This flag can be set to 1 to enable automatically fixing files on save. let g:ale_fix_on_save = get(g:, 'ale_fix_on_save', 0) diff --git a/test/lsp/test_lsp_root_detection.vader b/test/lsp/test_lsp_root_detection.vader new file mode 100644 index 00000000..2575a62c --- /dev/null +++ b/test/lsp/test_lsp_root_detection.vader @@ -0,0 +1,63 @@ +Before: + call ale#assert#SetUpLinterTest('c', 'clangd') + + function! Hook1(buffer) + return 'abc123' + endfunction + +After: + let g:ale_lsp_root = {} + unlet! b:ale_lsp_root + delfunction Hook1 + + call ale#assert#TearDownLinterTest() + +Execute(The buffer-specific variable can be a string): + let b:ale_lsp_root = '/some/path' + call ale#test#SetFilename('other-file.c') + + AssertLSPProjectFull '/some/path' + +Execute(The buffer-specific variable can be a dictionary): + let b:ale_lsp_root = {'clangd': '/some/path', 'golangserver': '/other/path'} + call ale#test#SetFilename('other-file.c') + + AssertLSPProjectFull '/some/path' + +Execute(The buffer-specific variable can have funcrefs): + let b:ale_lsp_root = {'clangd': function('Hook1'), 'golangserver': '/path'} + call ale#test#SetFilename('other-file.c') + + AssertLSPProjectFull 'abc123' + +Execute(The global variable can be a dictionary): + let g:ale_lsp_root = {'clangd': '/some/path', 'golangserver': '/other/path'} + call ale#test#SetFilename('other-file.c') + + AssertLSPProjectFull '/some/path' + +Execute(The global variable can have funcrefs): + let g:ale_lsp_root = {'clangd': function('Hook1'), 'golangserver': '/path'} + call ale#test#SetFilename('other-file.c') + + AssertLSPProjectFull 'abc123' + +Execute(The buffer-specific variable overrides the global variable): + let b:ale_lsp_root = {'clangd': '/some/path', 'golangserver': '/other/path'} + let g:ale_lsp_root = {'clangd': '/not/this/path', 'golangserver': '/elsewhere'} + call ale#test#SetFilename('other-file.c') + + AssertLSPProjectFull '/some/path' + +Execute(The global variable is queried if the buffer-specific has no value): + let b:ale_lsp_root = {'golangserver': '/other/path'} + let g:ale_lsp_root = {'clangd': '/some/path', 'golangserver': '/elsewhere'} + call ale#test#SetFilename('other-file.c') + + AssertLSPProjectFull '/some/path' + + +Execute(The default hook value is acceptable): + call ale#test#SetFilename('other-file.c') + + AssertLSPProjectFull '' diff --git a/test/test_ale_info.vader b/test/test_ale_info.vader index 325c2aa8..29c19b8e 100644 --- a/test/test_ale_info.vader +++ b/test/test_ale_info.vader @@ -97,6 +97,7 @@ Before: \ 'let g:ale_list_vertical = 0', \ 'let g:ale_list_window_size = 10', \ 'let g:ale_loclist_msg_format = ''%code: %%s''', + \ 'let g:ale_lsp_root = {}', \ 'let g:ale_max_buffer_history_size = 20', \ 'let g:ale_max_signs = -1', \ 'let g:ale_maximum_file_size = 0',