diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/diagnostic.lua | 56 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 68 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/sync.lua | 15 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 216 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 38 |
6 files changed, 286 insertions, 111 deletions
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 59cb9f8c5b..742ebf69b2 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -91,23 +91,22 @@ local function filter_by_severity(severity, diagnostics) end ---@private -local function prefix_source(source, diagnostics) - vim.validate { source = {source, function(v) - return v == "always" or v == "if_many" - end, "'always' or 'if_many'" } } - - if source == "if_many" then - local sources = {} - for _, d in pairs(diagnostics) do - if d.source then - sources[d.source] = true +local function count_sources(bufnr) + local seen = {} + local count = 0 + for _, namespace_diagnostics in pairs(diagnostic_cache[bufnr]) do + for _, diagnostic in ipairs(namespace_diagnostics) do + if diagnostic.source and not seen[diagnostic.source] then + seen[diagnostic.source] = true + count = count + 1 end end - if #vim.tbl_keys(sources) <= 1 then - return diagnostics - end end + return count +end +---@private +local function prefix_source(diagnostics) return vim.tbl_map(function(d) if not d.source then return d @@ -272,6 +271,8 @@ end ---@private local function set_diagnostic_cache(namespace, bufnr, diagnostics) for _, diagnostic in ipairs(diagnostics) do + assert(diagnostic.lnum, "Diagnostic line number is required") + assert(diagnostic.col, "Diagnostic column is required") diagnostic.severity = diagnostic.severity and to_severity(diagnostic.severity) or M.severity.ERROR diagnostic.end_lnum = diagnostic.end_lnum or diagnostic.lnum diagnostic.end_col = diagnostic.end_col or diagnostic.col @@ -297,11 +298,6 @@ local function restore_extmarks(bufnr, last) if not found[extmark[1]] then local opts = extmark[4] opts.id = extmark[1] - -- HACK: end_row should be end_line - if opts.end_row then - opts.end_line = opts.end_row - opts.end_row = nil - end pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts) end end @@ -563,8 +559,10 @@ end --- - virtual_text: (default true) Use virtual text for diagnostics. Options: --- * severity: Only show virtual text for diagnostics matching the given --- severity |diagnostic-severity| ---- * source: (string) Include the diagnostic source in virtual ---- text. One of "always" or "if_many". +--- * source: (boolean or string) Include the diagnostic source in virtual +--- text. Use "if_many" to only show sources if there is more than +--- one diagnostic source in the buffer. Otherwise, any truthy value +--- means to always show the diagnostic source. --- * format: (function) A function that takes a diagnostic as input and --- returns a string. The return value is the text used to display --- the diagnostic. Example: @@ -928,8 +926,11 @@ M.handlers.virtual_text = { if opts.virtual_text.format then diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics) end - if opts.virtual_text.source then - diagnostics = prefix_source(opts.virtual_text.source, diagnostics) + if + opts.virtual_text.source + and (opts.virtual_text.source ~= "if_many" or count_sources(bufnr) > 1) + then + diagnostics = prefix_source(diagnostics) end if opts.virtual_text.severity then severity = opts.virtual_text.severity @@ -1154,8 +1155,11 @@ end --- - header: (string or table) String to use as the header for the floating window. If a --- table, it is interpreted as a [text, hl_group] tuple. Overrides the setting --- from |vim.diagnostic.config()|. ---- - source: (string) Include the diagnostic source in the message. One of "always" or ---- "if_many". Overrides the setting from |vim.diagnostic.config()|. +--- - source: (boolean or string) Include the diagnostic source in the message. +--- Use "if_many" to only show sources if there is more than one source of +--- diagnostics in the buffer. Otherwise, any truthy value means to always show +--- the diagnostic source. Overrides the setting from +--- |vim.diagnostic.config()|. --- - format: (function) A function that takes a diagnostic as input and returns a --- string. The return value is the text used to display the diagnostic. --- Overrides the setting from |vim.diagnostic.config()|. @@ -1267,8 +1271,8 @@ function M.open_float(opts, ...) diagnostics = reformat_diagnostics(opts.format, diagnostics) end - if opts.source then - diagnostics = prefix_source(opts.source, diagnostics) + if opts.source and (opts.source ~= "if_many" or count_sources(bufnr) > 1) then + diagnostics = prefix_source(diagnostics) end local prefix_opt = if_nil(opts.prefix, (scope == "cursor" and #diagnostics <= 1) and "" or function(_, i) diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 95bc4635b0..00839ec181 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -777,6 +777,10 @@ function lsp.start_client(config) ---@param code (number) exit code of the process ---@param signal (number) the signal used to terminate (if any) function dispatch.on_exit(code, signal) + if config.on_exit then + pcall(config.on_exit, code, signal, client_id) + end + active_clients[client_id] = nil uninitialized_clients[client_id] = nil @@ -792,10 +796,6 @@ function lsp.start_client(config) vim.notify(msg, vim.log.levels.WARN) end) end - - if config.on_exit then - pcall(config.on_exit, code, signal, client_id) - end end -- Start the RPC client. @@ -1112,9 +1112,9 @@ local text_document_did_change_handler do text_document_did_change_handler = function(_, bufnr, changedtick, firstline, lastline, new_lastline) - -- Don't do anything if there are no clients attached. + -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then - return + return true end util.buf_versions[bufnr] = changedtick local compute_change_and_notify = changetracking.prepare(bufnr, firstline, lastline, new_lastline) @@ -1156,6 +1156,12 @@ function lsp.buf_attach_client(bufnr, client_id) client_id = {client_id, 'n'}; } bufnr = resolve_bufnr(bufnr) + if not vim.api.nvim_buf_is_loaded(bufnr) then + local _ = log.warn() and log.warn( + string.format("buf_attach_client called on unloaded buffer (id: %d): ", bufnr) + ) + return false + end local buffer_client_ids = all_buffer_active_clients[bufnr] -- This is our first time attaching to this buffer. if not buffer_client_ids then @@ -1214,6 +1220,50 @@ function lsp.buf_attach_client(bufnr, client_id) return true end +--- Detaches client from the specified buffer. +--- Note: While the server is notified that the text document (buffer) +--- was closed, it is still able to send notifications should it ignore this notification. +--- +---@param bufnr number Buffer handle, or 0 for current +---@param client_id number Client id +function lsp.buf_detach_client(bufnr, client_id) + validate { + bufnr = {bufnr, 'n', true}; + client_id = {client_id, 'n'}; + } + bufnr = resolve_bufnr(bufnr) + + local client = lsp.get_client_by_id(client_id) + if not client or not client.attached_buffers[bufnr] then + vim.notify( + string.format('Buffer (id: %d) is not attached to client (id: %d). Cannot detach.', client_id, bufnr) + ) + return + end + + changetracking.reset_buf(client, bufnr) + + if client.resolved_capabilities.text_document_open_close then + local uri = vim.uri_from_bufnr(bufnr) + local params = { textDocument = { uri = uri; } } + client.notify('textDocument/didClose', params) + end + + client.attached_buffers[bufnr] = nil + util.buf_versions[bufnr] = nil + + all_buffer_active_clients[bufnr][client_id] = nil + if #vim.tbl_keys(all_buffer_active_clients[bufnr]) == 0 then + all_buffer_active_clients[bufnr] = nil + end + + local namespace = vim.lsp.diagnostic.get_namespace(client_id) + vim.diagnostic.reset(namespace, bufnr) + + vim.notify(string.format('Detached buffer (id: %d) from client (id: %d)', bufnr, client_id)) + +end + --- Checks if a buffer is attached for a particular client. --- ---@param bufnr (number) Buffer handle, or 0 for current @@ -1493,11 +1543,7 @@ local function adjust_start_col(lnum, line, items, encoding) end end if min_start_char then - if encoding == 'utf-8' then - return min_start_char - else - return vim.str_byteindex(line, min_start_char, encoding == 'utf-16') - end + return util._str_byteindex_enc(line, min_start_char, encoding) else return nil end diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 22089aaaa6..c974d1a6b4 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -285,7 +285,7 @@ local function location_handler(_, result, ctx, _) util.jump_to_location(result[1]) if #result > 1 then - util.set_qflist(util.locations_to_items(result)) + vim.fn.setqflist({}, ' ', {title = 'LSP locations', items = util.locations_to_items(result)}) api.nvim_command("copen") end else @@ -379,7 +379,7 @@ local make_call_hierarchy_handler = function(direction) }) end end - util.set_qflist(items) + vim.fn.setqflist({}, ' ', {title = 'LSP call hierarchy', items = items}) api.nvim_command("copen") end end diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua index 5df2a4d144..d01f45ad8f 100644 --- a/runtime/lua/vim/lsp/sync.lua +++ b/runtime/lua/vim/lsp/sync.lua @@ -105,15 +105,16 @@ local function align_end_position(line, byte, offset_encoding) char = compute_line_length(line, offset_encoding) + 1 else -- Modifying line, find the nearest utf codepoint - local offset = str_utf_end(line, byte) + local offset = str_utf_start(line, byte) -- If the byte does not fall on the start of the character, then -- align to the start of the next character. - if offset > 0 then - char = byte_to_utf(line, byte, offset_encoding) + 1 - byte = byte + offset - else + if offset < 0 then + byte = byte + str_utf_end(line, byte) + 1 + end + if byte <= #line then char = byte_to_utf(line, byte, offset_encoding) - byte = byte + offset + else + char = compute_line_length(line, offset_encoding) + 1 end -- Extending line, find the nearest utf codepoint for the last valid character end @@ -167,7 +168,7 @@ local function compute_start_range(prev_lines, curr_lines, firstline, lastline, char_idx = compute_line_length(prev_line, offset_encoding) + 1 else byte_idx = start_byte_idx + str_utf_start(prev_line, start_byte_idx) - char_idx = byte_to_utf(prev_line, start_byte_idx, offset_encoding) + char_idx = byte_to_utf(prev_line, byte_idx, offset_encoding) end -- Return the start difference (shared for new and prev lines) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index a3a91a34f7..5921eb5bf0 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -90,6 +90,49 @@ local function split_lines(value) return split(value, '\n', true) end +--- Convert byte index to `encoding` index. +--- Convenience wrapper around vim.str_utfindex +---@param line string line to be indexed +---@param index number byte index (utf-8), or `nil` for length +---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 +---@return number `encoding` index of `index` in `line` +function M._str_utfindex_enc(line, index, encoding) + if not encoding then encoding = 'utf-16' end + if encoding == 'utf-8' then + if index then return index else return #line end + elseif encoding == 'utf-16' then + local _, col16 = vim.str_utfindex(line, index) + return col16 + elseif encoding == 'utf-32' then + local col32, _ = vim.str_utfindex(line, index) + return col32 + else + error("Invalid encoding: " .. vim.inspect(encoding)) + end +end + +--- Convert UTF index to `encoding` index. +--- Convenience wrapper around vim.str_byteindex +---Alternative to vim.str_byteindex that takes an encoding. +---@param line string line to be indexed +---@param index number UTF index +---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 +---@return number byte (utf-8) index of `encoding` index `index` in `line` +function M._str_byteindex_enc(line, index, encoding) + if not encoding then encoding = 'utf-16' end + if encoding == 'utf-8' then + if index then return index else return #line end + elseif encoding == 'utf-16' then + return vim.str_byteindex(line, index, true) + elseif encoding == 'utf-32' then + return vim.str_byteindex(line, index) + else + error("Invalid encoding: " .. vim.inspect(encoding)) + end +end + +local _str_utfindex_enc = M._str_utfindex_enc +local _str_byteindex_enc = M._str_byteindex_enc --- Replaces text in a range with new text. --- --- CAUTION: Changes in-place! @@ -237,6 +280,7 @@ end ---@private --- 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 +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 --- 1-indexed local function get_line_byte_from_position(bufnr, position, offset_encoding) -- LSP's line and characters are 0-indexed @@ -247,13 +291,7 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding) if col > 0 then local line = get_line(bufnr, position.line) local ok, result - - if offset_encoding == "utf-16" or not offset_encoding then - ok, result = pcall(vim.str_byteindex, line, col, true) - elseif offset_encoding == "utf-32" then - ok, result = pcall(vim.str_byteindex, line, col, false) - end - + ok, result = pcall(_str_byteindex_enc, line, col, offset_encoding) if ok then return result end @@ -325,12 +363,15 @@ end --- Applies a list of text edits to a buffer. ---@param text_edits table list of `TextEdit` objects ---@param bufnr number Buffer id +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to encoding of first client of `bufnr` ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit -function M.apply_text_edits(text_edits, bufnr) +function M.apply_text_edits(text_edits, bufnr, offset_encoding) validate { text_edits = { text_edits, 't', false }; bufnr = { bufnr, 'number', false }; + offset_encoding = { offset_encoding, 'string', true }; } + offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) if not next(text_edits) then return end if not api.nvim_buf_is_loaded(bufnr) then vim.fn.bufload(bufnr) @@ -367,8 +408,7 @@ function M.apply_text_edits(text_edits, bufnr) -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here. local has_eol_text_edit = false local max = vim.api.nvim_buf_line_count(bufnr) - -- TODO handle offset_encoding - local _, len = vim.str_utfindex(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') + local len = _str_utfindex_enc(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '', nil, offset_encoding) text_edits = vim.tbl_map(function(text_edit) if max <= text_edit.range.start.line then text_edit.range.start.line = max - 1 @@ -1220,17 +1260,57 @@ function M.stylize_markdown(bufnr, contents, opts) return stripped end +---@private --- Creates autocommands to close a preview window when events happen. --- ----@param events (table) list of events ----@param winnr (number) window id of preview window +---@param events table list of events +---@param winnr number window id of preview window +---@param bufnrs table list of buffers where the preview window will remain visible ---@see |autocmd-events| -function M.close_preview_autocmd(events, winnr) +local function close_preview_autocmd(events, winnr, bufnrs) + local augroup = 'preview_window_'..winnr + + -- close the preview window when entered a buffer that is not + -- the floating window buffer or the buffer that spawned it + vim.cmd(string.format([[ + augroup %s + autocmd! + autocmd BufEnter * lua vim.lsp.util._close_preview_window(%d, {%s}) + augroup end + ]], augroup, winnr, table.concat(bufnrs, ','))) + if #events > 0 then - api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)") + vim.cmd(string.format([[ + augroup %s + autocmd %s <buffer> lua vim.lsp.util._close_preview_window(%d) + augroup end + ]], augroup, table.concat(events, ','), winnr)) end end +---@private +--- Closes the preview window +--- +---@param winnr number window id of preview window +---@param bufnrs table|nil optional list of ignored buffers +function M._close_preview_window(winnr, bufnrs) + vim.schedule(function() + -- exit if we are in one of ignored buffers + if bufnrs and vim.tbl_contains(bufnrs, api.nvim_get_current_buf()) then + return + end + + local augroup = 'preview_window_'..winnr + vim.cmd(string.format([[ + augroup %s + autocmd! + augroup end + augroup! %s + ]], augroup, augroup)) + pcall(vim.api.nvim_win_close, winnr, true) + end) +end + ---@internal --- Computes size of float needed to show contents (with optional wrapping) --- @@ -1337,7 +1417,7 @@ function M.open_floating_preview(contents, syntax, opts) opts.wrap = opts.wrap ~= false -- wrapping by default opts.stylize_markdown = opts.stylize_markdown ~= false opts.focus = opts.focus ~= false - opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre"} + opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "InsertCharPre"} local bufnr = api.nvim_get_current_buf() @@ -1406,7 +1486,7 @@ function M.open_floating_preview(contents, syntax, opts) api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe') api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true, nowait = true}) - M.close_preview_autocmd(opts.close_events, floating_winnr) + close_preview_autocmd(opts.close_events, floating_winnr, {floating_bufnr, bufnr}) -- save focus_id if opts.focus_id then @@ -1432,11 +1512,11 @@ do --[[ References ]] --- ---@param bufnr number Buffer id ---@param references table List of `DocumentHighlight` objects to highlight - ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32", or nil. Defaults to utf-16 + ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32", or nil. Defaults to `offset_encoding` of first client of `bufnr` ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight function M.buf_highlight_references(bufnr, references, offset_encoding) validate { bufnr = {bufnr, 'n', true} } - offset_encoding = offset_encoding or 'utf-16' + offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) for _, reference in ipairs(references) do local start_line, start_char = reference["range"]["start"]["line"], reference["range"]["start"]["character"] local end_line, end_char = reference["range"]["end"]["line"], reference["range"]["end"]["character"] @@ -1649,30 +1729,61 @@ function M.try_trim_markdown_code_blocks(lines) return 'markdown' end -local str_utfindex = vim.str_utfindex ---@private -local function make_position_param() - local row, col = unpack(api.nvim_win_get_cursor(0)) +---@param window (optional, number): window handle or 0 for current, defaults to current +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` +local function make_position_param(window, offset_encoding) + window = window or 0 + local buf = vim.api.nvim_win_get_buf(window) + local row, col = unpack(api.nvim_win_get_cursor(window)) + offset_encoding = offset_encoding or M._get_offset_encoding(buf) row = row - 1 - local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] + local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1] if not line then return { line = 0; character = 0; } end - -- TODO handle offset_encoding - local _ - _, col = str_utfindex(line, col) + + col = _str_utfindex_enc(line, col, offset_encoding) + return { line = row; character = col; } end --- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position. --- +---@param window (optional, number): window handle or 0 for current, defaults to current +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` ---@returns `TextDocumentPositionParams` object ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams -function M.make_position_params() +function M.make_position_params(window, offset_encoding) + window = window or 0 + local buf = vim.api.nvim_win_get_buf(window) + offset_encoding = offset_encoding or M._get_offset_encoding(buf) return { - textDocument = M.make_text_document_params(); - position = make_position_param() + textDocument = M.make_text_document_params(buf); + position = make_position_param(window, offset_encoding) + } +end + +--- Utility function for getting the encoding of the first LSP client on the given buffer. +---@param bufnr (number) buffer handle or 0 for current, defaults to current +---@returns (string) encoding first client if there is one, nil otherwise +function M._get_offset_encoding(bufnr) + validate { + bufnr = {bufnr, 'n', true}; } + + local offset_encoding + + for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do + local this_offset_encoding = client.offset_encoding or "utf-16" + if not offset_encoding then + offset_encoding = this_offset_encoding + elseif offset_encoding ~= this_offset_encoding then + vim.notify("warning: multiple different client offset_encodings detected for buffer, this is not supported yet", vim.log.levels.WARN) + end + end + + return offset_encoding end --- Using the current position in the current buffer, creates an object that @@ -1680,12 +1791,16 @@ end --- `textDocument/codeAction`, `textDocument/colorPresentation`, --- `textDocument/rangeFormatting`. --- +---@param window (optional, number): window handle or 0 for current, defaults to current +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` ---@returns { textDocument = { uri = `current_file_uri` }, range = { start = ---`current_position`, end = `current_position` } } -function M.make_range_params() - local position = make_position_param() +function M.make_range_params(window, offset_encoding) + local buf = vim.api.nvim_win_get_buf(window) + offset_encoding = offset_encoding or M._get_offset_encoding(buf) + local position = make_position_param(window, offset_encoding) return { - textDocument = M.make_text_document_params(), + textDocument = M.make_text_document_params(buf), range = { start = position; ["end"] = position; } } end @@ -1697,27 +1812,29 @@ end ---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. +---@param bufnr (optional, number): buffer handle or 0 for current, defaults to current +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `bufnr` ---@returns { textDocument = { uri = `current_file_uri` }, range = { start = ---`start_position`, end = `end_position` } } -function M.make_given_range_params(start_pos, end_pos) +function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding) validate { start_pos = {start_pos, 't', true}; end_pos = {end_pos, 't', true}; + offset_encoding = {offset_encoding, 's', 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, '>')) + bufnr = bufnr or 0 + offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) + local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<')) + local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>')) -- convert to 0-index A[1] = A[1] - 1 B[1] = B[1] - 1 - -- account for encoding. - -- TODO handle offset_encoding + -- account for offset_encoding. if A[2] > 0 then - local _, char = M.character_offset(0, A[1], A[2]) - A = {A[1], char} + A = {A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding)} end if B[2] > 0 then - local _, char = M.character_offset(0, B[1], B[2]) - B = {B[1], char} + B = {B[1], M.character_offset(bufnr, B[1], B[2], offset_encoding)} end -- we need to offset the end character position otherwise we loose the last -- character of the selection, as LSP end position is exclusive @@ -1726,7 +1843,7 @@ function M.make_given_range_params(start_pos, end_pos) B[2] = B[2] + 1 end return { - textDocument = M.make_text_document_params(), + textDocument = M.make_text_document_params(bufnr), range = { start = {line = A[1], character = A[2]}, ['end'] = {line = B[1], character = B[2]} @@ -1736,10 +1853,11 @@ end --- Creates a `TextDocumentIdentifier` object for the current buffer. --- +---@param bufnr (optional, number): Buffer handle, defaults to current ---@returns `TextDocumentIdentifier` ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier -function M.make_text_document_params() - return { uri = vim.uri_from_bufnr(0) } +function M.make_text_document_params(bufnr) + return { uri = vim.uri_from_bufnr(bufnr or 0) } end --- Create the workspace params @@ -1782,14 +1900,16 @@ end ---@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} -function M.character_offset(bufnr, row, col) - local line = get_line(bufnr, row) +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `buf` +---@returns (number, number) `offset_encoding` index of the character in line {row} column {col} in buffer {buf} +function M.character_offset(buf, row, col, offset_encoding) + local line = get_line(buf, row) + offset_encoding = offset_encoding or M._get_offset_encoding(buf) -- If the col is past the EOL, use the line length. if col > #line then - return str_utfindex(line) + return _str_utfindex_enc(line, nil, offset_encoding) end - return str_utfindex(line, col) + return _str_utfindex_enc(line, col, offset_encoding) end --- Helper function to return nested values in language server settings diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index b8255e61ed..ebed502c92 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -164,22 +164,36 @@ function M.parse_query(lang, query) return self end --- TODO(vigoux): support multiline nodes too - --- Gets the text corresponding to a given node --- ---@param node the node ----@param bsource The buffer or string from which the node is extracted +---@param source The buffer or string from which the node is extracted function M.get_node_text(node, source) local start_row, start_col, start_byte = node:start() local end_row, end_col, end_byte = node:end_() if type(source) == "number" then - if start_row ~= end_row then + local lines + local eof_row = a.nvim_buf_line_count(source) + if start_row >= eof_row then return nil end - local line = a.nvim_buf_get_lines(source, start_row, start_row+1, true)[1] - return string.sub(line, start_col+1, end_col) + + if end_col == 0 then + lines = a.nvim_buf_get_lines(source, start_row, end_row, true) + end_col = -1 + else + lines = a.nvim_buf_get_lines(source, start_row, end_row + 1, true) + end + + if #lines == 1 then + lines[1] = string.sub(lines[1], start_col+1, end_col) + else + lines[1] = string.sub(lines[1], start_col+1) + lines[#lines] = string.sub(lines[#lines], 1, end_col) + end + + return table.concat(lines, "\n") elseif type(source) == "string" then return source:sub(start_byte+1, end_byte) end @@ -211,11 +225,6 @@ local predicate_handlers = { ["lua-match?"] = function(match, _, source, predicate) local node = match[predicate[2]] local regex = predicate[3] - local start_row, _, end_row, _ = node:range() - if start_row ~= end_row then - return false - end - return string.find(M.get_node_text(node, source), regex) end, @@ -239,13 +248,8 @@ local predicate_handlers = { return function(match, _, source, pred) local node = match[pred[2]] - local start_row, start_col, end_row, end_col = node:range() - if start_row ~= end_row then - return false - end - local regex = compiled_vim_regexes[pred[3]] - return regex:match_line(source, start_row, start_col, end_col) + return regex:match_str(M.get_node_text(node, source)) end end)(), |