" 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 empty(get(self, 'current', {})) || empty(get(self.current, 'job', {})) 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 " 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. " workspaceDirectories is an array of named workspaces. " wd is the working directory for gopls " diagnostics is a dictionary whose keys are filenames and each value is a " list of diagnostic messages for the file. " diagnosticsQueue is a queue of diagnostics notifications that have been " received, but not yet processed. " fileVersions is a dictionary of filenames to versions. " notificationQueue is a dictionary of filenames to functions. For a given " filename, each notification will call the first function in the list of " function values and remove it from the list. The functions should accept " two arguments: an absolute path and a list of diagnotics messages for " the file. let l:lsp = { \ 'job': '', \ 'ready': 0, \ 'queue': [], \ 'last_request_id': 0, \ 'buf': '', \ 'handlers': {}, \ 'workspaceDirectories': [], \ 'wd' : '', \ 'diagnosticsQueue': [], \ 'diagnostics': {}, \ 'fileVersions': {}, \ 'notificationQueue': {}, \ } if !go#config#GoplsEnabled() let l:lsp.sendMessage = funcref('s:noop') return l:lsp endif if !go#util#has_job() let l:oldshortmess=&shortmess if has('nvim') set shortmess-=F endif call go#util#EchoWarning('Features that rely on gopls will not work without either Vim 8.0.0087 or newer with +job or Neovim') " Sleep one second to make sure people see the message. Otherwise it is " often immediately overwritten by an async message. sleep 1 let &shortmess=l:oldshortmess return l:lsp endif 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:next_start_idx = l:body_start_idx + str2nr(l:length_match[1]) if len(l:rest) < l:next_start_idx " incomplete response body break endif call s:debug('received', l:rest[:l:next_start_idx - 1]) let l:body = l:rest[l:body_start_idx : l:next_start_idx - 1] let l:rest = l:rest[l:next_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:messages] = self.readMessage(self.buf) for l:message in l:messages if has_key(l:message, 'method') if has_key(l:message, 'id') call self.handleRequest(l:message) else call self.handleNotification(l:message) endif elseif has_key(l:message, 'result') || has_key(l:message, 'error') call self.handleResponse(l:message) endif endfor endfunction function! l:lsp.handleRequest(req) dict abort if a:req.method == 'workspace/workspaceFolders' let l:resp = go#lsp#message#WorkspaceFoldersResult(self.workspaceDirectories) elseif a:req.method == 'workspace/configuration' && has_key(a:req, 'params') && has_key(a:req.params, 'items') let l:resp = go#lsp#message#ConfigurationResult(a:req.params.items) elseif a:req.method == 'client/registerCapability' && has_key(a:req, 'params') && has_key(a:req.params, 'registrations') let l:resp = v:null else return endif if get(self, 'exited', 0) return endif let l:msg = self.newResponse(a:req.id, l:resp) call self.write(l:msg) endfunction function! l:lsp.handleResponse(resp) dict abort if has_key(a:resp, 'id') && has_key(self.handlers, a:resp.id) try let l:handler = self.handlers[a:resp.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 request. call win_gotoid(l:handler.winid) if has_key(a:resp, 'error') call l:handler.requestComplete(0) if has_key(l:handler, 'error') call call(l:handler.error, [a:resp.error.message]) else call go#util#EchoError(a:resp.error.message) endif call win_gotoid(l:winid) return endif call l:handler.requestComplete(1) let l:winidBeforeHandler = l:handler.winid call call(l:handler.handleResult, [a:resp.result]) " change the window back to the window that was active when " starting to handle the message _only_ if the handler didn't " update the winid, so that handlers can set the winid if needed " (e.g. :GoDef). if l:handler.winid == l:winidBeforeHandler call win_gotoid(l:winid) endif finally call remove(self.handlers, a:resp.id) endtry endif endfunction function! l:lsp.handleNotification(req) dict abort " TODO(bc): handle more notifications (e.g. window/showMessage). if a:req.method == 'textDocument/publishDiagnostics' call self.handleDiagnostics(a:req.params) endif endfunction function! l:lsp.handleDiagnostics(data) dict abort let self.diagnosticsQueue = add(self.diagnosticsQueue, a:data) call self.updateDiagnostics() endfunction " TODO(bc): process the queue asynchronously function! l:lsp.updateDiagnostics() dict abort for l:data in self.diagnosticsQueue call remove(self.diagnosticsQueue, 0) try let l:diagnostics = [] let l:errorMatches = [] let l:warningMatches = [] let l:fname = go#path#FromURI(l:data.uri) " get the buffer name relative to the current directory, because " Vim says that a buffer name can't be an absolute path. let l:bufname = fnamemodify(l:fname, ':.') if len(l:data.diagnostics) > 0 && (go#config#DiagnosticsEnabled() || bufnr(l:bufname) == bufnr('')) " make sure the buffer is listed and loaded before calling getbufline() on it if !bufexists(l:bufname) "let l:starttime = reltime() call bufadd(l:bufname) endif if !bufloaded(l:bufname) "let l:starttime = reltime() call bufload(l:bufname) endif for l:diag in l:data.diagnostics " TODO(bc): cache the raw diagnostics when they're not for the " current buffer so that they can be processed when it is the " current buffer and highlight the areas of concern. let [l:error, l:matchpos] = s:errorFromDiagnostic(l:diag, l:bufname, l:fname) let l:diagnostics = add(l:diagnostics, l:error) if empty(l:matchpos) continue endif if l:diag.severity == 1 let l:errorMatches = add(l:errorMatches, l:matchpos) elseif l:diag.severity == 2 let l:warningMatches = add(l:warningMatches, l:matchpos) endif endfor endif if bufnr(l:bufname) == bufnr('') " only apply highlighting when the diagnostics are for the current " version. let l:lsp = s:lspfactory.get() let l:version = get(l:lsp.fileVersions, l:fname, 0) " it's tempting to only highlight matches when they are for the " current version of the buffer, but that causes problems when the " version number has been updated and the content has not. In such a " case, the diagnostics may not be sent for later versions. call s:highlightMatches(l:errorMatches, l:warningMatches) endif let self.diagnostics[l:fname] = l:diagnostics if has_key(self.notificationQueue, l:fname) && len(self.notificationQueue[l:fname]) > 0 call call(self.notificationQueue[l:fname][0], copy(l:diagnostics)) call remove(self.notificationQueue[l:fname], 0) endif catch call go#util#EchoError(printf('%s: %s', v:throwpoint, v:exception)) endtry endfor endfunction function! l:lsp.handleInitializeResult(result) dict abort if go#config#EchoCommandInfo() call go#util#EchoProgress("initialized gopls") endif let status = { \ 'desc': '', \ 'type': 'gopls', \ 'state': 'initialized', \ } call go#statusline#Update(self.wd, status) let self.ready = 1 let l:msg = self.newMessage(go#lsp#message#Initialized()) call self.write(l:msg) " 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 let l:wd = go#util#ModuleRoot() if l:wd == -1 call go#util#EchoError('could not determine appropriate working directory for gopls') return -1 endif if l:wd == '' let l:wd = getcwd() endif let self.wd = l:wd if go#config#EchoCommandInfo() call go#util#EchoProgress("initializing gopls") endif let l:status = { \ 'desc': '', \ 'type': 'gopls', \ 'state': 'initializing', \ } call go#statusline#Update(l:wd, l:status) let self.workspaceDirectories = add(self.workspaceDirectories, l:wd) 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.newResponse(id, result) dict abort let l:msg = { \ 'jsonrpc': '2.0', \ 'id': a:id, \ 'result': a:result, \ } return l:msg endfunction function! l:lsp.write(msg) dict abort if empty(get(self, 'job', {})) return endif let l:body = json_encode(a:msg) let l:data = 'Content-Length: ' . strlen(l:body) . "\r\n\r\n" . l:body call s:debug('sent', l:data) 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 let self.exited = 1 if !get(self, 'restarting', 0) return endif let l:queue = self.queue let l:workspaces = self.workspaceDirectories call s:lspfactory.reset() let l:lsp = s:lspfactory.get() " restore workspaces call call('go#lsp#AddWorkspaceDirectory', l:workspaces) " * send DidOpen messages for all buffers that have b:did_lsp_open set " TODO(bc): check modifiable and filetype, too? bufdo if get(b:, 'go_lsp_did_open', 0) | if &modified | call go#lsp#DidOpen(expand('%:p')) | else | call go#lsp#DidChange(expand('%:p')) | endif | endif let l:lsp.queue = extend(l:lsp.queue, l:queue) return endfunction function! l:lsp.close_cb(ch) dict abort " TODO(bc): remove the buffer variables that indicate that gopls has been " informed that the file is open endfunction function! l:lsp.err_cb(ch, msg) dict abort if a:msg =~ '^\d\{4}/\d\d/\d\d\ \d\d:\d\d:\d\d debug server listening on port \d\+$' && !get(self, 'debugport', 0) let self.debugport = substitute(a:msg, '\d\{4}/\d\d/\d\d\ \d\d:\d\d:\d\d debug server listening on port \(\d\+\).*$', '\1', '') endif call s:debug('stderr', a:msg) 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 l:lsp endif let l:cmd = [l:bin_path] let l:cmdopts = go#config#GoplsOptions() if go#util#HasDebug('lsp') " debugging can be enabled either with g:go_debug or with " g:go_gopls_options; use g:go_gopls_options if it's given in case users " are running the gopls debug server on a known port. let l:needsDebug = 1 for l:item in l:cmdopts let l:idx = stridx(l:item, '-debug') if l:idx == 0 || l:idx == 1 let l:needsDebug = 0 endif endfor if l:needsDebug let l:cmd = extend(l:cmd, ['-debug', 'localhost:0']) endif endif let l:lsp.job = go#job#Start(l:cmd+l:cmdopts, l:opts) 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(), \ 'handleResult': funcref('s:noop'), \ } " 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 let self.started_at = reltime() if self.statustype == '' return endif let status = { \ 'desc': 'current status', \ 'type': self.statustype, \ 'state': "started", \ } call go#statusline#Update(self.jobdir, status) 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) return l:lsp.sendMessage(l:msg, l:state) endfunction function! s:definitionHandler(next, msg) abort dict if a:msg is v:null || len(a:msg) == 0 return endif " 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, go#lsp#lsp#PositionOf(getline(l:msg.range.start.line+1), l:msg.range.start.character), '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) return l:lsp.sendMessage(l:msg, l:state) endfunction function! s:typeDefinitionHandler(next, msg) abort dict if a:msg is v:null || len(a:msg) == 0 return endif " 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, go#lsp#lsp#PositionOf(getline(l:msg.range.start.line+1), l:msg.range.start.character), '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 let l:fname = fnamemodify(a:fname, ':p') if !isdirectory(fnamemodify(l:fname, ':h')) return endif let l:lsp = s:lspfactory.get() if !has_key(l:lsp.notificationQueue, l:fname) let l:lsp.notificationQueue[l:fname] = [] endif let l:lsp.fileVersions[l:fname] = getbufvar(l:fname, 'changedtick') let l:msg = go#lsp#message#DidOpen(l:fname, join(go#util#GetLines(), "\n") . "\n", l:lsp.fileVersions[l:fname]) let l:state = s:newHandlerState('') " TODO(bc): setting a buffer level variable here assumes that a:fname is the " current buffer. Change to a:fname first before setting it and then change " back to active buffer. let b:go_lsp_did_open = 1 return l:lsp.sendMessage(l:msg, l:state) 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 let l:fname = fnamemodify(a:fname, ':p') if !isdirectory(fnamemodify(l:fname, ':h')) return endif call go#lsp#DidOpen(a:fname) let l:lsp = s:lspfactory.get() let l:version = getbufvar(l:fname, 'changedtick') if has_key(l:lsp.fileVersions, l:fname) && l:lsp.fileVersions[l:fname] == l:version return endif let l:lsp.fileVersions[l:fname] = l:version let l:msg = go#lsp#message#DidChange(l:fname, join(go#util#GetLines(), "\n") . "\n", l:lsp.fileVersions[l:fname]) let l:state = s:newHandlerState('') return l:lsp.sendMessage(l:msg, l:state) endfunction function! go#lsp#DidClose(fname) abort let l:fname = fnamemodify(a:fname, ':p') if !isdirectory(fnamemodify(l:fname, ':h')) 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(l:fname) let l:state = s:newHandlerState('') " TODO(bc): setting a buffer level variable here assumes that a:fname is the " current buffer. Change to a:fname first before setting it and then change " back to active buffer. let b:go_lsp_did_open = 0 return l:lsp.sendMessage(l:msg, l:state) 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) return 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), 'user_data': '', 'icase': go#config#CodeCompletionIcase()} if has_key(l:item, 'detail') let l:match.menu = l:item.detail if go#lsp#completionitemkind#IsFunction(l:item.kind) || go#lsp#completionitemkind#IsMethod(l:item.kind) let l:match.info = printf('%s %s', l:item.label, l:item.detail) " The detail provided by gopls hasn't always provided the the full " signature including the return value. The label used to be the " function signature and the detail was the return value. Handle " that case for backward compatibility. This can be removed in the " future once it's likely that the majority of users are on a recent " version of gopls. if l:item.detail !~ '^func' let l:match.info = printf('func %s %s', l:item.label, l:item.detail) endif endif endif let l:match.user_data = l:match.info 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, [-1, []]) endfunction " go#lsp#SameIDs calls gopls to get the references to 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.). handler " should take three arguments: an exit_code, a JSON object encoded to a string " that mimics guru's ouput for `what`, and third mode parameter that only " exists for compatibility with the guru implementation of SameIDs. " TODO(bc): refactor to not need the guru adapter. function! go#lsp#SameIDs(showstatus, fname, line, col, handler) abort call go#lsp#DidChange(a:fname) let l:lsp = s:lspfactory.get() let l:msg = go#lsp#message#References(a:fname, a:line, a:col) if a:showstatus let l:state = s:newHandlerState('same ids') else let l:state = s:newHandlerState('') endif let l:state.handleResult = funcref('s:sameIDsHandler', [function(a:handler, [], l:state)], l:state) let l:state.error = funcref('s:noop') return l:lsp.sendMessage(l:msg, l:state) endfunction function! s:sameIDsHandler(next, msg) abort dict let l:furi = go#path#ToURI(expand('%:p')) let l:result = { \ 'sameids': [], \ 'enclosing': [], \ } let l:msg = a:msg if a:msg is v:null let l:msg = [] endif for l:loc in l:msg if l:loc.uri !=# l:furi continue endif if len(l:result.enclosing) == 0 let l:result.enclosing = [{ \ 'desc': 'identifier', \ 'start': l:loc.range.start.character+1, \ 'end': l:loc.range.end.character+1, \ }] endif let l:result.sameids = add(l:result.sameids, printf('%s:%s:%s', go#path#FromURI(l:loc.uri), l:loc.range.start.line+1, l:loc.range.start.character+1)) endfor call call(a:next, [0, json_encode(l:result), '']) endfunction " go#lsp#Referrers calls gopls to get the references to 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.). handler " should take three arguments: an exit_code, a JSON object encoded to a string " that mimics guru's ouput for `what`, and third mode parameter that only " exists for compatibility with the guru implementation of SameIDs. " TODO(bc): refactor to not need the guru adapter. function! go#lsp#Referrers(fname, line, col, handler) abort call go#lsp#DidChange(a:fname) let l:lsp = s:lspfactory.get() let l:msg = go#lsp#message#References(a:fname, a:line, a:col) let l:state = s:newHandlerState('referrers') let l:state.handleResult = funcref('s:handleReferences', [function(a:handler, [], l:state)], l:state) let l:state.error = funcref('s:noop') return l:lsp.sendMessage(l:msg, l:state) endfunction function! s:handleReferences(next, msg) abort dict call s:handleLocations(a:next, a:msg) endfunction function! s:handleLocations(next, msg) abort let l:result = [] let l:msg = a:msg if l:msg is v:null let l:msg = [] endif call sort(l:msg, funcref('s:compareLocations')) for l:loc in l:msg let l:fname = go#path#FromURI(l:loc.uri) let l:line = l:loc.range.start.line+1 let l:bufnr = bufnr(l:fname) let l:bufinfo = getbufinfo(l:fname) try if l:bufnr == -1 || len(l:bufinfo) == 0 || l:bufinfo[0].loaded == 0 let l:filecontents = readfile(l:fname, '', l:line) else let l:filecontents = getbufline(l:fname, l:line) endif if len(l:filecontents) == 0 continue endif let l:content = l:filecontents[-1] catch call go#util#EchoError(printf('%s (line %s): %s at %s', l:fname, l:line, v:exception, v:throwpoint)) endtry let l:item = printf('%s:%s:%s: %s', go#path#FromURI(l:loc.uri), l:line, go#lsp#lsp#PositionOf(l:content, l:loc.range.start.character), l:content) let l:result = add(l:result, l:item) endfor call call(a:next, [0, l:result, '']) endfunction " go#lsp#Implementations calls gopls to get the implementations to 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.). handler should take three arguments: an exit_code, a JSON " object encoded to a string that mimics guru's ouput for guru implements, and " a third parameter that only exists for compatibility with guru implements. function! go#lsp#Implements(fname, line, col, handler) abort call go#lsp#DidChange(a:fname) let l:lsp = s:lspfactory.get() let l:msg = go#lsp#message#Implementation(a:fname, a:line, a:col) let l:state = s:newHandlerState('implements') let l:state.handleResult = funcref('s:handleImplements', [function(a:handler, [], l:state)], l:state) let l:state.error = funcref('s:handleImplementsError', [function(a:handler, [], l:state)], l:state) return l:lsp.sendMessage(l:msg, l:state) endfunction function! s:handleImplements(next, msg) abort dict call s:handleLocations(a:next, a:msg) endfunction function! s:handleImplementsError(next, error) abort dict call call(a:next, [1, [a:error], '']) 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') return l:lsp.sendMessage(l:msg, l:state) endfunction function! s:hoverHandler(next, msg) abort dict if a:msg is v:null || !has_key(a:msg, 'contents') return endif try let l:value = json_decode(a:msg.contents.value) let l:args = [l:value.signature] call call(a:next, l:args) catch " TODO(bc): log the message and/or show an error message. endtry endfunction function! go#lsp#Doc() abort let l:fname = expand('%:p') let [l:line, l:col] = go#lsp#lsp#Position() call go#lsp#DidChange(l:fname) let l:lsp = s:lspfactory.get() let l:msg = go#lsp#message#Hover(l:fname, l:line, l:col) let l:state = s:newHandlerState('doc') let l:resultHandler = go#promise#New(function('s:docFromHoverResult', [], l:state), 10000, '') let l:state.handleResult = l:resultHandler.wrapper let l:state.error = l:resultHandler.wrapper call l:lsp.sendMessage(l:msg, l:state) return l:resultHandler.await() endfunction function! s:docFromHoverResult(msg) abort dict if type(a:msg) is type('') return [a:msg, 1] endif if a:msg is v:null || !has_key(a:msg, 'contents') return ['Undocumented', 0] endif let l:value = json_decode(a:msg.contents.value) let l:doc = l:value.fullDocumentation if len(l:doc) is 0 let l:doc = 'Undocumented' endif let l:content = printf("%s\n\n%s", l:value.signature, l:doc) return [l:content, 0] endfunction function! go#lsp#DocLink() abort let l:fname = expand('%:p') let [l:line, l:col] = go#lsp#lsp#Position() call go#lsp#DidChange(l:fname) let l:lsp = s:lspfactory.get() let l:msg = go#lsp#message#Hover(l:fname, l:line, l:col) let l:state = s:newHandlerState('doc url') let l:resultHandler = go#promise#New(function('s:docLinkFromHoverResult', [], l:state), 10000, '') let l:state.handleResult = l:resultHandler.wrapper let l:state.error = l:resultHandler.wrapper call l:lsp.sendMessage(l:msg, l:state) return l:resultHandler.await() endfunction function! s:docLinkFromHoverResult(msg) abort dict if type(a:msg) is type('') return [a:msg, 1] endif if a:msg is v:null || !has_key(a:msg, 'contents') return endif let l:doc = json_decode(a:msg.contents.value) return [l:doc.link, ''] endfunction function! go#lsp#Info(showstatus) let l:fname = expand('%:p') let [l:line, l:col] = go#lsp#lsp#Position() 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', [1], l:state), a:showstatus], l:state) let l:state.error = funcref('s:noop') let l:msg = go#lsp#message#Definition(l:fname, l:line, l:col) return l:lsp.sendMessage(l:msg, l:state) endfunction function! go#lsp#GetInfo() let l:fname = expand('%:p') let [l:line, l:col] = go#lsp#lsp#Position() call go#lsp#DidChange(l:fname) let l:lsp = s:lspfactory.get() let l:state = s:newHandlerState('') let l:info = go#promise#New(function('s:info', [0], l:state), 10000, '') let l:state.handleResult = funcref('s:infoDefinitionHandler', [l:info.wrapper, 0], 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) return l:info.await() endfunction function! s:infoDefinitionHandler(next, showstatus, msg) abort dict " gopls returns a []Location; just take the first one. if a:msg is v:null || len(a:msg) == 0 return endif let l:msg = a:msg[0] let l:fname = go#path#FromURI(l:msg.uri) let l:line = l:msg.range.start.line let l:col = l:msg.range.start.character 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 = a:next let l:state.error = funcref('s:noop') return l:lsp.sendMessage(l:msg, l:state) endfunction function! s:info(show, msg) abort dict if a:msg is v:null || !has_key(a:msg, 'contents') return endif let l:value = json_decode(a:msg.contents.value) let l:content = [l:value.singleLine] let l:content = s:infoFromHoverContent(l:content) if a:show call go#util#ShowInfo(l:content) endif return l:content endfunction function! s:infoFromHoverContent(content) abort if len(a:content) < 1 return '' endif 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 return l:content endfunction function! go#lsp#AddWorkspaceDirectory(...) abort if a:0 == 0 return endif call go#lsp#CleanWorkspaces() let l:workspaces = [] for l:dir in a:000 let l:dir = fnamemodify(l:dir, ':p') if !isdirectory(l:dir) continue endif let l:workspaces = add(l:workspaces, l:dir) endfor let l:lsp = s:lspfactory.get() let l:state = s:newHandlerState('') let l:lsp.workspaceDirectories = extend(l:lsp.workspaceDirectories, l:workspaces) let l:msg = go#lsp#message#ChangeWorkspaceFolders(l:workspaces, []) call l:lsp.sendMessage(l:msg, l:state) return 0 endfunction function! go#lsp#CleanWorkspaces() abort let l:workspaces = [] let l:lsp = s:lspfactory.get() let l:i = 0 let l:missing = [] for l:dir in l:lsp.workspaceDirectories if !isdirectory(l:dir) let l:missing = add(l:missing, l:dir) call remove(l:lsp.workspaceDirectories, l:i) continue endif let l:i += 1 endfor if len(l:missing) == 0 return 0 endif let l:state = s:newHandlerState('') let l:msg = go#lsp#message#ChangeWorkspaceFolders([], l:missing) call l:lsp.sendMessage(l:msg, l:state) return 0 endfunction " go#lsp#ResetWorkspaceDiretories removes and then re-adds all workspace " folders to cause gopls to send configuration requests for all of them again. " This is useful, for instance, when build tags have been added and gopls " needs to use them. function! go#lsp#ResetWorkspaceDirectories() abort call go#lsp#CleanWorkspaces() let l:lsp = s:lspfactory.get() let l:state = s:newHandlerState('') let l:msg = go#lsp#message#ChangeWorkspaceFolders(l:lsp.workspaceDirectories, l:lsp.workspaceDirectories) call l:lsp.sendMessage(l:msg, l:state) return 0 endfunction function! go#lsp#DebugBrowser() abort let l:lsp = s:lspfactory.get() let l:port = get(l:lsp, 'debugport', 0) if !l:port call go#util#EchoError("gopls was not started with debugging enabled. See :help g:go_debug.") return endif call go#util#OpenBrowser(printf('http://localhost:%d', l:port)) endfunction function! go#lsp#Exit() abort call s:exit(0) endfunction function! go#lsp#Restart() abort call s:exit(1) endfunction function! s:exit(restart) abort if !go#util#has_job() || len(s:lspfactory) == 0 || !has_key(s:lspfactory, 'current') return endif let l:lsp = s:lspfactory.get() " reset the factory so that future requests don't use the same instance of " gopls. call s:lspfactory.reset() let l:lsp.restarting = a:restart let l:state = s:newHandlerState('exit') let l:msg = go#lsp#message#Shutdown() let l:retval = l:lsp.sendMessage(l:msg, l:state) let l:msg = go#lsp#message#Exit() let l:retval = l:lsp.sendMessage(l:msg, l:state) return l:retval endfunction let s:log = [] function! s:debugasync(timer) abort if !go#util#HasDebug('lsp') let s:log = [] return endif let l:winid = win_getid() let l:name = '__GOLSP_LOG__' let l:log_winid = bufwinid(l:name) if l:log_winid == -1 silent keepalt botright 10new silent file `='__GOLSP_LOG__'` setlocal buftype=nofile bufhidden=wipe nomodified nobuflisted noswapfile nowrap nonumber nocursorline setlocal filetype=golsplog else call win_gotoid(l:log_winid) endif try setlocal modifiable for [l:event, l:data] in s:log call remove(s:log, 0) if getline(1) == '' call setline('$', printf('===== %s =====', l:event)) else call append('$', printf('===== %s =====', l:event)) endif call append('$', split(l:data, "\r\n")) endfor normal! G setlocal nomodifiable finally call win_gotoid(l:winid) endtry endfunction function! s:debug(event, data) abort let l:shouldStart = len(s:log) > 0 let s:log = add(s:log, [a:event, a:data]) if l:shouldStart call timer_start(10, function('s:debugasync', [])) endif endfunction function! s:compareLocations(left, right) abort if a:left.uri < a:right.uri return -1 endif if a:left.uri == a:right.uri && a:left.range.start.line < a:right.range.start.line return -1 endif if a:left.uri == a:right.uri && a:left.range.start.line == a:right.range.start.line && a:left.range.start.character < a:right.range.start.character return -1 endif if a:left.uri == a:right.uri && a:left.range.start.line == a:right.range.start.line && a:left.range.start.character == a:right.range.start.character return 0 endif return 1 endfunction function! go#lsp#Diagnostics(...) abort if a:0 == 0 return [] endif let l:dirsToPackages = {} let l:lsp = s:lspfactory.get() let l:diagnostics = [] for [l:key, l:val] in items(l:lsp.diagnostics) let l:dir = fnamemodify(l:key, ':h') if !has_key(l:dirsToPackages, l:dir) let l:pkg = go#package#FromPath(l:dir) let l:dirsToPackages[l:dir] = l:pkg else let l:pkg = l:dirsToPackages[l:dir] endif if type(l:pkg) == type(0) continue endif for l:arg in a:000 if l:arg == l:pkg || l:arg == 'all' let l:diagnostics = extend(l:diagnostics, l:val) endif endfor endfor return sort(l:diagnostics) endfunction function! go#lsp#AnalyzeFile(fname) abort let l:fname = fnamemodify(a:fname, ':p') if !isdirectory(fnamemodify(l:fname, ':h')) return [] endif let l:lsp = s:lspfactory.get() let l:lastdiagnostics = get(l:lsp.diagnostics, l:fname, []) let l:version = l:lsp.fileVersions[a:fname] if l:version == getbufvar(a:fname, 'changedtick') return l:lastdiagnostics endif call go#lsp#DidChange(a:fname) let l:diagnostics = go#promise#New(function('s:setDiagnostics', []), 10000, l:lastdiagnostics) let l:lsp.notificationQueue[l:fname] = add(l:lsp.notificationQueue[l:fname], l:diagnostics.wrapper) return l:diagnostics.await() endfunction function! s:setDiagnostics(...) abort return a:000 endfunction " s:processDiagnostic converts a diagnostic into an error string. It returns " the errors string and the match position described in the diagnostic. The " match position will be an empty list when bufname is not a valid name for " the current buffer. function! s:errorFromDiagnostic(diagnostic, bufname, fname) abort let l:range = a:diagnostic.range let l:line = l:range.start.line + 1 let l:buflines = getbufline(a:bufname, l:line) let l:col = '' if len(l:buflines) > 0 let l:col = go#lsp#lsp#PositionOf(l:buflines[0], l:range.start.character) endif let l:error = printf('%s:%s:%s:%s: %s', a:fname, l:line, l:col, go#lsp#lsp#SeverityToErrorType(a:diagnostic.severity), a:diagnostic.message) if !(a:diagnostic.severity == 1 || a:diagnostic.severity == 2) return [l:error, []] endif " return when the diagnostic is not for the current buffer. if bufnr(a:bufname) != bufnr('') return [l:error, []] end let l:endline = l:range.end.line + 1 " don't bother trying to highlight errors or warnings that span " the whole file (e.g when there's missing package documentation). if l:line == 1 && (l:endline) == line('$') return [l:error, []] endif let l:endcol = go#lsp#lsp#PositionOf(getline(l:endline), l:range.end.character) " the length of the match is the number of bytes between the start of " the match and the end of the match. let l:matchLength = line2byte(l:endline) + l:endcol - (line2byte(l:line) + l:col) let l:pos = [l:line, l:col, l:matchLength] return [l:error, l:pos] endfunction function! s:highlightMatches(errorMatches, warningMatches) abort " set buffer variables for errors and warnings to zero values let b:go_diagnostic_matches = {'errors': [], 'warnings': []} if hlexists('goDiagnosticError') " clear the old matches just before adding the new ones to keep flicker " to a minimum. call go#util#ClearHighlights('goDiagnosticError') if go#config#HighlightDiagnosticErrors() let b:go_diagnostic_matches.errors = copy(a:errorMatches) call go#util#HighlightPositions('goDiagnosticError', a:errorMatches) endif endif if hlexists('goDiagnosticWarning') " clear the old matches just before adding the new ones to keep flicker " to a minimum. call go#util#ClearHighlights('goDiagnosticWarning') if go#config#HighlightDiagnosticWarnings() let b:go_diagnostic_matches.warnings = copy(a:warningMatches) call go#util#HighlightPositions('goDiagnosticWarning', a:warningMatches) endif endif " re-apply matches at the time the buffer is displayed in a new window or " redisplayed in an existing window: e.g. :edit, augroup vim-go-diagnostics autocmd! * autocmd BufDelete autocmd! vim-go-diagnostics * if has('textprop') autocmd BufReadPost nested call s:highlightMatches(b:go_diagnostic_matches.errors, b:go_diagnostic_matches.warnings) else autocmd BufWinEnter nested call s:highlightMatches(b:go_diagnostic_matches.errors, b:go_diagnostic_matches.warnings) endif augroup end endfunction " ClearDiagnosticHighlights removes all goDiagnosticError and " goDiagnosticWarning matches. function! go#lsp#ClearDiagnosticHighlights() abort call go#util#ClearHighlights('goDiagnosticError') call go#util#ClearHighlights('goDiagnosticWarning') endfunction " Format formats the current buffer. function! go#lsp#Format() abort let l:fname = expand('%:p') " send the current file so that TextEdits will be relative to the current " state of the buffer. call go#lsp#DidChange(l:fname) let l:lsp = s:lspfactory.get() let l:state = s:newHandlerState('') let l:handleFormat = go#promise#New(function('s:handleFormat', [], l:state), 10000, '') let l:state.handleResult = l:handleFormat.wrapper let l:state.error = l:handleFormat.wrapper let l:state.handleError = function('s:handleFormatError', [l:fname], l:state) let l:msg = go#lsp#message#Format(l:fname) call l:lsp.sendMessage(l:msg, l:state) call go#fmt#CleanErrors() " await the result to avoid any race conditions among autocmds (e.g. " BufWritePre and BufWritePost) call l:handleFormat.await() endfunction " Imports executes the source.organizeImports code action for the current " buffer. function! go#lsp#Imports() abort let l:fname = expand('%:p') " send the current file so that TextEdits will be relative to the current " state of the buffer. call go#lsp#DidChange(l:fname) let l:lsp = s:lspfactory.get() let l:state = s:newHandlerState('') let l:handler = go#promise#New(function('s:handleCodeAction', [], l:state), 10000, '') let l:state.handleResult = l:handler.wrapper let l:state.error = l:handler.wrapper let l:state.handleError = function('s:handleCodeActionError', [l:fname], l:state) let l:msg = go#lsp#message#CodeActionImports(l:fname) call l:lsp.sendMessage(l:msg, l:state) " await the result to avoid any race conditions among autocmds (e.g. " BufWritePre and BufWritePost) call l:handler.await() endfunction function! s:handleFormat(msg) abort dict call go#fmt#CleanErrors() if type(a:msg) is type('') call self.handleError(a:msg) return endif call s:applyTextEdits(a:msg) endfunction function! s:handleCodeAction(msg) abort dict if type(a:msg) is type('') call self.handleError(a:msg) return endif if a:msg is v:null return endif for l:item in a:msg if get(l:item, 'kind', '') is 'source.organizeImports' if !has_key(l:item, 'edit') continue endif if !has_key(l:item.edit, 'documentChanges') continue endif for l:change in l:item.edit.documentChanges if !has_key(l:change, 'edits') continue endif " TODO(bc): change to the buffer for l:change.textDocument.uri call s:applyTextEdits(l:change.edits) endfor endif endfor endfunction function s:applyTextEdits(msg) abort if a:msg is v:null return endif " process the TextEdit list in reverse order, because the positions are " based on the current line numbers; processing in forward order would " require keeping track of how the proper position of each TextEdit would be " affected by all the TextEdits that came before. call reverse(sort(a:msg, function('s:textEditLess'))) for l:msg in a:msg let l:startline = l:msg.range.start.line+1 let l:endline = l:msg.range.end.line+1 let l:text = l:msg.newText " handle the deletion of whole lines if len(l:text) == 0 && l:msg.range.start.character == 0 && l:msg.range.end.character == 0 && l:startline < l:endline call s:deleteline(l:startline, l:endline-1) continue endif let l:startcontent = getline(l:startline) let l:preSliceEnd = 0 if l:msg.range.start.character > 0 let l:preSliceEnd = go#lsp#lsp#PositionOf(l:startcontent, l:msg.range.start.character-1) - 1 let l:startcontent = l:startcontent[:l:preSliceEnd] elseif l:endline == l:startline && (l:msg.range.end.character == 0 || l:msg.range.start.character == 0) " l:startcontent should be the empty string when l:text is a " replacement at the beginning of the line. let l:startcontent = '' endif let l:endcontent = getline(l:endline) let l:postSliceStart = 0 if l:msg.range.end.character > 0 let l:postSliceStart = go#lsp#lsp#PositionOf(l:endcontent, l:msg.range.end.character-1) let l:endcontent = l:endcontent[(l:postSliceStart):] endif " There isn't an easy way to replace the text in a byte or character " range, so append to l:text any text on l:endline starting from " l:postSliceStart and prepend to l:text any text on l:startline prior to " l:preSliceEnd, and finally replace the lines with a delete followed by " and append. let l:text = printf('%s%s%s', l:startcontent, l:text, l:endcontent) " TODO(bc): deal with the undo file " TODO(bc): deal with folds call s:deleteline(l:startline, l:endline) for l:line in split(l:text, "\n", 1) call append(l:startline-1, l:line) let l:startline += 1 endfor endfor call go#lsp#DidChange(expand('%:p')) return endfunction function! s:handleFormatError(filename, msg) abort dict if go#config#FmtFailSilently() return endif let l:errors = split(a:msg, '\n') let l:errors = map(l:errors, printf('substitute(v:val, ''^'', ''%s:'', '''')', a:filename)) let l:errors = join(l:errors, "\n") call go#fmt#ShowErrors(l:errors) endfunction function! s:handleCodeActionError(filename, msg) abort dict " TODO(bc): handle the error? endfunction function! s:textEditLess(left, right) abort " TextEdits in a TextEdit[] never overlap and Vim's sort() is stable. if a:left.range.start.line < a:right.range.start.line return -1 endif if a:left.range.start.line > a:right.range.start.line return 1 endif if a:left.range.start.line == a:right.range.start.line if a:left.range.start.character < a:right.range.start.character return -1 endif if a:left.range.start.character > a:right.range.start.character return 1 endif endif " return 0, because a:left an a:right refer to the same position. return 0 endfunction function! s:deleteline(start, end) abort if exists('*deletebufline') call deletebufline('', a:start, a:end) else call execute(printf('%d,%d d_', a:start, a:end)) endif endfunction " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save " vim: sw=2 ts=2 et