From 4f53b8944017a937d2cfc9b50794f42674b47cab Mon Sep 17 00:00:00 2001 From: Jannik Buhr <17450586+jmbuhr@users.noreply.github.com> Date: Wed, 8 Jan 2025 21:28:05 +0100 Subject: [PATCH] fix: textlock (#195) closes #194 fix: document config and make sure config is required first in init doc: config feat: extened blink capabilites fix: always call the handler for a request response even if the result is empty because we are not in an otter context to no stall other requests --- README.md | 19 +++- lua/otter/config.lua | 23 ++++- lua/otter/diagnostics.lua | 5 +- lua/otter/init.lua | 82 ++++++++++------- lua/otter/keeper.lua | 160 ++++++++++++++++++++++------------ lua/otter/lsp/handlers.lua | 83 +++++++++++------- lua/otter/lsp/init.lua | 78 +++++++++++------ lua/otter/tools/functions.lua | 3 + 8 files changed, 297 insertions(+), 156 deletions(-) diff --git a/README.md b/README.md index 559e722..439e1e8 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ and the latest [Neovim stable version](https://github.com/neovim/neovim/releases 'nvim-treesitter/nvim-treesitter', }, opts = {}, -}, +} ``` ### Configure otter @@ -151,10 +151,11 @@ otter.setup{ }) or vim.fn.getcwd(0) end, }, + -- options related to the otter buffers buffers = { -- if set to true, the filetype of the otterbuffers will be set. -- otherwise only the autocommand of lspconfig that attaches - -- the language server will be executed without setting the filetype + -- the language server will be executed without setting the filetype set_filetype = false, -- write .otter. files -- to disk on save of main buffer. @@ -162,10 +163,20 @@ otter.setup{ -- otter files are deleted on quit or main buffer close write_to_disk = false, }, + -- list of characters that should be stripped from the beginning and end of the code chunks strip_wrapping_quote_characters = { "'", '"', "`" }, - -- otter may not work the way you expect when entire code blocks are indented (eg. in Org files) - -- When true, otter handles these cases fully. + -- remove whitespace from the beginning of the code chunks when writing to the ottter buffers + -- and calculate it back in when handling lsp requests handle_leading_whitespace = true, + -- mapping of filetypes to extensions for those not already included in otter.tools.extensions + -- e.g. ["bash"] = "sh" + extensions = { + }, + -- add event listeners for LSP events for debugging + debug = false, + verbose = { -- set to false to disable all verbose messages + no_code_found = false -- warn if otter.activate is called, but no injected code was found + }, } ``` diff --git a/lua/otter/config.lua b/lua/otter/config.lua index 40484a0..26a23ed 100644 --- a/lua/otter/config.lua +++ b/lua/otter/config.lua @@ -1,8 +1,14 @@ local M = {} + +---@class OtterConfig local default_config = { lsp = { + -- `:h events` that cause the diagnostics to update. Set to: + -- { "BufWritePost", "InsertLeave", "TextChanged" } for less performant + -- but more instant diagnostic updates diagnostic_update_events = { "BufWritePost" }, + -- function to find the root dir where the otter-ls is started root_dir = function(_, bufnr) return vim.fs.root(bufnr or 0, { ".git", @@ -11,18 +17,31 @@ local default_config = { }) or vim.fn.getcwd(0) end, }, + -- options related to the otter buffers buffers = { -- if set to true, the filetype of the otterbuffers will be set. -- otherwise only the autocommand of lspconfig that attaches - -- the language server will be executed without setting the filetype + -- the language server will be executed without setting the filetype set_filetype = false, + -- write .otter. files + -- to disk on save of main buffer. + -- usefule for some linters that require actual files + -- otter files are deleted on quit or main buffer close write_to_disk = false, }, + -- list of characters that should be stripped from the beginning and end of the code chunks strip_wrapping_quote_characters = { "'", '"', "`" }, + -- remove whitespace from the beginning of the code chunks when writing to the ottter buffers + -- and calculate it back in when handling lsp requests handle_leading_whitespace = true, + -- mapping of filetypes to extensions for those not already included in otter.tools.extensions + -- e.g. ["bash"] = "sh" + extensions = { + }, + -- add event listeners for LSP events for debugging debug = false, verbose = { -- set to false to disable all verbose messages - no_code_found = true -- warn if otter.activate is called, but no injected code was found + no_code_found = false -- warn if otter.activate is called, but no injected code was found }, } diff --git a/lua/otter/diagnostics.lua b/lua/otter/diagnostics.lua index fcff9fd..2a44a3f 100644 --- a/lua/otter/diagnostics.lua +++ b/lua/otter/diagnostics.lua @@ -4,12 +4,13 @@ local keeper = require("otter.keeper") M = {} M.setup = function(main_nr) + ---@type integer[] local nss = {} for lang, bufnr in pairs(keeper.rafts[main_nr].buffers) do local ns = api.nvim_create_namespace("otter-lang-" .. lang) nss[bufnr] = ns end - keeper.rafts[main_nr].nss = nss + keeper.rafts[main_nr].diagnostics_namespaces = nss local sync_diagnostics = function(args) if vim.tbl_contains(vim.tbl_values(keeper.rafts[main_nr].buffers), args.buf) then @@ -31,7 +32,7 @@ M.setup = function(main_nr) group = group, callback = sync_diagnostics, }) - keeper.rafts[main_nr].dianostics_group = group + keeper.rafts[main_nr].diagnostics_group = group api.nvim_create_autocmd(config.cfg.lsp.diagnostic_update_events, { buffer = main_nr, diff --git a/lua/otter/init.lua b/lua/otter/init.lua index 8383cc5..82ee867 100644 --- a/lua/otter/init.lua +++ b/lua/otter/init.lua @@ -2,14 +2,27 @@ local M = {} local api = vim.api local ts = vim.treesitter + +local config = require("otter.config") local extensions = require("otter.tools.extensions") local keeper = require("otter.keeper") local otterls = require("otter.lsp") + local path_to_otterpath = require("otter.tools.functions").path_to_otterpath -local config = require("otter.config") M.setup = function(opts) + if M.did_setup then + return vim.notify("[otter] otter.nvim is already setup", vim.log.levels.ERROR) + end + M.did_setup = true + + if vim.fn.has("nvim-0.10.0") ~= 1 then + return vim.notify("[otter] otter.nvim requires Neovim >= 0.10.0", vim.log.levels.ERROR) + end + config.cfg = vim.tbl_deep_extend("force", config.cfg, opts or {}) + + extensions = vim.tbl_deep_extend("force", extensions, config.cfg.extensions or {}) end -- expose some functions from the otter keeper directly @@ -19,10 +32,10 @@ M.export_otter_as = keeper.export_otter_as --- Activate the current buffer by adding and synchronizing --- otter buffers. ----@param languages table|nil List of languages to activate. If nil, all available languages will be activated. ----@param completion boolean|nil Enable completion for otter buffers. Default: true ----@param diagnostics boolean|nil Enable diagnostics for otter buffers. Default: true ----@param tsquery string|nil Explicitly provide a treesitter query. If nil, the injections query for the current filetyepe will be used. See :h treesitter-language-injections. +---@param languages string[]? List of languages to activate. If nil, all available languages will be activated. +---@param completion boolean? Enable completion for otter buffers. Default: true +---@param diagnostics boolean? Enable diagnostics for otter buffers. Default: true +---@param tsquery string? Explicitly provide a treesitter query. If nil, the injections query for the current filetyepe will be used. See :h treesitter-language-injections. M.activate = function(languages, completion, diagnostics, tsquery) languages = languages or vim.tbl_keys(require("otter.tools.extensions")) completion = completion ~= false @@ -49,19 +62,31 @@ M.activate = function(languages, completion, diagnostics, tsquery) ) return end - keeper.rafts[main_nr] = {} - keeper.rafts[main_nr].languages = {} - keeper.rafts[main_nr].buffers = {} - keeper.rafts[main_nr].paths = {} - keeper.rafts[main_nr].otter_nr_to_lang = {} - keeper.rafts[main_nr].tsquery = tsquery - keeper.rafts[main_nr].query = query - keeper.rafts[main_nr].parser = ts.get_parser(main_nr, parsername) - keeper.rafts[main_nr].code_chunks = nil - keeper.rafts[main_nr].last_changetick = nil - keeper.rafts[main_nr].otterls = {} + local parser = ts.get_parser(main_nr, parsername) + if parser == nil then + vim.notify_once("[otter] No parser found for current buffer. Can't activate.", vim.log.levels.WARN, {}) + return + end + keeper.rafts[main_nr] = { + languages = {}, + buffers = {}, + paths = {}, + otter_nr_to_lang = {}, + tsquery = tsquery, + query = query, + parser = parser, + code_chunks = {}, + last_changetick = nil, + otterls = { + client_id = nil, + }, + diagnostics_namespaces = {}, + diagnostics_group = nil, + } local all_code_chunks = keeper.extract_code_chunks(main_nr) + + ---@type string[] local found_languages = {} for _, lang in ipairs(languages) do if all_code_chunks[lang] ~= nil and lang ~= main_lang then @@ -70,11 +95,9 @@ M.activate = function(languages, completion, diagnostics, tsquery) end languages = found_languages if #languages == 0 then - -- just return quietly until - -- TODO: config handling is more robust - -- if config.cfg.verbose and config.cfg.verbose.no_code_found then - -- vim.notify_once("[otter] No code chunks found. Not activating. You can activate after having added code chunks with require'otter'.activate(). You can turn of this message by setting the option verbose.no_code_found to false", vim.log.levels.INFO, {}) - -- end + if config.cfg.verbose and config.cfg.verbose.no_code_found then + vim.notify_once("[otter] No code chunks found. Not activating. You can activate after having added code chunks with require'otter'.activate(). You can turn of this message by setting the option verbose.no_code_found to false", vim.log.levels.INFO, {}) + end return end @@ -151,6 +174,7 @@ M.activate = function(languages, completion, diagnostics, tsquery) end) end + -- or if requested set the filetype if config.cfg.buffers.set_filetype then api.nvim_set_option_value("filetype", lang, { buf = otter_nr }) else @@ -192,21 +216,19 @@ M.activate = function(languages, completion, diagnostics, tsquery) -- remove the need to use keybindings for otter ask_ functions -- by being our own lsp server-client combo local otterclient_id = otterls.start(main_nr, completion) - if otterclient_id == nil then + if otterclient_id ~= nil then + keeper.rafts[main_nr].otterls.client_id = otterclient_id + else vim.notify_once("[otter] activation of otter-ls failed", vim.log.levels.WARN, {}) end - keeper.rafts[main_nr].otterls.client_id = otterclient_id -- debugging if config.cfg.debug == true then -- listen to lsp requests and notifications vim.api.nvim_create_autocmd("LspNotify", { - callback = function(args) - local bufnr = args.buf - local client_id = args.data.client_id - local method = args.data.method - local params = args.data.params + ---@param _ {buf: number, data: {client_id: number, method: string, params: any}} + callback = function(_) end, }) @@ -237,11 +259,11 @@ M.deactivate = function(completion, diagnostics) end if diagnostics then - for _, ns in pairs(keeper.rafts[main_nr].nss) do + for _, ns in pairs(keeper.rafts[main_nr].diagnostics_namespaces) do vim.diagnostic.reset(ns, main_nr) end -- remove diagnostics autocommands - local id = keeper.rafts[main_nr].dianostics_group + local id = keeper.rafts[main_nr].diagnostics_group if id ~= nil then vim.api.nvim_del_augroup_by_id(id) end diff --git a/lua/otter/keeper.lua b/lua/otter/keeper.lua index 1cdcb12..7f4a91b 100644 --- a/lua/otter/keeper.lua +++ b/lua/otter/keeper.lua @@ -11,18 +11,27 @@ local api = vim.api local ts = vim.treesitter local cfg = require("otter.config").cfg +---@class Raft +---@field languages string[] +---@field buffers table +---@field paths table +---@field otter_nr_to_lang table +---@field tsquery string? +---@field query vim.treesitter.Query +---@field parser vim.treesitter.LanguageTree +---@field otterls OtterLSInfo +---@field diagnostics_namespaces integer[] +---@field diagnostics_group integer? +---@field last_changetick integer? +---@field code_chunks table + +---@class OtterLSInfo +---@field client_id integer? + +---@class Rafts +---@field [number] Raft ---One raft per main buffer ---stored in the rafts table ----contains the following fields: ----keeper.rafts[main_nr].languages = {} ----keeper.rafts[main_nr].buffers = {} ----keeper.rafts[main_nr].paths = {} ----keeper.rafts[main_nr].otter_nr_to_lang = {} ----keeper.rafts[main_nr].tsquery = tsquery ----keeper.rafts[main_nr].query = query ----keeper.rafts[main_nr].parser = ts.get_parser(main_nr, parsername) ----keeper.rafts[main_nr].code_chunks = nil ----keeper.rafts[main_nr].last_changetick = nil keeper.rafts = {} --- table of languages that can be injected @@ -67,9 +76,9 @@ end ---trims the leading whitespace from text ---@param text string ----@param bufnr number host buffer number ----@param starting_ln number ----@return string, number +---@param bufnr integer host buffer number +---@param starting_ln integer +---@return string, integer local function trim_leading_witespace(text, bufnr, starting_ln) if not cfg.handle_leading_whitespace then return text, 0 @@ -93,44 +102,40 @@ local function trim_leading_witespace(text, bufnr, starting_ln) end ---@class CodeChunk ----@field range table +---@field range { from: [integer, integer], to: [integer, integer] } ---@field lang string ----@field node any ----@field text string +---@field node TSNode +---@field text string[] ---@field leading_offset number ---Extract code chunks from the specified buffer. ---Updates M.rafts[main_nr].code_chunks ---@param main_nr integer The main buffer number ----@param lang string|nil language to extract. All languages if nil. ----@param exclude_eval_false boolean | nil Exclude code chunks with eval: false ----@param row_start integer|nil Row to start from, inclusive, 1-indexed. ----@param row_end integer|nil Row to end at, inclusive, 1-indexed. ----@return CodeChunk[] -keeper.extract_code_chunks = function(main_nr, lang, exclude_eval_false, row_start, row_end) +---@param lang string? language to extract. All languages if nil. +---@param exclude_eval_false boolean? Exclude code chunks with eval: false +---@param range_start_row integer? Row to start from, inclusive, 1-indexed. +---@param range_end_row integer? Row to end at, inclusive, 1-indexed. +---@return table +keeper.extract_code_chunks = function(main_nr, lang, exclude_eval_false, range_start_row, range_end_row) local query = keeper.rafts[main_nr].query local parser = keeper.rafts[main_nr].parser local tree = parser:parse() local root = tree[1]:root() + ---@type table local code_chunks = {} local lang_capture = nil for _, match, metadata in query:iter_matches(root, main_nr, 0, -1, { all = true }) do for id, nodes in pairs(match) do local name = query.captures[id] - -- TODO: maybe can be removed with nvim v0.10 - if type(nodes) ~= "table" then - nodes = { nodes } - end - for _, node in ipairs(nodes) do local text lang_capture = determine_language(main_nr, name, node, metadata, lang_capture) if - lang_capture - and (name == "content" or name == "injection.content") - and (lang == nil or lang_capture == lang) + lang_capture + and (name == "content" or name == "injection.content") + and (lang == nil or lang_capture == lang) then -- the actual code content text = ts.get_node_text(node, main_nr, { metadata = metadata[id] }) @@ -140,14 +145,17 @@ keeper.extract_code_chunks = function(main_nr, lang, exclude_eval_false, row_sta if exclude_eval_false and string.find(text, "| *eval: *false") then text = "" end - local row1, col1, row2, col2 = node:range() - if row_start ~= nil and row_end ~= nil and ((row1 >= row_end and row_end > 0) or row2 < row_start) then + + ---@type integer + ---@diagnostic disable-next-line: assign-type-mismatch + local start_row, start_col, end_row, end_col = node:range() + if range_start_row ~= nil and range_end_row ~= nil and ((start_row >= range_end_row and range_end_row > 0) or end_row < range_start_row) then goto continue end local leading_offset - text, leading_offset = trim_leading_witespace(text, main_nr, row1) + text, leading_offset = trim_leading_witespace(text, main_nr, start_row) local result = { - range = { from = { row1, col1 }, to = { row2, col2 } }, + range = { from = { start_row, start_col }, to = { end_row, end_col } }, lang = lang_capture, node = node, text = fn.lines(text), @@ -164,11 +172,14 @@ keeper.extract_code_chunks = function(main_nr, lang, exclude_eval_false, row_sta if lang == nil or name == lang then text = ts.get_node_text(node, main_nr, { metadata = metadata[id] }) text, _ = fn.strip_wrapping_quotes(text) - local row1, col1, row2, col2 = node:range() + + ---@type integer + ---@diagnostic disable-next-line: assign-type-mismatch + local start_row, start_col, end_row, end_col = node:range() local leading_offset - text, leading_offset = trim_leading_witespace(text, main_nr, row1) + text, leading_offset = trim_leading_witespace(text, main_nr, start_row) local result = { - range = { from = { row1, col1 }, to = { row2, col2 } }, + range = { from = { start_row, start_col }, to = { end_row, end_col } }, lang = name, node = node, text = fn.lines(text), @@ -188,13 +199,13 @@ keeper.extract_code_chunks = function(main_nr, lang, exclude_eval_false, row_sta end --- Get the language context of a position ---- @param main_nr integer|nil bufnr of the parent buffer. Default is 0 ---- @param position table|nil position (row, col). Default is the current cursor position ---- @return string|nil language nil if no language context is found ---- @return integer|nil start_row ---- @return integer|nil start_col ---- @return integer|nil end_row ---- @return integer|nil end_col +--- @param main_nr integer? bufnr of the parent buffer. Default is 0 +--- @param position table? position (row, col). Default is the current cursor position +--- @return string? language nil if no language context is found +--- @return integer? start_row +--- @return integer? start_col +--- @return integer? end_row +--- @return integer? end_col keeper.get_current_language_context = function(main_nr, position) main_nr = main_nr or api.nvim_get_current_buf() position = position or api.nvim_win_get_cursor(0) @@ -214,11 +225,6 @@ keeper.get_current_language_context = function(main_nr, position) for id, nodes in pairs(match) do local name = query.captures[id] - -- TODO: maybe can be removed with nvim v0.10 - if type(nodes) ~= "table" then - nodes = { nodes } - end - for _, node in ipairs(nodes) do lang_capture = determine_language(main_nr, name, node, metadata, lang_capture) local start_row, start_col, end_row, end_col = node:range() @@ -346,26 +352,61 @@ keeper.has_raft = function(main_nr) return keeper.rafts[main_nr] ~= nil end +---@alias SyncResult +---| '"success"' -- The sync was successful +---| '"no_raft"' -- The buffer has no raft +---| '"textlock_active"' -- The buffer is currently locked +---| '"error"' -- Some other error occurred + --- Synchronize the raft of otters attached to a buffer ---@param main_nr integer bufnr of the parent buffer ---@param language string|nil only sync one otter buffer matching a language ----@return boolean success true on success, otherwise false +---@return SyncResult result keeper.sync_raft = function(main_nr, language) if not keeper.has_raft(main_nr) then - return false + return "no_raft" end local all_code_chunks local changetick = api.nvim_buf_get_changedtick(main_nr) if keeper.rafts[main_nr].last_changetick == changetick then all_code_chunks = keeper.rafts[main_nr].code_chunks - return true - else - all_code_chunks = keeper.extract_code_chunks(main_nr) + return "success" + end + + + ---@param callback function + ---@return SyncResult + --- + --- Assumption: If textlock is currently active, + --- we can't sync, but those are also the cases + --- in which it is not necessary to sync + --- and can be delayed until the textlock is released. + --- The lsp request should still be valid + local function do_with_maybe_texlock(callback) + local texlock_err_msg = 'Vim(normal):E5556: API call: E565: Not allowed to change text or change window' + local success, result = pcall(callback) + if success then + return "success" + end + + vim.notify_once("[otter.nvim] Hi there! You triggered an LSP request that is routed through otter.nvim while textlock is active. We would like to fix this, but need to find the exact form of the error message to match against. Please be so kind and open an issue with how you triggered this and the error object below:", vim.log.levels.WARN) + vim.notify_once(vim.inspect(result), vim.log.levels.WARN) + + if result == texlock_err_msg then + vim.schedule(callback) + return "textlock_active" + else + error(result) + return "error" + end end + all_code_chunks = keeper.extract_code_chunks(main_nr) + keeper.rafts[main_nr].last_changetick = changetick keeper.rafts[main_nr].code_chunks = all_code_chunks + local result local langs if language == nil then langs = keeper.rafts[main_nr].languages @@ -393,14 +434,19 @@ keeper.sync_raft = function(main_nr, language) end -- replace language lines - api.nvim_buf_set_lines(otter_nr, 0, -1, false, ls) + result = do_with_maybe_texlock(function() + api.nvim_buf_set_lines(otter_nr, 0, -1, false, ls) + end) else -- no code chunks so we wipe the otter buffer - api.nvim_buf_set_lines(otter_nr, 0, -1, false, {}) + result = do_with_maybe_texlock(function() + api.nvim_buf_set_lines(otter_nr, 0, -1, false, {}) + end) end end end - return true + + return result end --- Export the raft of otters as files. diff --git a/lua/otter/lsp/handlers.lua b/lua/otter/lsp/handlers.lua index 59d6dcb..dd61cd8 100644 --- a/lua/otter/lsp/handlers.lua +++ b/lua/otter/lsp/handlers.lua @@ -4,6 +4,7 @@ local fn = require("otter.tools.functions") local ms = vim.lsp.protocol.Methods local modify_position = require("otter.keeper").modify_position +---@type table local M = {} local function filter_one_or_many(response, filter) @@ -19,11 +20,11 @@ local function filter_one_or_many(response, filter) end --- see e.g. ---- vim.lsp.handlers.hover(_, result, ctx, conf) +--- vim.lsp.handlers.hover(_, result, ctx) ---@param err lsp.ResponseError? ---@param response lsp.Hover ---@param ctx lsp.HandlerContext -M[ms.textDocument_hover] = function(err, response, ctx, conf) +M[ms.textDocument_hover] = function(err, response, ctx) if not response then -- no response, nothing to do return @@ -33,10 +34,10 @@ M[ms.textDocument_hover] = function(err, response, ctx, conf) ctx.params.textDocument.uri = ctx.params.otter.main_uri -- pass modified response on to the default handler - return err, response, ctx, conf + return err, response, ctx end -M[ms.textDocument_inlayHint] = function(err, response, ctx, conf) +M[ms.textDocument_inlayHint] = function(err, response, ctx) if not response then return end @@ -44,10 +45,10 @@ M[ms.textDocument_inlayHint] = function(err, response, ctx, conf) -- pretend the response is coming from the main buffer ctx.params.textDocument.uri = ctx.params.otter.main_uri - return err, response, ctx, conf + return err, response, ctx end -M[ms.textDocument_definition] = function(err, response, ctx, conf) +M[ms.textDocument_definition] = function(err, response, ctx) if not response then return end @@ -66,11 +67,10 @@ M[ms.textDocument_definition] = function(err, response, ctx, conf) return res end response = filter_one_or_many(response, filter) - return err, response, ctx, conf + return err, response, ctx end -M[ms.textDocument_documentSymbol] = function(err, response, ctx, conf) - conf = conf or {} +M[ms.textDocument_documentSymbol] = function(err, response, ctx) if not response then return end @@ -89,10 +89,10 @@ M[ms.textDocument_documentSymbol] = function(err, response, ctx, conf) response = filter_one_or_many(response, filter) ctx.params.textDocument.uri = fn.otterpath_to_path(ctx.params.textDocument.uri) - return err, response, ctx, conf + return err, response, ctx end -M[ms.textDocument_typeDefinition] = function(err, response, ctx, conf) +M[ms.textDocument_typeDefinition] = function(err, response, ctx) if not response then return end @@ -112,10 +112,10 @@ M[ms.textDocument_typeDefinition] = function(err, response, ctx, conf) end response = filter_one_or_many(response, filter) - return err, response, ctx, conf + return err, response, ctx end -M[ms.textDocument_rename] = function(err, response, ctx, conf) +M[ms.textDocument_rename] = function(err, response, ctx) if not response then return end @@ -148,10 +148,10 @@ M[ms.textDocument_rename] = function(err, response, ctx, conf) end end response = filter_one_or_many(response, filter) - return err, response, ctx, conf + return err, response, ctx end -M[ms.textDocument_references] = function(err, response, ctx, conf) +M[ms.textDocument_references] = function(err, response, ctx) if not response then return end @@ -170,10 +170,10 @@ M[ms.textDocument_references] = function(err, response, ctx, conf) -- change the ctx after the otter buffer has responded ctx.params.textDocument.uri = fn.otterpath_to_path(ctx.params.textDocument.uri) - return err, response, ctx, conf + return err, response, ctx end -M[ms.textDocument_implementation] = function(err, response, ctx, conf) +M[ms.textDocument_implementation] = function(err, response, ctx) if not response then return end @@ -193,10 +193,10 @@ M[ms.textDocument_implementation] = function(err, response, ctx, conf) end response = filter_one_or_many(response, filter) - return err, response, ctx, conf + return err, response, ctx end -M[ms.textDocument_declaration] = function(err, response, ctx, conf) +M[ms.textDocument_declaration] = function(err, response, ctx) if not response then return end @@ -215,21 +215,38 @@ M[ms.textDocument_declaration] = function(err, response, ctx, conf) return res end response = filter_one_or_many(response, filter) - return err, response, ctx, conf + return err, response, ctx end --- M[ms.textDocument_completion] = function(err, response, ctx, conf) --- -- this handler doesn't actually get called --- -- the magic happened before where we modified the request --- -- I assume nvim-cmp and nvims omnifunc handle the response directly --- vim.lsp.handlers[ms.textDocument_completion](err, response, ctx, conf) --- end --- --- M[ms.completionItem_resolve] = function(err, response, ctx, conf) --- -- this handler doesn't actually get called --- -- the magic happened before where we modified the request --- -- I assume nvim-cmp and nvims omnifunc handle the response directly --- vim.lsp.handlers[ms.completionItem_resolve](err, response, ctx, conf) --- end +--- Modifying textDocument_completion and completionItem_resolve +--- was not strictly required in the completion handlers tested so far, +--- but why not. +--- Might come in handy down the line. +M[ms.textDocument_completion] = function(err, response, ctx) + ctx.params.textDocument.uri = ctx.params.otter.main_uri + ctx.bufnr = ctx.params.otter.main_nr + -- response.data.uri = ctx.params.otter.main_uri + -- response.textDocument.uri = ctx.params.otter.main_uri + for _, item in ipairs(response.items) do + if item.data ~= nil then + item.data.uri = ctx.params.otter.main_uri + end + -- not needed for now: + -- item.position = modify_position(item.position, ctx.params.otter.main_nr) + end + + return err, response, ctx +end + +M[ms.completionItem_resolve] = function(err, response, ctx) + ctx.params.data.uri = ctx.params.otter.main_uri + ctx.params.textDocument.uri = ctx.params.otter.main_uri + ctx.bufnr = ctx.params.otter.main_nr + + response.data.uri = ctx.params.otter.main_uri + response.textDocument.uri = ctx.params.otter.main_uri + + return err, response, ctx +end return M diff --git a/lua/otter/lsp/init.lua b/lua/otter/lsp/init.lua index a48db3c..2d6550c 100644 --- a/lua/otter/lsp/init.lua +++ b/lua/otter/lsp/init.lua @@ -6,6 +6,11 @@ local fn = require("otter.tools.functions") local capabilities = vim.lsp.protocol.make_client_capabilities() +local has_blink, blink = pcall(require, 'blink.cmp') +if has_blink then + capabilities = blink.get_lsp_capabilities({}, true) +end + local otterls = {} --- @param main_nr integer main buffer @@ -17,21 +22,14 @@ otterls.start = function(main_nr, completion) name = "otter-ls" .. "[" .. main_nr .. "]", capabilities = capabilities, cmd = function(dispatchers) + local _ = dispatchers local members = { --- Send a request to the otter buffers and handle the response. --- The response can optionally be filtered through a function. - ---@param method string lsp request method. One of ms - ---@param params table params passed from nvim with the request - ---@param handler function function(err, response, ctx, conf) + ---@param method string one of vim.lsp.protocol.Methods + ---@param params table params passed from nvim with the request params are created when vim.lsp.buf. is called and modified here to be used with the otter buffers + ---@param handler lsp.Handler function(err, response, ctx) handler is a callback function that should be called with the result depending on the method it is either a user-defined handler (e.g. user's using telescope to list references) or the default vim.lsp.handlers[method] handler ---@param _ function notify_reply_callback function. Not currently used - --- - -- params are created when vim.lsp.buf. is called - -- and modified here to be used with the otter buffers - --- - --- handler is a callback function that should be called with the result - --- depending on the method it is either our custom handler - --- (e.g. for retargeting got-to-definition results) - --- or the default vim.lsp.handlers[method] handler request = function(method, params, handler, _) -- handle initialization first if method == ms.initialize then @@ -40,6 +38,9 @@ otterls.start = function(main_nr, completion) completion_options = { triggerCharacters = { "." }, resolveProvider = true, + completionItem = { + labelDetailsSupport = true, + } } else completion_options = false @@ -52,7 +53,7 @@ otterls.start = function(main_nr, completion) declarationProvider = true, signatureHelpProvider = { triggerCharacters = { "(", "," }, - retriggerCharacters = {}, + retriggerCharacters = {} }, typeDefinitionProvider = true, renameProvider = true, @@ -72,20 +73,19 @@ otterls.start = function(main_nr, completion) } -- default handler for initialize - handler(nil, initializeResult) + handler(nil, initializeResult, params.context) + return + elseif params == nil then + -- can params be nil? return elseif method == ms.shutdown then -- TODO: how do we actually stop otter-ls? -- it's just a function in memory, -- no external process + handler(nil, nil, params.context) return elseif method == ms.exit then - return - end - - if params == nil then - -- empty params - -- nothing to be done + handler(nil, nil, params.context) return end @@ -107,6 +107,7 @@ otterls.start = function(main_nr, completion) local has_otter = fn.contains(keeper.rafts[main_nr].languages, lang) if not has_otter then -- if we don't have an otter for lang, there is nothing to be done + handler(nil, nil, params.context) return end @@ -124,6 +125,7 @@ otterls.start = function(main_nr, completion) end if not supports_method then -- no server attached to the otter buffer supports this method + handler(nil, nil, params.context) return end @@ -131,6 +133,7 @@ otterls.start = function(main_nr, completion) local success = keeper.sync_raft(main_nr, lang) if not success then -- no otter buffer for lang + handler(nil, nil, params.context) return end @@ -156,19 +159,22 @@ otterls.start = function(main_nr, completion) -- send the request to the otter buffer -- modification of the response is done by our handler -- and then passed on to the default handler or user-defined handler - vim.lsp.buf_request(otter_nr, method, params, function(err, result, context, config) + vim.lsp.buf_request(otter_nr, method, params, function(err, result, ctx) if handlers[method] ~= nil then - err, result, context, config = handlers[method](err, result, context, config) + err, result, ctx = handlers[method](err, result, ctx) end - handler(err, result, context, config) + handler(err, result, ctx) end) end, + --- Handle notify events + --- @param method string one of vim.lsp.protocol.Methods + --- @param params table notify = function(method, params) + local _, _ = method, params -- we don't actually notify otter buffers - -- they get their notifications + -- they get their change notifications -- via nvim's clients attached to - -- the buffers - -- when we change their text + -- the buffers when we sync their text end, is_closing = function() end, terminate = function() end, @@ -176,10 +182,26 @@ otterls.start = function(main_nr, completion) return members end, init_options = {}, - before_init = function(params, config) end, - on_init = function(client, initialize_result) end, + ---@param params lsp.ConfigurationParams + ---@param config table + before_init = function(params, config) + local _, _ = params, config + -- nothing to be done + end, + ---@param client vim.lsp.Client + ---@param initialize_result lsp.InitializeResult + on_init = function(client, initialize_result) + local _, _ = client, initialize_result + -- nothing to be done + end, root_dir = require("otter.config").cfg.lsp.root_dir(), - on_exit = function(code, signal, client_id) end, + ---@param code integer + ---@param signal integer + ---@param client_id integer + on_exit = function(code, signal, client_id) + local _, _, _ = code, signal, client_id + -- nothing to be done + end, }) return client_id diff --git a/lua/otter/tools/functions.lua b/lua/otter/tools/functions.lua index c10665c..ca78de1 100644 --- a/lua/otter/tools/functions.lua +++ b/lua/otter/tools/functions.lua @@ -29,6 +29,9 @@ M.strip_wrapping_quotes = function(s) return s, false end +---split a string by newlines +---@param str string +---@return string[] M.lines = function(str) local result = {} for line in str:gmatch("([^\n]*)\n?") do