diff --git a/autoload/ale/linter.vim b/autoload/ale/linter.vim index 8c9f83ad..dba59893 100644 --- a/autoload/ale/linter.vim +++ b/autoload/ale/linter.vim @@ -195,9 +195,16 @@ function! ale#linter#PreProcess(filetype, linter) abort endif if !l:needs_address - if has_key(a:linter, 'address_callback') - throw '`address_callback` cannot be used when lsp != ''socket''' + if has_key(a:linter, 'address') || has_key(a:linter, 'address_callback') + throw '`address` or `address_callback` cannot be used when lsp != ''socket''' endif + elseif has_key(a:linter, 'address') + if type(a:linter.address) isnot v:t_string + \&& type(a:linter.address) isnot v:t_func + throw '`address` must be a String or Function if defined' + endif + + let l:obj.address = a:linter.address elseif has_key(a:linter, 'address_callback') let l:obj.address_callback = a:linter.address_callback @@ -205,7 +212,7 @@ function! ale#linter#PreProcess(filetype, linter) abort throw '`address_callback` must be a callback if defined' endif else - throw '`address_callback` must be defined for getting the LSP address' + throw '`address` or `address_callback` must be defined for getting the LSP address' endif if l:needs_lsp_details @@ -222,14 +229,17 @@ function! ale#linter#PreProcess(filetype, linter) abort endif else " Default to using the filetype as the language. - let l:obj.language = get(a:linter, 'language', a:filetype) + let l:Language = get(a:linter, 'language', a:filetype) - if type(l:obj.language) isnot v:t_string - throw '`language` must be a string' + if type(l:Language) is v:t_string + " Make 'language_callback' return the 'language' value. + let l:obj.language = l:Language + let l:obj.language_callback = function('s:LanguageGetter') + elseif type(l:Language) is v:t_func + let l:obj.language_callback = l:Language + else + throw '`language` must be a String or Funcref' endif - - " Make 'language_callback' return the 'language' value. - let l:obj.language_callback = function('s:LanguageGetter') endif let l:obj.project_root_callback = get(a:linter, 'project_root_callback') @@ -259,6 +269,11 @@ function! ale#linter#PreProcess(filetype, linter) abort endif elseif has_key(a:linter, 'initialization_options') let l:obj.initialization_options = a:linter.initialization_options + + if type(l:obj.initialization_options) isnot v:t_dict + \&& type(l:obj.initialization_options) isnot v:t_func + throw '`initialization_options` must be a String or Function if defined' + endif endif if has_key(a:linter, 'lsp_config_callback') @@ -273,7 +288,8 @@ function! ale#linter#PreProcess(filetype, linter) abort endif elseif has_key(a:linter, 'lsp_config') if type(a:linter.lsp_config) isnot v:t_dict - throw '`lsp_config` must be a Dictionary' + \&& type(a:linter.lsp_config) isnot v:t_func + throw '`lsp_config` must be a Dictionary or Function if defined' endif let l:obj.lsp_config = a:linter.lsp_config @@ -501,7 +517,11 @@ endfunction " Given a buffer and linter, get the address for connecting to the server. function! ale#linter#GetAddress(buffer, linter) abort - return has_key(a:linter, 'address_callback') - \ ? ale#util#GetFunction(a:linter.address_callback)(a:buffer) + let l:Address = has_key(a:linter, 'address_callback') + \ ? function(a:linter.address_callback) \ : a:linter.address + + return type(l:Address) is v:t_func + \ ? l:Address(a:buffer) + \ : l:Address endfunction diff --git a/autoload/ale/lsp_linter.vim b/autoload/ale/lsp_linter.vim index 610d897f..b92f5104 100644 --- a/autoload/ale/lsp_linter.vim +++ b/autoload/ale/lsp_linter.vim @@ -129,27 +129,39 @@ function! ale#lsp_linter#HandleLSPResponse(conn_id, response) abort endfunction function! ale#lsp_linter#GetOptions(buffer, linter) abort - let l:initialization_options = {} - if has_key(a:linter, 'initialization_options_callback') - let l:initialization_options = ale#util#GetFunction(a:linter.initialization_options_callback)(a:buffer) - elseif has_key(a:linter, 'initialization_options') - let l:initialization_options = a:linter.initialization_options + return ale#util#GetFunction(a:linter.initialization_options_callback)(a:buffer) endif - return l:initialization_options + if has_key(a:linter, 'initialization_options') + let l:Options = a:linter.initialization_options + + if type(l:Options) is v:t_func + let l:Options = l:Options(a:buffer) + endif + + return l:Options + endif + + return {} endfunction function! ale#lsp_linter#GetConfig(buffer, linter) abort - let l:config = {} - if has_key(a:linter, 'lsp_config_callback') - let l:config = ale#util#GetFunction(a:linter.lsp_config_callback)(a:buffer) - elseif has_key(a:linter, 'lsp_config') - let l:config = a:linter.lsp_config + return ale#util#GetFunction(a:linter.lsp_config_callback)(a:buffer) endif - return l:config + if has_key(a:linter, 'lsp_config') + let l:Config = a:linter.lsp_config + + if type(l:Config) is v:t_func + let l:Config = l:Config(a:buffer) + endif + + return l:Config + endif + + return {} endfunction function! ale#lsp_linter#FindProjectRoot(buffer, linter) abort diff --git a/doc/ale.txt b/doc/ale.txt index 083b0507..ca895fe0 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -3231,8 +3231,9 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()* When this argument is set to `'socket'`, then the linter will be defined as an LSP linter via a TCP - socket connection. `address_callback` must be set - with a callback returning an address to connect to. + socket connection. Either `address` or + `address_callback` must be set. + ALE will not start a server automatically. When this argument is not empty @@ -3255,6 +3256,13 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()* An optional `lsp_config` or `lsp_config_callback` may be defined to pass configuration settings to the LSP. + `address` A |String| representing an address to connect to, + or a |Funcref| accepting a buffer number and + returning the |String|. + + This argument must only be set if the `lsp` argument + is set to `'socket'`. + `address_callback` A |String| or |Funcref| for a callback function accepting a buffer number. A |String| should be returned with an address to connect to. @@ -3273,8 +3281,10 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()* is also set to a non-empty string. `language` A |String| representing the name of the language - being checked. This string will be sent to the LSP to - tell it what type of language is being checked. + being checked, or a |Funcref| accepting a buffer + number and returning the |String|. This string will + be sent to the LSP to tell it what type of language + is being checked. If this or `language_callback` isn't set, the language will default to the value of the filetype @@ -3304,7 +3314,10 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()* setting can make it easier to guess the linter name by offering a few alternatives. - `initialization_options` A |Dictionary| of initialization options for LSPs. + `initialization_options` A |Dictionary| of initialization options for LSPs, + or a |Funcref| for a callback function accepting + a buffer number and returning the |Dictionary|. + This will be fed (as JSON) to the LSP in the initialize command. @@ -3315,11 +3328,14 @@ ale#linter#Define(filetype, linter) *ale#linter#Define()* This can be used in place of `initialization_options` when more complicated processing is needed. - `lsp_config` A |Dictionary| of configuration settings for LSPs. + `lsp_config` A |Dictionary| for configuring a language server, + or a |Funcref| for a callback function accepting + a buffer number and returning the |Dictionary|. + This will be fed (as JSON) to the LSP in the workspace/didChangeConfiguration command. - `lsp_config_callback` A |String| or |Funcref| for a callback function + `lsp_config_callback` A |String| or |Funcref| for a callback function accepting a buffer number. A |Dictionary| should be returned for configuration settings to pass the LSP. This can be used in place of `lsp_config` when more diff --git a/test/test_linter_defintion_processing.vader b/test/test_linter_defintion_processing.vader index 0f54cf0e..a78b9838 100644 --- a/test/test_linter_defintion_processing.vader +++ b/test/test_linter_defintion_processing.vader @@ -461,6 +461,18 @@ Execute(PreProcess should complain about using language and language_callback to AssertThrows call ale#linter#PreProcess('testft', g:linter) AssertEqual 'Only one of `language` or `language_callback` should be set', g:vader_exception +Execute(PreProcess should complain about invalid language values): + let g:linter = { + \ 'name': 'x', + \ 'lsp': 'socket', + \ 'address_callback': 'X', + \ 'language': 0, + \ 'project_root_callback': 'x', + \} + + AssertThrows call ale#linter#PreProcess('testft', g:linter) + AssertEqual '`language` must be a String or Funcref', g:vader_exception + Execute(PreProcess should use the filetype as the language string by default): let g:linter = { \ 'name': 'x', @@ -471,6 +483,17 @@ Execute(PreProcess should use the filetype as the language string by default): AssertEqual 'testft', ale#linter#PreProcess('testft', g:linter).language_callback(0) +Execute(PreProcess should allow language to be set to a callback): + let g:linter = { + \ 'name': 'x', + \ 'lsp': 'socket', + \ 'address_callback': 'X', + \ 'language': {-> 'foo'}, + \ 'project_root_callback': 'x', + \} + + AssertEqual 'foo', ale#linter#PreProcess('testft', g:linter).language_callback(0) + Execute(PreProcess should require an address_callback for LSP socket configurations): let g:linter = { \ 'name': 'x', @@ -478,7 +501,7 @@ Execute(PreProcess should require an address_callback for LSP socket configurati \} AssertThrows call ale#linter#PreProcess('testft', g:linter) - AssertEqual '`address_callback` must be defined for getting the LSP address', g:vader_exception + AssertEqual '`address` or `address_callback` must be defined for getting the LSP address', g:vader_exception Execute(PreProcess should complain about address_callback for non-LSP linters): let g:linter = { @@ -490,7 +513,50 @@ Execute(PreProcess should complain about address_callback for non-LSP linters): \} AssertThrows call ale#linter#PreProcess('testft', g:linter) - AssertEqual '`address_callback` cannot be used when lsp != ''socket''', g:vader_exception + AssertEqual '`address` or `address_callback` cannot be used when lsp != ''socket''', g:vader_exception + +Execute(PreProcess accept valid address_callback values): + let g:linter = ale#linter#PreProcess('testft', { + \ 'name': 'x', + \ 'lsp': 'socket', + \ 'address_callback': {-> 'foo:123'}, + \ 'language': 'x', + \ 'project_root_callback': 'x', + \}) + + AssertEqual 'foo:123', ale#linter#GetAddress(0, g:linter) + +Execute(PreProcess accept address as a String): + let g:linter = ale#linter#PreProcess('testft', { + \ 'name': 'x', + \ 'lsp': 'socket', + \ 'address': 'foo:123', + \ 'language': 'x', + \ 'project_root_callback': 'x', + \}) + + AssertEqual 'foo:123', ale#linter#GetAddress(0, g:linter) + +Execute(PreProcess accept address as a Function): + let g:linter = ale#linter#PreProcess('testft', { + \ 'name': 'x', + \ 'lsp': 'socket', + \ 'address': {-> 'foo:123'}, + \ 'language': 'x', + \ 'project_root_callback': 'x', + \}) + + AssertEqual 'foo:123', ale#linter#GetAddress(0, g:linter) + +Execute(PreProcess should complain about invalid address values): + AssertThrows call ale#linter#PreProcess('testft', { + \ 'name': 'x', + \ 'lsp': 'socket', + \ 'address': 0, + \ 'language': 'x', + \ 'project_root_callback': 'x', + \}) + AssertEqual '`address` must be a String or Function if defined', g:vader_exception Execute(PreProcess should complain about using initialization_options and initialization_options_callback together): let g:linter = { @@ -517,6 +583,41 @@ Execute(PreProcess should throw when initialization_options_callback is not a ca \}) AssertEqual '`initialization_options_callback` must be a callback if defined', g:vader_exception +Execute(PreProcess should throw when initialization_options is not a Dictionary or callback): + AssertThrows call ale#linter#PreProcess('testft', { + \ 'name': 'foo', + \ 'lsp': 'socket', + \ 'address_callback': 'X', + \ 'language': 'x', + \ 'project_root_callback': 'x', + \ 'initialization_options': 0, + \}) + AssertEqual '`initialization_options` must be a String or Function if defined', g:vader_exception + +Execute(PreProcess should accept initialization_options as a Dictionary): + let g:linter = ale#linter#PreProcess('testft', { + \ 'name': 'foo', + \ 'lsp': 'socket', + \ 'address_callback': 'X', + \ 'language': 'x', + \ 'project_root_callback': 'x', + \ 'initialization_options': {'foo': v:true}, + \}) + + AssertEqual {'foo': v:true}, ale#lsp_linter#GetOptions(0, g:linter) + +Execute(PreProcess should accept initialization_options as a Funcref): + let g:linter = ale#linter#PreProcess('testft', { + \ 'name': 'foo', + \ 'lsp': 'socket', + \ 'address_callback': 'X', + \ 'language': 'x', + \ 'project_root_callback': 'x', + \ 'initialization_options': {-> {'foo': v:true}}, + \}) + + AssertEqual {'foo': v:true}, ale#lsp_linter#GetOptions(0, g:linter) + Execute(PreProcess should complain about using lsp_config and lsp_config_callback together): let g:linter = { \ 'name': 'x', @@ -543,22 +644,30 @@ Execute(PreProcess should throw when lsp_config_callback is not a callback): AssertEqual '`lsp_config_callback` must be a callback if defined', g:vader_exception Execute(PreProcess should accept LSP configuration options via lsp_config): - let g:ale_lsp_configuration = { - \ 'foo': 'bar' - \} - let g:linter = { \ 'name': 'x', \ 'lsp': 'socket', \ 'address_callback': 'X', \ 'language_callback': 'x', \ 'project_root_callback': 'x', - \ 'lsp_config': g:ale_lsp_configuration, + \ 'lsp_config': {'foo': 'bar'}, \} - AssertEqual {'foo': 'bar'}, ale#linter#PreProcess('testft', g:linter).lsp_config + AssertEqual {'foo': 'bar'}, ale#lsp_linter#GetConfig(0, g:linter) -Execute(PreProcess should throw when lsp_config is not a Dictionary): +Execute(PreProcess should accept LSP configuration options via lsp_config as a function): + let g:linter = { + \ 'name': 'x', + \ 'lsp': 'socket', + \ 'address_callback': 'X', + \ 'language_callback': 'x', + \ 'project_root_callback': 'x', + \ 'lsp_config': {-> {'foo': 'bar'}}, + \} + + AssertEqual {'foo': 'bar'}, ale#lsp_linter#GetConfig(0, g:linter) + +Execute(PreProcess should throw when lsp_config is not a Dictionary or Function): AssertThrows call ale#linter#PreProcess('testft', { \ 'name': 'foo', \ 'lsp': 'socket', @@ -567,4 +676,4 @@ Execute(PreProcess should throw when lsp_config is not a Dictionary): \ 'project_root_callback': 'x', \ 'lsp_config': 'x', \}) - AssertEqual '`lsp_config` must be a Dictionary', g:vader_exception + AssertEqual '`lsp_config` must be a Dictionary or Function if defined', g:vader_exception