diff options
Diffstat (limited to 'runtime/lua/vim/lsp/util.lua')
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 213 |
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 |