2019-06-03 14:58:18 +00:00
" 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
2020-10-28 13:35:24 +00:00
if empty ( get ( self , 'current' , {}) ) | | empty ( get ( self .current , 'job' , {}) )
2019-06-03 14:58:18 +00:00
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.
2020-10-28 13:35:24 +00:00
" 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.
2019-06-03 14:58:18 +00:00
let l :lsp = {
\ 'job' : '' ,
\ 'ready' : 0 ,
\ 'queue' : [],
\ 'last_request_id' : 0 ,
\ 'buf' : '' ,
\ 'handlers' : {},
2020-10-28 13:35:24 +00:00
\ 'workspaceDirectories' : [],
\ 'wd' : '' ,
\ 'diagnosticsQueue' : [],
\ 'diagnostics' : {},
\ 'fileVersions' : {},
\ 'notificationQueue' : {},
2019-06-03 14:58:18 +00:00
\ }
2020-10-28 13:35:24 +00:00
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
2019-06-03 14:58:18 +00:00
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
2020-10-28 13:35:24 +00:00
let l :next_start_idx = l :body_start_idx + str2nr ( l :length_match [1 ])
2019-06-03 14:58:18 +00:00
2020-10-28 13:35:24 +00:00
if len ( l :rest ) < l :next_start_idx
2019-06-03 14:58:18 +00:00
" incomplete response body
break
endif
2020-10-28 13:35:24 +00:00
call s :debug ( 'received' , l :rest [:l :next_start_idx - 1 ])
2019-06-03 14:58:18 +00:00
2020-10-28 13:35:24 +00:00
let l :body = l :rest [l :body_start_idx : l :next_start_idx - 1 ]
let l :rest = l :rest [l :next_start_idx :]
2019-06-03 14:58:18 +00:00
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
2020-10-28 13:35:24 +00:00
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
2019-06-03 14:58:18 +00:00
2020-10-28 13:35:24 +00:00
if get ( self , 'exited' , 0 )
return
endif
2019-06-03 14:58:18 +00:00
2020-10-28 13:35:24 +00:00
let l :msg = self .newResponse ( a :req .id , l :resp )
call self .write ( l :msg )
endfunction
2019-06-03 14:58:18 +00:00
2020-10-28 13:35:24 +00:00
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 )
2019-06-03 14:58:18 +00:00
endif
2020-10-28 13:35:24 +00:00
endfor
2019-06-03 14:58:18 +00:00
endif
2020-10-28 13:35:24 +00:00
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
2019-06-03 14:58:18 +00:00
endfunction
function ! l :lsp .handleInitializeResult ( result ) dict abort
2020-10-28 13:35:24 +00:00
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 )
2019-06-03 14:58:18 +00:00
let self .ready = 1
2020-10-28 13:35:24 +00:00
let l :msg = self .newMessage ( go #lsp #message #Initialized ( ) )
call self .write ( l :msg )
2019-06-03 14:58:18 +00:00
" 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
2020-10-28 13:35:24 +00:00
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 ) )
2019-06-03 14:58:18 +00:00
let l :state = s :newHandlerState ( '' )
let l :state .handleResult = funcref ( 'self.handleInitializeResult' , [], l :self )
2020-10-28 13:35:24 +00:00
2019-06-03 14:58:18 +00:00
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' ,
2020-10-28 13:35:24 +00:00
\ }
2019-06-03 14:58:18 +00:00
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
2020-10-28 13:35:24 +00:00
function l :lsp .newResponse ( id , result ) dict abort
let l :msg = {
\ 'jsonrpc' : '2.0' ,
\ 'id' : a :id ,
\ 'result' : a :result ,
\ }
2019-06-03 14:58:18 +00:00
2020-10-28 13:35:24 +00:00
return l :msg
endfunction
function ! l :lsp .write ( msg ) dict abort
if empty ( get ( self , 'job' , {}) )
return
2019-06-03 14:58:18 +00:00
endif
2020-10-28 13:35:24 +00:00
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 )
2019-06-03 14:58:18 +00:00
if has ( 'nvim' )
call chansend ( self .job , l :data )
return
endif
call ch_sendraw ( self .job , l :data )
endfunction
2020-10-28 13:35:24 +00:00
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
2019-06-03 14:58:18 +00:00
call s :lspfactory .reset ( )
2020-10-28 13:35:24 +00:00
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
2019-06-03 14:58:18 +00:00
endfunction
function ! l :lsp .close_cb ( ch ) dict abort
2020-10-28 13:35:24 +00:00
" TODO(bc): remove the buffer variables that indicate that gopls has been
" informed that the file is open
2019-06-03 14:58:18 +00:00
endfunction
function ! l :lsp .err_cb ( ch , msg ) dict abort
2020-10-28 13:35:24 +00:00
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' , '' )
2019-06-03 14:58:18 +00:00
endif
2020-10-28 13:35:24 +00:00
call s :debug ( 'stderr' , a :msg )
2019-06-03 14:58:18 +00:00
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 )
2020-10-28 13:35:24 +00:00
return l :lsp
2019-06-03 14:58:18 +00:00
endif
2020-10-28 13:35:24 +00:00
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 )
2019-06-03 14:58:18 +00:00
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 ( ) ,
2020-10-28 13:35:24 +00:00
\ 'handleResult' : funcref ( 's:noop' ) ,
2019-06-03 14:58:18 +00:00
\ }
" 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 ( )
2020-10-28 13:35:24 +00:00
if self .statustype = = ''
return
endif
let status = {
\ 'desc' : 'current status' ,
\ 'type' : self .statustype ,
\ 'state' : "started" ,
\ }
call go #statusline #Update ( self .jobdir , status )
2019-06-03 14:58:18 +00:00
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 )
2020-10-28 13:35:24 +00:00
return l :lsp .sendMessage ( l :msg , l :state )
2019-06-03 14:58:18 +00:00
endfunction
function ! s :definitionHandler ( next , msg ) abort dict
2020-10-28 13:35:24 +00:00
if a :msg is v :null | | len ( a :msg ) = = 0
return
endif
2019-06-03 14:58:18 +00:00
" gopls returns a []Location; just take the first one.
let l :msg = a :msg [0 ]
2020-10-28 13:35:24 +00:00
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' ) ]]
2019-06-03 14:58:18 +00:00
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 )
2020-10-28 13:35:24 +00:00
return l :lsp .sendMessage ( l :msg , l :state )
2019-06-03 14:58:18 +00:00
endfunction
function ! s :typeDefinitionHandler ( next , msg ) abort dict
2020-10-28 13:35:24 +00:00
if a :msg is v :null | | len ( a :msg ) = = 0
return
endif
2019-06-03 14:58:18 +00:00
" gopls returns a []Location; just take the first one.
let l :msg = a :msg [0 ]
2020-10-28 13:35:24 +00:00
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' ) ]]
2019-06-03 14:58:18 +00:00
call call ( a :next , l :args )
endfunction
function ! go #lsp #DidOpen ( fname ) abort
if get ( b :, 'go_lsp_did_open' , 0 )
return
endif
2020-10-28 13:35:24 +00:00
let l :fname = fnamemodify ( a :fname , ':p' )
if ! isdirectory ( fnamemodify ( l :fname , ':h' ) )
2019-06-03 14:58:18 +00:00
return
endif
let l :lsp = s :lspfactory .get ( )
2020-10-28 13:35:24 +00:00
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 ])
2019-06-03 14:58:18 +00:00
let l :state = s :newHandlerState ( '' )
2020-10-28 13:35:24 +00:00
" 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.
2019-06-03 14:58:18 +00:00
let b :go_lsp_did_open = 1
2020-10-28 13:35:24 +00:00
return l :lsp .sendMessage ( l :msg , l :state )
2019-06-03 14:58:18 +00:00
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
2020-10-28 13:35:24 +00:00
let l :fname = fnamemodify ( a :fname , ':p' )
if ! isdirectory ( fnamemodify ( l :fname , ':h' ) )
return
endif
2019-06-03 14:58:18 +00:00
call go #lsp #DidOpen ( a :fname )
2020-10-28 13:35:24 +00:00
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
2019-06-03 14:58:18 +00:00
return
endif
2020-10-28 13:35:24 +00:00
let l :lsp .fileVersions [l :fname ] = l :version
2019-06-03 14:58:18 +00:00
2020-10-28 13:35:24 +00:00
let l :msg = go #lsp #message #DidChange ( l :fname , join ( go #util #GetLines ( ) , "\n" ) . "\n" , l :lsp .fileVersions [l :fname ])
2019-06-03 14:58:18 +00:00
let l :state = s :newHandlerState ( '' )
2020-10-28 13:35:24 +00:00
return l :lsp .sendMessage ( l :msg , l :state )
2019-06-03 14:58:18 +00:00
endfunction
function ! go #lsp #DidClose ( fname ) abort
2020-10-28 13:35:24 +00:00
let l :fname = fnamemodify ( a :fname , ':p' )
if ! isdirectory ( fnamemodify ( l :fname , ':h' ) )
2019-06-03 14:58:18 +00:00
return
endif
if ! get ( b :, 'go_lsp_did_open' , 0 )
return
endif
let l :lsp = s :lspfactory .get ( )
2020-10-28 13:35:24 +00:00
let l :msg = go #lsp #message #DidClose ( l :fname )
2019-06-03 14:58:18 +00:00
let l :state = s :newHandlerState ( '' )
2020-10-28 13:35:24 +00:00
" 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.
2019-06-03 14:58:18 +00:00
let b :go_lsp_did_open = 0
2020-10-28 13:35:24 +00:00
return l :lsp .sendMessage ( l :msg , l :state )
2019-06-03 14:58:18 +00:00
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 )
2020-10-28 13:35:24 +00:00
return l :lsp .sendMessage ( l :msg , l :state )
2019-06-03 14:58:18 +00:00
endfunction
function ! s :completionHandler ( next , msg ) abort dict
" gopls returns a CompletionList.
let l :matches = []
2020-10-28 13:35:24 +00:00
let l :start = -1
2019-06-03 14:58:18 +00:00
for l :item in a :msg .items
2020-10-28 13:35:24 +00:00
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 ( ) }
2019-06-03 14:58:18 +00:00
if has_key ( l :item , 'detail' )
2020-10-28 13:35:24 +00:00
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
2019-06-03 14:58:18 +00:00
endif
2020-10-28 13:35:24 +00:00
let l :match .user_data = l :match .info
2019-06-03 14:58:18 +00:00
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
2020-10-28 13:35:24 +00:00
let l :args = [l :start , l :matches ]
2019-06-03 14:58:18 +00:00
call call ( a :next , l :args )
endfunction
function ! s :completionErrorHandler ( next , error ) abort dict
2020-10-28 13:35:24 +00:00
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 ], '' ])
2019-06-03 14:58:18 +00:00
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' )
2020-10-28 13:35:24 +00:00
return l :lsp .sendMessage ( l :msg , l :state )
2019-06-03 14:58:18 +00:00
endfunction
function ! s :hoverHandler ( next , msg ) abort dict
2020-10-28 13:35:24 +00:00
if a :msg is v :null | | ! has_key ( a :msg , 'contents' )
return
2019-06-03 14:58:18 +00:00
endif
2020-10-28 13:35:24 +00:00
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 , '' ]
2019-06-03 14:58:18 +00:00
endfunction
function ! go #lsp #Info ( showstatus )
let l :fname = expand ( '%:p' )
2020-10-28 13:35:24 +00:00
let [l :line , l :col ] = go #lsp #lsp #Position ( )
2019-06-03 14:58:18 +00:00
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
2020-10-28 13:35:24 +00:00
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 )
2019-06-03 14:58:18 +00:00
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 )
2020-10-28 13:35:24 +00:00
return l :info .await ( )
2019-06-03 14:58:18 +00:00
endfunction
function ! s :infoDefinitionHandler ( next , showstatus , msg ) abort dict
" gopls returns a []Location; just take the first one.
2020-10-28 13:35:24 +00:00
if a :msg is v :null | | len ( a :msg ) = = 0
return
endif
2019-06-03 14:58:18 +00:00
let l :msg = a :msg [0 ]
let l :fname = go #path #FromURI ( l :msg .uri )
2020-10-28 13:35:24 +00:00
let l :line = l :msg .range .start .line
let l :col = l :msg .range .start .character
2019-06-03 14:58:18 +00:00
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
2020-10-28 13:35:24 +00:00
let l :state .handleResult = a :next
2019-06-03 14:58:18 +00:00
let l :state .error = funcref ( 's:noop' )
2020-10-28 13:35:24 +00:00
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
2019-06-03 14:58:18 +00:00
endfunction
2020-10-28 13:35:24 +00:00
function ! s :infoFromHoverContent ( content ) abort
if len ( a :content ) < 1
return ''
endif
let l :content = a :content [0 ]
2019-06-03 14:58:18 +00:00
" strip off the method set and fields of structs and interfaces.
2020-10-28 13:35:24 +00:00
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 10 new
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 ! * < buffer >
autocmd BufDelete < buffer > autocmd ! vim - go - diagnostics * < buffer = abuf >
if has ( 'textprop' )
autocmd BufReadPost < buffer > nested call s :highlightMatches ( b :go_diagnostic_matches .errors , b :go_diagnostic_matches .warnings )
else
autocmd BufWinEnter < buffer > 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
2019-06-03 14:58:18 +00:00
endfunction
" restore Vi compatibility settings
let &cpo = s :cpo_save
unlet s :cpo_save
" vim: sw=2 ts=2 et