diff options
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | ale_linters/r/lintr.vim | 2 | ||||
-rw-r--r-- | autoload/ale/completion.vim | 7 | ||||
-rw-r--r-- | autoload/ale/definition.vim | 7 | ||||
-rw-r--r-- | autoload/ale/engine.vim | 137 | ||||
-rw-r--r-- | autoload/ale/fix/registry.vim | 5 | ||||
-rw-r--r-- | autoload/ale/fixers/qmlfmt.vim | 11 | ||||
-rw-r--r-- | autoload/ale/hover.vim | 8 | ||||
-rw-r--r-- | autoload/ale/linter.vim | 78 | ||||
-rw-r--r-- | autoload/ale/lsp.vim | 85 | ||||
-rw-r--r-- | autoload/ale/lsp/reset.vim | 4 | ||||
-rw-r--r-- | autoload/ale/lsp_linter.vim | 228 | ||||
-rw-r--r-- | autoload/ale/references.vim | 7 | ||||
-rw-r--r-- | test/command_callback/test_lintr_command_callback.vader | 6 | ||||
-rw-r--r-- | test/completion/test_lsp_completion_messages.vader | 9 | ||||
-rw-r--r-- | test/completion/test_lsp_completion_parsing.vader | 20 | ||||
-rw-r--r-- | test/fixers/test_qmlfmt_fixer_callback.vader | 12 | ||||
-rw-r--r-- | test/lsp/test_did_save_event.vader | 9 | ||||
-rw-r--r-- | test/test_engine_lsp_response_handling.vader | 18 | ||||
-rw-r--r-- | test/test_find_references.vader | 9 | ||||
-rw-r--r-- | test/test_go_to_definition.vader | 9 | ||||
-rw-r--r-- | test/test_hover.vader | 2 |
23 files changed, 415 insertions, 264 deletions
diff --git a/.travis.yml b/.travis.yml index 24237322..f5389f74 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,6 @@ sudo: required services: - docker -language: python +language: generic script: | ./run-tests @@ -29,6 +29,10 @@ features, including: * Finding references (`:ALEFindReferences`) * Hover information (`:ALEHover`) +If you don't care about Language Server Protocol, ALE won't load any of the code +for working with it unless needed. One of ALE's general missions is that you +won't pay for the features that you don't use. + ## Table of Contents 1. [Supported Languages and Tools](#supported-languages) diff --git a/ale_linters/r/lintr.vim b/ale_linters/r/lintr.vim index 51e5c562..8f74c9b8 100644 --- a/ale_linters/r/lintr.vim +++ b/ale_linters/r/lintr.vim @@ -22,7 +22,7 @@ function! ale_linters#r#lintr#GetCommand(buffer) abort \ . l:lint_cmd return ale#path#BufferCdString(a:buffer) - \ . 'Rscript -e ' + \ . 'Rscript --vanilla -e ' \ . ale#Escape(l:cmd_string) . ' %t' endfunction diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim index d8c2b4e2..4823b00c 100644 --- a/autoload/ale/completion.vim +++ b/autoload/ale/completion.vim @@ -389,14 +389,13 @@ function! s:GetLSPCompletions(linter) abort \ ? function('ale#completion#HandleTSServerResponse') \ : function('ale#completion#HandleLSPResponse') - let l:lsp_details = ale#linter#StartLSP(l:buffer, a:linter, l:Callback) + let l:lsp_details = ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) if empty(l:lsp_details) return 0 endif let l:id = l:lsp_details.connection_id - let l:root = l:lsp_details.project_root if a:linter.lsp is# 'tsserver' let l:message = ale#lsp#tsserver_message#Completions( @@ -408,7 +407,7 @@ function! s:GetLSPCompletions(linter) abort else " Send a message saying the buffer has changed first, otherwise " completions won't know what text is nearby. - call ale#lsp#Send(l:id, ale#lsp#message#DidChange(l:buffer), l:root) + call ale#lsp#NotifyForChanges(l:lsp_details) " For LSP completions, we need to clamp the column to the length of " the line. python-language-server and perhaps others do not implement @@ -424,7 +423,7 @@ function! s:GetLSPCompletions(linter) abort \) endif - let l:request_id = ale#lsp#Send(l:id, l:message, l:root) + let l:request_id = ale#lsp#Send(l:id, l:message, l:lsp_details.project_root) if l:request_id let b:ale_completion_info.conn_id = l:id diff --git a/autoload/ale/definition.vim b/autoload/ale/definition.vim index a17eb2e7..6c70b64c 100644 --- a/autoload/ale/definition.vim +++ b/autoload/ale/definition.vim @@ -65,14 +65,13 @@ function! s:GoToLSPDefinition(linter, options) abort \ ? function('ale#definition#HandleTSServerResponse') \ : function('ale#definition#HandleLSPResponse') - let l:lsp_details = ale#linter#StartLSP(l:buffer, a:linter, l:Callback) + let l:lsp_details = ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) if empty(l:lsp_details) return 0 endif let l:id = l:lsp_details.connection_id - let l:root = l:lsp_details.project_root if a:linter.lsp is# 'tsserver' let l:message = ale#lsp#tsserver_message#Definition( @@ -83,7 +82,7 @@ function! s:GoToLSPDefinition(linter, options) abort else " Send a message saying the buffer has changed first, or the " definition position probably won't make sense. - call ale#lsp#Send(l:id, ale#lsp#message#DidChange(l:buffer), l:root) + call ale#lsp#NotifyForChanges(l:lsp_details) let l:column = min([l:column, len(getline(l:line))]) @@ -93,7 +92,7 @@ function! s:GoToLSPDefinition(linter, options) abort let l:message = ale#lsp#message#Definition(l:buffer, l:line, l:column) endif - let l:request_id = ale#lsp#Send(l:id, l:message, l:root) + let l:request_id = ale#lsp#Send(l:id, l:message, l:lsp_details.project_root) let s:go_to_definition_map[l:request_id] = { \ 'open_in_tab': get(a:options, 'open_in_tab', 0), diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim index 10589869..563c37a2 100644 --- a/autoload/ale/engine.vim +++ b/autoload/ale/engine.vim @@ -14,11 +14,6 @@ if !has_key(s:, 'job_info_map') let s:job_info_map = {} endif -" Associates LSP connection IDs with linter names. -if !has_key(s:, 'lsp_linter_map') - let s:lsp_linter_map = {} -endif - if !has_key(s:, 'executable_cache_map') let s:executable_cache_map = {} endif @@ -79,16 +74,6 @@ function! ale#engine#InitBufferInfo(buffer) abort return 0 endfunction -" Clear LSP linter data for the linting engine. -function! ale#engine#ClearLSPData() abort - let s:lsp_linter_map = {} -endfunction - -" Just for tests. -function! ale#engine#SetLSPLinterMap(replacement_map) abort - let s:lsp_linter_map = a:replacement_map -endfunction - " This function is documented and part of the public API. " " Return 1 if ALE is busy checking a given buffer @@ -241,88 +226,6 @@ function! s:HandleExit(job_id, exit_code) abort call ale#engine#HandleLoclist(l:linter.name, l:buffer, l:loclist) endfunction -function! s:HandleLSPDiagnostics(conn_id, response) abort - let l:linter_name = s:lsp_linter_map[a:conn_id] - let l:filename = ale#path#FromURI(a:response.params.uri) - let l:buffer = bufnr(l:filename) - - if l:buffer <= 0 - return - endif - - let l:loclist = ale#lsp#response#ReadDiagnostics(a:response) - - call ale#engine#HandleLoclist(l:linter_name, l:buffer, l:loclist) -endfunction - -function! s:HandleTSServerDiagnostics(response, error_type) abort - let l:buffer = bufnr(a:response.body.file) - let l:info = get(g:ale_buffer_info, l:buffer, {}) - - if empty(l:info) - return - endif - - let l:thislist = ale#lsp#response#ReadTSServerDiagnostics(a:response) - - " tsserver sends syntax and semantic errors in separate messages, so we - " have to collect the messages separately for each buffer and join them - " back together again. - if a:error_type is# 'syntax' - let l:info.syntax_loclist = l:thislist - else - let l:info.semantic_loclist = l:thislist - endif - - let l:loclist = get(l:info, 'semantic_loclist', []) - \ + get(l:info, 'syntax_loclist', []) - - call ale#engine#HandleLoclist('tsserver', l:buffer, l:loclist) -endfunction - -function! s:HandleLSPErrorMessage(linter_name, response) abort - if !g:ale_history_enabled || !g:ale_history_log_output - return - endif - - if empty(a:linter_name) - return - endif - - let l:message = ale#lsp#response#GetErrorMessage(a:response) - - if empty(l:message) - return - endif - - " This global variable is set here so we don't load the debugging.vim file - " until someone uses :ALEInfo. - let g:ale_lsp_error_messages = get(g:, 'ale_lsp_error_messages', {}) - - if !has_key(g:ale_lsp_error_messages, a:linter_name) - let g:ale_lsp_error_messages[a:linter_name] = [] - endif - - call add(g:ale_lsp_error_messages[a:linter_name], l:message) -endfunction - -function! ale#engine#HandleLSPResponse(conn_id, response) abort - let l:method = get(a:response, 'method', '') - let l:linter_name = get(s:lsp_linter_map, a:conn_id, '') - - if get(a:response, 'jsonrpc', '') is# '2.0' && has_key(a:response, 'error') - call s:HandleLSPErrorMessage(l:linter_name, a:response) - elseif l:method is# 'textDocument/publishDiagnostics' - call s:HandleLSPDiagnostics(a:conn_id, a:response) - elseif get(a:response, 'type', '') is# 'event' - \&& get(a:response, 'event', '') is# 'semanticDiag' - call s:HandleTSServerDiagnostics(a:response, 'semantic') - elseif get(a:response, 'type', '') is# 'event' - \&& get(a:response, 'event', '') is# 'syntaxDiag' - call s:HandleTSServerDiagnostics(a:response, 'syntax') - endif -endfunction - function! ale#engine#SetResults(buffer, loclist) abort let l:linting_is_done = !ale#engine#IsCheckingBuffer(a:buffer) @@ -739,44 +642,6 @@ function! s:StopCurrentJobs(buffer, include_lint_file_jobs) abort let l:info.active_linter_list = l:new_active_linter_list endfunction -function! s:CheckWithLSP(buffer, linter) abort - let l:info = g:ale_buffer_info[a:buffer] - let l:lsp_details = ale#linter#StartLSP( - \ a:buffer, - \ a:linter, - \ function('ale#engine#HandleLSPResponse'), - \) - - if empty(l:lsp_details) - return 0 - endif - - let l:id = l:lsp_details.connection_id - let l:root = l:lsp_details.project_root - - " Remember the linter this connection is for. - let s:lsp_linter_map[l:id] = a:linter.name - - let l:change_message = a:linter.lsp is# 'tsserver' - \ ? ale#lsp#tsserver_message#Geterr(a:buffer) - \ : ale#lsp#message#DidChange(a:buffer) - let l:request_id = ale#lsp#Send(l:id, l:change_message, l:root) - - " If this was a file save event, also notify the server of that. - if a:linter.lsp isnot# 'tsserver' - \&& getbufvar(a:buffer, 'ale_save_event_fired', 0) - let l:save_message = ale#lsp#message#DidSave(a:buffer) - let l:request_id = ale#lsp#Send(l:id, l:save_message, l:root) - endif - - if l:request_id != 0 - if index(l:info.active_linter_list, a:linter.name) < 0 - call add(l:info.active_linter_list, a:linter.name) - endif - endif - - return l:request_id != 0 -endfunction function! s:RemoveProblemsForDisabledLinters(buffer, linters) abort " Figure out which linters are still enabled, and remove @@ -832,7 +697,7 @@ endfunction " Returns 1 if the linter was successfully run. function! s:RunLinter(buffer, linter) abort if !empty(a:linter.lsp) - return s:CheckWithLSP(a:buffer, a:linter) + return ale#lsp_linter#CheckWithLSP(a:buffer, a:linter) else let l:executable = ale#linter#GetExecutable(a:buffer, a:linter) diff --git a/autoload/ale/fix/registry.vim b/autoload/ale/fix/registry.vim index 72a9a5f8..7b55acfc 100644 --- a/autoload/ale/fix/registry.vim +++ b/autoload/ale/fix/registry.vim @@ -200,6 +200,11 @@ let s:default_registry = { \ 'suggested_filetypes': ['javascript'], \ 'description': 'Fix JavaScript files using xo --fix.', \ }, +\ 'qmlfmt': { +\ 'function': 'ale#fixers#qmlfmt#Fix', +\ 'suggested_filetypes': ['qml'], +\ 'description': 'Fix QML files with qmlfmt.', +\ }, \} " Reset the function registry to the default entries. diff --git a/autoload/ale/fixers/qmlfmt.vim b/autoload/ale/fixers/qmlfmt.vim new file mode 100644 index 00000000..d750d1c4 --- /dev/null +++ b/autoload/ale/fixers/qmlfmt.vim @@ -0,0 +1,11 @@ +call ale#Set('qml_qmlfmt_executable', 'qmlfmt') + +function! ale#fixers#qmlfmt#GetExecutable(buffer) abort + return ale#Var(a:buffer, 'qml_qmlfmt_executable') +endfunction + +function! ale#fixers#qmlfmt#Fix(buffer) abort + return { + \ 'command': ale#Escape(ale#fixers#qmlfmt#GetExecutable(a:buffer)), + \} +endfunction diff --git a/autoload/ale/hover.vim b/autoload/ale/hover.vim index 3bf92488..6d131adc 100644 --- a/autoload/ale/hover.vim +++ b/autoload/ale/hover.vim @@ -97,14 +97,14 @@ function! s:ShowDetails(linter, buffer, line, column, opt) abort \ ? function('ale#hover#HandleTSServerResponse') \ : function('ale#hover#HandleLSPResponse') - let l:lsp_details = ale#linter#StartLSP(a:buffer, a:linter, l:Callback) + let l:lsp_details = ale#lsp_linter#StartLSP(a:buffer, a:linter, l:Callback) if empty(l:lsp_details) return 0 endif let l:id = l:lsp_details.connection_id - let l:root = l:lsp_details.project_root + let l:language_id = l:lsp_details.language_id if a:linter.lsp is# 'tsserver' let l:column = a:column @@ -117,14 +117,14 @@ function! s:ShowDetails(linter, buffer, line, column, opt) abort else " Send a message saying the buffer has changed first, or the " hover position probably won't make sense. - call ale#lsp#Send(l:id, ale#lsp#message#DidChange(a:buffer), l:root) + call ale#lsp#NotifyForChanges(l:lsp_details) let l:column = min([a:column, len(getbufline(a:buffer, a:line)[0])]) let l:message = ale#lsp#message#Hover(a:buffer, a:line, l:column) endif - let l:request_id = ale#lsp#Send(l:id, l:message, l:root) + let l:request_id = ale#lsp#Send(l:id, l:message, l:lsp_details.project_root) let s:hover_map[l:request_id] = { \ 'buffer': a:buffer, diff --git a/autoload/ale/linter.vim b/autoload/ale/linter.vim index 5eb2fd82..cc7be518 100644 --- a/autoload/ale/linter.vim +++ b/autoload/ale/linter.vim @@ -451,81 +451,3 @@ function! ale#linter#GetAddress(buffer, linter) abort \ ? ale#util#GetFunction(a:linter.address_callback)(a:buffer) \ : a:linter.address endfunction - -" Given a buffer, an LSP linter, and a callback to register for handling -" messages, start up an LSP linter and get ready to receive errors or -" completions. -function! ale#linter#StartLSP(buffer, linter, callback) abort - let l:command = '' - let l:address = '' - let l:root = ale#util#GetFunction(a:linter.project_root_callback)(a:buffer) - - if empty(l:root) && a:linter.lsp isnot# 'tsserver' - " If there's no project root, then we can't check files with LSP, - " unless we are using tsserver, which doesn't use project roots. - return {} - endif - - 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 - endif - - if a:linter.lsp is# 'socket' - let l:address = ale#linter#GetAddress(a:buffer, a:linter) - let l:conn_id = ale#lsp#ConnectToAddress( - \ l:address, - \ l:root, - \ a:callback, - \ l:initialization_options, - \) - else - let l:executable = ale#linter#GetExecutable(a:buffer, a:linter) - - if !executable(l:executable) - return {} - endif - - let l:command = ale#job#PrepareCommand( - \ a:buffer, - \ ale#linter#GetCommand(a:buffer, a:linter), - \) - let l:conn_id = ale#lsp#StartProgram( - \ l:executable, - \ l:command, - \ l:root, - \ a:callback, - \ l:initialization_options, - \) - endif - - let l:language_id = ale#util#GetFunction(a:linter.language_callback)(a:buffer) - - if !l:conn_id - if g:ale_history_enabled && !empty(l:command) - call ale#history#Add(a:buffer, 'failed', l:conn_id, l:command) - endif - - return {} - endif - - if ale#lsp#OpenDocumentIfNeeded(l:conn_id, a:buffer, l:root, l:language_id) - if g:ale_history_enabled && !empty(l:command) - call ale#history#Add(a:buffer, 'started', l:conn_id, l:command) - endif - endif - - " The change message needs to be sent for tsserver before doing anything. - if a:linter.lsp is# 'tsserver' - call ale#lsp#Send(l:conn_id, ale#lsp#tsserver_message#Change(a:buffer)) - endif - - return { - \ 'connection_id': l:conn_id, - \ 'command': l:command, - \ 'project_root': l:root, - \ 'language_id': l:language_id, - \} -endfunction diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index df4f16dc..29759f66 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -6,17 +6,20 @@ let s:connections = [] let g:ale_lsp_next_message_id = 1 -function! s:NewConnection(initialization_options) abort +" Exposed only so tests can get at it. +" Do not call this function basically anywhere. +function! ale#lsp#NewConnection(initialization_options) abort " id: The job ID as a Number, or the server address as a string. " data: The message data received so far. " executable: An executable only set for program connections. - " open_documents: A list of buffers we told the server we opened. + " open_documents: A Dictionary mapping buffers to b:changedtick, keeping + " track of when documents were opened, and when we last changed them. " callback_list: A list of callbacks for handling LSP responses. let l:conn = { \ 'id': '', \ 'data': '', \ 'projects': {}, - \ 'open_documents': [], + \ 'open_documents': {}, \ 'callback_list': [], \ 'initialization_options': a:initialization_options, \} @@ -26,6 +29,11 @@ function! s:NewConnection(initialization_options) abort return l:conn endfunction +" Remove an LSP connection with a given ID. This is only for tests. +function! ale#lsp#RemoveConnectionWithID(id) abort + call filter(s:connections, 'v:val.id isnot a:id') +endfunction + function! s:FindConnection(key, value) abort for l:conn in s:connections if has_key(l:conn, a:key) && get(l:conn, a:key) == a:value @@ -280,7 +288,7 @@ function! ale#lsp#StartProgram(executable, command, project_root, callback, init let l:conn = s:FindConnection('executable', a:executable) " Get the current connection or a new one. - let l:conn = !empty(l:conn) ? l:conn : s:NewConnection(a:initialization_options) + let l:conn = !empty(l:conn) ? l:conn : ale#lsp#NewConnection(a:initialization_options) let l:conn.executable = a:executable if !has_key(l:conn, 'id') || !ale#job#IsRunning(l:conn.id) @@ -309,7 +317,7 @@ endfunction function! ale#lsp#ConnectToAddress(address, project_root, callback, initialization_options) abort let l:conn = s:FindConnection('id', a:address) " Get the current connection or a new one. - let l:conn = !empty(l:conn) ? l:conn : s:NewConnection(a:initialization_options) + let l:conn = !empty(l:conn) ? l:conn : ale#lsp#NewConnection(a:initialization_options) if !has_key(l:conn, 'channel') || ch_status(l:conn.channel) isnot# 'open' let l:conn.channnel = ch_open(a:address, { @@ -406,21 +414,72 @@ function! ale#lsp#Send(conn_id, message, ...) abort return l:id == 0 ? -1 : l:id endfunction -function! ale#lsp#OpenDocumentIfNeeded(conn_id, buffer, project_root, language_id) abort - let l:conn = s:FindConnection('id', a:conn_id) +" The Document details Dictionary should contain the following keys. +" +" buffer - The buffer number for the document. +" connection_id - The connection ID for the LSP server. +" command - The command to run to start the LSP connection. +" project_root - The project root for the LSP project. +" language_id - The language ID for the project, like 'python', 'rust', etc. + +" Create a new Dictionary containing more connection details, with the +" following information added: +" +" conn - An existing LSP connection for the document. +" document_open - 1 if the document is currently open, 0 otherwise. +function! s:ExtendDocumentDetails(details) abort + let l:extended = copy(a:details) + let l:conn = s:FindConnection('id', a:details.connection_id) + + let l:extended.conn = l:conn + let l:extended.document_open = !empty(l:conn) + \ && has_key(l:conn.open_documents, a:details.buffer) + + return l:extended +endfunction + +" Notify LSP servers or tsserver if a document is opened, if needed. +" If a document is opened, 1 will be returned, otherwise 0 will be returned. +function! ale#lsp#OpenDocument(basic_details) abort + let l:d = s:ExtendDocumentDetails(a:basic_details) let l:opened = 0 - if !empty(l:conn) && index(l:conn.open_documents, a:buffer) < 0 - if empty(a:language_id) - let l:message = ale#lsp#tsserver_message#Open(a:buffer) + if !empty(l:d.conn) && !l:d.document_open + if empty(l:d.language_id) + let l:message = ale#lsp#tsserver_message#Open(l:d.buffer) else - let l:message = ale#lsp#message#DidOpen(a:buffer, a:language_id) + let l:message = ale#lsp#message#DidOpen(l:d.buffer, l:d.language_id) endif - call ale#lsp#Send(a:conn_id, l:message, a:project_root) - call add(l:conn.open_documents, a:buffer) + call ale#lsp#Send(l:d.connection_id, l:message, l:d.project_root) + let l:d.conn.open_documents[l:d.buffer] = getbufvar(l:d.buffer, 'changedtick') let l:opened = 1 endif return l:opened endfunction + +" Notify LSP servers or tsserver that a document has changed, if needed. +" If a notification is sent, 1 will be returned, otherwise 0 will be returned. +function! ale#lsp#NotifyForChanges(basic_details) abort + let l:d = s:ExtendDocumentDetails(a:basic_details) + let l:notified = 0 + + if l:d.document_open + let l:new_tick = getbufvar(l:d.buffer, 'changedtick') + + if l:d.conn.open_documents[l:d.buffer] < l:new_tick + if empty(l:d.language_id) + let l:message = ale#lsp#tsserver_message#Change(l:d.buffer) + else + let l:message = ale#lsp#message#DidChange(l:d.buffer) + endif + + call ale#lsp#Send(l:d.connection_id, l:message, l:d.project_root) + let l:d.conn.open_documents[l:d.buffer] = l:new_tick + let l:notified = 1 + endif + endif + + return l:notified +endfunction diff --git a/autoload/ale/lsp/reset.vim b/autoload/ale/lsp/reset.vim index c206ed08..c7c97a47 100644 --- a/autoload/ale/lsp/reset.vim +++ b/autoload/ale/lsp/reset.vim @@ -7,9 +7,9 @@ function! ale#lsp#reset#StopAllLSPs() abort call ale#definition#ClearLSPData() endif - if exists('*ale#engine#ClearLSPData') + if exists('*ale#lsp_linter#ClearLSPData') " Clear the mapping for connections, etc. - call ale#engine#ClearLSPData() + call ale#lsp_linter#ClearLSPData() " Remove the problems for all of the LSP linters in every buffer. for l:buffer_string in keys(g:ale_buffer_info) diff --git a/autoload/ale/lsp_linter.vim b/autoload/ale/lsp_linter.vim new file mode 100644 index 00000000..4aef8ff5 --- /dev/null +++ b/autoload/ale/lsp_linter.vim @@ -0,0 +1,228 @@ +" Author: w0rp <devw0rp@gmail.com> +" Description: Integration between linters and LSP/tsserver. + +" This code isn't loaded if a user never users LSP features or linters. + +" Associates LSP connection IDs with linter names. +if !has_key(s:, 'lsp_linter_map') + let s:lsp_linter_map = {} +endif + +function! s:HandleLSPDiagnostics(conn_id, response) abort + let l:linter_name = s:lsp_linter_map[a:conn_id] + let l:filename = ale#path#FromURI(a:response.params.uri) + let l:buffer = bufnr(l:filename) + + if l:buffer <= 0 + return + endif + + let l:loclist = ale#lsp#response#ReadDiagnostics(a:response) + + call ale#engine#HandleLoclist(l:linter_name, l:buffer, l:loclist) +endfunction + +function! s:HandleTSServerDiagnostics(response, error_type) abort + let l:buffer = bufnr(a:response.body.file) + let l:info = get(g:ale_buffer_info, l:buffer, {}) + + if empty(l:info) + return + endif + + let l:thislist = ale#lsp#response#ReadTSServerDiagnostics(a:response) + + " tsserver sends syntax and semantic errors in separate messages, so we + " have to collect the messages separately for each buffer and join them + " back together again. + if a:error_type is# 'syntax' + let l:info.syntax_loclist = l:thislist + else + let l:info.semantic_loclist = l:thislist + endif + + let l:loclist = get(l:info, 'semantic_loclist', []) + \ + get(l:info, 'syntax_loclist', []) + + call ale#engine#HandleLoclist('tsserver', l:buffer, l:loclist) +endfunction + +function! s:HandleLSPErrorMessage(linter_name, response) abort + if !g:ale_history_enabled || !g:ale_history_log_output + return + endif + + if empty(a:linter_name) + return + endif + + let l:message = ale#lsp#response#GetErrorMessage(a:response) + + if empty(l:message) + return + endif + + " This global variable is set here so we don't load the debugging.vim file + " until someone uses :ALEInfo. + let g:ale_lsp_error_messages = get(g:, 'ale_lsp_error_messages', {}) + + if !has_key(g:ale_lsp_error_messages, a:linter_name) + let g:ale_lsp_error_messages[a:linter_name] = [] + endif + + call add(g:ale_lsp_error_messages[a:linter_name], l:message) +endfunction + +function! ale#lsp_linter#HandleLSPResponse(conn_id, response) abort + let l:method = get(a:response, 'method', '') + let l:linter_name = get(s:lsp_linter_map, a:conn_id, '') + + if get(a:response, 'jsonrpc', '') is# '2.0' && has_key(a:response, 'error') + call s:HandleLSPErrorMessage(l:linter_name, a:response) + elseif l:method is# 'textDocument/publishDiagnostics' + call s:HandleLSPDiagnostics(a:conn_id, a:response) + elseif get(a:response, 'type', '') is# 'event' + \&& get(a:response, 'event', '') is# 'semanticDiag' + call s:HandleTSServerDiagnostics(a:response, 'semantic') + elseif get(a:response, 'type', '') is# 'event' + \&& get(a:response, 'event', '') is# 'syntaxDiag' + call s:HandleTSServerDiagnostics(a:response, 'syntax') + endif +endfunction + +" Given a buffer, an LSP linter, and a callback to register for handling +" messages, start up an LSP linter and get ready to receive errors or +" completions. +function! ale#lsp_linter#StartLSP(buffer, linter, callback) abort + let l:command = '' + let l:address = '' + let l:root = ale#util#GetFunction(a:linter.project_root_callback)(a:buffer) + + if empty(l:root) && a:linter.lsp isnot# 'tsserver' + " If there's no project root, then we can't check files with LSP, + " unless we are using tsserver, which doesn't use project roots. + return {} + endif + + 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 + endif + + if a:linter.lsp is# 'socket' + let l:address = ale#linter#GetAddress(a:buffer, a:linter) + let l:conn_id = ale#lsp#ConnectToAddress( + \ l:address, + \ l:root, + \ a:callback, + \ l:initialization_options, + \) + else + let l:executable = ale#linter#GetExecutable(a:buffer, a:linter) + + if !executable(l:executable) + return {} + endif + + let l:command = ale#job#PrepareCommand( + \ a:buffer, + \ ale#linter#GetCommand(a:buffer, a:linter), + \) + let l:conn_id = ale#lsp#StartProgram( + \ l:executable, + \ l:command, + \ l:root, + \ a:callback, + \ l:initialization_options, + \) + endif + + let l:language_id = ale#util#GetFunction(a:linter.language_callback)(a:buffer) + + if !l:conn_id + if g:ale_history_enabled && !empty(l:command) + call ale#history#Add(a:buffer, 'failed', l:conn_id, l:command) + endif + + return {} + endif + + let l:details = { + \ 'buffer': a:buffer, + \ 'connection_id': l:conn_id, + \ 'command': l:command, + \ 'project_root': l:root, + \ 'language_id': l:language_id, + \} + + if ale#lsp#OpenDocument(l:details) + if g:ale_history_enabled && !empty(l:command) + call ale#history#Add(a:buffer, 'started', l:conn_id, l:command) + endif + endif + + " The change message needs to be sent for tsserver before doing anything. + if a:linter.lsp is# 'tsserver' + call ale#lsp#NotifyForChanges(l:details) + endif + + return l:details +endfunction + +function! ale#lsp_linter#CheckWithLSP(buffer, linter) abort + let l:info = g:ale_buffer_info[a:buffer] + let l:lsp_details = ale#lsp_linter#StartLSP( + \ a:buffer, + \ a:linter, + \ function('ale#lsp_linter#HandleLSPResponse'), + \) + + if empty(l:lsp_details) + return 0 + endif + + let l:id = l:lsp_details.connection_id + let l:root = l:lsp_details.project_root + + " Remember the linter this connection is for. + let s:lsp_linter_map[l:id] = a:linter.name + + if a:linter.lsp is# 'tsserver' + let l:message = ale#lsp#tsserver_message#Geterr(a:buffer) + let l:request_id = ale#lsp#Send(l:id, l:message, l:root) + + let l:notified = l:request_id != 0 + else + let l:notified = ale#lsp#NotifyForChanges(l:lsp_details) + endif + + " If this was a file save event, also notify the server of that. + if a:linter.lsp isnot# 'tsserver' + \&& getbufvar(a:buffer, 'ale_save_event_fired', 0) + let l:save_message = ale#lsp#message#DidSave(a:buffer) + let l:request_id = ale#lsp#Send(l:id, l:save_message, l:root) + + let l:notified = l:request_id != 0 + endif + + if l:notified + if index(l:info.active_linter_list, a:linter.name) < 0 + call add(l:info.active_linter_list, a:linter.name) + endif + endif + + return l:notified +endfunction + +" Clear LSP linter data for the linting engine. +function! ale#lsp_linter#ClearLSPData() abort + let s:lsp_linter_map = {} +endfunction + +" Just for tests. +function! ale#lsp_linter#SetLSPLinterMap(replacement_map) abort + let s:lsp_linter_map = a:replacement_map +endfunction diff --git a/autoload/ale/references.vim b/autoload/ale/references.vim index 9777519d..89df69eb 100644 --- a/autoload/ale/references.vim +++ b/autoload/ale/references.vim @@ -72,14 +72,13 @@ function! s:FindReferences(linter) abort \ ? function('ale#references#HandleTSServerResponse') \ : function('ale#references#HandleLSPResponse') - let l:lsp_details = ale#linter#StartLSP(l:buffer, a:linter, l:Callback) + let l:lsp_details = ale#lsp_linter#StartLSP(l:buffer, a:linter, l:Callback) if empty(l:lsp_details) return 0 endif let l:id = l:lsp_details.connection_id - let l:root = l:lsp_details.project_root if a:linter.lsp is# 'tsserver' let l:message = ale#lsp#tsserver_message#References( @@ -90,14 +89,14 @@ function! s:FindReferences(linter) abort else " Send a message saying the buffer has changed first, or the " references position probably won't make sense. - call ale#lsp#Send(l:id, ale#lsp#message#DidChange(l:buffer), l:root) + call ale#lsp#NotifyForChanges(l:lsp_details) let l:column = min([l:column, len(getline(l:line))]) let l:message = ale#lsp#message#References(l:buffer, l:line, l:column) endif - let l:request_id = ale#lsp#Send(l:id, l:message, l:root) + let l:request_id = ale#lsp#Send(l:id, l:message, l:lsp_details.project_root) let s:references_map[l:request_id] = {} endfunction diff --git a/test/command_callback/test_lintr_command_callback.vader b/test/command_callback/test_lintr_command_callback.vader index e655328b..2f7dfb1d 100644 --- a/test/command_callback/test_lintr_command_callback.vader +++ b/test/command_callback/test_lintr_command_callback.vader @@ -16,7 +16,7 @@ After: Execute(The default lintr command should be correct): AssertEqual \ 'cd ' . ale#Escape(getcwd()) . ' && ' - \ . 'Rscript -e ' + \ . 'Rscript --vanilla -e ' \ . ale#Escape('suppressPackageStartupMessages(library(lintr));' \ . 'lint(cache = FALSE, commandArgs(TRUE), ' \ . 'with_defaults())') @@ -28,7 +28,7 @@ Execute(The lintr options should be configurable): AssertEqual \ 'cd ' . ale#Escape(getcwd()) . ' && ' - \ . 'Rscript -e ' + \ . 'Rscript --vanilla -e ' \ . ale#Escape('suppressPackageStartupMessages(library(lintr));' \ . 'lint(cache = FALSE, commandArgs(TRUE), ' \ . 'with_defaults(object_usage_linter = NULL))') @@ -40,7 +40,7 @@ Execute(If the lint_package flag is set, lintr::lint_package should be called): AssertEqual \ 'cd ' . ale#Escape(getcwd()) . ' && ' - \ . 'Rscript -e ' + \ . 'Rscript --vanilla -e ' \ . ale#Escape('suppressPackageStartupMessages(library(lintr));' \ . 'lint_package(cache = FALSE, ' \ . 'linters = with_defaults())') diff --git a/test/completion/test_lsp_completion_messages.vader b/test/completion/test_lsp_completion_messages.vader index 734b330c..8ba2ad38 100644 --- a/test/completion/test_lsp_completion_messages.vader +++ b/test/completion/test_lsp_completion_messages.vader @@ -15,12 +15,18 @@ Before: let g:message_list = [] let g:Callback = '' - function! ale#linter#StartLSP(buffer, linter, callback) abort + function! ale#lsp_linter#StartLSP(buffer, linter, callback) abort let g:Callback = a:callback + let l:conn = ale#lsp#NewConnection({}) + let l:conn.id = 347 + let l:conn.open_documents = {a:buffer : -1} + return { + \ 'buffer': a:buffer, \ 'connection_id': 347, \ 'project_root': '/foo/bar', + \ 'language_id': 'python', \} endfunction @@ -43,6 +49,7 @@ After: unlet! b:ale_linters unlet! b:ale_tsserver_completion_names + call ale#lsp#RemoveConnectionWithID(347) call ale#test#RestoreDirectory() call ale#linter#Reset() diff --git a/test/completion/test_lsp_completion_parsing.vader b/test/completion/test_lsp_completion_parsing.vader index 23bbd14c..736353e3 100644 --- a/test/completion/test_lsp_completion_parsing.vader +++ b/test/completion/test_lsp_completion_parsing.vader @@ -429,3 +429,23 @@ Execute(Should handle Python completion results correctly): \ ] \ } \ }) + +Execute(Should handle missing detail keys): + AssertEqual + \ [ + \ {'word': 'x', 'menu': '', 'info': 'y', 'kind': 'f', 'icase': 1}, + \ ], + \ ale#completion#ParseLSPCompletions({ + \ 'jsonrpc': '2.0', + \ 'id': 6, + \ 'result': { + \ 'isIncomplete': v:false, + \ 'items': [ + \ { + \ 'label': 'x', + \ 'kind': 3, + \ 'documentation': 'y', + \ }, + \ ] + \ } + \ }) diff --git a/test/fixers/test_qmlfmt_fixer_callback.vader b/test/fixers/test_qmlfmt_fixer_callback.vader new file mode 100644 index 00000000..e216f2e1 --- /dev/null +++ b/test/fixers/test_qmlfmt_fixer_callback.vader @@ -0,0 +1,12 @@ +Before: + Save g:ale_qml_qmlfmt_executable + +After: + Restore + +Execute(The qmlfmt fixer should use the options you set): + let g:ale_qml_qmlfmt_executable = 'foo-exe' + + AssertEqual + \ {'command': ale#Escape('foo-exe')}, + \ ale#fixers#qmlfmt#Fix(bufnr('')) diff --git a/test/lsp/test_did_save_event.vader b/test/lsp/test_did_save_event.vader index 042a3ce2..97774372 100644 --- a/test/lsp/test_did_save_event.vader +++ b/test/lsp/test_did_save_event.vader @@ -34,12 +34,18 @@ Before: \ }) let g:ale_linters = {'foobar': ['dummy_linter']} - function! ale#linter#StartLSP(buffer, linter, callback) abort + function! ale#lsp_linter#StartLSP(buffer, linter, callback) abort let g:Callback = a:callback + let l:conn = ale#lsp#NewConnection({}) + let l:conn.id = 347 + let l:conn.open_documents = {a:buffer : -1} + return { + \ 'buffer': a:buffer, \ 'connection_id': 347, \ 'project_root': '/foo/bar', + \ 'language_id': 'foobar', \} endfunction @@ -59,6 +65,7 @@ After: delfunction LanguageCallback delfunction ProjectRootCallback + call ale#lsp#RemoveConnectionWithID(347) call ale#test#RestoreDirectory() call ale#linter#Reset() diff --git a/test/test_engine_lsp_response_handling.vader b/test/test_engine_lsp_response_handling.vader index 3d317e9c..18bad0a1 100644 --- a/test/test_engine_lsp_response_handling.vader +++ b/test/test_engine_lsp_response_handling.vader @@ -11,7 +11,7 @@ After: call ale#test#RestoreDirectory() call ale#linter#Reset() - call ale#engine#ClearLSPData() + call ale#lsp_linter#ClearLSPData() Given foobar(An empty file): Execute(tsserver syntax error responses should be handled correctly): @@ -21,7 +21,7 @@ Execute(tsserver syntax error responses should be handled correctly): " When we get syntax errors and no semantic errors, we should keep the " syntax errors. - call ale#engine#HandleLSPResponse(1, { + call ale#lsp_linter#HandleLSPResponse(1, { \ 'seq': 0, \ 'type': 'event', \ 'event': 'syntaxDiag', @@ -43,7 +43,7 @@ Execute(tsserver syntax error responses should be handled correctly): \ ], \ }, \}) - call ale#engine#HandleLSPResponse(1, { + call ale#lsp_linter#HandleLSPResponse(1, { \ 'seq': 0, \ 'type': 'event', \ 'event': 'semanticDiag', @@ -71,7 +71,7 @@ Execute(tsserver syntax error responses should be handled correctly): \ getloclist(0) " After we get empty syntax errors, we should clear them. - call ale#engine#HandleLSPResponse(1, { + call ale#lsp_linter#HandleLSPResponse(1, { \ 'seq': 0, \ 'type': 'event', \ 'event': 'syntaxDiag', @@ -94,7 +94,7 @@ Execute(tsserver semantic error responses should be handled correctly): " When we get syntax errors and no semantic errors, we should keep the " syntax errors. - call ale#engine#HandleLSPResponse(1, { + call ale#lsp_linter#HandleLSPResponse(1, { \ 'seq': 0, \ 'type': 'event', \ 'event': 'syntaxDiag', @@ -104,7 +104,7 @@ Execute(tsserver semantic error responses should be handled correctly): \ ], \ }, \}) - call ale#engine#HandleLSPResponse(1, { + call ale#lsp_linter#HandleLSPResponse(1, { \ 'seq': 0, \ 'type': 'event', \ 'event': 'semanticDiag', @@ -144,7 +144,7 @@ Execute(tsserver semantic error responses should be handled correctly): \ getloclist(0) " After we get empty syntax errors, we should clear them. - call ale#engine#HandleLSPResponse(1, { + call ale#lsp_linter#HandleLSPResponse(1, { \ 'seq': 0, \ 'type': 'event', \ 'event': 'semanticDiag', @@ -161,8 +161,8 @@ Execute(tsserver semantic error responses should be handled correctly): \ getloclist(0) Execute(LSP errors should be logged in the history): - call ale#engine#SetLSPLinterMap({'347': 'foobar'}) - call ale#engine#HandleLSPResponse(347, { + call ale#lsp_linter#SetLSPLinterMap({'347': 'foobar'}) + call ale#lsp_linter#HandleLSPResponse(347, { \ 'jsonrpc': '2.0', \ 'error': { \ 'code': -32602, diff --git a/test/test_find_references.vader b/test/test_find_references.vader index 6ab8e8fb..c2290ca3 100644 --- a/test/test_find_references.vader +++ b/test/test_find_references.vader @@ -14,12 +14,18 @@ Before: runtime autoload/ale/util.vim runtime autoload/ale/preview.vim - function! ale#linter#StartLSP(buffer, linter, callback) abort + function! ale#lsp_linter#StartLSP(buffer, linter, callback) abort let g:Callback = a:callback + let l:conn = ale#lsp#NewConnection({}) + let l:conn.id = 347 + let l:conn.open_documents = {a:buffer : -1} + return { + \ 'buffer': a:buffer, \ 'connection_id': 347, \ 'project_root': '/foo/bar', + \ 'language_id': 'python', \} endfunction @@ -39,6 +45,7 @@ Before: endfunction After: + call ale#lsp#RemoveConnectionWithID(347) call ale#references#SetMap({}) call ale#test#RestoreDirectory() call ale#linter#Reset() diff --git a/test/test_go_to_definition.vader b/test/test_go_to_definition.vader index 6af97099..3a1d7458 100644 --- a/test/test_go_to_definition.vader +++ b/test/test_go_to_definition.vader @@ -11,12 +11,18 @@ Before: runtime autoload/ale/lsp.vim runtime autoload/ale/util.vim - function! ale#linter#StartLSP(buffer, linter, callback) abort + function! ale#lsp_linter#StartLSP(buffer, linter, callback) abort let g:Callback = a:callback + let l:conn = ale#lsp#NewConnection({}) + let l:conn.id = 347 + let l:conn.open_documents = {a:buffer : -1} + return { + \ 'buffer': a:buffer, \ 'connection_id': 347, \ 'project_root': '/foo/bar', + \ 'language_id': 'python', \} endfunction @@ -31,6 +37,7 @@ Before: endfunction After: + call ale#lsp#RemoveConnectionWithID(347) call ale#definition#SetMap({}) call ale#test#RestoreDirectory() call ale#linter#Reset() diff --git a/test/test_hover.vader b/test/test_hover.vader index 18dcebaf..15f164f0 100644 --- a/test/test_hover.vader +++ b/test/test_hover.vader @@ -11,7 +11,7 @@ Before: runtime autoload/ale/lsp.vim runtime autoload/ale/util.vim - function! ale#linter#StartLSP(buffer, linter, callback) abort + function! ale#lsp_linter#StartLSP(buffer, linter, callback) abort let g:Callback = a:callback return { |