summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--README.md4
-rw-r--r--ale_linters/r/lintr.vim2
-rw-r--r--autoload/ale/completion.vim7
-rw-r--r--autoload/ale/definition.vim7
-rw-r--r--autoload/ale/engine.vim137
-rw-r--r--autoload/ale/fix/registry.vim5
-rw-r--r--autoload/ale/fixers/qmlfmt.vim11
-rw-r--r--autoload/ale/hover.vim8
-rw-r--r--autoload/ale/linter.vim78
-rw-r--r--autoload/ale/lsp.vim85
-rw-r--r--autoload/ale/lsp/reset.vim4
-rw-r--r--autoload/ale/lsp_linter.vim228
-rw-r--r--autoload/ale/references.vim7
-rw-r--r--test/command_callback/test_lintr_command_callback.vader6
-rw-r--r--test/completion/test_lsp_completion_messages.vader9
-rw-r--r--test/completion/test_lsp_completion_parsing.vader20
-rw-r--r--test/fixers/test_qmlfmt_fixer_callback.vader12
-rw-r--r--test/lsp/test_did_save_event.vader9
-rw-r--r--test/test_engine_lsp_response_handling.vader18
-rw-r--r--test/test_find_references.vader9
-rw-r--r--test/test_go_to_definition.vader9
-rw-r--r--test/test_hover.vader2
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
diff --git a/README.md b/README.md
index b742212b..c328e4a2 100644
--- a/README.md
+++ b/README.md
@@ -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 {