summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorw0rp <devw0rp@gmail.com>2017-07-26 10:37:37 +0100
committerw0rp <devw0rp@gmail.com>2017-07-26 10:37:37 +0100
commitcd860e3e8d2b8d35920f27279bbd1ec346ac4d62 (patch)
tree03c63eb3e37d2e2b5761f3bbe3af7b1b0e8e05df
parent86297a7c65eadc5dc7668e129c92316aa0a89898 (diff)
downloadale-cd860e3e8d2b8d35920f27279bbd1ec346ac4d62.zip
#517 Add more code LSP support which makes the tssserver linter behave more like the LSP linters
-rw-r--r--ale_linters/php/langserver.vim38
-rw-r--r--ale_linters/typescript/tsserver.vim11
-rw-r--r--autoload/ale/completion.vim44
-rw-r--r--autoload/ale/engine.vim96
-rw-r--r--autoload/ale/job.vim4
-rw-r--r--autoload/ale/linter.vim84
-rw-r--r--autoload/ale/lsp.vim127
-rw-r--r--autoload/ale/lsp/message.vim52
-rw-r--r--autoload/ale/lsp/response.vim7
-rw-r--r--autoload/ale/path.vim23
-rw-r--r--autoload/ale/uri.vim18
-rw-r--r--test/lsp/test_lsp_client_messages.vader79
-rw-r--r--test/lsp/test_read_lsp_diagnostics.vader40
-rw-r--r--test/test_linter_defintion_processing.vader6
-rw-r--r--test/test_path_uri.vader16
-rw-r--r--test/util/test_cd_string_commands.vader9
16 files changed, 485 insertions, 169 deletions
diff --git a/ale_linters/php/langserver.vim b/ale_linters/php/langserver.vim
new file mode 100644
index 00000000..8dad5ac7
--- /dev/null
+++ b/ale_linters/php/langserver.vim
@@ -0,0 +1,38 @@
+" Author: Eric Stern <eric@ericstern.com>
+" Description: PHP Language server integration for ALE
+
+" This linter is disabled for now.
+finish
+
+call ale#Set('php_langserver_executable', 'php-language-server.php')
+call ale#Set('php_langserver_config_path', '')
+call ale#Set('php_langserver_use_global', 0)
+
+function! ale_linters#php#langserver#GetExecutable(buffer) abort
+ return ale#node#FindExecutable(a:buffer, 'php_langserver', [
+ \ 'vendor/bin/php-language-server.php',
+ \])
+endfunction
+
+function! ale_linters#php#langserver#GetCommand(buffer) abort
+ return 'php ' . ale_linters#php#langserver#GetExecutable(a:buffer)
+endfunction
+
+function! ale_linters#php#langserver#GetLanguage(buffer) abort
+ return 'php'
+endfunction
+
+function! ale_linters#php#langserver#GetProjectRoot(buffer) abort
+ let l:git_path = ale#path#FindNearestDirectory(a:buffer, '.git')
+
+ return !empty(l:git_path) ? fnamemodify(l:git_path, ':h') : ''
+endfunction
+
+call ale#linter#Define('php', {
+\ 'name': 'langserver',
+\ 'lsp': 'stdio',
+\ 'executable_callback': 'ale_linters#php#langserver#GetExecutable',
+\ 'command_callback': 'ale_linters#php#langserver#GetCommand',
+\ 'language_callback': 'ale_linters#php#langserver#GetLanguage',
+\ 'project_root_callback': 'ale_linters#php#langserver#GetProjectRoot',
+\})
diff --git a/ale_linters/typescript/tsserver.vim b/ale_linters/typescript/tsserver.vim
index 465e80c7..7a155bd9 100644
--- a/ale_linters/typescript/tsserver.vim
+++ b/ale_linters/typescript/tsserver.vim
@@ -5,6 +5,15 @@ call ale#Set('typescript_tsserver_executable', 'tsserver')
call ale#Set('typescript_tsserver_config_path', '')
call ale#Set('typescript_tsserver_use_global', 0)
+" These functions need to be defined just to comply with the API for LSP.
+function! ale_linters#typescript#tsserver#GetProjectRoot(buffer) abort
+ return ''
+endfunction
+
+function! ale_linters#typescript#tsserver#GetLanguage(buffer) abort
+ return ''
+endfunction
+
function! ale_linters#typescript#tsserver#GetExecutable(buffer) abort
return ale#node#FindExecutable(a:buffer, 'typescript_tsserver', [
\ 'node_modules/.bin/tsserver',
@@ -16,4 +25,6 @@ call ale#linter#Define('typescript', {
\ 'lsp': 'tsserver',
\ 'executable_callback': 'ale_linters#typescript#tsserver#GetExecutable',
\ 'command_callback': 'ale_linters#typescript#tsserver#GetExecutable',
+\ 'project_root_callback': 'ale_linters#typescript#tsserver#GetProjectRoot',
+\ 'language_callback': 'ale_linters#typescript#tsserver#GetLanguage',
\})
diff --git a/autoload/ale/completion.vim b/autoload/ale/completion.vim
index 334e2570..214891fb 100644
--- a/autoload/ale/completion.vim
+++ b/autoload/ale/completion.vim
@@ -203,41 +203,29 @@ function! s:HandleTSServerLSPResponse(response) abort
endif
endfunction
-function! s:GetCompletionsForTSServer(linter) abort
+function! s:GetLSPCompletions(linter) abort
let l:buffer = bufnr('')
- let l:executable = ale#linter#GetExecutable(l:buffer, a:linter)
- let l:command = ale#job#PrepareCommand(
- \ ale#linter#GetCommand(l:buffer, a:linter),
- \)
- let l:id = ale#lsp#StartProgram(
- \ l:executable,
- \ l:command,
+ let l:lsp_details = ale#linter#StartLSP(
+ \ l:buffer,
+ \ a:linter,
\ function('s:HandleTSServerLSPResponse'),
\)
- if !l:id
- if g:ale_history_enabled
- call ale#history#Add(l:buffer, 'failed', l:id, l:command)
- endif
- endif
-
- if ale#lsp#OpenTSServerDocumentIfNeeded(l:id, l:buffer)
- if g:ale_history_enabled
- call ale#history#Add(l:buffer, 'started', l:id, l:command)
- endif
+ if empty(l:lsp_details)
+ return 0
endif
- call ale#lsp#Send(l:id, ale#lsp#tsserver_message#Change(l:buffer))
+ let l:id = l:lsp_details.connection_id
+ let l:command = l:lsp_details.command
+ let l:root = l:lsp_details.project_root
- let l:request_id = ale#lsp#Send(
- \ l:id,
- \ ale#lsp#tsserver_message#Completions(
- \ l:buffer,
- \ b:ale_completion_info.line,
- \ b:ale_completion_info.column,
- \ b:ale_completion_info.prefix,
- \ ),
+ let l:message = ale#lsp#tsserver_message#Completions(
+ \ l:buffer,
+ \ b:ale_completion_info.line,
+ \ b:ale_completion_info.column,
+ \ b:ale_completion_info.prefix,
\)
+ let l:request_id = ale#lsp#Send(l:id, l:message, l:root)
if l:request_id
let b:ale_completion_info.conn_id = l:id
@@ -268,7 +256,7 @@ function! ale#completion#GetCompletions() abort
for l:linter in ale#linter#Get(&filetype)
if l:linter.lsp ==# 'tsserver'
- call s:GetCompletionsForTSServer(l:linter)
+ call s:GetLSPCompletions(l:linter)
endif
endfor
endfunction
diff --git a/autoload/ale/engine.vim b/autoload/ale/engine.vim
index 60cdf489..1ffdf443 100644
--- a/autoload/ale/engine.vim
+++ b/autoload/ale/engine.vim
@@ -31,13 +31,15 @@ endfunction
function! ale#engine#InitBufferInfo(buffer) abort
if !has_key(g:ale_buffer_info, a:buffer)
- " job_list will hold the list of jobs
+ " job_list will hold the list of job IDs
+ " active_linter_list will hold the list of active linter names
" loclist holds the loclist items after all jobs have completed.
" temporary_file_list holds temporary files to be cleaned up
" temporary_directory_list holds temporary directories to be cleaned up
" history holds a list of previously run commands for this buffer
let g:ale_buffer_info[a:buffer] = {
\ 'job_list': [],
+ \ 'active_linter_list': [],
\ 'loclist': [],
\ 'temporary_file_list': [],
\ 'temporary_directory_list': [],
@@ -114,6 +116,16 @@ function! s:GatherOutput(job_id, line) abort
endfunction
function! s:HandleLoclist(linter_name, buffer, loclist) abort
+ let l:buffer_info = get(g:ale_buffer_info, a:buffer, {})
+
+ if empty(l:buffer_info)
+ return
+ endif
+
+ " Remove this linter from the list of active linters.
+ " This may have already been done when the job exits.
+ call filter(l:buffer_info.active_linter_list, 'v:val !=# a:linter_name')
+
" Make some adjustments to the loclists to fix common problems, and also
" to set default values for loclist items.
let l:linter_loclist = ale#engine#FixLocList(a:buffer, a:linter_name, a:loclist)
@@ -154,6 +166,7 @@ function! s:HandleExit(job_id, exit_code) abort
call ale#job#Stop(a:job_id)
call remove(s:job_info_map, a:job_id)
call filter(g:ale_buffer_info[l:buffer].job_list, 'v:val !=# a:job_id')
+ call filter(g:ale_buffer_info[l:buffer].active_linter_list, 'v:val !=# l:linter.name')
" Stop here if we land in the handle for a job completing if we're in
" a sandbox.
@@ -180,29 +193,32 @@ function! s:HandleExit(job_id, exit_code) abort
call s:HandleLoclist(l:linter.name, l:buffer, l:loclist)
endfunction
-function! s:HandleLSPResponse(response) abort
- let l:is_diag_response = get(a:response, 'type', '') ==# 'event'
- \ && get(a:response, 'event', '') ==# 'semanticDiag'
+function! s:HandleLSPDiagnostics(response) abort
+ let l:filename = ale#path#FromURI(a:response.params.uri)
+ let l:buffer = bufnr(l:filename)
+ let l:loclist = ale#lsp#response#ReadDiagnostics(a:response)
- if !l:is_diag_response
- return
- endif
+ call s:HandleLoclist('langserver', l:buffer, l:loclist)
+endfunction
+function! s:HandleTSServerDiagnostics(response) 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:info.waiting_for_tsserver = 0
-
let l:loclist = ale#lsp#response#ReadTSServerDiagnostics(a:response)
call s:HandleLoclist('tsserver', l:buffer, l:loclist)
endfunction
+function! s:HandleLSPResponse(response) abort
+ let l:method = get(a:response, 'method', '')
+
+ if l:method ==# 'textDocument/publishDiagnostics'
+ call s:HandleLSPDiagnostics(a:response)
+ elseif get(a:response, 'type', '') ==# 'event'
+ \&& get(a:response, 'event', '') ==# 'semanticDiag'
+ call s:HandleTSServerDiagnostics(a:response)
+ endif
+endfunction
+
function! ale#engine#SetResults(buffer, loclist) abort
let l:linting_is_done = !ale#engine#IsCheckingBuffer(a:buffer)
@@ -430,6 +446,7 @@ function! s:RunJob(options) abort
if l:job_id
" Add the job to the list of jobs, so we can track them.
call add(g:ale_buffer_info[l:buffer].job_list, l:job_id)
+ call add(g:ale_buffer_info[l:buffer].active_linter_list, l:linter.name)
let l:status = 'started'
" Store the ID for the job in the map to read back again.
@@ -555,41 +572,27 @@ function! s:StopCurrentJobs(buffer, include_lint_file_jobs) abort
let l:info.job_list = l:new_job_list
endfunction
-function! s:CheckWithTSServer(buffer, linter, executable) abort
- let l:info = g:ale_buffer_info[a:buffer]
-
- let l:command = ale#job#PrepareCommand(
- \ ale#linter#GetCommand(a:buffer, a:linter),
- \)
- let l:id = ale#lsp#StartProgram(
- \ a:executable,
- \ l:command,
+function! s:CheckWithLSP(buffer, linter) abort
+ let l:lsp_details = ale#linter#StartLSP(
+ \ a:buffer,
+ \ a:linter,
\ function('s:HandleLSPResponse'),
\)
- if !l:id
- if g:ale_history_enabled
- call ale#history#Add(a:buffer, 'failed', l:id, l:command)
- endif
-
+ if empty(l:lsp_details)
return 0
endif
- if ale#lsp#OpenTSServerDocumentIfNeeded(l:id, a:buffer)
- if g:ale_history_enabled
- call ale#history#Add(a:buffer, 'started', l:id, l:command)
- endif
- endif
+ let l:id = l:lsp_details.connection_id
+ let l:root = l:lsp_details.project_root
- call ale#lsp#Send(l:id, ale#lsp#tsserver_message#Change(a:buffer))
-
- let l:request_id = ale#lsp#Send(
- \ l:id,
- \ ale#lsp#tsserver_message#Geterr(a:buffer),
- \)
+ let l:change_message = a:linter.lsp ==# '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 l:request_id != 0
- let l:info.waiting_for_tsserver = 1
+ call add(g:ale_buffer_info[a:buffer].active_linter_list, a:linter.name)
endif
return l:request_id != 0
@@ -614,15 +617,12 @@ endfunction
"
" Returns 1 if the linter was successfully run.
function! s:RunLinter(buffer, linter) abort
- if empty(a:linter.lsp) || a:linter.lsp ==# 'tsserver'
+ if !empty(a:linter.lsp) || a:linter.lsp ==# 'tsserver'
+ return s:CheckWithLSP(a:buffer, a:linter)
+ else
let l:executable = ale#linter#GetExecutable(a:buffer, a:linter)
- " Run this program if it can be executed.
if s:IsExecutable(l:executable)
- if a:linter.lsp ==# 'tsserver'
- return s:CheckWithTSServer(a:buffer, a:linter, l:executable)
- endif
-
return s:InvokeChain(a:buffer, a:linter, 0, [])
endif
endif
diff --git a/autoload/ale/job.vim b/autoload/ale/job.vim
index 93f28824..63e42f7b 100644
--- a/autoload/ale/job.vim
+++ b/autoload/ale/job.vim
@@ -199,6 +199,10 @@ function! ale#job#Start(command, options) abort
let l:job_info = copy(a:options)
let l:job_options = {}
+ if exists('*ch_logfile')
+ call ch_logfile(expand('~/channel.log'), 'a')
+ endif
+
if has('nvim')
if has_key(a:options, 'out_cb')
let l:job_options.on_stdout = function('s:NeoVimCallback')
diff --git a/autoload/ale/linter.vim b/autoload/ale/linter.vim
index 1c99a0cc..0af42af9 100644
--- a/autoload/ale/linter.vim
+++ b/autoload/ale/linter.vim
@@ -62,6 +62,7 @@ function! ale#linter#PreProcess(linter) abort
let l:needs_address = l:obj.lsp ==# 'socket'
let l:needs_executable = l:obj.lsp !=# 'socket'
let l:needs_command = l:obj.lsp !=# 'socket'
+ let l:needs_lsp_details = !empty(l:obj.lsp)
if empty(l:obj.lsp)
let l:obj.callback = get(a:linter, 'callback')
@@ -176,6 +177,20 @@ function! ale#linter#PreProcess(linter) abort
throw '`address_callback` must be defined for getting the LSP address'
endif
+ if l:needs_lsp_details
+ let l:obj.language_callback = get(a:linter, 'language_callback')
+
+ if !s:IsCallback(l:obj.language_callback)
+ throw '`language_callback` must be a callback for LSP linters'
+ endif
+
+ let l:obj.project_root_callback = get(a:linter, 'project_root_callback')
+
+ if !s:IsCallback(l:obj.project_root_callback)
+ throw '`project_root_callback` must be a callback for LSP linters'
+ endif
+ endif
+
let l:obj.output_stream = get(a:linter, 'output_stream', 'stdout')
if type(l:obj.output_stream) != type('')
@@ -346,3 +361,72 @@ function! ale#linter#GetCommand(buffer, linter) abort
\ ? ale#util#GetFunction(a:linter.command_callback)(a:buffer)
\ : a:linter.command
endfunction
+
+" Given a buffer and linter, get the address for connecting to the server.
+function! ale#linter#GetAddress(buffer, linter) abort
+ return has_key(a:linter, 'address_callback')
+ \ ? ale#util#GetFunction(a:linter.address_callback)(a:buffer)
+ \ : 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 a:linter.lsp ==# 'socket'
+ let l:address = ale#linter#GetAddress(a:buffer, a:linter)
+ let l:conn_id = ale#lsp#ConnectToAddress(
+ \ l:address,
+ \ l:root,
+ \ a:callback,
+ \)
+ else
+ let l:executable = ale#linter#GetExecutable(a:buffer, a:linter)
+
+ if !executable(l:executable)
+ return {}
+ endif
+
+ let l:command = ale#job#PrepareCommand(
+ \ ale#linter#GetCommand(a:buffer, a:linter),
+ \)
+ let l:conn_id = ale#lsp#StartProgram(
+ \ l:executable,
+ \ l:command,
+ \ l:root,
+ \ a:callback,
+ \)
+ 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 ==# '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 083a27ec..2c9b2990 100644
--- a/autoload/ale/lsp.vim
+++ b/autoload/ale/lsp.vim
@@ -11,10 +11,13 @@ function! s:NewConnection() abort
" 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.
+ " callback_list: A list of callbacks for handling LSP responses.
let l:conn = {
\ 'id': '',
\ 'data': '',
+ \ 'projects': {},
\ 'open_documents': [],
+ \ 'callback_list': [],
\}
call add(s:connections, l:conn)
@@ -141,6 +144,35 @@ function! ale#lsp#ReadMessageData(data) abort
return [l:remainder, l:response_list]
endfunction
+function! s:FindProjectWithInitRequestID(conn, init_request_id) abort
+ for l:project_root in keys(a:conn.projects)
+ let l:project = a:conn.projects[l:project_root]
+
+ if l:project.init_request_id == a:init_request_id
+ return l:project
+ endif
+ endfor
+
+ return {}
+endfunction
+
+function! s:HandleInitializeResponse(conn, response) abort
+ let l:request_id = a:response.request_id
+ let l:project = s:FindProjectWithInitRequestID(a:conn, l:request_id)
+
+ if empty(l:project)
+ return
+ endif
+
+ " After the server starts, send messages we had queued previously.
+ for l:message_data in l:project.message_queue
+ call s:SendMessageData(a:conn, l:message_data)
+ endfor
+
+ " Remove the messages now.
+ let a:conn.message_queue = []
+endfunction
+
function! ale#lsp#HandleMessage(conn, message) abort
let a:conn.data .= a:message
@@ -149,8 +181,13 @@ function! ale#lsp#HandleMessage(conn, message) abort
" Call our callbacks.
for l:response in l:response_list
- if has_key(a:conn, 'callback')
- call ale#util#GetFunction(a:conn.callback)(l:response)
+ if get(l:response, 'method', '') ==# 'initialize'
+ call s:HandleInitializeResponse(a:conn, l:response)
+ else
+ " Call all of the registered handlers with the response.
+ for l:Callback in a:conn.callback_list
+ call ale#util#GetFunction(l:Callback)(l:response)
+ endfor
endif
endfor
endfunction
@@ -169,11 +206,22 @@ function! s:HandleCommandMessage(job_id, message) abort
call ale#lsp#HandleMessage(l:conn, a:message)
endfunction
+function! s:RegisterProject(conn, project_root) abort
+ if !has_key(a:conn, a:project_root)
+ " Tools without project roots are ready right away, like tsserver.
+ let a:conn.projects[a:project_root] = {
+ \ 'initialized': empty(a:project_root),
+ \ 'init_messsage_id': 0,
+ \ 'message_queue': [],
+ \}
+ endif
+endfunction
+
" Start a program for LSP servers which run with executables.
"
" The job ID will be returned for for the program if it ran, otherwise
" 0 will be returned.
-function! ale#lsp#StartProgram(executable, command, callback) abort
+function! ale#lsp#StartProgram(executable, command, project_root, callback) abort
if !executable(a:executable)
return 0
endif
@@ -199,13 +247,15 @@ function! ale#lsp#StartProgram(executable, command, callback) abort
endif
let l:conn.id = l:job_id
- let l:conn.callback = a:callback
+ " Add the callback to the List if it's not there already.
+ call uniq(sort(add(l:conn.callback_list, a:callback)))
+ call s:RegisterProject(l:conn, a:project_root)
return l:job_id
endfunction
" Connect to an address and set up a callback for handling responses.
-function! ale#lsp#ConnectToAddress(address, callback) abort
+function! ale#lsp#ConnectToAddress(address, project_root, callback) 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()
@@ -223,7 +273,22 @@ function! ale#lsp#ConnectToAddress(address, callback) abort
endif
let l:conn.id = a:address
- let l:conn.callback = a:callback
+ " Add the callback to the List if it's not there already.
+ call uniq(sort(add(l:conn.callback_list, a:callback)))
+ call s:RegisterProject(l:conn, a:project_root)
+
+ return 1
+endfunction
+
+function! s:SendMessageData(conn, data) abort
+ if has_key(a:conn, 'executable')
+ call ale#job#SendRaw(a:conn.id, a:data)
+ elseif has_key(a:conn, 'channel') && ch_status(a:conn.channnel) ==# 'open'
+ " Send the message to the server
+ call ch_sendraw(a:conn.channel, a:data)
+ else
+ return 0
+ endif
return 1
endfunction
@@ -234,28 +299,60 @@ endfunction
" Returns -1 when a message is sent, but no response is expected
" 0 when the message is not sent and
" >= 1 with the message ID when a response is expected.
-function! ale#lsp#Send(conn_id, message) abort
+function! ale#lsp#Send(conn_id, message, ...) abort
+ let l:project_root = get(a:000, 0, '')
+
let l:conn = s:FindConnection('id', a:conn_id)
+
+ if empty(l:conn)
+ return 0
+ endif
+
+ let l:project = get(l:conn.projects, l:project_root, {})
+
+ if empty(l:project)
+ return 0
+ endif
+
+ " If we haven't initialized the server yet, then send the message for it.
+ if !l:project.initialized
+ " Only send the init message once.
+ if !l:project.init_request_id
+ let [l:init_id, l:init_data] = ale#lsp#CreateMessageData(
+ \ ale#lsp#message#Initialize(l:conn.project_root),
+ \)
+
+ let l:project.init_request_id = l:init_id
+
+ call s:SendMessageData(l:conn, l:init_data)
+ endif
+ endif
+
let [l:id, l:data] = ale#lsp#CreateMessageData(a:message)
- if has_key(l:conn, 'executable')
- call ale#job#SendRaw(l:conn.id, l:data)
- elseif has_key(l:conn, 'channel') && ch_status(l:conn.channnel) ==# 'open'
- " Send the message to the server
- call ch_sendraw(l:conn.channel, l:data)
+ if l:project.initialized
+ " Send the message now.
+ call s:SendMessageData(l:conn, l:data)
else
- return 0
+ " Add the message we wanted to send to a List to send later.
+ call add(l:project.message_queue, l:data)
endif
return l:id == 0 ? -1 : l:id
endfunction
-function! ale#lsp#OpenTSServerDocumentIfNeeded(conn_id, buffer) abort
+function! ale#lsp#OpenDocumentIfNeeded(conn_id, buffer, project_root, language_id) abort
let l:conn = s:FindConnection('id', a:conn_id)
let l:opened = 0
if !empty(l:conn) && index(l:conn.open_documents, a:buffer) < 0
- call ale#lsp#Send(a:conn_id, ale#lsp#tsserver_message#Open(a:buffer))
+ if empty(a:language_id)
+ let l:message = ale#lsp#tsserver_message#Open(a:buffer)
+ else
+ let l:message = ale#lsp#message#DidOpen(a:buffer, a:language_id)
+ endif
+
+ call ale#lsp#Send(a:conn_id, l:message, a:project_root)
call add(l:conn.open_documents, a:buffer)
let l:opened = 1
endif
diff --git a/autoload/ale/lsp/message.vim b/autoload/ale/lsp/message.vim
index 937e4f46..7910247d 100644
--- a/autoload/ale/lsp/message.vim
+++ b/autoload/ale/lsp/message.vim
@@ -3,12 +3,32 @@
"
" Messages in this movie will be returned in the format
" [is_notification, method_name, params?]
+let g:ale_lsp_next_version_id = 1
-function! ale#lsp#message#Initialize(root_uri) abort
+" The LSP protocols demands that we send every change to a document, including
+" undo, with incrementing version numbers, so we'll just use one incrementing
+" ID for everything.
+function! ale#lsp#message#GetNextVersionID() abort
+ " Use the current ID
+ let l:id = g:ale_lsp_next_version_id
+
+ " Increment the ID variable.
+ let g:ale_lsp_next_version_id += 1
+
+ " When the ID overflows, reset it to 1. By the time we hit the initial ID
+ " again, the messages will be long gone.
+ if g:ale_lsp_next_version_id < 1
+ let g:ale_lsp_next_version_id = 1
+ endif
+
+ return l:id
+endfunction
+
+function! ale#lsp#message#Initialize(root_path) abort
" TODO: Define needed capabilities.
return [0, 'initialize', {
\ 'processId': getpid(),
- \ 'rootUri': a:root_uri,
+ \ 'rootPath': a:root_path,
\ 'capabilities': {},
\}]
endfunction
@@ -25,40 +45,44 @@ function! ale#lsp#message#Exit() abort
return [1, 'exit']
endfunction
-function! ale#lsp#message#DidOpen(uri, language_id, version, text) abort
+function! ale#lsp#message#DidOpen(buffer, language_id) abort
+ let l:lines = getbufline(a:buffer, 1, '$')
+
return [1, 'textDocument/didOpen', {
\ 'textDocument': {
- \ 'uri': a:uri,
+ \ 'uri': ale#path#ToURI(expand('#' . a:buffer . ':p')),
\ 'languageId': a:language_id,
- \ 'version': a:version,
- \ 'text': a:text,
+ \ 'version': ale#lsp#message#GetNextVersionID(),
+ \ 'text': join(l:lines, "\n"),
\ },
\}]
endfunction
-function! ale#lsp#message#DidChange(uri, version, text) abort
+function! ale#lsp#message#DidChange(buffer) abort
+ let l:lines = getbufline(a:buffer, 1, '$')
+
" For changes, we simply send the full text of the document to the server.
return [1, 'textDocument/didChange', {
\ 'textDocument': {
- \ 'uri': a:uri,
- \ 'version': a:version,
+ \ 'uri': ale#path#ToURI(expand('#' . a:buffer . ':p')),
+ \ 'version': ale#lsp#message#GetNextVersionID(),
\ },
- \ 'contentChanges': [{'text': a:text}]
+ \ 'contentChanges': [{'text': join(l:lines, "\n")}]
\}]
endfunction
-function! ale#lsp#message#DidSave(uri) abort
+function! ale#lsp#message#DidSave(buffer) abort
return [1, 'textDocument/didSave', {
\ 'textDocument': {
- \ 'uri': a:uri,
+ \ 'uri': ale#path#ToURI(expand('#' . a:buffer . ':p')),
\ },
\}]
endfunction
-function! ale#lsp#message#DidClose(uri) abort
+function! ale#lsp#message#DidClose(buffer) abort
return [1, 'textDocument/didClose', {
\ 'textDocument': {
- \ 'uri': a:uri,
+ \ 'uri': ale#path#ToURI(expand('#' . a:buffer . ':p')),
\ },
\}]
endfunction
diff --git a/autoload/ale/lsp/response.vim b/autoload/ale/lsp/response.vim
index a2146f6a..13219ef6 100644
--- a/autoload/ale/lsp/response.vim
+++ b/autoload/ale/lsp/response.vim
@@ -8,11 +8,10 @@ let s:SEVERITY_INFORMATION = 3
let s:SEVERITY_HINT = 4
" Parse the message for textDocument/publishDiagnostics
-function! ale#lsp#response#ReadDiagnostics(params) abort
- let l:filename = a:params.uri
+function! ale#lsp#response#ReadDiagnostics(response) abort
let l:loclist = []
- for l:diagnostic in a:params.diagnostics
+ for l:diagnostic in a:response.params.diagnostics
let l:severity = get(l:diagnostic, 'severity', 0)
let l:loclist_item = {
\ 'text': l:diagnostic.message,
@@ -40,7 +39,7 @@ function! ale#lsp#response#ReadDiagnostics(params) abort
call add(l:loclist, l:loclist_item)
endfor
- return [l:filename, l:loclist]
+ return l:loclist
endfunction
function! ale#lsp#response#ReadTSServerDiagnostics(response) abort
diff --git a/autoload/ale/path.vim b/autoload/ale/path.vim
index 9ac3d8f2..c68114ae 100644
--- a/autoload/ale/path.vim
+++ b/autoload/ale/path.vim
@@ -141,3 +141,26 @@ function! ale#path#Upwards(path) abort
return l:path_list
endfunction
+
+" Convert a filesystem path to a file:// URI
+" relatives paths will not be prefixed with the protocol.
+" For Windows paths, the `:` in C:\ etc. will not be percent-encoded.
+function! ale#path#ToURI(path) abort
+ let l:has_drive_letter = a:path[1:2] ==# ':\'
+
+ return substitute(
+ \ ((l:has_drive_letter || a:path[:0] ==# '/') ? 'file://' : '')
+ \ . (l:has_drive_letter ? '/' . a:path[:2] : '')
+ \ . ale#uri#Encode(l:has_drive_letter ? a:path[3:] : a:path),
+ \ '\\',
+ \ '/',
+ \ 'g',
+ \)
+endfunction
+
+function! ale#path#FromURI(uri) abort
+ let l:i = len('file://')
+ let l:encoded_path = a:uri[: l:i - 1] ==# 'file://' ? a:uri[l:i :] : a:uri
+
+ return ale#uri#Decode(l:encoded_path)
+endfunction
diff --git a/autoload/ale/uri.vim b/autoload/ale/uri.vim
new file mode 100644
index 00000000..934637d9
--- /dev/null
+++ b/autoload/ale/uri.vim
@@ -0,0 +1,18 @@
+" This probably doesn't handle Unicode characters well.
+function! ale#uri#Encode(value) abort
+ return substitute(
+ \ a:value,
+ \ '\([^a-zA-Z0-9\\/$\-_.!*''(),]\)',
+ \ '\=printf(''%%%02x'', char2nr(submatch(1)))',
+ \ 'g'
+ \)
+endfunction
+
+function! ale#uri#Decode(value) abort
+ return substitute(
+ \ a:value,
+ \ '%\(\x\x\)',
+ \ '\=nr2char(''0x'' . submatch(1))',
+ \ 'g'
+ \)
+endfunction
diff --git a/test/lsp/test_lsp_client_messages.vader b/test/lsp/test_lsp_client_messages.vader
index abf733ca..057abad4 100644
--- a/test/lsp/test_lsp_client_messages.vader
+++ b/test/lsp/test_lsp_client_messages.vader
@@ -1,10 +1,13 @@
Before:
silent! cd /testplugin/test/lsp
- let b:dir = getcwd()
+ let g:dir = getcwd()
+ let g:ale_lsp_next_version_id = 1
+
+ call ale#test#SetFilename('foo/bar.ts')
After:
- silent execute 'cd ' . fnameescape(b:dir)
- unlet! b:dir
+ silent execute 'cd ' . fnameescape(g:dir)
+ unlet! g:dir
Execute(ale#lsp#message#Initialize() should return correct messages):
AssertEqual
@@ -13,7 +16,7 @@ Execute(ale#lsp#message#Initialize() should return correct messages):
\ 'initialize',
\ {
\ 'processId': getpid(),
- \ 'rootUri': '/foo/bar',
+ \ 'rootPath': '/foo/bar',
\ 'capabilities': {},
\ }
\ ],
@@ -28,36 +31,51 @@ Execute(ale#lsp#message#Shutdown() should return correct messages):
Execute(ale#lsp#message#Exit() should return correct messages):
AssertEqual [1, 'exit'], ale#lsp#message#Exit(),
+Given typescript(A TypeScript file with 3 lines):
+ foo()
+ bar()
+ baz()
+
Execute(ale#lsp#message#DidOpen() should return correct messages):
+ let g:ale_lsp_next_version_id = 12
AssertEqual
\ [
\ 1,
\ 'textDocument/didOpen',
\ {
\ 'textDocument': {
- \ 'uri': '/foo/bar',
+ \ 'uri': 'file://' . g:dir . '/foo/bar.ts',
\ 'languageId': 'typescript',
- \ 'version': 123,
- \ 'text': 'foobar',
+ \ 'version': 12,
+ \ 'text': "foo()\nbar()\nbaz()",
\ },
\ }
\ ],
- \ ale#lsp#message#DidOpen('/foo/bar', 'typescript', 123, 'foobar')
+ \ ale#lsp#message#DidOpen(bufnr(''), 'typescript')
Execute(ale#lsp#message#DidChange() should return correct messages):
+ let g:ale_lsp_next_version_id = 34
+
AssertEqual
\ [
\ 1,
\ 'textDocument/didChange',
\ {
\ 'textDocument': {
- \ 'uri': '/foo/bar',
- \ 'version': 123,
+ \ 'uri': 'file://' . g:dir . '/foo/bar.ts',
+ \ 'version': 34,
\ },
- \ 'contentChanges': [{'text': 'foobar'}],
+ \ 'contentChanges': [{'text': "foo()\nbar()\nbaz()"}],
\ }
\ ],
- \ ale#lsp#message#DidChange('/foo/bar', 123, 'foobar')
+ \ ale#lsp#message#DidChange(bufnr(''))
+ " The version numbers should increment.
+ AssertEqual
+ \ 35,
+ \ ale#lsp#message#DidChange(bufnr(''))[2].textDocument.version
+ AssertEqual
+ \ 36,
+ \ ale#lsp#message#DidChange(bufnr(''))[2].textDocument.version
Execute(ale#lsp#message#DidSave() should return correct messages):
AssertEqual
@@ -66,11 +84,11 @@ Execute(ale#lsp#message#DidSave() should return correct messages):
\ 'textDocument/didSave',
\ {
\ 'textDocument': {
- \ 'uri': '/foo/bar',
+ \ 'uri': 'file://' . g:dir . '/foo/bar.ts',
\ },
\ }
\ ],
- \ ale#lsp#message#DidSave('/foo/bar')
+ \ ale#lsp#message#DidSave(bufnr(''))
Execute(ale#lsp#message#DidClose() should return correct messages):
AssertEqual
@@ -79,52 +97,41 @@ Execute(ale#lsp#message#DidClose() should return correct messages):
\ 'textDocument/didClose',
\ {
\ 'textDocument': {
- \ 'uri': '/foo/bar',
+ \ 'uri': 'file://' . g:dir . '/foo/bar.ts',
\ },
\ }
\ ],
- \ ale#lsp#message#DidClose('/foo/bar')
+ \ ale#lsp#message#DidClose(bufnr(''))
Execute(ale#lsp#tsserver_message#Open() should return correct messages):
- silent! noautocmd file foo.ts
-
AssertEqual
\ [
\ 1,
\ 'ts@open',
\ {
- \ 'file': b:dir . '/foo.ts',
+ \ 'file': g:dir . '/foo/bar.ts',
\ }
\ ],
\ ale#lsp#tsserver_message#Open(bufnr(''))
Execute(ale#lsp#tsserver_message#Close() should return correct messages):
- silent! noautocmd file foo.ts
-
AssertEqual
\ [
\ 1,
\ 'ts@close',
\ {
- \ 'file': b:dir . '/foo.ts',
+ \ 'file': g:dir . '/foo/bar.ts',
\ }
\ ],
\ ale#lsp#tsserver_message#Close(bufnr(''))
-Given typescript(A TypeScript file with 3 lines):
- foo()
- bar()
- baz()
-
Execute(ale#lsp#tsserver_message#Change() should return correct messages):
- silent! noautocmd file foo.ts
-
AssertEqual
\ [
\ 1,
\ 'ts@change',
\ {
- \ 'file': b:dir . '/foo.ts',
+ \ 'file': g:dir . '/foo/bar.ts',
\ 'line': 1,
\ 'offset': 1,
\ 'endLine': 1073741824,
@@ -135,27 +142,23 @@ Execute(ale#lsp#tsserver_message#Change() should return correct messages):
\ ale#lsp#tsserver_message#Change(bufnr(''))
Execute(ale#lsp#tsserver_message#Geterr() should return correct messages):
- silent! noautocmd file foo.ts
-
AssertEqual
\ [
\ 1,
\ 'ts@geterr',
\ {
- \ 'files': [b:dir . '/foo.ts'],
+ \ 'files': [g:dir . '/foo/bar.ts'],
\ }
\ ],
\ ale#lsp#tsserver_message#Geterr(bufnr(''))
Execute(ale#lsp#tsserver_message#Completions() should return correct messages):
- silent! noautocmd file foo.ts
-
AssertEqual
\ [
\ 0,
\ 'ts@completions',
\ {
- \ 'file': b:dir . '/foo.ts',
+ \ 'file': g:dir . '/foo/bar.ts',
\ 'line': 347,
\ 'offset': 12,
\ 'prefix': 'abc',
@@ -164,14 +167,12 @@ Execute(ale#lsp#tsserver_message#Completions() should return correct messages):
\ ale#lsp#tsserver_message#Completions(bufnr(''), 347, 12, 'abc')
Execute(ale#lsp#tsserver_message#CompletionEntryDetails() should return correct messages):
- silent! noautocmd file foo.ts
-
AssertEqual
\ [
\ 0,
\ 'ts@completionEntryDetails',
\ {
- \ 'file': b:dir . '/foo.ts',
+ \ 'file': g:dir . '/foo/bar.ts',
\ 'line': 347,
\ 'offset': 12,
\ 'entryNames': ['foo', 'bar'],
diff --git a/test/lsp/test_read_lsp_diagnostics.vader b/test/lsp/test_read_lsp_diagnostics.vader
index 63086a7f..3e637418 100644
--- a/test/lsp/test_read_lsp_diagnostics.vader
+++ b/test/lsp/test_read_lsp_diagnostics.vader
@@ -10,7 +10,7 @@ After:
delfunction Range
Execute(ale#lsp#response#ReadDiagnostics() should handle errors):
- AssertEqual ['filename.ts', [
+ AssertEqual [
\ {
\ 'type': 'E',
\ 'text': 'Something went wrong!',
@@ -20,18 +20,18 @@ Execute(ale#lsp#response#ReadDiagnostics() should handle errors):
\ 'end_col': 16,
\ 'nr': 'some-error',
\ }
- \ ]],
- \ ale#lsp#response#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [
+ \ ],
+ \ ale#lsp#response#ReadDiagnostics({'params': {'uri': 'filename.ts', 'diagnostics': [
\ {
\ 'severity': 1,
\ 'range': Range(2, 10, 4, 15),
\ 'code': 'some-error',
\ 'message': 'Something went wrong!',
\ },
- \ ]})
+ \ ]}})
Execute(ale#lsp#response#ReadDiagnostics() should handle warnings):
- AssertEqual ['filename.ts', [
+ AssertEqual [
\ {
\ 'type': 'W',
\ 'text': 'Something went wrong!',
@@ -41,18 +41,18 @@ Execute(ale#lsp#response#ReadDiagnostics() should handle warnings):
\ 'end_col': 4,
\ 'nr': 'some-warning',
\ }
- \ ]],
- \ ale#lsp#response#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [
+ \ ],
+ \ ale#lsp#response#ReadDiagnostics({'params': {'uri': 'filename.ts', 'diagnostics': [
\ {
\ 'severity': 2,
\ 'range': Range(1, 3, 1, 3),
\ 'code': 'some-warning',
\ 'message': 'Something went wrong!',
\ },
- \ ]})
+ \ ]}})
Execute(ale#lsp#response#ReadDiagnostics() should treat messages with missing severity as errors):
- AssertEqual ['filename.ts', [
+ AssertEqual [
\ {
\ 'type': 'E',
\ 'text': 'Something went wrong!',
@@ -62,17 +62,17 @@ Execute(ale#lsp#response#ReadDiagnostics() should treat messages with missing se
\ 'end_col': 16,
\ 'nr': 'some-error',
\ }
- \ ]],
- \ ale#lsp#response#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [
+ \ ],
+ \ ale#lsp#response#ReadDiagnostics({'params': {'uri': 'filename.ts', 'diagnostics': [
\ {
\ 'range': Range(2, 10, 4, 15),
\ 'code': 'some-error',
\ 'message': 'Something went wrong!',
\ },
- \ ]})
+ \ ]}})
Execute(ale#lsp#response#ReadDiagnostics() should handle messages without codes):
- AssertEqual ['filename.ts', [
+ AssertEqual [
\ {
\ 'type': 'E',
\ 'text': 'Something went wrong!',
@@ -81,16 +81,16 @@ Execute(ale#lsp#response#ReadDiagnostics() should handle messages without codes)
\ 'end_lnum': 5,
\ 'end_col': 16,
\ }
- \ ]],
- \ ale#lsp#response#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [
+ \ ],
+ \ ale#lsp#response#ReadDiagnostics({'params': {'uri': 'filename.ts', 'diagnostics': [
\ {
\ 'range': Range(2, 10, 4, 15),
\ 'message': 'Something went wrong!',
\ },
- \ ]})
+ \ ]}})
Execute(ale#lsp#response#ReadDiagnostics() should handle multiple messages):
- AssertEqual ['filename.ts', [
+ AssertEqual [
\ {
\ 'type': 'E',
\ 'text': 'Something went wrong!',
@@ -107,8 +107,8 @@ Execute(ale#lsp#response#ReadDiagnostics() should handle multiple messages):
\ 'end_lnum': 2,
\ 'end_col': 5,
\ },
- \ ]],
- \ ale#lsp#response#ReadDiagnostics({'uri': 'filename.ts', 'diagnostics': [
+ \ ],
+ \ ale#lsp#response#ReadDiagnostics({'params': {'uri': 'filename.ts', 'diagnostics': [
\ {
\ 'range': Range(0, 2, 0, 2),
\ 'message': 'Something went wrong!',
@@ -118,7 +118,7 @@ Execute(ale#lsp#response#ReadDiagnostics() should handle multiple messages):
\ 'range': Range(1, 4, 1, 4),
\ 'message': 'A warning',
\ },
- \ ]})
+ \ ]}})
Execute(ale#lsp#response#ReadTSServerDiagnostics() should handle tsserver responses):
AssertEqual [
diff --git a/test/test_linter_defintion_processing.vader b/test/test_linter_defintion_processing.vader
index 572591db..d946a600 100644
--- a/test/test_linter_defintion_processing.vader
+++ b/test/test_linter_defintion_processing.vader
@@ -372,6 +372,8 @@ Execute(PreProcess should accept tsserver LSP configuration):
\ 'executable': 'x',
\ 'command': 'x',
\ 'lsp': 'tsserver',
+ \ 'language_callback': 'x',
+ \ 'project_root_callback': 'x',
\}
AssertEqual 'tsserver', ale#linter#PreProcess(g:linter).lsp
@@ -392,6 +394,8 @@ Execute(PreProcess should accept stdio LSP configuration):
\ 'executable': 'x',
\ 'command': 'x',
\ 'lsp': 'stdio',
+ \ 'language_callback': 'x',
+ \ 'project_root_callback': 'x',
\}
AssertEqual 'stdio', ale#linter#PreProcess(g:linter).lsp
@@ -411,6 +415,8 @@ Execute(PreProcess should accept LSP server configurations):
\ 'name': 'x',
\ 'lsp': 'socket',
\ 'address_callback': 'X',
+ \ 'language_callback': 'x',
+ \ 'project_root_callback': 'x',
\}
AssertEqual 'socket', ale#linter#PreProcess(g:linter).lsp
diff --git a/test/test_path_uri.vader b/test/test_path_uri.vader
new file mode 100644
index 00000000..dbceac3a
--- /dev/null
+++ b/test/test_path_uri.vader
@@ -0,0 +1,16 @@
+Execute(ale#path#ToURI should work for Windows paths):
+ AssertEqual 'file:///C:/foo/bar/baz.tst', ale#path#ToURI('C:\foo\bar\baz.tst')
+ AssertEqual 'foo/bar/baz.tst', ale#path#ToURI('foo\bar\baz.tst')
+
+Execute(ale#path#ToURI should work for Unix paths):
+ AssertEqual 'file:///foo/bar/baz.tst', ale#path#ToURI('/foo/bar/baz.tst')
+ AssertEqual 'foo/bar/baz.tst', ale#path#ToURI('foo/bar/baz.tst')
+
+Execute(ale#path#ToURI should keep safe characters):
+ AssertEqual '//a-zA-Z0-9$-_.!*''(),', ale#path#ToURI('\/a-zA-Z0-9$-_.!*''(),')
+
+Execute(ale#path#ToURI should percent encode unsafe characters):
+ AssertEqual '%20%2b%3a%3f%26%3d', ale#path#ToURI(' +:?&=')
+
+Execute(ale#path#FromURI should decode percent encodings):
+ AssertEqual ' +:?&=', ale#path#FromURI('%20%2b%3a%3f%26%3d')
diff --git a/test/util/test_cd_string_commands.vader b/test/util/test_cd_string_commands.vader
index b0b6c157..f8a97cb8 100644
--- a/test/util/test_cd_string_commands.vader
+++ b/test/util/test_cd_string_commands.vader
@@ -1,8 +1,15 @@
Before:
silent! cd /testplugin/test/util
+ let g:dir = getcwd()
+
+After:
+ silent execute 'cd ' . fnameescape(g:dir)
+ unlet! g:dir
Execute(CdString should output the correct command string):
AssertEqual 'cd ''/foo bar/baz'' && ', ale#path#CdString('/foo bar/baz')
Execute(BufferCdString should output the correct command string):
- AssertEqual 'cd ' . shellescape(getcwd()) . ' && ', ale#path#BufferCdString(bufnr(''))
+ call ale#test#SetFilename('foo.txt')
+
+ AssertEqual 'cd ' . shellescape(g:dir) . ' && ', ale#path#BufferCdString(bufnr(''))