562 lines
17 KiB
VimL
562 lines
17 KiB
VimL
" don't spam the user when Vim is started in Vi compatibility mode
|
|
let s:cpo_save = &cpo
|
|
set cpo&vim
|
|
|
|
scriptencoding utf-8
|
|
|
|
let s:lspfactory = {}
|
|
|
|
function! s:lspfactory.get() dict abort
|
|
if !has_key(self, 'current') || empty(self.current)
|
|
let self.current = s:newlsp()
|
|
endif
|
|
|
|
return self.current
|
|
endfunction
|
|
|
|
function! s:lspfactory.reset() dict abort
|
|
if has_key(self, 'current')
|
|
call remove(self, 'current')
|
|
endif
|
|
endfunction
|
|
|
|
function! s:newlsp() abort
|
|
if !go#util#has_job()
|
|
" TODO(bc): start the server in the background using a shell that waits for the right output before returning.
|
|
call go#util#EchoError('This feature requires either Vim 8.0.0087 or newer with +job or Neovim.')
|
|
return
|
|
endif
|
|
|
|
" job is the job used to talk to the backing instance of gopls.
|
|
" ready is 0 until the initialize response has been received. 1 afterwards.
|
|
" queue is messages to send after initialization
|
|
" last_request_id is id of the most recently sent request.
|
|
" buf is unprocessed/incomplete responses
|
|
" handlers is a mapping of request ids to dictionaries of functions.
|
|
" request id -> {start, requestComplete, handleResult, error}
|
|
" * start is a function that takes no arguments
|
|
" * requestComplete is a function that takes 1 argument. The parameter will be 1
|
|
" if the call was succesful.
|
|
" * handleResult takes a single argument, the result message received from gopls
|
|
" * error takes a single argument, the error message received from gopls.
|
|
" The error method is optional.
|
|
let l:lsp = {
|
|
\ 'job': '',
|
|
\ 'ready': 0,
|
|
\ 'queue': [],
|
|
\ 'last_request_id': 0,
|
|
\ 'buf': '',
|
|
\ 'handlers': {},
|
|
\ }
|
|
|
|
function! l:lsp.readMessage(data) dict abort
|
|
let l:responses = []
|
|
let l:rest = a:data
|
|
|
|
while 1
|
|
" Look for the end of the HTTP headers
|
|
let l:body_start_idx = matchend(l:rest, "\r\n\r\n")
|
|
|
|
if l:body_start_idx < 0
|
|
" incomplete header
|
|
break
|
|
endif
|
|
|
|
" Parse the Content-Length header.
|
|
let l:header = l:rest[:l:body_start_idx - 4]
|
|
let l:length_match = matchlist(
|
|
\ l:header,
|
|
\ '\vContent-Length: *(\d+)'
|
|
\)
|
|
|
|
if empty(l:length_match)
|
|
" TODO(bc): shutdown gopls?
|
|
throw "invalid JSON-RPC header:\n" . l:header
|
|
endif
|
|
|
|
" get the start of the rest
|
|
let l:rest_start_idx = l:body_start_idx + str2nr(l:length_match[1])
|
|
|
|
if len(l:rest) < l:rest_start_idx
|
|
" incomplete response body
|
|
break
|
|
endif
|
|
|
|
if go#util#HasDebug('lsp')
|
|
let g:go_lsp_log = add(go#config#LspLog(), "<-\n" . l:rest[:l:rest_start_idx - 1])
|
|
endif
|
|
|
|
let l:body = l:rest[l:body_start_idx : l:rest_start_idx - 1]
|
|
let l:rest = l:rest[l:rest_start_idx :]
|
|
|
|
try
|
|
" add the json body to the list.
|
|
call add(l:responses, json_decode(l:body))
|
|
catch
|
|
" TODO(bc): log the message and/or show an error message.
|
|
finally
|
|
" intentionally left blank.
|
|
endtry
|
|
endwhile
|
|
|
|
return [l:rest, l:responses]
|
|
endfunction
|
|
|
|
function! l:lsp.handleMessage(ch, data) dict abort
|
|
let self.buf .= a:data
|
|
|
|
let [self.buf, l:responses] = self.readMessage(self.buf)
|
|
|
|
" TODO(bc): handle notifications (e.g. window/showMessage).
|
|
|
|
for l:response in l:responses
|
|
if has_key(l:response, 'id') && has_key(self.handlers, l:response.id)
|
|
try
|
|
let l:handler = self.handlers[l:response.id]
|
|
|
|
let l:winid = win_getid(winnr())
|
|
" Always set the active window to the window that was active when
|
|
" the request was sent. Among other things, this makes sure that
|
|
" the correct window's location list will be populated when the
|
|
" list type is 'location' and the user has moved windows since
|
|
" sending the reques.
|
|
call win_gotoid(l:handler.winid)
|
|
|
|
if has_key(l:response, 'error')
|
|
call l:handler.requestComplete(0)
|
|
if has_key(l:handler, 'error')
|
|
call call(l:handler.error, [l:response.error.message])
|
|
else
|
|
call go#util#EchoError(l:response.error.message)
|
|
endif
|
|
call win_gotoid(l:winid)
|
|
return
|
|
endif
|
|
call l:handler.requestComplete(1)
|
|
call call(l:handler.handleResult, [l:response.result])
|
|
call win_gotoid(l:winid)
|
|
finally
|
|
call remove(self.handlers, l:response.id)
|
|
endtry
|
|
endif
|
|
endfor
|
|
endfunction
|
|
|
|
function! l:lsp.handleInitializeResult(result) dict abort
|
|
let self.ready = 1
|
|
" TODO(bc): send initialized message to the server?
|
|
|
|
" send messages queued while waiting for ready.
|
|
for l:item in self.queue
|
|
call self.sendMessage(l:item.data, l:item.handler)
|
|
endfor
|
|
|
|
" reset the queue
|
|
let self.queue = []
|
|
endfunction
|
|
|
|
function! l:lsp.sendMessage(data, handler) dict abort
|
|
if !self.last_request_id
|
|
" TODO(bc): run a server per module and one per GOPATH? (may need to
|
|
" keep track of servers by rootUri).
|
|
let l:wd = go#util#ModuleRoot()
|
|
if l:wd == -1
|
|
call go#util#EchoError('could not determine appropriate working directory for gopls')
|
|
return
|
|
endif
|
|
|
|
if l:wd == ''
|
|
let l:wd = getcwd()
|
|
endif
|
|
|
|
let l:msg = self.newMessage(go#lsp#message#Initialize(l:wd))
|
|
|
|
let l:state = s:newHandlerState('')
|
|
let l:state.handleResult = funcref('self.handleInitializeResult', [], l:self)
|
|
let self.handlers[l:msg.id] = l:state
|
|
|
|
call l:state.start()
|
|
call self.write(l:msg)
|
|
endif
|
|
|
|
if !self.ready
|
|
call add(self.queue, {'data': a:data, 'handler': a:handler})
|
|
return
|
|
endif
|
|
|
|
let l:msg = self.newMessage(a:data)
|
|
if has_key(l:msg, 'id')
|
|
let self.handlers[l:msg.id] = a:handler
|
|
endif
|
|
|
|
call a:handler.start()
|
|
call self.write(l:msg)
|
|
endfunction
|
|
|
|
" newMessage returns a message constructed from data. data should be a dict
|
|
" with 2 or 3 keys: notification, method, and optionally params.
|
|
function! l:lsp.newMessage(data) dict abort
|
|
let l:msg = {
|
|
\ 'method': a:data.method,
|
|
\ 'jsonrpc': '2.0',
|
|
\ }
|
|
|
|
if !a:data.notification
|
|
let self.last_request_id += 1
|
|
let l:msg.id = self.last_request_id
|
|
endif
|
|
|
|
if has_key(a:data, 'params')
|
|
let l:msg.params = a:data.params
|
|
endif
|
|
|
|
return l:msg
|
|
endfunction
|
|
|
|
function! l:lsp.write(msg) dict abort
|
|
let l:body = json_encode(a:msg)
|
|
let l:data = 'Content-Length: ' . strlen(l:body) . "\r\n\r\n" . l:body
|
|
|
|
if go#util#HasDebug('lsp')
|
|
let g:go_lsp_log = add(go#config#LspLog(), "->\n" . l:data)
|
|
endif
|
|
|
|
if has('nvim')
|
|
call chansend(self.job, l:data)
|
|
return
|
|
endif
|
|
|
|
call ch_sendraw(self.job, l:data)
|
|
endfunction
|
|
|
|
function! l:lsp.exit_cb(job, exit_status) dict abort
|
|
call s:lspfactory.reset()
|
|
endfunction
|
|
" explicitly bind close_cb to state so that within it, self will always refer
|
|
|
|
function! l:lsp.close_cb(ch) dict abort
|
|
" TODO(bc): does anything need to be done here?
|
|
endfunction
|
|
|
|
function! l:lsp.err_cb(ch, msg) dict abort
|
|
if go#util#HasDebug('lsp')
|
|
let g:go_lsp_log = add(go#config#LspLog(), "<-stderr\n" . a:msg)
|
|
endif
|
|
endfunction
|
|
|
|
" explicitly bind callbacks to l:lsp so that within it, self will always refer
|
|
" to l:lsp instead of l:opts. See :help Partial for more information.
|
|
let l:opts = {
|
|
\ 'in_mode': 'raw',
|
|
\ 'out_mode': 'raw',
|
|
\ 'err_mode': 'nl',
|
|
\ 'noblock': 1,
|
|
\ 'err_cb': funcref('l:lsp.err_cb', [], l:lsp),
|
|
\ 'out_cb': funcref('l:lsp.handleMessage', [], l:lsp),
|
|
\ 'close_cb': funcref('l:lsp.close_cb', [], l:lsp),
|
|
\ 'exit_cb': funcref('l:lsp.exit_cb', [], l:lsp),
|
|
\ 'cwd': getcwd(),
|
|
\}
|
|
|
|
let l:bin_path = go#path#CheckBinPath("gopls")
|
|
if empty(l:bin_path)
|
|
return
|
|
endif
|
|
|
|
" TODO(bc): output a message indicating which directory lsp is going to
|
|
" start in.
|
|
let l:lsp.job = go#job#Start([l:bin_path], l:opts)
|
|
|
|
" TODO(bc): send the initialize message now?
|
|
return l:lsp
|
|
endfunction
|
|
|
|
function! s:noop(...) abort
|
|
endfunction
|
|
|
|
function! s:newHandlerState(statustype) abort
|
|
let l:state = {
|
|
\ 'winid': win_getid(winnr()),
|
|
\ 'statustype': a:statustype,
|
|
\ 'jobdir': getcwd(),
|
|
\ }
|
|
|
|
" explicitly bind requestComplete to state so that within it, self will
|
|
" always refer to state. See :help Partial for more information.
|
|
let l:state.requestComplete = funcref('s:requestComplete', [], l:state)
|
|
|
|
" explicitly bind start to state so that within it, self will
|
|
" always refer to state. See :help Partial for more information.
|
|
let l:state.start = funcref('s:start', [], l:state)
|
|
|
|
return l:state
|
|
endfunction
|
|
|
|
function! s:requestComplete(ok) abort dict
|
|
if self.statustype == ''
|
|
return
|
|
endif
|
|
|
|
if go#config#EchoCommandInfo()
|
|
let prefix = '[' . self.statustype . '] '
|
|
if a:ok
|
|
call go#util#EchoSuccess(prefix . "SUCCESS")
|
|
else
|
|
call go#util#EchoError(prefix . "FAIL")
|
|
endif
|
|
endif
|
|
|
|
let status = {
|
|
\ 'desc': 'last status',
|
|
\ 'type': self.statustype,
|
|
\ 'state': "success",
|
|
\ }
|
|
|
|
if !a:ok
|
|
let status.state = "failed"
|
|
endif
|
|
|
|
if has_key(self, 'started_at')
|
|
let elapsed_time = reltimestr(reltime(self.started_at))
|
|
" strip whitespace
|
|
let elapsed_time = substitute(elapsed_time, '^\s*\(.\{-}\)\s*$', '\1', '')
|
|
let status.state .= printf(" (%ss)", elapsed_time)
|
|
endif
|
|
|
|
call go#statusline#Update(self.jobdir, status)
|
|
endfunction
|
|
|
|
function! s:start() abort dict
|
|
if self.statustype != ''
|
|
let status = {
|
|
\ 'desc': 'current status',
|
|
\ 'type': self.statustype,
|
|
\ 'state': "started",
|
|
\ }
|
|
|
|
call go#statusline#Update(self.jobdir, status)
|
|
endif
|
|
let self.started_at = reltime()
|
|
endfunction
|
|
|
|
" go#lsp#Definition calls gopls to get the definition of the identifier at
|
|
" line and col in fname. handler should be a dictionary function that takes a
|
|
" list of strings in the form 'file:line:col: message'. handler will be
|
|
" attached to a dictionary that manages state (statuslines, sets the winid,
|
|
" etc.)
|
|
function! go#lsp#Definition(fname, line, col, handler) abort
|
|
call go#lsp#DidChange(a:fname)
|
|
|
|
let l:lsp = s:lspfactory.get()
|
|
let l:state = s:newHandlerState('definition')
|
|
let l:state.handleResult = funcref('s:definitionHandler', [function(a:handler, [], l:state)], l:state)
|
|
let l:msg = go#lsp#message#Definition(fnamemodify(a:fname, ':p'), a:line, a:col)
|
|
call l:lsp.sendMessage(l:msg, l:state)
|
|
endfunction
|
|
|
|
function! s:definitionHandler(next, msg) abort dict
|
|
" gopls returns a []Location; just take the first one.
|
|
let l:msg = a:msg[0]
|
|
let l:args = [[printf('%s:%d:%d: %s', go#path#FromURI(l:msg.uri), l:msg.range.start.line+1, l:msg.range.start.character+1, 'lsp does not supply a description')]]
|
|
call call(a:next, l:args)
|
|
endfunction
|
|
|
|
" go#lsp#Type calls gopls to get the type definition of the identifier at
|
|
" line and col in fname. handler should be a dictionary function that takes a
|
|
" list of strings in the form 'file:line:col: message'. handler will be
|
|
" attached to a dictionary that manages state (statuslines, sets the winid,
|
|
" etc.)
|
|
function! go#lsp#TypeDef(fname, line, col, handler) abort
|
|
call go#lsp#DidChange(a:fname)
|
|
|
|
let l:lsp = s:lspfactory.get()
|
|
let l:state = s:newHandlerState('type definition')
|
|
let l:msg = go#lsp#message#TypeDefinition(fnamemodify(a:fname, ':p'), a:line, a:col)
|
|
let l:state.handleResult = funcref('s:typeDefinitionHandler', [function(a:handler, [], l:state)], l:state)
|
|
call l:lsp.sendMessage(l:msg, l:state)
|
|
endfunction
|
|
|
|
function! s:typeDefinitionHandler(next, msg) abort dict
|
|
" gopls returns a []Location; just take the first one.
|
|
let l:msg = a:msg[0]
|
|
let l:args = [[printf('%s:%d:%d: %s', go#path#FromURI(l:msg.uri), l:msg.range.start.line+1, l:msg.range.start.character+1, 'lsp does not supply a description')]]
|
|
call call(a:next, l:args)
|
|
endfunction
|
|
|
|
function! go#lsp#DidOpen(fname) abort
|
|
if get(b:, 'go_lsp_did_open', 0)
|
|
return
|
|
endif
|
|
|
|
if !filereadable(a:fname)
|
|
return
|
|
endif
|
|
|
|
let l:lsp = s:lspfactory.get()
|
|
let l:msg = go#lsp#message#DidOpen(fnamemodify(a:fname, ':p'), join(go#util#GetLines(), "\n") . "\n")
|
|
let l:state = s:newHandlerState('')
|
|
let l:state.handleResult = funcref('s:noop')
|
|
call l:lsp.sendMessage(l:msg, l:state)
|
|
|
|
let b:go_lsp_did_open = 1
|
|
endfunction
|
|
|
|
function! go#lsp#DidChange(fname) abort
|
|
" DidChange is called even when fname isn't open in a buffer (e.g. via
|
|
" go#lsp#Info); don't report the file as open or as having changed when it's
|
|
" not actually a buffer.
|
|
if bufnr(a:fname) == -1
|
|
return
|
|
endif
|
|
|
|
call go#lsp#DidOpen(a:fname)
|
|
|
|
if !filereadable(a:fname)
|
|
return
|
|
endif
|
|
|
|
let l:lsp = s:lspfactory.get()
|
|
let l:msg = go#lsp#message#DidChange(fnamemodify(a:fname, ':p'), join(go#util#GetLines(), "\n") . "\n")
|
|
let l:state = s:newHandlerState('')
|
|
let l:state.handleResult = funcref('s:noop')
|
|
call l:lsp.sendMessage(l:msg, l:state)
|
|
endfunction
|
|
|
|
function! go#lsp#DidClose(fname) abort
|
|
if !filereadable(a:fname)
|
|
return
|
|
endif
|
|
|
|
if !get(b:, 'go_lsp_did_open', 0)
|
|
return
|
|
endif
|
|
|
|
let l:lsp = s:lspfactory.get()
|
|
let l:msg = go#lsp#message#DidClose(fnamemodify(a:fname, ':p'))
|
|
let l:state = s:newHandlerState('')
|
|
let l:state.handleResult = funcref('s:noop')
|
|
call l:lsp.sendMessage(l:msg, l:state)
|
|
|
|
let b:go_lsp_did_open = 0
|
|
endfunction
|
|
|
|
function! go#lsp#Completion(fname, line, col, handler) abort
|
|
call go#lsp#DidChange(a:fname)
|
|
|
|
let l:lsp = s:lspfactory.get()
|
|
let l:msg = go#lsp#message#Completion(a:fname, a:line, a:col)
|
|
let l:state = s:newHandlerState('completion')
|
|
let l:state.handleResult = funcref('s:completionHandler', [function(a:handler, [], l:state)], l:state)
|
|
let l:state.error = funcref('s:completionErrorHandler', [function(a:handler, [], l:state)], l:state)
|
|
call l:lsp.sendMessage(l:msg, l:state)
|
|
endfunction
|
|
|
|
function! s:completionHandler(next, msg) abort dict
|
|
" gopls returns a CompletionList.
|
|
let l:matches = []
|
|
let l:start = -1
|
|
|
|
for l:item in a:msg.items
|
|
let l:start = l:item.textEdit.range.start.character
|
|
|
|
let l:match = {'abbr': l:item.label, 'word': l:item.textEdit.newText, 'info': '', 'kind': go#lsp#completionitemkind#Vim(l:item.kind)}
|
|
if has_key(l:item, 'detail')
|
|
let l:match.info = l:item.detail
|
|
if go#lsp#completionitemkind#IsFunction(l:item.kind) || go#lsp#completionitemkind#IsMethod(l:item.kind)
|
|
let l:match.info = printf('func %s %s', l:item.label, l:item.detail)
|
|
endif
|
|
endif
|
|
|
|
if has_key(l:item, 'documentation')
|
|
let l:match.info .= "\n\n" . l:item.documentation
|
|
endif
|
|
|
|
let l:matches = add(l:matches, l:match)
|
|
endfor
|
|
let l:args = [l:start, l:matches]
|
|
call call(a:next, l:args)
|
|
endfunction
|
|
|
|
function! s:completionErrorHandler(next, error) abort dict
|
|
call call(a:next, [[]])
|
|
endfunction
|
|
|
|
function! go#lsp#Hover(fname, line, col, handler) abort
|
|
call go#lsp#DidChange(a:fname)
|
|
|
|
let l:lsp = s:lspfactory.get()
|
|
let l:msg = go#lsp#message#Hover(a:fname, a:line, a:col)
|
|
let l:state = s:newHandlerState('')
|
|
let l:state.handleResult = funcref('s:hoverHandler', [function(a:handler, [], l:state)], l:state)
|
|
let l:state.error = funcref('s:noop')
|
|
call l:lsp.sendMessage(l:msg, l:state)
|
|
endfunction
|
|
|
|
function! s:hoverHandler(next, msg) abort dict
|
|
let l:content = split(a:msg.contents.value, '; ')
|
|
if len(l:content) > 1
|
|
let l:curly = stridx(l:content[0], '{')
|
|
let l:content = extend([l:content[0][0:l:curly]], map(extend([l:content[0][l:curly+1:]], l:content[1:]), '"\t" . v:val'))
|
|
let l:content[len(l:content)-1] = '}'
|
|
endif
|
|
|
|
let l:args = [l:content]
|
|
call call(a:next, l:args)
|
|
endfunction
|
|
|
|
function! go#lsp#Info(showstatus)
|
|
let l:fname = expand('%:p')
|
|
let [l:line, l:col] = getpos('.')[1:2]
|
|
|
|
call go#lsp#DidChange(l:fname)
|
|
|
|
let l:lsp = s:lspfactory.get()
|
|
|
|
if a:showstatus
|
|
let l:state = s:newHandlerState('info')
|
|
else
|
|
let l:state = s:newHandlerState('')
|
|
endif
|
|
|
|
let l:state.handleResult = funcref('s:infoDefinitionHandler', [function('s:info', []), a:showstatus], l:state)
|
|
let l:state.error = funcref('s:noop')
|
|
let l:msg = go#lsp#message#Definition(l:fname, l:line, l:col)
|
|
call l:lsp.sendMessage(l:msg, l:state)
|
|
endfunction
|
|
|
|
function! s:infoDefinitionHandler(next, showstatus, msg) abort dict
|
|
" gopls returns a []Location; just take the first one.
|
|
let l:msg = a:msg[0]
|
|
|
|
let l:fname = go#path#FromURI(l:msg.uri)
|
|
let l:line = l:msg.range.start.line+1
|
|
let l:col = l:msg.range.start.character+1
|
|
|
|
let l:lsp = s:lspfactory.get()
|
|
let l:msg = go#lsp#message#Hover(l:fname, l:line, l:col)
|
|
|
|
if a:showstatus
|
|
let l:state = s:newHandlerState('info')
|
|
else
|
|
let l:state = s:newHandlerState('')
|
|
endif
|
|
|
|
let l:state.handleResult = funcref('s:hoverHandler', [function('s:info', [], l:state)], l:state)
|
|
let l:state.error = funcref('s:noop')
|
|
call l:lsp.sendMessage(l:msg, l:state)
|
|
endfunction
|
|
|
|
function! s:info(content) abort dict
|
|
let l:content = a:content[0]
|
|
" strip off the method set and fields of structs and interfaces.
|
|
if l:content =~# '^type [^ ]\+ \(struct\|interface\)'
|
|
let l:content = substitute(l:content, '{.*', '', '')
|
|
endif
|
|
call go#util#ShowInfo(l:content)
|
|
endfunction
|
|
|
|
" restore Vi compatibility settings
|
|
let &cpo = s:cpo_save
|
|
unlet s:cpo_save
|
|
|
|
" vim: sw=2 ts=2 et
|