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.
This commit is contained in:
brian m. carlson 2019-01-26 04:45:12 +00:00
parent 766636e0c4
commit 6fc016ad05
No known key found for this signature in database
GPG key ID: BF535D811F52F68B
7 changed files with 131 additions and 1 deletions

View file

@ -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(<args>)
command! -nargs=+ AssertLSPLanguage :call ale#assert#LSPLanguage(<args>)
command! -nargs=+ AssertLSPProject :call ale#assert#LSPProject(<args>)
command! -nargs=+ AssertLSPProjectFull :call ale#assert#LSPProjectFull(<args>)
command! -nargs=+ AssertLSPAddress :call ale#assert#LSPAddress(<args>)
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

View file

@ -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',

View file

@ -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,

View file

@ -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*

View file

@ -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)

View file

@ -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 ''

View file

@ -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',