2019-11-13 12:55:26 -08:00
local protocol = require ' vim.lsp.protocol '
2019-11-24 03:01:18 -08:00
local vim = vim
2019-11-13 12:55:26 -08:00
local validate = vim.validate
local api = vim.api
2019-11-26 05:59:40 -08:00
local list_extend = vim.list_extend
2020-05-31 20:56:00 +02:00
local highlight = require ' vim.highlight '
2019-11-13 12:55:26 -08:00
local M = { }
2020-08-23 13:28:56 +02:00
-- FIXME: DOC: Expose in vimdocs
2020-04-25 15:46:58 +02:00
--- Diagnostics received from the server via `textDocument/publishDiagnostics`
-- by buffer.
--
-- {<bufnr>: {diagnostics}}
--
-- This contains only entries for active buffers. Entries for detached buffers
-- are discarded.
--
-- If you override the `textDocument/publishDiagnostic` callback,
-- this will be empty unless you call `buf_diagnostics_save_positions`.
--
--
-- Diagnostic is:
--
-- {
-- range: Range
-- message: string
-- severity?: DiagnosticSeverity
-- code?: number | string
-- source?: string
-- tags?: DiagnosticTag[]
-- relatedInformation?: DiagnosticRelatedInformation[]
-- }
M.diagnostics_by_buf = { }
2019-11-13 12:55:26 -08:00
local split = vim.split
2020-08-19 18:17:08 +02:00
--@private
2019-11-13 12:55:26 -08:00
local function split_lines ( value )
return split ( value , ' \n ' , true )
end
2020-08-19 18:17:08 +02:00
--@private
2019-11-26 05:59:40 -08:00
local function ok_or_nil ( status , ... )
if not status then return end
return ...
end
2020-08-19 18:17:08 +02:00
--@private
2019-11-26 05:59:40 -08:00
local function npcall ( fn , ... )
return ok_or_nil ( pcall ( fn , ... ) )
end
2019-11-13 12:55:26 -08:00
2020-08-19 18:17:08 +02:00
--- Replaces text in a range with new text.
---
--- CAUTION: Changes in-place!
---
--@param lines (table) Original list of strings
--@param A (table) Start position; a 2-tuple of {line, col} numbers
--@param B (table) End position; a 2-tuple of {line, col} numbers
--@param new_lines A list of strings to replace the original
--@returns (table) The modified {lines} object
2019-11-21 15:19:06 -08:00
function M . set_lines ( lines , A , B , new_lines )
-- 0-indexing to 1-indexing
local i_0 = A [ 1 ] + 1
2019-12-20 02:50:37 -08:00
-- If it extends past the end, truncate it to the end. This is because the
-- way the LSP describes the range including the last newline is by
-- specifying a line number after what we would call the last line.
local i_n = math.min ( B [ 1 ] + 1 , # lines )
2019-11-21 15:19:06 -08:00
if not ( i_0 >= 1 and i_0 <= # lines and i_n >= 1 and i_n <= # lines ) then
error ( " Invalid range: " .. vim.inspect { A = A ; B = B ; # lines , new_lines } )
2019-11-20 20:51:44 -08:00
end
local prefix = " "
2019-11-21 15:19:06 -08:00
local suffix = lines [ i_n ] : sub ( B [ 2 ] + 1 )
if A [ 2 ] > 0 then
prefix = lines [ i_0 ] : sub ( 1 , A [ 2 ] )
2019-11-20 20:51:44 -08:00
end
2019-11-21 23:58:32 -08:00
local n = i_n - i_0 + 1
if n ~= # new_lines then
for _ = 1 , n - # new_lines do table.remove ( lines , i_0 ) end
for _ = 1 , # new_lines - n do table.insert ( lines , i_0 , ' ' ) end
end
for i = 1 , # new_lines do
lines [ i - 1 + i_0 ] = new_lines [ i ]
end
2019-11-20 20:51:44 -08:00
if # suffix > 0 then
2019-11-21 23:58:32 -08:00
local i = i_0 + # new_lines - 1
lines [ i ] = lines [ i ] .. suffix
2019-11-20 20:51:44 -08:00
end
if # prefix > 0 then
2019-11-21 23:58:32 -08:00
lines [ i_0 ] = prefix .. lines [ i_0 ]
2019-11-20 20:51:44 -08:00
end
2019-11-21 23:58:32 -08:00
return lines
2019-11-21 15:19:06 -08:00
end
2020-08-19 18:17:08 +02:00
--@private
2019-11-21 15:19:06 -08:00
local function sort_by_key ( fn )
return function ( a , b )
local ka , kb = fn ( a ) , fn ( b )
assert ( # ka == # kb )
for i = 1 , # ka do
if ka [ i ] ~= kb [ i ] then
return ka [ i ] < kb [ i ]
end
2019-11-20 20:51:44 -08:00
end
2019-11-21 15:19:06 -08:00
-- every value must have been equal here, which means it's not less than.
return false
2019-11-20 20:51:44 -08:00
end
end
2020-08-19 18:17:08 +02:00
--@private
2019-11-21 15:19:06 -08:00
local edit_sort_key = sort_by_key ( function ( e )
2020-07-30 19:37:19 +02:00
return { e.A [ 1 ] , e.A [ 2 ] , e.i }
2019-11-21 15:19:06 -08:00
end )
2019-11-20 20:51:44 -08:00
2020-08-19 18:17:08 +02:00
--@private
2020-05-19 08:49:13 +02:00
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
2020-08-19 18:17:08 +02:00
--- Returns a zero-indexed column, since set_lines() does the conversion to
--- 1-indexed
2020-05-19 08:49:13 +02:00
local function get_line_byte_from_position ( bufnr , position )
-- LSP's line and characters are 0-indexed
-- Vim's line and columns are 1-indexed
local col = position.character
-- When on the first character, we can ignore the difference between byte and
-- character
if col > 0 then
local line = position.line
local lines = api.nvim_buf_get_lines ( bufnr , line , line + 1 , false )
2020-05-17 19:47:14 +02:00
if # lines > 0 then
2020-05-19 08:49:13 +02:00
return vim.str_byteindex ( lines [ 1 ] , col )
2020-05-17 19:47:14 +02:00
end
end
2020-05-19 08:49:13 +02:00
return col
2020-05-17 19:47:14 +02:00
end
2020-08-19 18:17:08 +02:00
--- Applies a list of text edits to a buffer.
--@param text_edits (table) list of `TextEdit` objects
--@param buf_nr (number) Buffer id
2019-11-20 20:51:44 -08:00
function M . apply_text_edits ( text_edits , bufnr )
if not next ( text_edits ) then return end
2020-05-08 16:04:41 +02:00
if not api.nvim_buf_is_loaded ( bufnr ) then
vim.fn . bufload ( bufnr )
end
2020-06-23 11:50:37 -04:00
api.nvim_buf_set_option ( bufnr , ' buflisted ' , true )
2019-11-20 20:51:44 -08:00
local start_line , finish_line = math.huge , - 1
local cleaned = { }
2019-11-21 15:19:06 -08:00
for i , e in ipairs ( text_edits ) do
2020-05-08 16:04:41 +02:00
-- adjust start and end column for UTF-16 encoding of non-ASCII characters
local start_row = e.range . start.line
2020-05-19 08:49:13 +02:00
local start_col = get_line_byte_from_position ( bufnr , e.range . start )
2020-05-08 16:04:41 +02:00
local end_row = e.range [ " end " ] . line
2020-05-19 08:49:13 +02:00
local end_col = get_line_byte_from_position ( bufnr , e.range [ ' end ' ] )
2019-11-20 20:51:44 -08:00
start_line = math.min ( e.range . start.line , start_line )
finish_line = math.max ( e.range [ " end " ] . line , finish_line )
2019-11-21 15:19:06 -08:00
-- TODO(ashkan) sanity check ranges for overlap.
2019-11-20 20:51:44 -08:00
table.insert ( cleaned , {
2019-11-21 15:19:06 -08:00
i = i ;
2020-05-08 16:04:41 +02:00
A = { start_row ; start_col } ;
B = { end_row ; end_col } ;
2019-11-20 20:51:44 -08:00
lines = vim.split ( e.newText , ' \n ' , true ) ;
} )
end
2019-11-21 15:19:06 -08:00
-- Reverse sort the orders so we can apply them without interfering with
-- eachother. Also add i as a sort key to mimic a stable sort.
table.sort ( cleaned , edit_sort_key )
2019-11-20 20:51:44 -08:00
local lines = api.nvim_buf_get_lines ( bufnr , start_line , finish_line + 1 , false )
2019-11-21 15:19:06 -08:00
local fix_eol = api.nvim_buf_get_option ( bufnr , ' fixeol ' )
2019-12-20 02:50:37 -08:00
local set_eol = fix_eol and api.nvim_buf_line_count ( bufnr ) <= finish_line + 1
2019-11-21 15:19:06 -08:00
if set_eol and # lines [ # lines ] ~= 0 then
table.insert ( lines , ' ' )
end
for i = # cleaned , 1 , - 1 do
local e = cleaned [ i ]
local A = { e.A [ 1 ] - start_line , e.A [ 2 ] }
local B = { e.B [ 1 ] - start_line , e.B [ 2 ] }
2019-11-23 16:14:24 -08:00
lines = M.set_lines ( lines , A , B , e.lines )
2019-11-21 15:19:06 -08:00
end
if set_eol and # lines [ # lines ] == 0 then
table.remove ( lines )
2019-11-20 20:51:44 -08:00
end
api.nvim_buf_set_lines ( bufnr , start_line , finish_line + 1 , false , lines )
end
2019-11-13 12:55:26 -08:00
-- local valid_windows_path_characters = "[^<>:\"/\\|?*]"
-- local valid_unix_path_characters = "[^/]"
-- https://github.com/davidm/lua-glob-pattern
-- https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
-- function M.glob_to_regex(glob)
-- end
2020-08-19 18:17:08 +02:00
--- Can be used to extract the completion items from a
--- `textDocument/completion` request, which may return one of
--- `CompletionItem[]`, `CompletionList` or null.
--@param result (table) The result of a `textDocument/completion` request
--@returns (table) List of completion items
--@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
2019-11-13 12:55:26 -08:00
function M . extract_completion_items ( result )
if type ( result ) == ' table ' and result.items then
2020-08-19 18:17:08 +02:00
-- result is a `CompletionList`
2019-11-13 12:55:26 -08:00
return result.items
elseif result ~= nil then
2020-08-19 18:17:08 +02:00
-- result is `CompletionItem[]`
2019-11-13 12:55:26 -08:00
return result
else
2020-08-19 18:17:08 +02:00
-- result is `null`
2019-11-13 12:55:26 -08:00
return { }
end
end
2020-08-19 18:17:08 +02:00
--- Applies a `TextDocumentEdit`, which is a list of changes to a single
-- document.
---
--@param text_document_edit (table) a `TextDocumentEdit` object
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
2019-11-20 20:51:44 -08:00
function M . apply_text_document_edit ( text_document_edit )
local text_document = text_document_edit.textDocument
local bufnr = vim.uri_to_bufnr ( text_document.uri )
2020-05-14 04:14:52 +01:00
if text_document.version then
-- `VersionedTextDocumentIdentifier`s version may be null https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier
if text_document.version ~= vim.NIL and M.buf_versions [ bufnr ] ~= nil and M.buf_versions [ bufnr ] > text_document.version then
print ( " Buffer " , text_document.uri , " newer than edits. " )
return
end
2019-11-13 12:55:26 -08:00
end
2019-11-20 20:51:44 -08:00
M.apply_text_edits ( text_document_edit.edits , bufnr )
2019-11-13 12:55:26 -08:00
end
2020-08-19 18:17:08 +02:00
--@private
--- Recursively parses snippets in a completion entry.
---
--@param input (string) Snippet text to parse for snippets
--@param inner (bool) Whether this function is being called recursively
--@returns 2-tuple of strings: The first is the parsed result, the second is the
---unparsed rest of the input
2020-05-28 14:31:56 +02:00
local function parse_snippet_rec ( input , inner )
local res = " "
local close , closeend = nil , nil
if inner then
close , closeend = input : find ( " } " , 1 , true )
while close ~= nil and input : sub ( close - 1 , close - 1 ) == " \\ " do
close , closeend = input : find ( " } " , closeend + 1 , true )
end
end
local didx = input : find ( ' $ ' , 1 , true )
if didx == nil and close == nil then
return input , " "
elseif close ~= nil and ( didx == nil or close < didx ) then
-- No inner placeholders
return input : sub ( 0 , close - 1 ) , input : sub ( closeend + 1 )
end
res = res .. input : sub ( 0 , didx - 1 )
input = input : sub ( didx + 1 )
local tabstop , tabstopend = input : find ( ' ^%d+ ' )
local placeholder , placeholderend = input : find ( ' ^{%d+: ' )
local choice , choiceend = input : find ( ' ^{%d+| ' )
if tabstop then
input = input : sub ( tabstopend + 1 )
elseif choice then
input = input : sub ( choiceend + 1 )
close , closeend = input : find ( " |} " , 1 , true )
res = res .. input : sub ( 0 , close - 1 )
input = input : sub ( closeend + 1 )
elseif placeholder then
-- TODO: add support for variables
input = input : sub ( placeholderend + 1 )
-- placeholders and variables are recursive
while input ~= " " do
local r , tail = parse_snippet_rec ( input , true )
r = r : gsub ( " \\ } " , " } " )
res = res .. r
input = tail
end
else
res = res .. " $ "
end
return res , input
end
2020-08-19 18:17:08 +02:00
--- Parses snippets in a completion entry.
---
--@param input (string) unparsed snippet
--@returns (string) parsed snippet
2020-05-28 14:31:56 +02:00
function M . parse_snippet ( input )
local res , _ = parse_snippet_rec ( input , false )
return res
end
2020-08-19 18:17:08 +02:00
--@private
--- Sorts by CompletionItem.sortText.
---
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
2020-02-19 07:39:56 +09:00
local function sort_completion_items ( items )
2020-08-31 01:29:47 -04:00
table.sort ( items , function ( a , b )
return ( a.sortText or a.label ) < ( b.sortText or b.label )
end )
2020-02-19 07:39:56 +09:00
end
2020-08-19 18:17:08 +02:00
--@private
--- Returns text that should be inserted when selecting completion item. The
--- precedence is as follows: textEdit.newText > insertText > label
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
2020-04-20 01:59:09 +03:00
local function get_completion_word ( item )
if item.textEdit ~= nil and item.textEdit . newText ~= nil then
2020-05-28 14:31:56 +02:00
if protocol.InsertTextFormat [ item.insertTextFormat ] == " PlainText " then
return item.textEdit . newText
else
return M.parse_snippet ( item.textEdit . newText )
end
2020-04-20 01:59:09 +03:00
elseif item.insertText ~= nil then
2020-05-28 14:31:56 +02:00
if protocol.InsertTextFormat [ item.insertTextFormat ] == " PlainText " then
return item.insertText
else
return M.parse_snippet ( item.insertText )
end
2020-04-20 01:59:09 +03:00
end
return item.label
end
2020-08-19 18:17:08 +02:00
--@private
--- Some language servers return complementary candidates whose prefixes do not
--- match are also returned. So we exclude completion candidates whose prefix
--- does not match.
2020-02-19 07:39:56 +09:00
local function remove_unmatch_completion_items ( items , prefix )
return vim.tbl_filter ( function ( item )
2020-04-20 01:59:09 +03:00
local word = get_completion_word ( item )
2020-02-19 07:39:56 +09:00
return vim.startswith ( word , prefix )
end , items )
end
2020-08-19 18:17:08 +02:00
--- Acording to LSP spec, if the client set `completionItemKind.valueSet`,
--- the client must handle it properly even if it receives a value outside the
--- specification.
---
--@param completion_item_kind (`vim.lsp.protocol.completionItemKind`)
--@returns (`vim.lsp.protocol.completionItemKind`)
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
2020-05-08 05:23:25 +09:00
function M . _get_completion_item_kind_name ( completion_item_kind )
return protocol.CompletionItemKind [ completion_item_kind ] or " Unknown "
end
2020-08-19 18:17:08 +02:00
--- Turns the result of a `textDocument/completion` request into vim-compatible
--- |complete-items|.
---
--@param result The result of a `textDocument/completion` call, e.g. from
---|vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`,
--- `CompletionList` or `null`
--@param prefix (string) the prefix to filter the completion items
--@returns { matches = complete-items table, incomplete = bool }
--@see |complete-items|
2020-02-18 13:38:52 +09:00
function M . text_document_completion_list_to_complete_items ( result , prefix )
2019-11-13 12:55:26 -08:00
local items = M.extract_completion_items ( result )
if vim.tbl_isempty ( items ) then
return { }
end
2020-02-19 07:39:56 +09:00
items = remove_unmatch_completion_items ( items , prefix )
sort_completion_items ( items )
2020-02-18 13:38:52 +09:00
2019-11-13 12:55:26 -08:00
local matches = { }
for _ , completion_item in ipairs ( items ) do
local info = ' '
local documentation = completion_item.documentation
if documentation then
if type ( documentation ) == ' string ' and documentation ~= ' ' then
info = documentation
elseif type ( documentation ) == ' table ' and type ( documentation.value ) == ' string ' then
info = documentation.value
-- else
-- TODO(ashkan) Validation handling here?
end
end
2020-04-20 01:59:09 +03:00
local word = get_completion_word ( completion_item )
2019-11-13 12:55:26 -08:00
table.insert ( matches , {
2019-12-20 05:46:47 -05:00
word = word ,
2019-11-13 12:55:26 -08:00
abbr = completion_item.label ,
2020-05-08 05:23:25 +09:00
kind = M._get_completion_item_kind_name ( completion_item.kind ) ,
2019-11-13 12:55:26 -08:00
menu = completion_item.detail or ' ' ,
info = info ,
icase = 1 ,
2020-02-21 09:34:07 +01:00
dup = 1 ,
2019-11-13 12:55:26 -08:00
empty = 1 ,
2020-04-29 10:32:34 +09:00
user_data = {
nvim = {
lsp = {
completion_item = completion_item
}
}
} ,
2019-11-13 12:55:26 -08:00
} )
end
return matches
end
2020-08-19 18:17:08 +02:00
--- Applies a `WorkspaceEdit`.
---
--@param workspace_edit (table) `WorkspaceEdit`
-- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
2019-11-20 20:51:44 -08:00
function M . apply_workspace_edit ( workspace_edit )
2019-11-13 12:55:26 -08:00
if workspace_edit.documentChanges then
for _ , change in ipairs ( workspace_edit.documentChanges ) do
if change.kind then
-- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile
error ( string.format ( " Unsupported change: %q " , vim.inspect ( change ) ) )
else
2019-11-20 20:51:44 -08:00
M.apply_text_document_edit ( change )
2019-11-13 12:55:26 -08:00
end
end
return
end
2019-11-20 16:35:11 -08:00
local all_changes = workspace_edit.changes
if not ( all_changes and not vim.tbl_isempty ( all_changes ) ) then
2019-11-13 12:55:26 -08:00
return
end
2019-11-20 16:35:11 -08:00
for uri , changes in pairs ( all_changes ) do
local bufnr = vim.uri_to_bufnr ( uri )
2019-11-20 20:51:44 -08:00
M.apply_text_edits ( changes , bufnr )
2019-11-13 12:55:26 -08:00
end
end
2020-08-19 18:17:08 +02:00
--- Converts any of `MarkedString` | `MarkedString[]` | `MarkupContent` into
--- a list of lines containing valid markdown. Useful to populate the hover
--- window for `textDocument/hover`, for parsing the result of
--- `textDocument/signatureHelp`, and potentially others.
---
--@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`)
--@param contents (table, optional, default `{}`) List of strings to extend with converted lines
--@returns {contents}, extended with lines of converted markdown.
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
2019-11-13 12:55:26 -08:00
function M . convert_input_to_markdown_lines ( input , contents )
contents = contents or { }
-- MarkedString variation 1
if type ( input ) == ' string ' then
list_extend ( contents , split_lines ( input ) )
else
assert ( type ( input ) == ' table ' , " Expected a table for Hover.contents " )
-- MarkupContent
if input.kind then
-- The kind can be either plaintext or markdown. However, either way we
-- will just be rendering markdown, so we handle them both the same way.
-- TODO these can have escaped/sanitized html codes in markdown. We
-- should make sure we handle this correctly.
-- Some servers send input.value as empty, so let's ignore this :(
-- assert(type(input.value) == 'string')
list_extend ( contents , split_lines ( input.value or ' ' ) )
-- MarkupString variation 2
elseif input.language then
-- Some servers send input.value as empty, so let's ignore this :(
-- assert(type(input.value) == 'string')
table.insert ( contents , " ``` " .. input.language )
list_extend ( contents , split_lines ( input.value or ' ' ) )
table.insert ( contents , " ``` " )
-- By deduction, this must be MarkedString[]
else
-- Use our existing logic to handle MarkedString
for _ , marked_string in ipairs ( input ) do
M.convert_input_to_markdown_lines ( marked_string , contents )
end
end
end
2020-02-27 00:00:06 +01:00
if ( contents [ 1 ] == ' ' or contents [ 1 ] == nil ) and # contents == 1 then
2019-11-13 12:55:26 -08:00
return { }
end
return contents
end
2020-08-19 18:17:08 +02:00
--- Converts `textDocument/SignatureHelp` response to markdown lines.
---
--@param signature_help Response of `textDocument/SignatureHelp`
--@returns list of lines of converted markdown.
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
2020-04-20 06:40:54 +09:00
function M . convert_signature_help_to_markdown_lines ( signature_help )
if not signature_help.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 = signature_help.activeSignature or 0
-- If the activeSignature is not inside the valid range, then clip it.
if active_signature >= # signature_help.signatures then
active_signature = 0
end
local signature = signature_help.signatures [ active_signature + 1 ]
if not signature then
return
end
vim.list_extend ( contents , vim.split ( signature.label , ' \n ' , true ) )
if signature.documentation then
M.convert_input_to_markdown_lines ( signature.documentation , contents )
end
2020-09-13 00:33:31 +08:00
if signature.parameters and # signature.parameters > 0 then
2020-04-20 06:40:54 +09:00
local active_parameter = signature_help.activeParameter or 0
-- If the activeParameter is not inside the valid range, then clip it.
2020-09-13 00:33:31 +08:00
if active_parameter >= # signature.parameters then
2020-04-20 06:40:54 +09:00
active_parameter = 0
end
2020-09-13 00:33:31 +08:00
local parameter = signature.parameters [ active_parameter + 1 ]
2020-04-20 06:40:54 +09:00
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
2020-09-13 00:33:31 +08:00
if parameter.documentation and parameter.documentation ~= vim.NIL then
M.convert_input_to_markdown_lines ( parameter.documentation , contents )
2020-04-20 06:40:54 +09:00
end
end
end
return contents
end
2020-08-19 18:17:08 +02:00
--- Creates a table with sensible default options for a floating window. The
--- table can be passed to |nvim_open_win()|.
---
--@param width (number) window width (in character cells)
--@param height (number) window height (in character cells)
--@param opts (table, optional)
--@returns (table) Options
2019-11-13 12:55:26 -08:00
function M . make_floating_popup_options ( width , height , opts )
validate {
opts = { opts , ' t ' , true } ;
}
opts = opts or { }
validate {
[ " opts.offset_x " ] = { opts.offset_x , ' n ' , true } ;
[ " opts.offset_y " ] = { opts.offset_y , ' n ' , true } ;
}
local anchor = ' '
local row , col
2020-01-03 14:39:25 +02:00
local lines_above = vim.fn . winline ( ) - 1
local lines_below = vim.fn . winheight ( 0 ) - lines_above
if lines_above < lines_below then
2019-11-13 12:55:26 -08:00
anchor = anchor .. ' N '
2020-01-03 14:39:25 +02:00
height = math.min ( lines_below , height )
2019-11-13 12:55:26 -08:00
row = 1
else
anchor = anchor .. ' S '
2020-01-03 14:39:25 +02:00
height = math.min ( lines_above , height )
2019-11-13 12:55:26 -08:00
row = 0
end
if vim.fn . wincol ( ) + width <= api.nvim_get_option ( ' columns ' ) then
anchor = anchor .. ' W '
col = 0
else
anchor = anchor .. ' E '
col = 1
end
return {
anchor = anchor ,
col = col + ( opts.offset_x or 0 ) ,
height = height ,
relative = ' cursor ' ,
row = row + ( opts.offset_y or 0 ) ,
style = ' minimal ' ,
width = width ,
}
end
2020-08-19 18:17:08 +02:00
--- Jumps to a location.
---
--@param location (`Location`|`LocationLink`)
--@returns `true` if the jump succeeded
2019-11-26 05:59:40 -08:00
function M . jump_to_location ( location )
2020-05-02 15:21:07 +02:00
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
if uri == nil then return end
local bufnr = vim.uri_to_bufnr ( uri )
2019-12-07 12:34:22 +01:00
-- Save position in jumplist
vim.cmd " normal! m' "
2020-04-28 16:47:22 +02:00
-- Push a new item into tagstack
2020-05-05 00:12:35 -03:00
local from = { vim.fn . bufnr ( ' % ' ) , vim.fn . line ( ' . ' ) , vim.fn . col ( ' . ' ) , 0 }
local items = { { tagname = vim.fn . expand ( ' <cword> ' ) , from = from } }
vim.fn . settagstack ( vim.fn . win_getid ( ) , { items = items } , ' t ' )
2020-04-28 16:47:22 +02:00
2020-05-08 16:04:41 +02:00
--- Jump to new location (adjusting for UTF-16 encoding of characters)
2019-11-26 05:59:40 -08:00
api.nvim_set_current_buf ( bufnr )
2020-05-07 10:30:42 -03:00
api.nvim_buf_set_option ( 0 , ' buflisted ' , true )
2020-05-02 15:21:07 +02:00
local range = location.range or location.targetSelectionRange
local row = range.start . line
2020-05-19 08:50:31 +02:00
local col = get_line_byte_from_position ( 0 , range.start )
2019-11-26 05:59:40 -08:00
api.nvim_win_set_cursor ( 0 , { row + 1 , col } )
return true
end
2020-08-19 18:17:08 +02:00
--- Previews a location in a floating window
2020-05-26 15:07:10 +02:00
---
--- behavior depends on type of location:
--- - for Location, range is shown (e.g., function definition)
--- - for LocationLink, targetRange is shown (e.g., body of function definition)
---
2020-08-19 18:17:08 +02:00
--@param location a single `Location` or `LocationLink`
--@returns (bufnr,winnr) buffer and window number of floating window or nil
2020-05-26 15:07:10 +02:00
function M . preview_location ( location )
-- location may be LocationLink or Location (more useful for the former)
local uri = location.targetUri or location.uri
if uri == nil then return end
local bufnr = vim.uri_to_bufnr ( uri )
if not api.nvim_buf_is_loaded ( bufnr ) then
vim.fn . bufload ( bufnr )
end
local range = location.targetRange or location.range
local contents = api.nvim_buf_get_lines ( bufnr , range.start . line , range [ " end " ] . line + 1 , false )
local filetype = api.nvim_buf_get_option ( bufnr , ' filetype ' )
return M.open_floating_preview ( contents , filetype )
end
2020-08-19 18:17:08 +02:00
--@private
2019-11-26 05:59:40 -08:00
local function find_window_by_var ( name , value )
for _ , win in ipairs ( api.nvim_list_wins ( ) ) do
if npcall ( api.nvim_win_get_var , win , name ) == value then
return win
end
end
end
2020-08-19 18:17:08 +02:00
--- Enters/leaves the focusable window associated with the current buffer via the
--window - variable `unique_name`. If no such window exists, run the function
--{fn}.
---
--@param unique_name (string) Window variable
--@param fn (function) should return create a new window and return a tuple of
---({focusable_buffer_id}, {window_id}). if {focusable_buffer_id} is a valid
---buffer id, the newly created window will be the new focus associated with
---the current buffer via the tag `unique_name`.
--@returns (pbufnr, pwinnr) if `fn()` has created a new window; nil otherwise
2019-12-20 02:50:37 -08:00
function M . focusable_float ( unique_name , fn )
2020-08-19 18:17:08 +02:00
-- Go back to previous window if we are in a focusable one
2019-11-26 05:59:40 -08:00
if npcall ( api.nvim_win_get_var , 0 , unique_name ) then
return api.nvim_command ( " wincmd p " )
end
local bufnr = api.nvim_get_current_buf ( )
do
local win = find_window_by_var ( unique_name , bufnr )
2020-09-14 23:03:02 +08:00
if win and api.nvim_win_is_valid ( win ) and not vim.fn . pumvisible ( ) then
2019-11-26 05:59:40 -08:00
api.nvim_set_current_win ( win )
api.nvim_command ( " stopinsert " )
return
end
end
2019-12-20 02:50:37 -08:00
local pbufnr , pwinnr = fn ( )
if pbufnr then
api.nvim_win_set_var ( pwinnr , unique_name , bufnr )
return pbufnr , pwinnr
end
end
2020-08-19 18:17:08 +02:00
--- Focuses/unfocuses the floating preview window associated with the current
--- buffer via the window variable `unique_name`. If no such preview window
--- exists, makes a new one.
---
--@param unique_name (string) Window variable
--@param fn (function) The return values of this function will be passed
---directly to |vim.lsp.util.open_floating_preview()|, in the case that a new
---floating window should be created
2019-12-20 02:50:37 -08:00
function M . focusable_preview ( unique_name , fn )
return M.focusable_float ( unique_name , function ( )
return M.open_floating_preview ( fn ( ) )
end )
end
2020-08-19 18:17:08 +02:00
--- Trims empty lines from input and pad left and right with spaces
2020-06-04 20:23:03 +02:00
---
--@param contents table of lines to trim and pad
--@param opts dictionary with optional fields
2020-07-06 03:09:52 +02:00
-- - pad_left number of columns to pad contents at left (default 1)
-- - pad_right number of columns to pad contents at right (default 1)
-- - pad_top number of lines to pad contents at top (default 0)
-- - pad_bottom number of lines to pad contents at bottom (default 0)
2020-08-19 18:17:08 +02:00
--@returns contents table of trimmed and padded lines
2020-06-04 20:23:03 +02:00
function M . _trim_and_pad ( contents , opts )
validate {
contents = { contents , ' t ' } ;
opts = { opts , ' t ' , true } ;
}
opts = opts or { }
local left_padding = ( " " ) : rep ( opts.pad_left or 1 )
local right_padding = ( " " ) : rep ( opts.pad_right or 1 )
contents = M.trim_empty_lines ( contents )
for i , line in ipairs ( contents ) do
contents [ i ] = string.format ( ' %s%s%s ' , left_padding , line : gsub ( " \r " , " " ) , right_padding )
end
2020-07-06 03:09:52 +02:00
if opts.pad_top then
for _ = 1 , opts.pad_top do
table.insert ( contents , 1 , " " )
end
end
if opts.pad_bottom then
for _ = 1 , opts.pad_bottom do
table.insert ( contents , " " )
end
end
2020-06-04 20:23:03 +02:00
return contents
end
2020-08-19 18:17:08 +02:00
-- TODO: refactor to separate stripping/converting and make use of open_floating_preview
--
--- Converts markdown into syntax highlighted regions by stripping the code
2020-06-04 20:23:03 +02:00
--- blocks and converting them into highlighted code.
--- This will by default insert a blank line separator after those code block
--- regions to improve readability.
2020-08-19 18:17:08 +02:00
--- The result is shown in a floating preview.
2020-06-04 20:23:03 +02:00
---
--@param contents table of lines to show in window
--@param opts dictionary with optional fields
-- - height of floating window
-- - width of floating window
-- - wrap_at character to wrap at for computing height
2020-07-06 03:09:52 +02:00
-- - max_width maximal width of floating window
-- - max_height maximal height of floating window
-- - pad_left number of columns to pad contents at left
-- - pad_right number of columns to pad contents at right
-- - pad_top number of lines to pad contents at top
-- - pad_bottom number of lines to pad contents at bottom
2020-06-04 20:23:03 +02:00
-- - separator insert separator after code block
2020-08-19 18:17:08 +02:00
--@returns width,height size of float
2019-12-20 02:50:37 -08:00
function M . fancy_floating_markdown ( contents , opts )
2020-06-04 20:23:03 +02:00
validate {
contents = { contents , ' t ' } ;
opts = { opts , ' t ' , true } ;
}
opts = opts or { }
2019-12-20 02:50:37 -08:00
local stripped = { }
local highlights = { }
do
local i = 1
while i <= # contents do
local line = contents [ i ]
-- TODO(ashkan): use a more strict regex for filetype?
local ft = line : match ( " ^```([a-zA-Z0-9_]*)$ " )
-- local ft = line:match("^```(.*)$")
-- TODO(ashkan): validate the filetype here.
if ft then
local start = # stripped
i = i + 1
while i <= # contents do
line = contents [ i ]
if line == " ``` " then
i = i + 1
break
end
table.insert ( stripped , line )
i = i + 1
end
table.insert ( highlights , {
ft = ft ;
start = start + 1 ;
finish = # stripped + 1 - 1 ;
} )
else
table.insert ( stripped , line )
i = i + 1
end
end
end
2020-06-04 20:23:03 +02:00
-- Clean up and add padding
stripped = M._trim_and_pad ( stripped , opts )
-- Compute size of float needed to show (wrapped) lines
opts.wrap_at = opts.wrap_at or ( vim.wo [ " wrap " ] and api.nvim_win_get_width ( 0 ) )
local width , height = M._make_floating_popup_size ( stripped , opts )
-- Insert blank line separator after code block
local insert_separator = opts.separator or true
2019-12-20 02:50:37 -08:00
if insert_separator then
for i , h in ipairs ( highlights ) do
h.start = h.start + i - 1
h.finish = h.finish + i - 1
if h.finish + 1 <= # stripped then
table.insert ( stripped , h.finish + 1 , string.rep ( " ─ " , width ) )
2020-06-04 20:23:03 +02:00
height = height + 1
2019-12-20 02:50:37 -08:00
end
end
end
-- Make the floating window.
local bufnr = api.nvim_create_buf ( false , true )
local winnr = api.nvim_open_win ( bufnr , false , M.make_floating_popup_options ( width , height , opts ) )
vim.api . nvim_buf_set_lines ( bufnr , 0 , - 1 , false , stripped )
2020-07-19 17:36:04 +02:00
api.nvim_buf_set_option ( bufnr , ' modifiable ' , false )
2019-12-20 02:50:37 -08:00
-- Switch to the floating window to apply the syntax highlighting.
-- This is because the syntax command doesn't accept a target.
local cwin = vim.api . nvim_get_current_win ( )
vim.api . nvim_set_current_win ( winnr )
vim.cmd ( " ownsyntax markdown " )
local idx = 1
2020-08-19 18:17:08 +02:00
--@private
2020-05-31 20:56:00 +02:00
local function apply_syntax_to_region ( ft , start , finish )
2019-12-20 02:50:37 -08:00
if ft == ' ' then return end
local name = ft .. idx
idx = idx + 1
local lang = " @ " .. ft : upper ( )
-- TODO(ashkan): better validation before this.
if not pcall ( vim.cmd , string.format ( " syntax include %s syntax/%s.vim " , lang , ft ) ) then
return
end
vim.cmd ( string.format ( " syntax region %s start=+ \\ %%%dl+ end=+ \\ %%%dl+ contains=%s " , name , start , finish + 1 , lang ) )
end
-- Previous highlight region.
-- TODO(ashkan): this wasn't working for some reason, but I would like to
-- make sure that regions between code blocks are definitely markdown.
-- local ph = {start = 0; finish = 1;}
for _ , h in ipairs ( highlights ) do
2020-05-31 20:56:00 +02:00
-- apply_syntax_to_region('markdown', ph.finish, h.start)
apply_syntax_to_region ( h.ft , h.start , h.finish )
2019-12-20 02:50:37 -08:00
-- ph = h
end
vim.api . nvim_set_current_win ( cwin )
return bufnr , winnr
end
2020-08-19 18:17:08 +02:00
--- Creates autocommands to close a preview window when events happen.
---
--@param events (table) list of events
--@param winnr (number) window id of preview window
--@see |autocmd-events|
2019-12-20 02:50:37 -08:00
function M . close_preview_autocmd ( events , winnr )
api.nvim_command ( " autocmd " .. table.concat ( events , ' , ' ) .. " <buffer> ++once lua pcall(vim.api.nvim_win_close, " .. winnr .. " , true) " )
2019-11-26 05:59:40 -08:00
end
2020-08-19 18:17:08 +02:00
--@internal
--- Computes size of float needed to show contents (with optional wrapping)
2020-06-04 20:23:03 +02:00
---
--@param contents table of lines to show in window
--@param opts dictionary with optional fields
-- - height of floating window
-- - width of floating window
-- - wrap_at character to wrap at for computing height
2020-07-06 03:09:52 +02:00
-- - max_width maximal width of floating window
-- - max_height maximal height of floating window
2020-08-19 18:17:08 +02:00
--@returns width,height size of float
2020-06-04 20:23:03 +02:00
function M . _make_floating_popup_size ( contents , opts )
2019-11-13 12:55:26 -08:00
validate {
contents = { contents , ' t ' } ;
opts = { opts , ' t ' , true } ;
}
2019-11-20 15:35:18 -08:00
opts = opts or { }
2019-11-20 11:34:10 -08:00
2019-11-20 15:35:18 -08:00
local width = opts.width
2020-06-04 20:23:03 +02:00
local height = opts.height
2020-07-06 03:09:52 +02:00
local wrap_at = opts.wrap_at
local max_width = opts.max_width
local max_height = opts.max_height
2020-06-04 20:23:03 +02:00
local line_widths = { }
2019-11-20 15:35:18 -08:00
if not width then
width = 0
for i , line in ipairs ( contents ) do
-- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
2020-06-04 20:23:03 +02:00
line_widths [ i ] = vim.fn . strdisplaywidth ( line )
width = math.max ( line_widths [ i ] , width )
2019-11-13 12:55:26 -08:00
end
end
2020-07-06 03:09:52 +02:00
if max_width then
width = math.min ( width , max_width )
wrap_at = math.min ( wrap_at or max_width , max_width )
end
2019-11-13 12:55:26 -08:00
2020-06-04 20:23:03 +02:00
if not height then
height = # contents
2020-07-06 03:09:52 +02:00
if wrap_at and width >= wrap_at then
2020-06-04 20:23:03 +02:00
height = 0
if vim.tbl_isempty ( line_widths ) then
for _ , line in ipairs ( contents ) do
local line_width = vim.fn . strdisplaywidth ( line )
height = height + math.ceil ( line_width / wrap_at )
end
else
for i = 1 , # contents do
2020-07-06 03:09:52 +02:00
height = height + math.max ( 1 , math.ceil ( line_widths [ i ] / wrap_at ) )
2020-06-04 20:23:03 +02:00
end
end
end
end
2020-07-06 03:09:52 +02:00
if max_height then
height = math.min ( height , max_height )
end
2020-06-04 20:23:03 +02:00
return width , height
end
2020-08-19 18:17:08 +02:00
--- Shows contents in a floating window.
2020-06-04 20:23:03 +02:00
---
--@param contents table of lines to show in window
--@param filetype string of filetype to set for opened buffer
--@param opts dictionary with optional fields
-- - height of floating window
-- - width of floating window
-- - wrap_at character to wrap at for computing height
2020-07-06 03:09:52 +02:00
-- - max_width maximal width of floating window
-- - max_height maximal height of floating window
-- - pad_left number of columns to pad contents at left
-- - pad_right number of columns to pad contents at right
-- - pad_top number of lines to pad contents at top
-- - pad_bottom number of lines to pad contents at bottom
2020-08-19 18:17:08 +02:00
--@returns bufnr,winnr buffer and window number of the newly created floating
---preview window
2020-06-04 20:23:03 +02:00
function M . open_floating_preview ( contents , filetype , opts )
validate {
contents = { contents , ' t ' } ;
filetype = { filetype , ' s ' , true } ;
opts = { opts , ' t ' , true } ;
}
opts = opts or { }
-- Clean up input: trim empty lines from the end, pad
contents = M._trim_and_pad ( contents , opts )
-- Compute size of float needed to show (wrapped) lines
opts.wrap_at = opts.wrap_at or ( vim.wo [ " wrap " ] and api.nvim_win_get_width ( 0 ) )
local width , height = M._make_floating_popup_size ( contents , opts )
2019-11-13 12:55:26 -08:00
local floating_bufnr = api.nvim_create_buf ( false , true )
if filetype then
api.nvim_buf_set_option ( floating_bufnr , ' filetype ' , filetype )
end
local float_option = M.make_floating_popup_options ( width , height , opts )
local floating_winnr = api.nvim_open_win ( floating_bufnr , false , float_option )
if filetype == ' markdown ' then
api.nvim_win_set_option ( floating_winnr , ' conceallevel ' , 2 )
end
api.nvim_buf_set_lines ( floating_bufnr , 0 , - 1 , true , contents )
api.nvim_buf_set_option ( floating_bufnr , ' modifiable ' , false )
2020-06-12 12:38:33 -06:00
M.close_preview_autocmd ( { " CursorMoved " , " CursorMovedI " , " BufHidden " , " BufLeave " } , floating_winnr )
2019-11-13 12:55:26 -08:00
return floating_bufnr , floating_winnr
end
do
local diagnostic_ns = api.nvim_create_namespace ( " vim_lsp_diagnostics " )
2020-02-26 20:10:16 +01:00
local reference_ns = api.nvim_create_namespace ( " vim_lsp_references " )
2020-02-27 12:12:53 +01:00
local sign_ns = ' vim_lsp_signs '
2019-11-27 19:45:03 +01:00
local underline_highlight_name = " LspDiagnosticsUnderline "
2020-01-03 23:35:09 +01:00
vim.cmd ( string.format ( " highlight default %s gui=underline cterm=underline " , underline_highlight_name ) )
for kind , _ in pairs ( protocol.DiagnosticSeverity ) do
2020-01-08 09:46:25 -08:00
if type ( kind ) == ' string ' then
vim.cmd ( string.format ( " highlight default link %s%s %s " , underline_highlight_name , kind , underline_highlight_name ) )
end
2020-01-03 23:35:09 +01:00
end
2019-11-27 19:45:03 +01:00
local severity_highlights = { }
2020-06-18 08:04:49 -04:00
local severity_floating_highlights = { }
2019-11-13 12:55:26 -08:00
local default_severity_highlight = {
[ protocol.DiagnosticSeverity . Error ] = { guifg = " Red " } ;
[ protocol.DiagnosticSeverity . Warning ] = { guifg = " Orange " } ;
[ protocol.DiagnosticSeverity . Information ] = { guifg = " LightBlue " } ;
[ protocol.DiagnosticSeverity . Hint ] = { guifg = " LightGrey " } ;
}
2019-11-27 19:45:03 +01:00
-- Initialize default severity highlights
for severity , hi_info in pairs ( default_severity_highlight ) do
local severity_name = protocol.DiagnosticSeverity [ severity ]
local highlight_name = " LspDiagnostics " .. severity_name
2020-06-18 08:04:49 -04:00
local floating_highlight_name = highlight_name .. " Floating "
2019-11-27 19:45:03 +01:00
-- Try to fill in the foreground color with a sane default.
local cmd_parts = { " highlight " , " default " , highlight_name }
for k , v in pairs ( hi_info ) do
table.insert ( cmd_parts , k .. " = " .. v )
2019-11-13 12:55:26 -08:00
end
2019-11-27 19:45:03 +01:00
api.nvim_command ( table.concat ( cmd_parts , ' ' ) )
2020-04-29 16:53:13 +02:00
api.nvim_command ( ' highlight link ' .. highlight_name .. ' Sign ' .. highlight_name )
2020-06-18 08:04:49 -04:00
api.nvim_command ( ' highlight link ' .. highlight_name .. ' Floating ' .. highlight_name )
2019-11-27 19:45:03 +01:00
severity_highlights [ severity ] = highlight_name
2020-06-18 08:04:49 -04:00
severity_floating_highlights [ severity ] = floating_highlight_name
2019-11-13 12:55:26 -08:00
end
2020-08-19 18:17:08 +02:00
--- Clears diagnostics for a buffer.
---
--@param bufnr (number) buffer id
2019-11-13 12:55:26 -08:00
function M . buf_clear_diagnostics ( bufnr )
validate { bufnr = { bufnr , ' n ' , true } }
2020-02-27 12:12:53 +01:00
bufnr = bufnr == 0 and api.nvim_get_current_buf ( ) or bufnr
-- clear sign group
vim.fn . sign_unplace ( sign_ns , { buffer = bufnr } )
-- clear virtual text namespace
2019-11-13 12:55:26 -08:00
api.nvim_buf_clear_namespace ( bufnr , diagnostic_ns , 0 , - 1 )
end
2020-08-19 18:17:08 +02:00
--- Gets the name of a severity's highlight group.
---
--@param severity A member of `vim.lsp.protocol.DiagnosticSeverity`
--@returns (string) Highlight group name
2019-11-13 12:55:26 -08:00
function M . get_severity_highlight_name ( severity )
return severity_highlights [ severity ]
end
2020-08-19 18:17:08 +02:00
--- Gets list of diagnostics for the current line.
---
--@returns (table) list of `Diagnostic` tables
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
2020-05-16 01:18:59 +02:00
function M . get_line_diagnostics ( )
2019-11-13 12:55:26 -08:00
local bufnr = api.nvim_get_current_buf ( )
2020-05-16 01:18:59 +02:00
local linenr = api.nvim_win_get_cursor ( 0 ) [ 1 ] - 1
local buffer_diagnostics = M.diagnostics_by_buf [ bufnr ]
if not buffer_diagnostics then
return { }
end
local diagnostics_by_line = M.diagnostics_group_by_line ( buffer_diagnostics )
return diagnostics_by_line [ linenr ] or { }
end
2020-08-19 18:17:08 +02:00
--- Displays the diagnostics for the current line in a floating hover
--- window.
2020-05-16 01:18:59 +02:00
function M . show_line_diagnostics ( )
2019-11-13 12:55:26 -08:00
-- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {})
-- if #marks == 0 then
-- return
-- end
local lines = { " Diagnostics: " }
local highlights = { { 0 , " Bold " } }
2020-05-16 01:18:59 +02:00
local line_diagnostics = M.get_line_diagnostics ( )
2020-05-18 21:06:30 -04:00
if vim.tbl_isempty ( line_diagnostics ) then return end
2019-11-13 12:55:26 -08:00
for i , diagnostic in ipairs ( line_diagnostics ) do
-- for i, mark in ipairs(marks) do
-- local mark_id = mark[1]
-- local diagnostic = buffer_diagnostics[mark_id]
-- TODO(ashkan) make format configurable?
local prefix = string.format ( " %d. " , i )
2020-06-18 08:04:49 -04:00
local hiname = severity_floating_highlights [ diagnostic.severity ]
2020-04-26 23:56:30 +02:00
assert ( hiname , ' unknown severity: ' .. tostring ( diagnostic.severity ) )
2019-11-13 12:55:26 -08:00
local message_lines = split_lines ( diagnostic.message )
table.insert ( lines , prefix .. message_lines [ 1 ] )
table.insert ( highlights , { # prefix + 1 , hiname } )
for j = 2 , # message_lines do
table.insert ( lines , message_lines [ j ] )
table.insert ( highlights , { 0 , hiname } )
end
end
local popup_bufnr , winnr = M.open_floating_preview ( lines , ' plaintext ' )
for i , hi in ipairs ( highlights ) do
local prefixlen , hiname = unpack ( hi )
-- Start highlight after the prefix
api.nvim_buf_add_highlight ( popup_bufnr , - 1 , hiname , i - 1 , prefixlen , - 1 )
end
return popup_bufnr , winnr
end
2020-08-19 18:17:08 +02:00
--- Saves diagnostics into vim.lsp.util.diagnostics_by_buf[{bufnr}].
2020-07-02 07:09:17 -04:00
---
2020-08-19 18:17:08 +02:00
--@param bufnr (number) buffer id for which the diagnostics are for
--@param diagnostics list of `Diagnostic`s received from the LSP server
2019-11-13 12:55:26 -08:00
function M . buf_diagnostics_save_positions ( bufnr , diagnostics )
validate {
bufnr = { bufnr , ' n ' , true } ;
diagnostics = { diagnostics , ' t ' , true } ;
}
if not diagnostics then return end
bufnr = bufnr == 0 and api.nvim_get_current_buf ( ) or bufnr
2020-04-25 15:46:58 +02:00
if not M.diagnostics_by_buf [ bufnr ] then
2019-11-13 12:55:26 -08:00
-- Clean up our data when the buffer unloads.
api.nvim_buf_attach ( bufnr , false , {
on_detach = function ( b )
2020-04-25 15:46:58 +02:00
M.diagnostics_by_buf [ b ] = nil
2019-11-13 12:55:26 -08:00
end
} )
end
2020-04-25 15:46:58 +02:00
M.diagnostics_by_buf [ bufnr ] = diagnostics
2019-11-13 12:55:26 -08:00
end
2020-08-19 18:17:08 +02:00
--- Highlights a list of diagnostics in a buffer by underlining them.
---
--@param bufnr (number) buffer id
--@param diagnostics (list of `Diagnostic`s)
2019-11-13 12:55:26 -08:00
function M . buf_diagnostics_underline ( bufnr , diagnostics )
for _ , diagnostic in ipairs ( diagnostics ) do
2020-01-03 23:35:09 +01:00
local start = diagnostic.range [ " start " ]
2019-11-13 12:55:26 -08:00
local finish = diagnostic.range [ " end " ]
2020-01-03 23:35:09 +01:00
local hlmap = {
[ protocol.DiagnosticSeverity . Error ] = ' Error ' ,
[ protocol.DiagnosticSeverity . Warning ] = ' Warning ' ,
[ protocol.DiagnosticSeverity . Information ] = ' Information ' ,
[ protocol.DiagnosticSeverity . Hint ] = ' Hint ' ,
}
2020-05-31 20:56:00 +02:00
highlight.range ( bufnr , diagnostic_ns ,
2020-01-03 23:35:09 +01:00
underline_highlight_name .. hlmap [ diagnostic.severity ] ,
{ start.line , start.character } ,
{ finish.line , finish.character }
2019-11-13 12:55:26 -08:00
)
end
end
2020-08-19 18:17:08 +02:00
--- Removes document highlights from a buffer.
---
--@param bufnr buffer id
2020-02-26 20:10:16 +01:00
function M . buf_clear_references ( bufnr )
validate { bufnr = { bufnr , ' n ' , true } }
api.nvim_buf_clear_namespace ( bufnr , reference_ns , 0 , - 1 )
end
2020-08-19 18:17:08 +02:00
--- Shows a list of document highlights for a certain buffer.
---
--@param bufnr buffer id
--@param references List of `DocumentHighlight` objects to highlight
2020-02-26 20:10:16 +01:00
function M . buf_highlight_references ( bufnr , references )
validate { bufnr = { bufnr , ' n ' , true } }
for _ , reference in ipairs ( references ) do
local start_pos = { reference [ " range " ] [ " start " ] [ " line " ] , reference [ " range " ] [ " start " ] [ " character " ] }
local end_pos = { reference [ " range " ] [ " end " ] [ " line " ] , reference [ " range " ] [ " end " ] [ " character " ] }
local document_highlight_kind = {
[ protocol.DocumentHighlightKind . Text ] = " LspReferenceText " ;
[ protocol.DocumentHighlightKind . Read ] = " LspReferenceRead " ;
[ protocol.DocumentHighlightKind . Write ] = " LspReferenceWrite " ;
}
2020-04-17 00:30:03 +08:00
local kind = reference [ " kind " ] or protocol.DocumentHighlightKind . Text
2020-05-31 20:56:00 +02:00
highlight.range ( bufnr , reference_ns , document_highlight_kind [ kind ] , start_pos , end_pos )
2020-02-26 20:10:16 +01:00
end
end
2020-08-19 18:17:08 +02:00
--- Groups a list of diagnostics by line.
---
--@param diagnostics (table) list of `Diagnostic`s
--@returns (table) dictionary mapping lines to lists of diagnostics valid on
---those lines
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
2020-04-25 15:46:58 +02:00
function M . diagnostics_group_by_line ( diagnostics )
if not diagnostics then return end
local diagnostics_by_line = { }
for _ , diagnostic in ipairs ( diagnostics ) do
local start = diagnostic.range . start
2020-08-19 18:17:08 +02:00
-- TODO: Are diagnostics only valid for a single line? I don't understand
-- why this would be okay otherwise
2020-04-25 15:46:58 +02:00
local line_diagnostics = diagnostics_by_line [ start.line ]
if not line_diagnostics then
line_diagnostics = { }
diagnostics_by_line [ start.line ] = line_diagnostics
end
table.insert ( line_diagnostics , diagnostic )
2019-11-13 12:55:26 -08:00
end
2020-04-25 15:46:58 +02:00
return diagnostics_by_line
end
2020-08-19 18:17:08 +02:00
--- Given a list of diagnostics, sets the corresponding virtual text for a
--- buffer.
---
--@param bufnr buffer id
--@param diagnostics (table) list of `Diagnostic`s
2020-04-25 15:46:58 +02:00
function M . buf_diagnostics_virtual_text ( bufnr , diagnostics )
if not diagnostics then
2019-11-13 12:55:26 -08:00
return
end
2020-04-25 15:46:58 +02:00
local buffer_line_diagnostics = M.diagnostics_group_by_line ( diagnostics )
2019-11-13 12:55:26 -08:00
for line , line_diags in pairs ( buffer_line_diagnostics ) do
local virt_texts = { }
for i = 1 , # line_diags - 1 do
table.insert ( virt_texts , { " ■ " , severity_highlights [ line_diags [ i ] . severity ] } )
end
local last = line_diags [ # line_diags ]
-- TODO(ashkan) use first line instead of subbing 2 spaces?
table.insert ( virt_texts , { " ■ " .. last.message : gsub ( " \r " , " " ) : gsub ( " \n " , " " ) , severity_highlights [ last.severity ] } )
api.nvim_buf_set_virtual_text ( bufnr , diagnostic_ns , line , virt_texts , { } )
end
end
2020-04-26 18:36:40 -04:00
2020-07-02 07:09:17 -04:00
--- Returns the number of diagnostics of given kind for current buffer.
---
--- Useful for showing diagnostic counts in statusline. eg:
---
--- <pre>
--- function! LspStatus() abort
--- let sl = ''
--- if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))')
--- let sl.='%#MyStatuslineLSP#E:'
--- let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Error]])")}'
--- let sl.='%#MyStatuslineLSP# W:'
--- let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Warning]])")}'
--- else
--- let sl.='%#MyStatuslineLSPErrors#off'
--- endif
--- return sl
--- endfunction
--- let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus()
--- </pre>
---
--@param kind Diagnostic severity kind: See |vim.lsp.protocol.DiagnosticSeverity|
2020-08-19 18:17:08 +02:00
--@returns Count of diagnostics
2020-02-26 20:22:14 +01:00
function M . buf_diagnostics_count ( kind )
local bufnr = vim.api . nvim_get_current_buf ( )
2020-04-25 15:46:58 +02:00
local diagnostics = M.diagnostics_by_buf [ bufnr ]
if not diagnostics then return end
2020-02-26 20:22:14 +01:00
local count = 0
2020-04-25 15:46:58 +02:00
for _ , diagnostic in pairs ( diagnostics ) do
if protocol.DiagnosticSeverity [ kind ] == diagnostic.severity then
count = count + 1
2020-02-26 20:22:14 +01:00
end
end
return count
end
2020-02-27 12:12:53 +01:00
2020-04-26 18:36:40 -04:00
local diagnostic_severity_map = {
[ protocol.DiagnosticSeverity . Error ] = " LspDiagnosticsErrorSign " ;
[ protocol.DiagnosticSeverity . Warning ] = " LspDiagnosticsWarningSign " ;
[ protocol.DiagnosticSeverity . Information ] = " LspDiagnosticsInformationSign " ;
[ protocol.DiagnosticSeverity . Hint ] = " LspDiagnosticsHintSign " ;
}
2020-08-19 18:17:08 +02:00
--- Places signs for each diagnostic in the sign column.
2020-07-02 07:09:17 -04:00
---
--- Sign characters can be customized with the following commands:
---
--- <pre>
--- sign define LspDiagnosticsErrorSign text=E texthl=LspDiagnosticsError linehl= numhl=
--- sign define LspDiagnosticsWarningSign text=W texthl=LspDiagnosticsWarning linehl= numhl=
--- sign define LspDiagnosticsInformationSign text=I texthl=LspDiagnosticsInformation linehl= numhl=
--- sign define LspDiagnosticsHintSign text=H texthl=LspDiagnosticsHint linehl= numhl=
--- </pre>
2020-04-26 18:36:40 -04:00
function M . buf_diagnostics_signs ( bufnr , diagnostics )
2020-02-27 12:12:53 +01:00
for _ , diagnostic in ipairs ( diagnostics ) do
vim.fn . sign_place ( 0 , sign_ns , diagnostic_severity_map [ diagnostic.severity ] , bufnr , { lnum = ( diagnostic.range . start.line + 1 ) } )
end
end
2019-11-13 12:55:26 -08:00
end
2019-11-24 03:01:18 -08:00
local position_sort = sort_by_key ( function ( v )
2020-01-28 10:45:25 +01:00
return { v.start . line , v.start . character }
2019-11-24 03:01:18 -08:00
end )
2019-11-13 12:55:26 -08:00
2020-08-19 18:17:08 +02:00
--- Returns the items with the byte position calculated correctly and in sorted
--- order, for display in quickfix and location lists.
---
--@param locations (table) list of `Location`s or `LocationLink`s
--@returns (table) list of items
2019-11-24 03:01:18 -08:00
function M . locations_to_items ( locations )
2019-11-13 12:55:26 -08:00
local items = { }
2019-11-24 03:01:18 -08:00
local grouped = setmetatable ( { } , {
__index = function ( t , k )
local v = { }
rawset ( t , k , v )
return v
end ;
} )
2019-11-13 12:55:26 -08:00
for _ , d in ipairs ( locations ) do
2020-05-02 15:21:07 +02:00
-- locations may be Location or LocationLink
local uri = d.uri or d.targetUri
local range = d.range or d.targetSelectionRange
2020-06-30 17:48:04 +02:00
table.insert ( grouped [ uri ] , { start = range.start } )
2019-11-24 03:01:18 -08:00
end
2020-01-28 10:45:25 +01:00
2019-11-24 03:01:18 -08:00
local keys = vim.tbl_keys ( grouped )
table.sort ( keys )
-- TODO(ashkan) I wish we could do this lazily.
2020-06-30 17:48:04 +02:00
for _ , uri in ipairs ( keys ) do
local rows = grouped [ uri ]
2019-11-24 03:01:18 -08:00
table.sort ( rows , position_sort )
2020-06-30 17:48:04 +02:00
local bufnr = vim.uri_to_bufnr ( uri )
vim.fn . bufload ( bufnr )
local filename = vim.uri_to_fname ( uri )
for _ , temp in ipairs ( rows ) do
local pos = temp.start
local row = pos.line
local line = ( api.nvim_buf_get_lines ( bufnr , row , row + 1 , false ) or { " " } ) [ 1 ]
local col = M.character_offset ( bufnr , row , pos.character )
table.insert ( items , {
filename = filename ,
lnum = row + 1 ,
col = col + 1 ;
text = line ;
} )
2019-11-24 03:01:18 -08:00
end
2019-11-13 12:55:26 -08:00
end
2019-11-24 03:01:18 -08:00
return items
end
2020-08-19 18:17:08 +02:00
--- Fills current window's location list with given list of items.
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
---
--@param items (table) list of items
2020-02-22 21:20:38 +09:00
function M . set_loclist ( items )
2019-11-24 03:01:18 -08:00
vim.fn . setloclist ( 0 , { } , ' ' , {
title = ' Language Server ' ;
2020-02-22 21:20:38 +09:00
items = items ;
2019-11-24 03:01:18 -08:00
} )
end
2020-08-19 18:17:08 +02:00
--- Fills quickfix list with given list of items.
--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
---
--@param items (table) list of items
2020-02-22 21:20:38 +09:00
function M . set_qflist ( items )
2019-11-24 03:01:18 -08:00
vim.fn . setqflist ( { } , ' ' , {
title = ' Language Server ' ;
2020-02-22 21:20:38 +09:00
items = items ;
2019-11-24 03:01:18 -08:00
} )
2019-11-13 12:55:26 -08:00
end
2020-05-08 05:23:25 +09:00
-- Acording to LSP spec, if the client set "symbolKind.valueSet",
-- the client must handle it properly even if it receives a value outside the specification.
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
function M . _get_symbol_kind_name ( symbol_kind )
return protocol.SymbolKind [ symbol_kind ] or " Unknown "
end
2020-08-19 18:17:08 +02:00
--- Converts symbols to quickfix list items.
2020-02-22 21:20:38 +09:00
---
2020-07-02 07:09:17 -04:00
--@param symbols DocumentSymbol[] or SymbolInformation[]
2020-02-22 21:20:38 +09:00
function M . symbols_to_items ( symbols , bufnr )
2020-08-19 18:17:08 +02:00
--@private
2020-02-22 21:20:38 +09:00
local function _symbols_to_items ( _symbols , _items , _bufnr )
for _ , symbol in ipairs ( _symbols ) do
if symbol.location then -- SymbolInformation type
local range = symbol.location . range
2020-05-08 05:23:25 +09:00
local kind = M._get_symbol_kind_name ( symbol.kind )
2020-02-22 21:20:38 +09:00
table.insert ( _items , {
filename = vim.uri_to_fname ( symbol.location . uri ) ,
lnum = range.start . line + 1 ,
col = range.start . character + 1 ,
kind = kind ,
text = ' [ ' .. kind .. ' ] ' .. symbol.name ,
} )
elseif symbol.range then -- DocumentSymbole type
2020-05-08 05:23:25 +09:00
local kind = M._get_symbol_kind_name ( symbol.kind )
2020-02-22 21:20:38 +09:00
table.insert ( _items , {
-- bufnr = _bufnr,
filename = vim.api . nvim_buf_get_name ( _bufnr ) ,
lnum = symbol.range . start.line + 1 ,
col = symbol.range . start.character + 1 ,
kind = kind ,
text = ' [ ' .. kind .. ' ] ' .. symbol.name
} )
if symbol.children then
2020-05-02 15:08:52 +09:00
for _ , v in ipairs ( _symbols_to_items ( symbol.children , _items , _bufnr ) ) do
vim.list_extend ( _items , v )
2020-02-22 21:20:38 +09:00
end
end
end
end
return _items
end
return _symbols_to_items ( symbols , { } , bufnr )
end
2020-08-19 18:17:08 +02:00
--- Removes empty lines from the beginning and end.
--@param lines (table) list of lines to trim
--@returns (table) trimmed list of lines
2019-11-20 15:35:18 -08:00
function M . trim_empty_lines ( lines )
local start = 1
for i = 1 , # lines do
if # lines [ i ] > 0 then
start = i
break
end
end
local finish = 1
for i = # lines , 1 , - 1 do
if # lines [ i ] > 0 then
finish = i
break
end
end
2019-11-20 17:09:21 -08:00
return vim.list_extend ( { } , lines , start , finish )
2019-11-20 15:35:18 -08:00
end
2020-08-19 18:17:08 +02:00
--- Accepts markdown lines and tries to reduce them to a filetype if they
--- comprise just a single code block.
---
--- CAUTION: Modifies the input in-place!
---
--@param lines (table) list of lines
--@returns (string) filetype or 'markdown' if it was unchanged.
2019-11-20 15:35:18 -08:00
function M . try_trim_markdown_code_blocks ( lines )
local language_id = lines [ 1 ] : match ( " ^```(.*) " )
if language_id then
local has_inner_code_fence = false
for i = 2 , ( # lines - 1 ) do
local line = lines [ i ]
if line : sub ( 1 , 3 ) == ' ``` ' then
has_inner_code_fence = true
break
end
end
-- No inner code fences + starting with code fence = hooray.
if not has_inner_code_fence then
table.remove ( lines , 1 )
table.remove ( lines )
return language_id
end
end
return ' markdown '
end
2019-11-21 23:58:32 -08:00
local str_utfindex = vim.str_utfindex
2020-08-19 18:17:08 +02:00
--@private
2020-05-16 01:18:59 +02:00
local function make_position_param ( )
2019-11-21 15:41:32 -08:00
local row , col = unpack ( api.nvim_win_get_cursor ( 0 ) )
row = row - 1
local line = api.nvim_buf_get_lines ( 0 , row , row + 1 , true ) [ 1 ]
2020-09-02 06:45:47 +03:00
if not line then
return { line = 0 ; character = 0 ; }
end
2019-11-21 23:58:32 -08:00
col = str_utfindex ( line , col )
2020-05-16 01:18:59 +02:00
return { line = row ; character = col ; }
end
2020-08-19 18:17:08 +02:00
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
--@returns `TextDocumentPositionParams` object
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
2020-05-16 01:18:59 +02:00
function M . make_position_params ( )
2019-11-21 15:41:32 -08:00
return {
2020-02-22 21:20:38 +09:00
textDocument = M.make_text_document_params ( ) ;
2020-05-16 01:18:59 +02:00
position = make_position_param ( )
}
end
2020-08-19 18:17:08 +02:00
--- Using the current position in the current buffer, creates an object that
--- can be used as a building block for several LSP requests, such as
--- `textDocument/codeAction`, `textDocument/colorPresentation`,
--- `textDocument/rangeFormatting`.
---
--@returns { textDocument = { uri = `current_file_uri` }, range = { start =
---`current_position`, end = `current_position` } }
2020-05-16 01:18:59 +02:00
function M . make_range_params ( )
local position = make_position_param ( )
return {
2020-09-25 04:53:08 +09:00
textDocument = M.make_text_document_params ( ) ,
2020-05-16 01:18:59 +02:00
range = { start = position ; [ " end " ] = position ; }
2019-11-21 15:41:32 -08:00
}
end
2020-09-25 04:53:08 +09:00
--- Using the given range in the current buffer, creates an object that
--- is similar to |vim.lsp.util.make_range_params()|.
---
--@param start_pos ({number, number}, optional) mark-indexed position.
---Defaults to the start of the last visual selection.
--@param end_pos ({number, number}, optional) mark-indexed position.
---Defaults to the end of the last visual selection.
--@returns { textDocument = { uri = `current_file_uri` }, range = { start =
---`start_position`, end = `end_position` } }
function M . make_given_range_params ( start_pos , end_pos )
validate {
start_pos = { start_pos , ' t ' , true } ;
end_pos = { end_pos , ' t ' , true } ;
}
local A = list_extend ( { } , start_pos or api.nvim_buf_get_mark ( 0 , ' < ' ) )
local B = list_extend ( { } , end_pos or api.nvim_buf_get_mark ( 0 , ' > ' ) )
-- convert to 0-index
A [ 1 ] = A [ 1 ] - 1
B [ 1 ] = B [ 1 ] - 1
-- account for encoding.
if A [ 2 ] > 0 then
A = { A [ 1 ] , M.character_offset ( 0 , A [ 1 ] , A [ 2 ] ) }
end
if B [ 2 ] > 0 then
B = { B [ 1 ] , M.character_offset ( 0 , B [ 1 ] , B [ 2 ] ) }
end
return {
textDocument = M.make_text_document_params ( ) ,
range = {
start = { line = A [ 1 ] , character = A [ 2 ] } ,
[ ' end ' ] = { line = B [ 1 ] , character = B [ 2 ] }
}
}
end
2020-08-19 18:17:08 +02:00
--- Creates a `TextDocumentIdentifier` object for the current buffer.
---
--@returns `TextDocumentIdentifier`
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
2020-02-22 21:20:38 +09:00
function M . make_text_document_params ( )
return { uri = vim.uri_from_bufnr ( 0 ) }
end
2020-08-19 18:17:08 +02:00
--- Returns visual width of tabstop.
2020-05-05 17:23:45 +02:00
---
--@see |softtabstop|
--@param bufnr (optional, number): Buffer handle, defaults to current
--@returns (number) tabstop visual width
function M . get_effective_tabstop ( bufnr )
validate { bufnr = { bufnr , ' n ' , true } }
local bo = bufnr and vim.bo [ bufnr ] or vim.bo
local sts = bo.softtabstop
return ( sts > 0 and sts ) or ( sts < 0 and bo.shiftwidth ) or bo.tabstop
end
2020-08-19 18:17:08 +02:00
--- Creates a `FormattingOptions` object for the current buffer and cursor position.
---
--@param options Table with valid `FormattingOptions` entries
--@returns `FormattingOptions object
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
2020-05-05 17:23:45 +02:00
function M . make_formatting_params ( options )
validate { options = { options , ' t ' , true } }
options = vim.tbl_extend ( ' keep ' , options or { } , {
tabSize = M.get_effective_tabstop ( ) ;
insertSpaces = vim.bo . expandtab ;
} )
return {
textDocument = { uri = vim.uri_from_bufnr ( 0 ) } ;
options = options ;
}
end
2020-08-19 18:17:08 +02:00
--- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer.
---
--@param buf buffer id (0 for current)
--@param row 0-indexed line
--@param col 0-indexed byte offset in line
--@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf}
2019-11-21 16:23:12 -08:00
function M . character_offset ( buf , row , col )
local line = api.nvim_buf_get_lines ( buf , row , row + 1 , true ) [ 1 ]
2019-11-21 23:58:32 -08:00
-- If the col is past the EOL, use the line length.
2019-11-22 00:31:10 -08:00
if col > # line then
return str_utfindex ( line )
end
return str_utfindex ( line , col )
2019-11-21 16:23:12 -08:00
end
2019-11-20 15:35:18 -08:00
2020-01-24 12:31:52 +01:00
M.buf_versions = { }
2019-11-13 12:55:26 -08:00
return M
-- vim:sw=2 ts=2 et