From 5d785f0e9fd95124fa6332ebc30d66607ea3ca76 Mon Sep 17 00:00:00 2001 From: Minh Khoi Do <107194093+khoido2003@users.noreply.github.com> Date: Wed, 3 Sep 2025 22:09:46 +0700 Subject: feat(roslyn_ls): support override completion and refresh diagnostics #4050 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: - Diagnostics were only refreshed for the current buffer, not all buffers managed by the client. In multi-file C# projects, this left diagnostics stale unless the user switched buffers. - Roslyn did not handle override method completion correctly. Solution: - Added `roslyn.client.completionComplexEdit` handler to apply edits for override completions. - Added `refresh_diagnostics` function, called on project initialization and on `BufWritePost` / `InsertLeave` events, to proactively pull diagnostics for all client buffers. Rationale: - Roslyn LSP primarily uses pull-based diagnostics. Neovim currently only triggers diagnostic pulls on text changes in the active buffer. This makes `debounce_text_changes` insufficient for Roslyn, as changes in one file can affect diagnostics across the entire solution. - The autocmds act as a server-specific workaround to mimic an “on-save/on-idle” pull model. --- lsp/roslyn_ls.lua | 69 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/lsp/roslyn_ls.lua b/lsp/roslyn_ls.lua index b7437da5..0469e1ad 100644 --- a/lsp/roslyn_ls.lua +++ b/lsp/roslyn_ls.lua @@ -22,6 +22,8 @@ local uv = vim.uv local fs = vim.fs +local group = vim.api.nvim_create_augroup('lspconfig.roslyn_ls', { clear = true }) + ---@param client vim.lsp.Client ---@param target string local function on_init_sln(client, target) @@ -44,19 +46,30 @@ local function on_init_project(client, project_files) }) end +---@param client vim.lsp.Client +local function refresh_diagnostics(client) + local buffers = vim.lsp.get_buffers_by_client_id(client.id) + for _, buf in ipairs(buffers) do + if vim.api.nvim_buf_is_loaded(buf) then + client:request( + vim.lsp.protocol.Methods.textDocument_diagnostic, + { textDocument = vim.lsp.util.make_text_document_params(buf) }, + nil, + buf + ) + end + end +end + local function roslyn_handlers() return { ['workspace/projectInitializationComplete'] = function(_, _, ctx) vim.notify('Roslyn project initialization complete', vim.log.levels.INFO, { title = 'roslyn_ls' }) - - local buffers = vim.lsp.get_buffers_by_client_id(ctx.client_id) local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) - for _, buf in ipairs(buffers) do - client:request(vim.lsp.protocol.Methods.textDocument_diagnostic, { - textDocument = vim.lsp.util.make_text_document_params(buf), - }, nil, buf) - end + refresh_diagnostics(client) + return vim.NIL end, + ['workspace/_roslyn_projectHasUnresolvedDependencies'] = function() vim.notify('Detected missing dependencies. Run `dotnet restore` command.', vim.log.levels.ERROR, { title = 'roslyn_ls', @@ -105,6 +118,31 @@ return { }, filetypes = { 'cs' }, handlers = roslyn_handlers(), + + commands = { + ['roslyn.client.completionComplexEdit'] = function(command, ctx) + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + local args = command.arguments or {} + local uri, edit = args[1], args[2] + + if uri and edit and edit.newText and edit.range then + local workspace_edit = { + changes = { + [uri.uri] = { + { + range = edit.range, + newText = edit.newText, + }, + }, + }, + } + vim.lsp.util.apply_workspace_edit(workspace_edit, client.offset_encoding) + else + vim.notify('roslyn_ls: completionComplexEdit args not understood: ' .. vim.inspect(args), vim.log.levels.WARN) + end + end, + }, + root_dir = function(bufnr, cb) local bufname = vim.api.nvim_buf_get_name(bufnr) -- don't try to find sln or csproj for files from libraries @@ -147,6 +185,23 @@ return { end end, }, + + on_attach = function(client, bufnr) + -- avoid duplicate autocmds for same buffer + if vim.api.nvim_get_autocmds({ buffer = bufnr, group = group })[1] then + return + end + + vim.api.nvim_create_autocmd({ 'BufWritePost', 'InsertLeave' }, { + group = group, + buffer = bufnr, + callback = function() + refresh_diagnostics(client) + end, + desc = 'roslyn_ls: refresh diagnostics', + }) + end, + capabilities = { -- HACK: Doesn't show any diagnostics if we do not set this to true textDocument = { -- cgit v1.2.3-70-g09d2