diff --git a/docs/manual/release-notes/rl-0.9.md b/docs/manual/release-notes/rl-0.9.md index 995d74f0..a479d6c6 100644 --- a/docs/manual/release-notes/rl-0.9.md +++ b/docs/manual/release-notes/rl-0.9.md @@ -108,6 +108,10 @@ `languages.go.treesitter.gotmplPackage` to {option}`vim.languages.go.treesitter.gotmpl.package` +[CaueAnjos](https://github.com/caueanjos) + +- Renamed `roslyn_ls` to `roslyn-ls` + ## Changelog {#sec-release-0-9-changelog} [SecBear](https://github.com/SecBear): diff --git a/modules/plugins/lsp/presets/default.nix b/modules/plugins/lsp/presets/default.nix index bb3ce502..8118f4c5 100644 --- a/modules/plugins/lsp/presets/default.nix +++ b/modules/plugins/lsp/presets/default.nix @@ -49,6 +49,7 @@ ./python-lsp-server.nix ./qmlls.nix ./r-languageserver.nix + ./roslyn-ls.nix ./ruby-lsp.nix ./ruff.nix ./rumdl.nix diff --git a/modules/plugins/lsp/presets/roslyn-ls.nix b/modules/plugins/lsp/presets/roslyn-ls.nix new file mode 100644 index 00000000..f24f5a73 --- /dev/null +++ b/modules/plugins/lsp/presets/roslyn-ls.nix @@ -0,0 +1,402 @@ +{ + config, + lib, + pkgs, + ... +}: let + inherit (lib.meta) getExe; + inherit (lib.modules) mkIf; + inherit (lib.nvim.types) mkLspPresetEnableOption; + inherit (lib.generators) mkLuaInline; + + cfg = config.vim.lsp.presets.roslyn-ls; +in { + options.vim.lsp.presets.roslyn-ls = { + enable = mkLspPresetEnableOption "roslyn-ls" "Roslyn" []; + }; + + config = mkIf cfg.enable { + vim.lsp.servers.roslyn-ls = { + cmd = mkLuaInline '' + { + '${getExe pkgs.roslyn-ls}', + '--logLevel', + 'Information', + '--extensionLogDirectory', + vim.fs.joinpath(vim.uv.os_tmpdir(), 'roslyn_ls/logs'), + '--stdio', + } + ''; + cmd_env = mkLuaInline '' + { + -- Fixes LSP navigation in decompiled files for systems with symlinked TMPDIR (macOS) + TMPDIR = vim.env.TMPDIR and vim.env.TMPDIR ~= "" and vim.fn.resolve(vim.env.TMPDIR) or nil, + } + ''; + handlers = { + "workspace/projectInitializationComplete" = mkLuaInline '' + function(_, _, ctx) + vim.notify('Roslyn project initialization complete', vim.log.levels.INFO, { title = 'roslyn_ls' }) + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + + local function refresh_diagnostics(client) + for buf, _ in pairs(vim.lsp.get_client_by_id(client.id).attached_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 + + refresh_diagnostics(client) + return vim.NIL + end + ''; + "workspace/_roslyn_projectNeedsRestore" = mkLuaInline '' + function(_, result, ctx) + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + + ---@diagnostic disable-next-line: param-type-mismatch + client:request('workspace/_roslyn_restore', result, function(err, response) + if err then + vim.notify(err.message, vim.log.levels.ERROR, { title = 'roslyn_ls' }) + end + if response then + for _, v in ipairs(response) do + vim.notify(v.message, vim.log.levels.INFO, { title = 'roslyn_ls' }) + end + end + end) + + return vim.NIL + end + ''; + "razor/provideDynamicFileInfo" = mkLuaInline '' + function(_, _, _) + vim.notify( + 'Razor is not supported.\nPlease use https://github.com/tris203/rzls.nvim', + vim.log.levels.WARN, + { title = 'roslyn_ls' } + ) + return vim.NIL + end + ''; + }; + commands = { + "roslyn.client.completionComplexEdit" = mkLuaInline '' + 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] + + ---@diagnostic disable: undefined-field + 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) + ---@diagnostic enable: undefined-field + else + vim.notify('roslyn_ls: completionComplexEdit args not understood: ' .. vim.inspect(args), vim.log.levels.WARN) + end + end + ''; + + "roslyn.client.nestedCodeAction" = mkLuaInline '' + function(command, ctx) + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + local arg = command.arguments and command.arguments[1] + + if type(arg) ~= 'table' then + vim.notify('roslyn_ls: invalid nestedCodeAction arguments', vim.log.levels.ERROR) + return + end + + local function handle(action) + if not action then + return + end + + if action.data and not action.edit and not action.command then + client:request('codeAction/resolve', action, function(err, resolved) + if err then + vim.notify(err.message or tostring(err), vim.log.levels.ERROR) + return + end + if resolved then + handle(resolved) + end + end, ctx.bufnr) + return + end + + local nested = vim.islist(action) and action or action.NestedCodeActions + if type(nested) ~= 'table' or vim.tbl_isempty(nested) then + local function apply_action(client, action) + if action.edit then + vim.lsp.util.apply_workspace_edit(action.edit, client.offset_encoding) + end + if action.command then + client:exec_cmd(action.command) + end + end + apply_action(client, action) + return + end + + if #nested == 1 then + handle(nested[1]) + return + end + + vim.ui.select(nested, { + prompt = action.title or 'Select code action', + format_item = function(item) + return item.title or (item.command and item.command.title) or 'Unnamed action' + end, + }, function(choice) + if choice then + handle(choice) + end + end) + end + + handle(arg) + end + ''; + + "roslyn.client.fixAllCodeAction" = mkLuaInline '' + function(command, ctx) + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + + local function handle_fix_all_action(client, command, bufnr) + local arg = command.arguments and command.arguments[1] + if type(arg) ~= 'table' then + vim.notify('roslyn_ls: invalid fixAllCodeAction arguments', vim.log.levels.ERROR) + return + end + + local flavors = arg.FixAllFlavors + if type(flavors) ~= 'table' or vim.tbl_isempty(flavors) then + vim.notify('roslyn_ls: fixAllCodeAction has no FixAllFlavors', vim.log.levels.WARN) + return + end + + vim.ui.select(flavors, { + prompt = 'Fix All Scope:', + }, function(chosen_scope) + if not chosen_scope then + return + end + + client:request('codeAction/resolveFixAll', { + title = command.title, + data = arg, + scope = chosen_scope, + }, function(err, resolved) + if err then + vim.notify( + 'roslyn_ls: fixAllCodeAction resolve error: ' .. (err.message or tostring(err)), + vim.log.levels.ERROR + ) + return + end + if resolved then + local function apply_action(client, action) + if action.edit then + vim.lsp.util.apply_workspace_edit(action.edit, client.offset_encoding) + end + if action.command then + client:exec_cmd(action.command) + end + end + apply_action(client, resolved) + end + end, bufnr) + end) + end + + handle_fix_all_action(client, command, ctx.bufnr) + end + ''; + }; + root_dir = mkLuaInline '' + function(bufnr, cb) + local bufname = vim.api.nvim_buf_get_name(bufnr) + + local function is_decompiled(bufname) + local _, endpos = bufname:find('[/\\]MetadataAsSource[/\\]') + if endpos == nil then + return false + end + return vim.fn.finddir(bufname:sub(1, endpos), vim.uv.os_tmpdir()) ~= "" + end + + -- don't try to find sln or csproj for files from libraries + -- outside of the project + if not is_decompiled(bufname) then + -- try find solutions root first + local root_dir = vim.fs.root(bufnr, function(fname, _) + return fname:match('%.sln[x]?$') ~= nil + end) + + if not root_dir then + -- try find projects root + root_dir = vim.fs.root(bufnr, function(fname, _) + return fname:match('%.csproj$') ~= nil + end) + end + + if root_dir then + cb(root_dir) + end + else + -- Decompiled code (example: "/tmp/MetadataAsSource/f2bfba/DecompilationMetadataAsSourceFileProvider/d5782a/Console.cs") + local prev_buf = vim.fn.bufnr('#') + local client = vim.lsp.get_clients({ + name = 'roslyn_ls', + bufnr = prev_buf ~= 1 and prev_buf or nil, + })[1] + if client then + cb(client.config.root_dir) + end + end + end + ''; + on_init = [ + (mkLuaInline + '' + function(client) + local root_dir = client.config.root_dir + + local function on_init_sln(client, target) + vim.notify('Initializing: ' .. target, vim.log.levels.TRACE, { title = 'roslyn_ls' }) + ---@diagnostic disable-next-line: param-type-mismatch + client:notify('solution/open', { + solution = vim.uri_from_fname(target), + }) + end + + + local function on_init_project(client, project_files) + vim.notify('Initializing: projects', vim.log.levels.TRACE, { title = 'roslyn_ls' }) + ---@diagnostic disable-next-line: param-type-mismatch + client:notify('project/open', { + projects = vim.tbl_map(function(file) + return vim.uri_from_fname(file) + end, project_files), + }) + end + + -- try load first solution we find + for entry, type in vim.fs.dir(root_dir) do + if type == 'file' and (vim.endswith(entry, '.sln') or vim.endswith(entry, '.slnx')) then + on_init_sln(client, vim.fs.joinpath(root_dir, entry)) + return + end + end + + -- if no solution is found load project + for entry, type in vim.fs.dir(root_dir) do + if type == 'file' and vim.endswith(entry, '.csproj') then + on_init_project(client, { vim.fs.joinpath(root_dir, entry) }) + end + end + end + '') + ]; + on_attach = mkLuaInline '' + function(client, bufnr) + -- avoid duplicate autocmds for same buffer + if vim.api.nvim_get_autocmds({ buffer = bufnr, group = group })[1] then + return + end + + local function refresh_diagnostics(client) + for buf, _ in pairs(vim.lsp.get_client_by_id(client.id).attached_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 + + vim.api.nvim_create_autocmd({ 'BufWritePost', 'InsertLeave' }, { + group = group, + buffer = bufnr, + callback = function() + local function refresh_diagnostics(client) + for buf, _ in pairs(vim.lsp.get_client_by_id(client.id).attached_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 + 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 = { + diagnostic = { + dynamicRegistration = true; + }; + }; + }; + settings = { + "csharp|background_analysis" = { + dotnet_analyzer_diagnostics_scope = "fullSolution"; + dotnet_compiler_diagnostics_scope = "fullSolution"; + }; + "csharp|inlay_hints" = { + csharp_enable_inlay_hints_for_implicit_object_creation = true; + csharp_enable_inlay_hints_for_implicit_variable_types = true; + csharp_enable_inlay_hints_for_lambda_parameter_types = true; + csharp_enable_inlay_hints_for_types = true; + dotnet_enable_inlay_hints_for_indexer_parameters = true; + dotnet_enable_inlay_hints_for_literal_parameters = true; + dotnet_enable_inlay_hints_for_object_creation_parameters = true; + dotnet_enable_inlay_hints_for_other_parameters = true; + dotnet_enable_inlay_hints_for_parameters = true; + dotnet_suppress_inlay_hints_for_parameters_that_differ_only_by_suffix = true; + dotnet_suppress_inlay_hints_for_parameters_that_match_argument_name = true; + dotnet_suppress_inlay_hints_for_parameters_that_match_method_intent = true; + }; + "csharp|symbol_search" = { + dotnet_search_reference_assemblies = true; + }; + "csharp|completion" = { + dotnet_show_name_completion_suggestions = true; + dotnet_show_completion_items_from_unimported_namespaces = true; + dotnet_provide_regex_completions = true; + }; + "csharp|code_lens" = { + dotnet_enable_references_code_lens = true; + }; + }; + }; + }; +}