aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/lsp')
-rw-r--r--runtime/lua/vim/lsp/buf.lua135
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua16
-rw-r--r--runtime/lua/vim/lsp/handlers.lua70
-rw-r--r--runtime/lua/vim/lsp/log.lua2
-rw-r--r--runtime/lua/vim/lsp/rpc.lua11
-rw-r--r--runtime/lua/vim/lsp/util.lua143
6 files changed, 287 insertions, 90 deletions
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 31116985e2..5dd7109bb0 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -111,6 +111,39 @@ function M.completion(context)
return request('textDocument/completion', params)
end
+--@private
+--- If there is more than one client that supports the given method,
+--- asks the user to select one.
+--
+--@returns The client that the user selected or nil
+local function select_client(method)
+ local clients = vim.tbl_values(vim.lsp.buf_get_clients());
+ clients = vim.tbl_filter(function (client)
+ return client.supports_method(method)
+ end, clients)
+ -- better UX when choices are always in the same order (between restarts)
+ table.sort(clients, function (a, b) return a.name < b.name end)
+
+ if #clients > 1 then
+ local choices = {}
+ for k,v in ipairs(clients) do
+ table.insert(choices, string.format("%d %s", k, v.name))
+ end
+ local user_choice = vim.fn.confirm(
+ "Select a language server:",
+ table.concat(choices, "\n"),
+ 0,
+ "Question"
+ )
+ if user_choice == 0 then return nil end
+ return clients[user_choice]
+ elseif #clients < 1 then
+ return nil
+ else
+ return clients[1]
+ end
+end
+
--- Formats the current buffer.
---
--@param options (optional, table) Can be used to specify FormattingOptions.
@@ -119,8 +152,11 @@ end
--
--@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting
function M.formatting(options)
+ local client = select_client("textDocument/formatting")
+ if client == nil then return end
+
local params = util.make_formatting_params(options)
- return request('textDocument/formatting', params)
+ return client.request("textDocument/formatting", params)
end
--- Performs |vim.lsp.buf.formatting()| synchronously.
@@ -134,14 +170,62 @@ end
---
--@param options Table with valid `FormattingOptions` entries
--@param timeout_ms (number) Request timeout
+--@see |vim.lsp.buf.formatting_seq_sync|
function M.formatting_sync(options, timeout_ms)
+ local client = select_client("textDocument/formatting")
+ if client == nil then return end
+
local params = util.make_formatting_params(options)
- local result = vim.lsp.buf_request_sync(0, "textDocument/formatting", params, timeout_ms)
- if not result or vim.tbl_isempty(result) then return end
- local _, formatting_result = next(result)
- result = formatting_result.result
- if not result then return end
- vim.lsp.util.apply_text_edits(result)
+ local result, err = client.request_sync("textDocument/formatting", params, timeout_ms)
+ if result and result.result then
+ util.apply_text_edits(result.result)
+ elseif err then
+ vim.notify("vim.lsp.buf.formatting_sync: " .. err, vim.log.levels.WARN)
+ end
+end
+
+--- Formats the current buffer by sequentially requesting formatting from attached clients.
+---
+--- Useful when multiple clients with formatting capability are attached.
+---
+--- Since it's synchronous, can be used for running on save, to make sure buffer is formatted
+--- prior to being saved. {timeout_ms} is passed on to the |vim.lsp.client| `request_sync` method.
+--- Example:
+--- <pre>
+--- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_seq_sync()]]
+--- </pre>
+---
+--@param options (optional, table) `FormattingOptions` entries
+--@param timeout_ms (optional, number) Request timeout
+--@param order (optional, table) List of client names. Formatting is requested from clients
+---in the following order: first all clients that are not in the `order` list, then
+---the remaining clients in the order as they occur in the `order` list.
+function M.formatting_seq_sync(options, timeout_ms, order)
+ local clients = vim.tbl_values(vim.lsp.buf_get_clients());
+
+ -- sort the clients according to `order`
+ for _, client_name in ipairs(order or {}) do
+ -- if the client exists, move to the end of the list
+ for i, client in ipairs(clients) do
+ if client.name == client_name then
+ table.insert(clients, table.remove(clients, i))
+ break
+ end
+ end
+ end
+
+ -- loop through the clients and make synchronous formatting requests
+ for _, client in ipairs(clients) do
+ if client.resolved_capabilities.document_formatting then
+ local params = util.make_formatting_params(options)
+ local result, err = client.request_sync("textDocument/formatting", params, timeout_ms)
+ if result and result.result then
+ util.apply_text_edits(result.result)
+ elseif err then
+ vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN)
+ end
+ end
+ end
end
--- Formats a given range.
@@ -152,15 +236,12 @@ end
--@param end_pos ({number, number}, optional) mark-indexed position.
---Defaults to the end of the last visual selection.
function M.range_formatting(options, start_pos, end_pos)
- validate { options = {options, 't', true} }
- local sts = vim.bo.softtabstop;
- options = vim.tbl_extend('keep', options or {}, {
- tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop;
- insertSpaces = vim.bo.expandtab;
- })
+ local client = select_client("textDocument/rangeFormatting")
+ if client == nil then return end
+
local params = util.make_given_range_params(start_pos, end_pos)
- params.options = options
- return request('textDocument/rangeFormatting', params)
+ params.options = util.make_formatting_params(options).options
+ return client.request("textDocument/rangeFormatting", params)
end
--- Renames all references to the symbol under the cursor.
@@ -216,26 +297,30 @@ local function pick_call_hierarchy_item(call_hierarchy_items)
return choice
end
+local function call_hierarchy(method)
+ local params = util.make_position_params()
+ request('textDocument/prepareCallHierarchy', params, function(err, _, result)
+ if err then
+ vim.notify(err.message, vim.log.levels.WARN)
+ return
+ end
+ local call_hierarchy_item = pick_call_hierarchy_item(result)
+ vim.lsp.buf_request(0, method, { item = call_hierarchy_item })
+ end)
+end
+
--- Lists all the call sites of the symbol under the cursor in the
--- |quickfix| window. If the symbol can resolve to multiple
--- items, the user can pick one in the |inputlist|.
function M.incoming_calls()
- local params = util.make_position_params()
- request('textDocument/prepareCallHierarchy', params, function(_, _, result)
- local call_hierarchy_item = pick_call_hierarchy_item(result)
- vim.lsp.buf_request(0, 'callHierarchy/incomingCalls', { item = call_hierarchy_item })
- end)
+ call_hierarchy('callHierarchy/incomingCalls')
end
--- Lists all the items that are called by the symbol under the
--- cursor in the |quickfix| window. If the symbol can resolve to
--- multiple items, the user can pick one in the |inputlist|.
function M.outgoing_calls()
- local params = util.make_position_params()
- request('textDocument/prepareCallHierarchy', params, function(_, _, result)
- local call_hierarchy_item = pick_call_hierarchy_item(result)
- vim.lsp.buf_request(0, 'callHierarchy/outgoingCalls', { item = call_hierarchy_item })
- end)
+ call_hierarchy('callHierarchy/outgoingCalls')
end
--- List workspace folders.
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 4e82c46fef..6f2f846a3b 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -406,9 +406,7 @@ function M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
line_diagnostics = filter_by_severity_limit(opts.severity_limit, line_diagnostics)
end
- if opts.severity_sort then
- table.sort(line_diagnostics, function(a, b) return a.severity < b.severity end)
- end
+ table.sort(line_diagnostics, function(a, b) return a.severity < b.severity end)
return line_diagnostics
end
@@ -997,6 +995,8 @@ end
--- - See |vim.lsp.diagnostic.set_signs()|
--- - update_in_insert: (default=false)
--- - Update diagnostics in InsertMode or wait until InsertLeave
+--- - severity_sort: (default=false)
+--- - Sort diagnostics (and thus signs and virtual text)
function M.on_publish_diagnostics(_, _, params, client_id, _, config)
local uri = params.uri
local bufnr = vim.uri_to_bufnr(uri)
@@ -1007,6 +1007,10 @@ function M.on_publish_diagnostics(_, _, params, client_id, _, config)
local diagnostics = params.diagnostics
+ if config and if_nil(config.severity_sort, false) then
+ table.sort(diagnostics, function(a, b) return a.severity > b.severity end)
+ end
+
-- Always save the diagnostics, even if the buf is not loaded.
-- Language servers may report compile or build errors via diagnostics
-- Users should be able to find these, even if they're in files which
@@ -1034,6 +1038,7 @@ function M.display(diagnostics, bufnr, client_id, config)
underline = true,
virtual_text = true,
update_in_insert = false,
+ severity_sort = false,
}, config)
-- TODO(tjdevries): Consider how we can make this a "standardized" kind of thing for |lsp-handlers|.
@@ -1116,7 +1121,6 @@ end
---@return table {popup_bufnr, win_id}
function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
opts = opts or {}
- opts.severity_sort = if_nil(opts.severity_sort, true)
local show_header = if_nil(opts.show_header, true)
@@ -1140,14 +1144,14 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
local message_lines = vim.split(diagnostic.message, '\n', true)
table.insert(lines, prefix..message_lines[1])
- table.insert(highlights, {#prefix + 1, hiname})
+ table.insert(highlights, {#prefix, hiname})
for j = 2, #message_lines do
table.insert(lines, message_lines[j])
table.insert(highlights, {0, hiname})
end
end
- local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext')
+ local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext', opts)
for i, hi in ipairs(highlights) do
local prefixlen, hiname = unpack(hi)
-- Start highlight after the prefix
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index eacbd90077..18155ceb7e 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -245,9 +245,22 @@ M['textDocument/completion'] = function(_, _, result)
vim.fn.complete(textMatch+1, matches)
end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
-M['textDocument/hover'] = function(_, method, result)
- util.focusable_float(method, function()
+--- |lsp-handler| for the method "textDocument/hover"
+--- <pre>
+--- vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(
+--- vim.lsp.handlers.hover, {
+--- -- Use a sharp border with `FloatBorder` highlights
+--- border = "single"
+--- }
+--- )
+--- </pre>
+---@param config table Configuration table.
+--- - border: (default=nil)
+--- - Add borders to the floating window
+--- - See |vim.api.nvim_open_win()|
+function M.hover(_, method, result, _, _, config)
+ config = config or {}
+ local bufnr, winnr = util.focusable_float(method, function()
if not (result and result.contents) then
-- return { 'No information available' }
return
@@ -258,14 +271,16 @@ M['textDocument/hover'] = function(_, method, result)
-- return { 'No information available' }
return
end
- local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, {
- pad_left = 1; pad_right = 1;
- })
+ local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, config)
util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, winnr)
return bufnr, winnr
end)
+ return bufnr, winnr
end
+--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
+M['textDocument/hover'] = M.hover
+
--@private
--- Jumps to a location. Used as a handler for multiple LSP methods.
--@param _ (not used)
@@ -303,27 +318,46 @@ M['textDocument/typeDefinition'] = location_handler
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation
M['textDocument/implementation'] = location_handler
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
-M['textDocument/signatureHelp'] = function(_, method, result, _, bufnr)
+--- |lsp-handler| for the method "textDocument/signatureHelp"
+--- <pre>
+--- vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(
+--- vim.lsp.handlers.signature_help, {
+--- -- Use a sharp border with `FloatBorder` highlights
+--- border = "single"
+--- }
+--- )
+--- </pre>
+---@param config table Configuration table.
+--- - border: (default=nil)
+--- - Add borders to the floating window
+--- - See |vim.api.nvim_open_win()|
+function M.signature_help(_, method, result, _, bufnr, config)
+ config = config or {}
-- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler
-- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore
if not (result and result.signatures and result.signatures[1]) then
print('No signature help available')
return
end
- local lines = util.convert_signature_help_to_markdown_lines(result)
- lines = util.trim_empty_lines(lines)
- if vim.tbl_isempty(lines) then
- print('No signature help available')
- return
- end
- local syntax = api.nvim_buf_get_option(bufnr, 'syntax')
- local p_bufnr, _ = util.focusable_preview(method, function()
- return lines, util.try_trim_markdown_code_blocks(lines)
+ local p_bufnr, winnr = util.focusable_float(method, function()
+ local ft = api.nvim_buf_get_option(bufnr, 'filetype')
+ local lines = util.convert_signature_help_to_markdown_lines(result, ft)
+ lines = util.trim_empty_lines(lines)
+ if vim.tbl_isempty(lines) then
+ print('No signature help available')
+ return
+ end
+ local p_bufnr, p_winnr = util.fancy_floating_markdown(lines, config)
+ util.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre"}, p_winnr)
+
+ return p_bufnr, p_winnr
end)
- api.nvim_buf_set_option(p_bufnr, 'syntax', syntax)
+ return p_bufnr, winnr
end
+--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
+M['textDocument/signatureHelp'] = M.signature_help
+
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
M['textDocument/documentHighlight'] = function(_, _, result, _, bufnr, _)
if not result then return end
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
index 331e980e67..471a311c16 100644
--- a/runtime/lua/vim/lsp/log.lua
+++ b/runtime/lua/vim/lsp/log.lua
@@ -10,7 +10,7 @@ local log = {}
-- Can be used to lookup the number from the name or the name from the number.
-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
-- Level numbers begin with 'trace' at 0
-log.levels = vim.log.levels
+log.levels = vim.deepcopy(vim.log.levels)
-- Default log level is warn.
local current_log_level = log.levels.WARN
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 4e2dd7c8e8..0cabd1a0d4 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -315,8 +315,10 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
dispatchers = { dispatchers, 't', true };
}
- if not (vim.fn.executable(cmd) == 1) then
- error(string.format("The given command %q is not executable.", cmd))
+ if extra_spawn_params and extra_spawn_params.cwd then
+ assert(is_dir(extra_spawn_params.cwd), "cwd must be a directory")
+ elseif not (vim.fn.executable(cmd) == 1) then
+ error(string.format("The given command %q is not executable.", cmd))
end
if dispatchers then
local user_dispatchers = dispatchers
@@ -370,9 +372,6 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
}
if extra_spawn_params then
spawn_params.cwd = extra_spawn_params.cwd
- if spawn_params.cwd then
- assert(is_dir(spawn_params.cwd), "cwd must be a directory")
- end
spawn_params.env = env_merge(extra_spawn_params.env)
end
handle, pid = uv.spawn(cmd, spawn_params, onexit)
@@ -519,7 +518,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
send_response(decoded.id, err, result)
end)
-- This works because we are expecting vim.NIL here
- elseif decoded.id and (decoded.result or decoded.error) then
+ elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then
-- Server Result
decoded.error = convert_NIL(decoded.error)
decoded.result = convert_NIL(decoded.result)
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 8627e0f3fb..01fa22999a 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -18,6 +18,40 @@ end
local M = {}
+local default_border = {
+ {"", "NormalFloat"},
+ {"", "NormalFloat"},
+ {"", "NormalFloat"},
+ {" ", "NormalFloat"},
+ {"", "NormalFloat"},
+ {"", "NormalFloat"},
+ {"", "NormalFloat"},
+ {" ", "NormalFloat"},
+}
+
+--@private
+-- Check the border given by opts or the default border for the additional
+-- size it adds to a float.
+--@returns size of border in height and width
+local function get_border_size(opts)
+ local border = opts and opts.border or default_border
+ local height = 0
+ local width = 0
+
+ if type(border) == 'string' then
+ -- 'single', 'double', etc.
+ height = 2
+ width = 2
+ else
+ height = height + vim.fn.strdisplaywidth(border[2][1]) -- top
+ height = height + vim.fn.strdisplaywidth(border[6][1]) -- bottom
+ width = width + vim.fn.strdisplaywidth(border[4][1]) -- right
+ width = width + vim.fn.strdisplaywidth(border[8][1]) -- left
+ end
+
+ return { height = height, width = width }
+end
+
--@private
local function split_lines(value)
return split(value, '\n', true)
@@ -39,7 +73,7 @@ function M.set_lines(lines, A, B, new_lines)
-- 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)
- if not (i_0 >= 1 and i_0 <= #lines and i_n >= 1 and i_n <= #lines) then
+ if not (i_0 >= 1 and i_0 <= #lines + 1 and i_n >= 1 and i_n <= #lines) then
error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines})
end
local prefix = ""
@@ -436,6 +470,7 @@ function M.apply_text_document_edit(text_document_edit, index)
-- `VersionedTextDocumentIdentifier`s version may be null
-- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier
if should_check_version and (text_document.version
+ and text_document.version > 0
and M.buf_versions[bufnr]
and M.buf_versions[bufnr] > text_document.version) then
print("Buffer ", text_document.uri, " newer than edits.")
@@ -531,13 +566,15 @@ end
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
local function get_completion_word(item)
if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= "" then
- if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
+ local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
+ if insert_text_format == "PlainText" or insert_text_format == nil then
return item.textEdit.newText
else
return M.parse_snippet(item.textEdit.newText)
end
elseif item.insertText ~= nil and item.insertText ~= "" then
- if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
+ local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
+ if insert_text_format == "PlainText" or insert_text_format == nil then
return item.insertText
else
return M.parse_snippet(item.insertText)
@@ -767,9 +804,10 @@ end
--- Converts `textDocument/SignatureHelp` response to markdown lines.
---
--@param signature_help Response of `textDocument/SignatureHelp`
+--@param ft optional filetype that will be use as the `lang` for the label markdown code block
--@returns list of lines of converted markdown.
--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
-function M.convert_signature_help_to_markdown_lines(signature_help)
+function M.convert_signature_help_to_markdown_lines(signature_help, ft)
if not signature_help.signatures then
return
end
@@ -787,7 +825,12 @@ function M.convert_signature_help_to_markdown_lines(signature_help)
if not signature then
return
end
- vim.list_extend(contents, vim.split(signature.label, '\n', true))
+ local label = signature.label
+ if ft then
+ -- wrap inside a code block so fancy_markdown can render it properly
+ label = ("```%s\n%s\n```"):format(ft, label)
+ end
+ vim.list_extend(contents, vim.split(label, '\n', true))
if signature.documentation then
M.convert_input_to_markdown_lines(signature.documentation, contents)
end
@@ -856,7 +899,7 @@ function M.make_floating_popup_options(width, height, opts)
else
anchor = anchor..'S'
height = math.min(lines_above, height)
- row = 0
+ row = -get_border_size(opts).height
end
if vim.fn.wincol() + width <= api.nvim_get_option('columns') then
@@ -875,6 +918,7 @@ function M.make_floating_popup_options(width, height, opts)
row = row + (opts.offset_y or 0),
style = 'minimal',
width = width,
+ border = opts.border or default_border,
}
end
@@ -913,7 +957,7 @@ end
---
--@param location a single `Location` or `LocationLink`
--@returns (bufnr,winnr) buffer and window number of floating window or nil
-function M.preview_location(location)
+function M.preview_location(location, opts)
-- location may be LocationLink or Location (more useful for the former)
local uri = location.targetUri or location.uri
if uri == nil then return end
@@ -924,7 +968,13 @@ function M.preview_location(location)
local range = location.targetRange or location.range
local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false)
local syntax = api.nvim_buf_get_option(bufnr, 'syntax')
- return M.open_floating_preview(contents, syntax)
+ if syntax == "" then
+ -- When no syntax is set, we use filetype as fallback. This might not result
+ -- in a valid syntax definition. See also ft detection in fancy_floating_win.
+ -- An empty syntax is more common now with TreeSitter, since TS disables syntax.
+ syntax = api.nvim_buf_get_option(bufnr, 'filetype')
+ end
+ return M.open_floating_preview(contents, syntax, opts)
end
--@private
@@ -981,27 +1031,20 @@ function M.focusable_preview(unique_name, fn)
end)
end
---- Trims empty lines from input and pad left and right with spaces
+--- Trims empty lines from input and pad top and bottom with empty lines
---
---@param contents table of lines to trim and pad
---@param opts dictionary with optional fields
---- - 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)
---@return contents table of trimmed and padded lines
-function M._trim_and_pad(contents, opts)
+function M._trim(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
if opts.pad_top then
for _ = 1, opts.pad_top do
table.insert(contents, 1, "")
@@ -1055,12 +1098,16 @@ function M.fancy_floating_markdown(contents, opts)
local ft = line:match("^```([a-zA-Z0-9_]*)$")
-- local ft = line:match("^```(.*)$")
-- TODO(ashkan): validate the filetype here.
+ local is_pre = line:match("^%s*<pre>%s*$")
+ if is_pre then
+ ft = ""
+ end
if ft then
local start = #stripped
i = i + 1
while i <= #contents do
line = contents[i]
- if line == "```" then
+ if line == "```" or (is_pre and line:match("^%s*</pre>%s*$")) then
i = i + 1
break
end
@@ -1078,8 +1125,8 @@ function M.fancy_floating_markdown(contents, opts)
end
end
end
- -- Clean up and add padding
- stripped = M._trim_and_pad(stripped, opts)
+ -- Clean up
+ stripped = M._trim(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))
@@ -1089,12 +1136,19 @@ function M.fancy_floating_markdown(contents, opts)
local insert_separator = opts.separator
if insert_separator == nil then insert_separator = true end
if insert_separator then
- for i, h in ipairs(highlights) do
- h.start = h.start + i - 1
- h.finish = h.finish + i - 1
+ local offset = 0
+ for _, h in ipairs(highlights) do
+ h.start = h.start + offset
+ h.finish = h.finish + offset
+ -- check if a seperator already exists and use that one instead of creating a new one
if h.finish + 1 <= #stripped then
- table.insert(stripped, h.finish + 1, string.rep("─", math.min(width, opts.wrap_at or width)))
- height = height + 1
+ if stripped[h.finish + 1]:match("^---+$") then
+ stripped[h.finish + 1] = string.rep("─", math.min(width, opts.wrap_at or width))
+ else
+ table.insert(stripped, h.finish + 1, string.rep("─", math.min(width, opts.wrap_at or width)))
+ offset = offset + 1
+ height = height + 1
+ end
end
end
end
@@ -1113,27 +1167,28 @@ function M.fancy_floating_markdown(contents, opts)
api.nvim_win_set_option(winnr, 'concealcursor', 'n')
vim.cmd("ownsyntax lsp_markdown")
+
local idx = 1
--@private
local function apply_syntax_to_region(ft, start, finish)
- if ft == '' then return end
+ if ft == "" then
+ vim.cmd(string.format("syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend", start, finish + 1))
+ return
+ end
local name = ft..idx
idx = idx + 1
local lang = "@"..ft:upper()
+ -- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set
+ pcall(vim.api.nvim_buf_del_var, bufnr, "current_syntax")
-- 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))
+ vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend", 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
- -- apply_syntax_to_region('markdown', ph.finish, h.start)
apply_syntax_to_region(h.ft, h.start, h.finish)
- -- ph = h
end
vim.api.nvim_set_current_win(cwin)
@@ -1182,6 +1237,20 @@ function M._make_floating_popup_size(contents, opts)
width = math.max(line_widths[i], width)
end
end
+
+ local border_width = get_border_size(opts).width
+ local screen_width = api.nvim_win_get_width(0)
+ width = math.min(width, screen_width)
+
+ -- make sure borders are always inside the screen
+ if width + border_width > screen_width then
+ width = width - (width + border_width - screen_width)
+ end
+
+ if wrap_at and wrap_at > width then
+ wrap_at = width
+ end
+
if max_width then
width = math.min(width, max_width)
wrap_at = math.min(wrap_at or max_width, max_width)
@@ -1235,7 +1304,7 @@ function M.open_floating_preview(contents, syntax, opts)
opts = opts or {}
-- Clean up input: trim empty lines from the end, pad
- contents = M._trim_and_pad(contents, opts)
+ contents = M._trim(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))
@@ -1521,6 +1590,12 @@ function M.make_given_range_params(start_pos, end_pos)
if B[2] > 0 then
B = {B[1], M.character_offset(0, B[1], B[2])}
end
+ -- we need to offset the end character position otherwise we loose the last
+ -- character of the selection, as LSP end position is exclusive
+ -- see https://microsoft.github.io/language-server-protocol/specification#range
+ if vim.o.selection ~= 'exclusive' then
+ B[2] = B[2] + 1
+ end
return {
textDocument = M.make_text_document_params(),
range = {