2019-11-20 15:21:57 -07:00
local validate = vim.validate
local api = vim.api
2019-11-20 16:35:18 -07:00
local vfn = vim.fn
2019-11-20 15:21:57 -07:00
local util = require ' vim.lsp.util '
local protocol = require ' vim.lsp.protocol '
2019-11-20 16:35:18 -07:00
local log = require ' vim.lsp.log '
2019-11-20 15:21:57 -07:00
local M = { }
local function ok_or_nil ( status , ... )
2019-11-20 17:16:36 -07:00
if not status then return end
return ...
2019-11-20 15:21:57 -07:00
end
local function npcall ( fn , ... )
2019-11-20 17:16:36 -07:00
return ok_or_nil ( pcall ( fn , ... ) )
2019-11-20 15:21:57 -07:00
end
2019-11-20 17:16:13 -07:00
local function err_message ( ... )
api.nvim_err_writeln ( table.concat ( vim.tbl_flatten { ... } ) )
api.nvim_command ( " redraw " )
end
2019-11-20 15:21:57 -07:00
local function find_window_by_var ( name , value )
2019-11-20 17:16:36 -07:00
for _ , win in ipairs ( api.nvim_list_wins ( ) ) do
if npcall ( api.nvim_win_get_var , win , name ) == value then
return win
end
end
2019-11-20 15:21:57 -07:00
end
2019-11-20 16:35:18 -07:00
local function request ( method , params , callback )
2019-11-20 17:16:36 -07:00
-- TODO(ashkan) enable this.
-- callback = vim.lsp.default_callbacks[method] or callback
validate {
method = { method , ' s ' } ;
callback = { callback , ' f ' } ;
}
return vim.lsp . buf_request ( 0 , method , params , function ( err , _ , result , client_id )
2019-11-20 17:35:11 -07:00
local _ = log.debug ( ) and log.debug ( " vim.lsp.buf " , method , client_id , err , result )
2019-11-20 17:16:36 -07:00
if err then error ( tostring ( err ) ) end
return callback ( err , method , result , client_id )
end )
2019-11-20 16:35:18 -07:00
end
local function focusable_preview ( method , params , fn )
2019-11-20 17:16:36 -07:00
if npcall ( api.nvim_win_get_var , 0 , method ) then
return api.nvim_command ( " wincmd p " )
end
2019-11-20 15:21:57 -07:00
2019-11-20 17:16:36 -07:00
local bufnr = api.nvim_get_current_buf ( )
do
local win = find_window_by_var ( method , bufnr )
if win then
return api.nvim_set_current_win ( win )
end
end
return request ( method , params , function ( _ , _ , result , _ )
-- TODO(ashkan) could show error in preview...
local lines , filetype , opts = fn ( result )
if lines then
local _ , winnr = util.open_floating_preview ( lines , filetype , opts )
api.nvim_win_set_var ( winnr , method , bufnr )
end
end )
2019-11-20 16:35:18 -07:00
end
function M . hover ( )
2019-11-21 16:41:32 -07:00
local params = util.make_position_params ( )
2019-11-20 17:16:36 -07:00
focusable_preview ( ' textDocument/hover ' , params , function ( result )
if not ( result and result.contents ) then return end
2019-11-20 15:21:57 -07:00
2019-11-20 17:16:36 -07:00
local markdown_lines = util.convert_input_to_markdown_lines ( result.contents )
markdown_lines = util.trim_empty_lines ( markdown_lines )
if vim.tbl_isempty ( markdown_lines ) then
return { ' No information available ' }
end
return markdown_lines , util.try_trim_markdown_code_blocks ( markdown_lines )
end )
2019-11-20 15:21:57 -07:00
end
2019-11-20 16:35:18 -07:00
function M . peek_definition ( )
2019-11-21 16:41:32 -07:00
local params = util.make_position_params ( )
2019-11-20 17:16:36 -07:00
request ( ' textDocument/peekDefinition ' , params , function ( _ , _ , result , _ )
if not ( result and result [ 1 ] ) then return end
local loc = result [ 1 ]
local bufnr = vim.uri_to_bufnr ( loc.uri ) or error ( " couldn't find file " .. tostring ( loc.uri ) )
local start = loc.range . start
local finish = loc.range [ " end " ]
util.open_floating_peek_preview ( bufnr , start , finish , { offset_x = 1 } )
local headbuf = util.open_floating_preview ( { " Peek: " } , nil , {
offset_y = - ( finish.line - start.line ) ;
width = finish.character - start.character + 2 ;
} )
-- TODO(ashkan) change highlight group?
api.nvim_buf_add_highlight ( headbuf , - 1 , ' Keyword ' , 0 , - 1 )
end )
2019-11-20 16:35:18 -07:00
end
local function update_tagstack ( )
local bufnr = api.nvim_get_current_buf ( )
local line = vfn.line ( ' . ' )
local col = vfn.col ( ' . ' )
local tagname = vfn.expand ( ' <cWORD> ' )
local item = { bufnr = bufnr , from = { bufnr , line , col , 0 } , tagname = tagname }
local winid = vfn.win_getid ( )
local tagstack = vfn.gettagstack ( winid )
local action
if tagstack.length == tagstack.curidx then
action = ' r '
tagstack.items [ tagstack.curidx ] = item
elseif tagstack.length > tagstack.curidx then
action = ' r '
if tagstack.curidx > 1 then
tagstack.items = table.insert ( tagstack.items [ tagstack.curidx - 1 ] , item )
else
tagstack.items = { item }
end
else
action = ' a '
tagstack.items = { item }
end
tagstack.curidx = tagstack.curidx + 1
vfn.settagstack ( winid , tagstack , action )
end
local function handle_location ( result )
-- We can sometimes get a list of locations, so set the first value as the
-- only value we want to handle
-- TODO(ashkan) was this correct^? We could use location lists.
if result [ 1 ] ~= nil then
result = result [ 1 ]
end
if result.uri == nil then
2019-11-20 17:16:13 -07:00
err_message ( ' [LSP] Could not find a valid location ' )
2019-11-20 16:35:18 -07:00
return
end
2019-11-21 16:34:28 -07:00
local bufnr = vim.uri_to_bufnr ( result.uri )
2019-11-20 16:35:18 -07:00
update_tagstack ( )
api.nvim_set_current_buf ( bufnr )
2019-11-21 16:34:28 -07:00
local row = result.range . start.line
local col = result.range . start.character
local line = api.nvim_buf_get_lines ( 0 , row , row + 1 , true ) [ 1 ]
col = # line : sub ( 1 , col )
api.nvim_win_set_cursor ( 0 , { row + 1 , col } )
2019-11-20 16:35:18 -07:00
return true
end
local function location_callback ( _ , method , result )
if result == nil or vim.tbl_isempty ( result ) then
local _ = log.info ( ) and log.info ( method , ' No location found ' )
return nil
end
return handle_location ( result )
2019-11-20 15:21:57 -07:00
end
function M . declaration ( )
2019-11-21 16:41:32 -07:00
local params = util.make_position_params ( )
2019-11-20 17:16:36 -07:00
request ( ' textDocument/declaration ' , params , location_callback )
2019-11-20 16:35:18 -07:00
end
function M . definition ( )
2019-11-21 16:41:32 -07:00
local params = util.make_position_params ( )
2019-11-20 17:16:36 -07:00
request ( ' textDocument/definition ' , params , location_callback )
2019-11-20 15:21:57 -07:00
end
function M . type_definition ( )
2019-11-21 16:41:32 -07:00
local params = util.make_position_params ( )
2019-11-20 17:16:36 -07:00
request ( ' textDocument/typeDefinition ' , params , location_callback )
2019-11-20 15:21:57 -07:00
end
function M . implementation ( )
2019-11-21 16:41:32 -07:00
local params = util.make_position_params ( )
2019-11-20 17:16:36 -07:00
request ( ' textDocument/implementation ' , params , location_callback )
2019-11-20 16:35:18 -07:00
end
2019-11-20 17:03:32 -07:00
--- Convert SignatureHelp response to preview contents.
-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp
local function signature_help_to_preview_contents ( input )
if not input.signatures then
return
end
--The active signature. If omitted or the value lies outside the range of
--`signatures` the value defaults to zero or is ignored if `signatures.length
--=== 0`. Whenever possible implementors should make an active decision about
--the active signature and shouldn't rely on a default value.
local contents = { }
local active_signature = input.activeSignature or 0
-- If the activeSignature is not inside the valid range, then clip it.
if active_signature >= # input.signatures then
active_signature = 0
end
local signature = input.signatures [ active_signature + 1 ]
if not signature then
return
end
2019-11-20 17:37:23 -07:00
vim.list_extend ( contents , vim.split ( signature.label , ' \n ' , true ) )
2019-11-20 17:03:32 -07:00
if signature.documentation then
util.convert_input_to_markdown_lines ( signature.documentation , contents )
end
if input.parameters then
local active_parameter = input.activeParameter or 0
-- If the activeParameter is not inside the valid range, then clip it.
if active_parameter >= # input.parameters then
active_parameter = 0
end
local parameter = signature.parameters and signature.parameters [ active_parameter ]
if parameter then
--[=[
--Represents a parameter of a callable-signature. A parameter can
--have a label and a doc-comment.
interface ParameterInformation {
--The label of this parameter information.
--
--Either a string or an inclusive start and exclusive end offsets within its containing
--signature label. (see SignatureInformation.label). The offsets are based on a UTF-16
--string representation as `Position` and `Range` does.
--
--*Note*: a label of type string should be a substring of its containing signature label.
--Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`.
label : string | [ number , number ] ;
--The human-readable doc-comment of this parameter. Will be shown
--in the UI but can be omitted.
documentation ? : string | MarkupContent ;
}
--]=]
-- TODO highlight parameter
if parameter.documentation then
util.convert_input_to_markdown_lines ( parameter.documentation , contents )
end
end
end
return contents
end
2019-11-20 16:35:18 -07:00
function M . signature_help ( )
2019-11-21 16:41:32 -07:00
local params = util.make_position_params ( )
2019-11-20 17:16:36 -07:00
focusable_preview ( ' textDocument/signatureHelp ' , params , function ( result )
if not ( result and result.signatures and result.signatures [ 1 ] ) then return end
2019-11-20 17:03:32 -07:00
2019-11-20 17:16:36 -07:00
-- TODO show empty popup when signatures is empty?
local lines = signature_help_to_preview_contents ( result )
lines = util.trim_empty_lines ( lines )
if vim.tbl_isempty ( lines ) then
return { ' No signature available ' }
end
return lines , util.try_trim_markdown_code_blocks ( lines )
end )
2019-11-20 15:21:57 -07:00
end
-- TODO(ashkan) ?
2019-11-20 16:35:18 -07:00
function M . completion ( context )
2019-11-21 16:41:32 -07:00
local params = util.make_position_params ( )
2019-11-20 17:16:36 -07:00
params.context = context
return request ( ' textDocument/completion ' , params , function ( _ , _ , result )
if vim.tbl_isempty ( result or { } ) then return end
local row , col = unpack ( api.nvim_win_get_cursor ( 0 ) )
local line = assert ( api.nvim_buf_get_lines ( 0 , row - 1 , row , false ) [ 1 ] )
local line_to_cursor = line : sub ( col + 1 )
2019-11-20 16:35:18 -07:00
2019-11-20 17:16:36 -07:00
local matches = util.text_document_completion_list_to_complete_items ( result , line_to_cursor )
vim.fn . complete ( col , matches )
end )
2019-11-20 15:21:57 -07:00
end
2019-11-20 21:51:44 -07:00
function M . formatting ( options )
validate { options = { options , ' t ' , true } }
2019-11-21 16:19:06 -07:00
options = vim.tbl_extend ( ' keep ' , options or { } , {
tabSize = api.nvim_buf_get_option ( 0 , ' tabstop ' ) ;
insertSpaces = api.nvim_buf_get_option ( 0 , ' expandtab ' ) ;
} )
2019-11-20 21:51:44 -07:00
local params = {
textDocument = { uri = vim.uri_from_bufnr ( 0 ) } ;
2019-11-21 16:19:06 -07:00
options = options ;
2019-11-20 21:51:44 -07:00
}
return request ( ' textDocument/formatting ' , params , function ( _ , _ , result )
if not result then return end
2019-11-20 21:59:12 -07:00
util.apply_text_edits ( result )
2019-11-20 21:51:44 -07:00
end )
end
function M . range_formatting ( options , start_pos , end_pos )
validate {
options = { options , ' t ' , true } ;
start_pos = { start_pos , ' t ' , true } ;
end_pos = { end_pos , ' t ' , true } ;
}
2019-11-21 16:19:06 -07:00
options = vim.tbl_extend ( ' keep ' , options or { } , {
tabSize = api.nvim_buf_get_option ( 0 , ' tabstop ' ) ;
insertSpaces = api.nvim_buf_get_option ( 0 , ' expandtab ' ) ;
} )
2019-11-20 21:51:44 -07:00
start_pos = start_pos or vim.api . nvim_buf_get_mark ( 0 , ' < ' )
end_pos = end_pos or vim.api . nvim_buf_get_mark ( 0 , ' > ' )
local params = {
textDocument = { uri = vim.uri_from_bufnr ( 0 ) } ;
range = {
start = { line = start_pos [ 1 ] ; character = start_pos [ 2 ] ; } ;
[ " end " ] = { line = end_pos [ 1 ] ; character = end_pos [ 2 ] ; } ;
} ;
2019-11-21 16:19:06 -07:00
options = options ;
2019-11-20 21:51:44 -07:00
}
return request ( ' textDocument/rangeFormatting ' , params , function ( _ , _ , result )
if not result then return end
2019-11-20 21:59:12 -07:00
util.apply_text_edits ( result )
2019-11-20 21:51:44 -07:00
end )
2019-11-20 15:21:57 -07:00
end
2019-11-20 17:03:32 -07:00
function M . rename ( new_name )
2019-11-20 17:16:36 -07:00
-- TODO(ashkan) use prepareRename
-- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position.
2019-11-21 16:41:32 -07:00
local params = util.make_position_params ( )
2019-11-20 17:16:36 -07:00
new_name = new_name or npcall ( vfn.input , " New Name: " )
if not ( new_name and # new_name > 0 ) then return end
params.newName = new_name
request ( ' textDocument/rename ' , params , function ( _ , _ , result )
if not result then return end
2019-11-20 21:57:21 -07:00
util.apply_workspace_edit ( result )
2019-11-20 17:16:36 -07:00
end )
2019-11-20 17:03:32 -07:00
end
2019-11-20 15:21:57 -07:00
return M
2019-11-20 17:16:36 -07:00
-- vim:sw=2 ts=2 et