Lazy-load LSP linters, and check b:changedtick before notifying about changes
This commit is contained in:
parent
f1b72218c3
commit
bda89506ba
15 changed files with 358 additions and 259 deletions
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
228
autoload/ale/lsp_linter.vim
Normal file
228
autoload/ale/lsp_linter.vim
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
Reference in a new issue