aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp/util.lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/lsp/util.lua')
-rw-r--r--runtime/lua/vim/lsp/util.lua213
1 files changed, 141 insertions, 72 deletions
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 752d4ff439..49e2557c16 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -3,6 +3,7 @@ local vim = vim
local validate = vim.validate
local api = vim.api
local list_extend = vim.list_extend
+local highlight = require 'vim.highlight'
local M = {}
@@ -94,17 +95,23 @@ local edit_sort_key = sort_by_key(function(e)
return {e.A[1], e.A[2], e.i}
end)
-local function get_line_byte_from_line_character(bufnr, lnum, cnum)
- -- Skip check when the byte and character position is the same
- if cnum > 0 then
- local lines = api.nvim_buf_get_lines(bufnr, lnum, lnum+1, false)
-
+--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
+-- Returns a zero-indexed column, since set_lines() does the conversion to
+-- 1-indexed
+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)
if #lines > 0 then
- return vim.str_byteindex(lines[1], cnum)
+ return vim.str_byteindex(lines[1], col)
end
end
-
- return cnum
+ return col
end
function M.apply_text_edits(text_edits, bufnr)
@@ -117,15 +124,9 @@ function M.apply_text_edits(text_edits, bufnr)
for i, e in ipairs(text_edits) do
-- adjust start and end column for UTF-16 encoding of non-ASCII characters
local start_row = e.range.start.line
- local start_col = get_line_byte_from_line_character(
- bufnr,
- start_row,
- e.range.start.character)
+ local start_col = get_line_byte_from_position(bufnr, e.range.start)
local end_row = e.range["end"].line
- local end_col = get_line_byte_from_line_character(
- bufnr,
- end_row,
- e.range["end"].character)
+ local end_col = get_line_byte_from_position(bufnr, e.range['end'])
start_line = math.min(e.range.start.line, start_line)
finish_line = math.max(e.range["end"].line, finish_line)
-- TODO(ashkan) sanity check ranges for overlap.
@@ -288,7 +289,7 @@ local function get_completion_word(item)
return item.label
end
--- Some lanuguage servers return complementary candidates whose prefixes do not match are also returned.
+-- Some language servers return complementary candidates whose prefixes do not match are also returned.
-- So we exclude completion candidates whose prefix does not match.
local function remove_unmatch_completion_items(items, prefix)
return vim.tbl_filter(function(item)
@@ -542,9 +543,7 @@ function M.jump_to_location(location)
api.nvim_buf_set_option(0, 'buflisted', true)
local range = location.range or location.targetSelectionRange
local row = range.start.line
- local col = range.start.character
- local line = api.nvim_buf_get_lines(0, row, row+1, true)[1]
- col = vim.str_byteindex(line, col)
+ local col = get_line_byte_from_position(0, range.start)
api.nvim_win_set_cursor(0, {row + 1, col})
return true
end
@@ -615,13 +614,53 @@ function M.focusable_preview(unique_name, fn)
end)
end
--- Convert markdown into syntax highlighted regions by stripping the code
--- blocks and converting them into highlighted code.
--- This will by default insert a blank line separator after those code block
--- regions to improve readability.
+--- Trim empty lines from input and pad left and right with spaces
+---
+--@param contents table of lines to trim and pad
+--@param opts dictionary with optional fields
+-- - pad_left amount of columns to pad contents at left (default 1)
+-- - pad_right amount of columns to pad contents at right (default 1)
+--@return contents table of trimmed and padded lines
+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
+ return contents
+end
+
+
+
+--- Convert markdown into syntax highlighted regions by stripping the code
+--- blocks and converting them into highlighted code.
+--- This will by default insert a blank line separator after those code block
+--- regions to improve readability.
+--- The result is shown in a floating preview
+--- TODO: refactor to separate stripping/converting and make use of open_floating_preview
+---
+--@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
+-- - pad_left amount of columns to pad contents at left
+-- - pad_right amount of columns to pad contents at right
+-- - separator insert separator after code block
+--@return width,height size of float
function M.fancy_floating_markdown(contents, opts)
- local pad_left = opts and opts.pad_left
- local pad_right = opts and opts.pad_right
+ validate {
+ contents = { contents, 't' };
+ opts = { opts, 't', true };
+ }
+ opts = opts or {}
+
local stripped = {}
local highlights = {}
do
@@ -655,31 +694,27 @@ function M.fancy_floating_markdown(contents, opts)
end
end
end
- local width = 0
- for i, v in ipairs(stripped) do
- v = v:gsub("\r", "")
- if pad_left then v = (" "):rep(pad_left)..v end
- if pad_right then v = v..(" "):rep(pad_right) end
- stripped[i] = v
- width = math.max(width, #v)
- end
- if opts and opts.max_width then
- width = math.min(opts.max_width, width)
- end
- -- TODO(ashkan): decide how to make this customizable.
- local insert_separator = true
+ -- 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
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))
+ height = height + 1
end
end
end
-- Make the floating window.
- local height = #stripped
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)
@@ -691,7 +726,7 @@ function M.fancy_floating_markdown(contents, opts)
vim.cmd("ownsyntax markdown")
local idx = 1
- local function highlight_region(ft, start, finish)
+ local function apply_syntax_to_region(ft, start, finish)
if ft == '' then return end
local name = ft..idx
idx = idx + 1
@@ -707,8 +742,8 @@ function M.fancy_floating_markdown(contents, opts)
-- make sure that regions between code blocks are definitely markdown.
-- local ph = {start = 0; finish = 1;}
for _, h in ipairs(highlights) do
- -- highlight_region('markdown', ph.finish, h.start)
- highlight_region(h.ft, h.start, h.finish)
+ -- apply_syntax_to_region('markdown', ph.finish, h.start)
+ apply_syntax_to_region(h.ft, h.start, h.finish)
-- ph = h
end
@@ -720,33 +755,81 @@ 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)")
end
-function M.open_floating_preview(contents, filetype, opts)
+--- Compute size of float needed to show contents (with optional wrapping)
+---
+--@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
+--@return width,height size of float
+function M._make_floating_popup_size(contents, opts)
validate {
contents = { contents, 't' };
- filetype = { filetype, 's', true };
opts = { opts, 't', true };
}
opts = opts or {}
- -- Trim empty lines from the end.
- contents = M.trim_empty_lines(contents)
-
local width = opts.width
- local height = opts.height or #contents
+ local height = opts.height
+ local line_widths = {}
+
if not width then
width = 0
for i, line in ipairs(contents) do
- -- Clean up the input and add left pad.
- line = " "..line:gsub("\r", "")
-- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
- local line_width = vim.fn.strdisplaywidth(line)
- width = math.max(line_width, width)
- contents[i] = line
+ line_widths[i] = vim.fn.strdisplaywidth(line)
+ width = math.max(line_widths[i], width)
end
- -- Add right padding of 1 each.
- width = width + 1
end
+ if not height then
+ height = #contents
+ local wrap_at = opts.wrap_at
+ if wrap_at and width > wrap_at then
+ 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
+ height = height + math.ceil(line_widths[i]/wrap_at)
+ end
+ end
+ end
+ end
+
+ return width, height
+end
+
+--- Show contents in a floating window
+---
+--@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
+-- - pad_left amount of columns to pad contents at left
+-- - pad_right amount of columns to pad contents at right
+--@return bufnr,winnr buffer and window number of floating window or nil
+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)
+
local floating_bufnr = api.nvim_create_buf(false, true)
if filetype then
api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype)
@@ -762,19 +845,6 @@ function M.open_floating_preview(contents, filetype, opts)
return floating_bufnr, floating_winnr
end
-local function highlight_range(bufnr, ns, hiname, start, finish)
- if start[1] == finish[1] then
- -- TODO care about encoding here since this is in byte index?
- api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], finish[2])
- else
- api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], -1)
- for line = start[1] + 1, finish[1] - 1 do
- api.nvim_buf_add_highlight(bufnr, ns, hiname, line, 0, -1)
- end
- api.nvim_buf_add_highlight(bufnr, ns, hiname, finish[1], 0, finish[2])
- end
-end
-
do
local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics")
local reference_ns = api.nvim_create_namespace("vim_lsp_references")
@@ -908,8 +978,7 @@ do
[protocol.DiagnosticSeverity.Hint]='Hint',
}
- -- TODO care about encoding here since this is in byte index?
- highlight_range(bufnr, diagnostic_ns,
+ highlight.range(bufnr, diagnostic_ns,
underline_highlight_name..hlmap[diagnostic.severity],
{start.line, start.character},
{finish.line, finish.character}
@@ -933,7 +1002,7 @@ do
[protocol.DocumentHighlightKind.Write] = "LspReferenceWrite";
}
local kind = reference["kind"] or protocol.DocumentHighlightKind.Text
- highlight_range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos)
+ highlight.range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos)
end
end