diff --git a/configuration.nix b/configuration.nix index 8831dab7..8182b06e 100644 --- a/configuration.nix +++ b/configuration.nix @@ -74,6 +74,8 @@ isMaximal: { typst.enable = isMaximal; rust = { enable = isMaximal; + # Can only be enabled if lsp.enable = false + extensions.rustaceanvim.enable = false; extensions.crates-nvim.enable = isMaximal; }; toml.enable = isMaximal; diff --git a/docs/manual/release-notes/rl-0.9.md b/docs/manual/release-notes/rl-0.9.md index ce0e34a4..48c40d73 100644 --- a/docs/manual/release-notes/rl-0.9.md +++ b/docs/manual/release-notes/rl-0.9.md @@ -133,6 +133,11 @@ - Turned `omnisharp-extended-lsp-nvim` into an extension disabled by default - Turned `csharpls-extended-lsp-nvim` into an extension disabled by default +[sjcobb2022](https://github.com/caueanjos) + +- Rust module is now no longer dependant on `rustaceanvim` by default. Use + `vim.languages.rust.extensions.rustaceanvim.enable` if needed. + ## Changelog {#sec-release-0-9-changelog} [bovf](https://github.com/bovf): @@ -600,4 +605,10 @@ https://github.com/gorbit99/codewindow.nvim - Add `prettier` and `prettierd` as supported formatters to `vim.languages.json`. +[sjcobb2022](https://github.com/sjcobb2022) + +- Modernize rust toolchain by defaulting to a `rustaceanvim` free default + configuration. Enabled via configuration + `vim.languages.rust.extensions.rustaceanvim.enable`. + diff --git a/lib/types/default.nix b/lib/types/default.nix index 57fb43fe..d4247df9 100644 --- a/lib/types/default.nix +++ b/lib/types/default.nix @@ -11,6 +11,6 @@ in { inherit (typesDag) dagOf; inherit (typesPlugin) pluginsOpt extraPluginType mkPluginSetupOption luaInline pluginType borderType; inherit (typesLanguage) diagnostics mkGrammarOption mkTreesitterGrammarOption; - inherit (typesLsp) mkLspPresetEnableOption; + inherit (typesLsp) mkLspPresetEnableOption mkLspPresetEnableOptionWithDesc; inherit (customTypes) char hexColor mergelessListOf deprecatedSingleOrListOf enumWithRename; } diff --git a/lib/types/lsp.nix b/lib/types/lsp.nix index d05b8783..7e9ce1ba 100644 --- a/lib/types/lsp.nix +++ b/lib/types/lsp.nix @@ -1,12 +1,23 @@ {lib}: let - inherit (lib.options) mkEnableOption; + inherit (lib.options) mkOption; mkLspPresetEnableOption = option: display: fileTypes: - mkEnableOption '' - the ${display} Language Server. - Default `filetypes = ${lib.generators.toPretty {} fileTypes}`. - Use {option}`vim.lsp.servers.${option}` for customization - ''; + mkLspPresetEnableOptionWithDesc option display fileTypes ""; + + mkLspPresetEnableOptionWithDesc = option: display: fileTypes: description: + mkOption { + type = lib.types.bool; + default = false; + description = lib.removeSuffix "\n" ('' + The ${display} Language Server. + Default `filetypes = ${lib.generators.toPretty {} fileTypes}`. + Use {option}`vim.lsp.servers.${option}` for customization. + '' + + lib.optionalString (description != "") '' + + ${description} + ''); + }; in { - inherit mkLspPresetEnableOption; + inherit mkLspPresetEnableOption mkLspPresetEnableOptionWithDesc; } diff --git a/modules/extra/deprecations.nix b/modules/extra/deprecations.nix index 8ec5a3ef..88cacb28 100644 --- a/modules/extra/deprecations.nix +++ b/modules/extra/deprecations.nix @@ -388,5 +388,11 @@ in { nvim-tree.lua removed system_open and now uses Neovim's vim.ui.open(). '') ] + + # 2026-06-05 + [ + (mkRemovedLspPackage "rust") + (mkRemovedLspOpt "rust") + ] ]; } diff --git a/modules/plugins/languages/rust.nix b/modules/plugins/languages/rust.nix index db12f033..aa0b711f 100644 --- a/modules/plugins/languages/rust.nix +++ b/modules/plugins/languages/rust.nix @@ -5,19 +5,23 @@ ... }: let inherit (lib.meta) getExe; + inherit (lib) genAttrs; inherit (lib.modules) mkIf mkMerge; inherit (lib.options) mkOption mkEnableOption literalMD literalExpression; - inherit (lib.strings) optionalString; - inherit (lib.lists) isList; inherit (lib.attrsets) attrNames; - inherit (lib.types) bool package str listOf either enum int; - inherit (lib.nvim.lua) toLuaObject; + inherit (lib.types) bool package listOf enum int; inherit (lib.nvim.attrsets) mapListToAttrs; + inherit (lib.nvim.dag) entryAfter; + inherit (lib.nvim.lua) toLuaObject; inherit (lib.nvim.types) mkGrammarOption mkPluginSetupOption deprecatedSingleOrListOf; - inherit (lib.nvim.dag) entryAfter entryAnywhere; + inherit (lib.strings) optionalString; + inherit (lib.generators) mkLuaInline; cfg = config.vim.languages.rust; + servers = ["rust-analyzer"]; + defaultServers = ["rust-analyzer"]; + defaultFormat = ["rustfmt"]; formats = { rustfmt = { @@ -40,31 +44,16 @@ in { lsp = { enable = - mkEnableOption "Rust LSP support (rust-analyzer with extra tools)" + mkEnableOption "Rust LSP support" // { default = config.vim.lsp.enable; defaultText = literalExpression "config.vim.lsp.enable"; }; - package = mkOption { - description = "rust-analyzer package, or the command to run as a list of strings"; - example = ''[lib.getExe pkgs.jdt-language-server "-data" "~/.cache/jdtls/workspace"]''; - type = either package (listOf str); - default = pkgs.rust-analyzer; - }; - opts = mkOption { - description = "Options to pass to rust analyzer"; - type = str; - default = ""; - example = '' - ['rust-analyzer'] = { - cargo = {allFeature = true}, - checkOnSave = true, - procMacro = { - enable = true, - }, - }, - ''; + servers = mkOption { + type = listOf (enum servers); + default = defaultServers; + description = "Rust LSP server to use"; }; }; @@ -166,6 +155,96 @@ in { }; }; }; + + rustaceanvim = { + enable = mkEnableOption "additional rust support [rustaceanvim]"; + setupOpts = mkPluginSetupOption "rustaceanvim" { + tools = mkOption { + description = "Plugin configuration"; + default = { + hover_actions = { + replace_builtin_hover = false; + }; + }; + example = { + hover_actions = { + replace_builtin_hover = true; + }; + }; + }; + server = mkOption { + description = "LSP configuration"; + default = { + # For some reason rustaceanvim needs the command set explicitly, and does not pick up on vim.lsp.config settings. + cmd = [(getExe pkgs.rust-analyzer)]; + + on_attach = mkLuaInline '' + function(client, bufnr) + default_on_attach(client, bufnr) + local opts = { noremap=true, silent=true, buffer = bufnr } + + ${optionalString config.vim.vendoredKeymaps.enable '' + vim.keymap.set("n", "rr", ":RustLsp runnables", opts) + vim.keymap.set("n", "rp", ":RustLsp parentModule", opts) + vim.keymap.set("n", "rm", ":RustLsp expandMacro", opts) + vim.keymap.set("n", "rc", ":RustLsp openCargo", opts) + vim.keymap.set("n", "rg", ":RustLsp crateGraph x11", opts) + ''} + + ${optionalString (cfg.dap.enable && config.vim.vendoredKeymaps.enable) '' + vim.keymap.set("n", "rd", ":RustLsp debuggables", opts) + ''} + + ${optionalString (cfg.dap.enable && config.vim.debugger.nvim-dap.mappings.continue != null) '' + vim.keymap.set( + "n", "${config.vim.debugger.nvim-dap.mappings.continue}", + function() + local dap = require("dap") + if dap.status() == "" then + vim.cmd "RustLsp debuggables" + else + dap.continue() + end + end, + opts + ) + ''} + end + ''; + }; + example = { + on_attach = mkLuaInline '' + function(client, bufnr) + -- you can also put keymaps in here + end, + ''; + }; + }; + dap = mkOption { + description = "DAP configuration"; + default = { + adapter = + if cfg.dap.adapter == "lldb-dap" + then + mkLuaInline '' + { + type = "executable", + command = "${cfg.dap.package}/bin/lldb-dap", + name = "rustacean_lldb", + }'' + else let + codelldb = pkgs.vscode-extensions.vadimcn.vscode-lldb.adapter; + codelldbPath = "${codelldb}/bin/codelldb"; + liblldbPath = "${codelldb}/share/lldb/lib/liblldb.so"; + in + mkLuaInline '' + require("rustaceanvim.config").get_codelldb_adapter("${codelldbPath}", "${liblldbPath}") + ''; + }; + example = {}; + }; + }; + }; }; }; @@ -190,82 +269,36 @@ in { }; }) - (mkIf (cfg.lsp.enable || cfg.dap.enable) { + (mkIf cfg.lsp.enable { + vim.lsp = { + presets = genAttrs cfg.lsp.servers (_: {enable = true;}); + servers = genAttrs cfg.lsp.servers (_: { + filetypes = [ + "rust" + ]; + }); + }; + }) + + (mkIf cfg.extensions.rustaceanvim.enable { vim = { startPlugins = ["rustaceanvim"]; pluginRC.rustaceanvim = entryAfter ["lsp-setup"] '' - vim.g.rustaceanvim = { - ${optionalString cfg.lsp.enable '' - -- LSP - tools = { - hover_actions = { - replace_builtin_hover = false - }, - }, - server = { - cmd = ${ - if isList cfg.lsp.package - then toLuaObject cfg.lsp.package - else ''{"${cfg.lsp.package}/bin/rust-analyzer"}'' - }, - default_settings = { - ${cfg.lsp.opts} - }, - on_attach = function(client, bufnr) - default_on_attach(client, bufnr) - local opts = { noremap=true, silent=true, buffer = bufnr } - - ${optionalString config.vim.vendoredKeymaps.enable '' - vim.keymap.set("n", "rr", ":RustLsp runnables", opts) - vim.keymap.set("n", "rp", ":RustLsp parentModule", opts) - vim.keymap.set("n", "rm", ":RustLsp expandMacro", opts) - vim.keymap.set("n", "rc", ":RustLsp openCargo", opts) - vim.keymap.set("n", "rg", ":RustLsp crateGraph x11", opts) - ''} - - ${optionalString (cfg.dap.enable && config.vim.vendoredKeymaps.enable) '' - vim.keymap.set("n", "rd", ":RustLsp debuggables", opts) - ''} - - ${optionalString (cfg.dap.enable && config.vim.debugger.nvim-dap.mappings.continue != null) '' - vim.keymap.set( - "n", "${config.vim.debugger.nvim-dap.mappings.continue}", - function() - local dap = require("dap") - if dap.status() == "" then - vim.cmd "RustLsp debuggables" - else - dap.continue() - end - end, - opts - ) - ''} - end - }, - ''} - - ${optionalString cfg.dap.enable '' - dap = { - adapter = ${ - if cfg.dap.adapter == "lldb-dap" - then '' - { - type = "executable", - command = "${cfg.dap.package}/bin/lldb-dap", - name = "rustacean_lldb", - }'' - else let - codelldb = pkgs.vscode-extensions.vadimcn.vscode-lldb.adapter; - codelldbPath = "${codelldb}/bin/codelldb"; - liblldbPath = "${codelldb}/share/lldb/lib/liblldb.so"; - in ''require("rustaceanvim.config").get_codelldb_adapter("${codelldbPath}", "${liblldbPath}")'' - }, - }, - ''} - } + vim.g.rustaceanvim = function() + return ${toLuaObject cfg.extensions.rustaceanvim.setupOpts} + end ''; }; + + assertions = [ + { + assertion = !(builtins.elem "rust-analyzer" cfg.lsp.server) && !config.vim.lsp.rust-analyzer.enable; + message = '' + Rustaceanvim fully manages its own rust-analyzer. + Therefore you can't use vim.langauges.rust.extensions.rustaceanvim.enable with rust-analyzer enabled. + ''; + } + ]; }) (mkIf cfg.extensions.crates-nvim.enable { diff --git a/modules/plugins/lsp/presets/default.nix b/modules/plugins/lsp/presets/default.nix index 1b4bdedf..71bf538f 100644 --- a/modules/plugins/lsp/presets/default.nix +++ b/modules/plugins/lsp/presets/default.nix @@ -58,6 +58,7 @@ ./ruby-lsp.nix ./ruff.nix ./rumdl.nix + ./rust-analyzer.nix ./solargraph.nix ./some-sass-language-server.nix ./sqls.nix diff --git a/modules/plugins/lsp/presets/rust-analyzer.nix b/modules/plugins/lsp/presets/rust-analyzer.nix new file mode 100644 index 00000000..df985160 --- /dev/null +++ b/modules/plugins/lsp/presets/rust-analyzer.nix @@ -0,0 +1,193 @@ +{ + config, + lib, + pkgs, + ... +}: let + inherit (lib.meta) getExe; + inherit (lib.modules) mkIf; + inherit (lib.nvim.types) mkLspPresetEnableOptionWithDesc; + inherit (lib.nvim.dag) entryBefore; + inherit (lib.generators) mkLuaInline; + + cfg = config.vim.lsp.presets.rust-analyzer; +in { + options.vim.lsp.presets.rust-analyzer = { + enable = + mkLspPresetEnableOptionWithDesc "rust-analyzer" "Rust Analyzer" [] ''Note: do not set `init_options` for this LS config, it will be automatically populated by the contents of settings["rust-analyzer"] per https://github.com/rust-lang/rust-analyzer/blob/eb5da56d839ae0a9e9f50774fa3eb78eb0964550/docs/dev/lsp-extensions.md?plain=1#L26''; + }; + + config = mkIf cfg.enable { + # Taken from https://github.com/neovim/nvim-lspconfig/blob/07dff35e7c95288861200b788ef32d6103f107f0/lsp/rust_analyzer.lua + + # This code provides utility for cargo workspace functionality, as it is not wired by default. + vim.luaConfigRC.rust-analyzer = entryBefore ["lsp-servers"] '' + local function rust_reload_workspace(bufnr) + local clients = vim.lsp.get_clients { bufnr = bufnr, name = 'rust-analyzer' } + for _, client in ipairs(clients) do + vim.notify 'Reloading Cargo Workspace' + ---@diagnostic disable-next-line:param-type-mismatch + client:request('rust-analyzer/reloadWorkspace', nil, function(err) + if err then + error(tostring(err)) + end + vim.notify 'Cargo workspace reloaded' + end, 0) + end + end + + local function rust_user_sysroot_src() + return vim.tbl_get(vim.lsp.config['rust-analyzer'], 'settings', 'rust-analyzer', 'cargo', 'sysrootSrc') + end + + -- Determine location of sysroot for stdlib + local function rust_default_sysroot_src() + local sysroot = vim.tbl_get(vim.lsp.config['rust-analyzer'], 'settings', 'rust-analyzer', 'cargo', 'sysroot') + if not sysroot then + local rustc = os.getenv 'RUSTC' or 'rustc' + local result = vim.system({ rustc, '--print', 'sysroot' }, { text = true }):wait() + + local stdout = result.stdout + if result.code == 0 and stdout then + if string.sub(stdout, #stdout) == '\n' then + if #stdout > 1 then + sysroot = string.sub(stdout, 1, #stdout - 1) + else + sysroot = ''' + end + else + sysroot = stdout + end + end + end + + return sysroot and vim.fs.joinpath(sysroot, 'lib/rustlib/src/rust/library') or nil + end + + -- Determine if a given file belongs to an external library or our own code. + local function rust_is_library(fname) + local user_home = vim.fs.normalize(vim.env.HOME) + local cargo_home = os.getenv 'CARGO_HOME' or user_home .. '/.cargo' + local registry = cargo_home .. '/registry/src' + local git_registry = cargo_home .. '/git/checkouts' + + local rustup_home = os.getenv 'RUSTUP_HOME' or user_home .. '/.rustup' + local toolchains = rustup_home .. '/toolchains' + + local sysroot_src = rust_user_sysroot_src() or rust_default_sysroot_src() + + for _, item in ipairs { toolchains, registry, git_registry, sysroot_src } do + if item and vim.fs.relpath(item, fname) then + local clients = vim.lsp.get_clients { name = 'rust-analyzer' } + return #clients > 0 and clients[#clients].config.root_dir or nil + end + end + end + ''; + + vim.lsp.servers.rust-analyzer = { + enable = true; + cmd = [(getExe pkgs.rust-analyzer)]; + + on_attach = mkLuaInline '' + function(client, bufnr) + vim.api.nvim_buf_create_user_command(bufnr, 'LspCargoReload', function() + rust_reload_workspace(bufnr) + end, { desc = 'Reload current cargo workspace' }) + end + ''; + + # Sends init_params beforehand according to rust-analyzer spec. + # See https://github.com/rust-lang/rust-analyzer/blob/eb5da56d839ae0a9e9f50774fa3eb78eb0964550/docs/dev/lsp-extensions.md?plain=1#L26 + before_init = mkLuaInline '' + function(init_params, config) + if config.settings and config.settings['rust-analyzer'] then + init_params.initializationOptions = config.settings['rust-analyzer'] + end + + -- Allow for a single run of a program + vim.lsp.commands['rust-analyzer.runSingle'] = function(command) + local r = command.arguments[1] + local cmd = { 'cargo', unpack(r.args.cargoArgs) } + if r.args.executableArgs and #r.args.executableArgs > 0 then + vim.list_extend(cmd, { '--', unpack(r.args.executableArgs) }) + end + + local proc = vim.system(cmd, { cwd = r.args.cwd, env = r.args.environment }) + + local result = proc:wait() + + if result.code == 0 then + vim.notify(result.stdout, vim.log.levels.INFO) + else + vim.notify(result.stderr, vim.log.levels.ERROR) + end + end + end + ''; + + # eval root dir taking workspaces into consideration + root_dir = mkLuaInline '' + function(bufnr, on_dir) + local fname = vim.api.nvim_buf_get_name(bufnr) + local reused_dir = rust_is_library(fname) + if reused_dir then + on_dir(reused_dir) + return + end + + local cargo_crate_dir = vim.fs.root(fname, { 'Cargo.toml' }) + local cargo_workspace_root + + if cargo_crate_dir == nil then + on_dir( + vim.fs.root(fname, { 'rust-project.json' }) + or vim.fs.dirname(vim.fs.find('.git', { path = fname, upward = true })[1]) + ) + return + end + + local cmd = { + 'cargo', + 'metadata', + '--no-deps', + '--format-version', + '1', + '--manifest-path', + cargo_crate_dir .. '/Cargo.toml', + } + + vim.system(cmd, { text = true }, function(output) + if output.code == 0 then + if output.stdout then + local result = vim.json.decode(output.stdout) + if result['workspace_root'] then + cargo_workspace_root = vim.fs.normalize(result['workspace_root']) + end + end + + on_dir(cargo_workspace_root or cargo_crate_dir) + else + vim.schedule(function() + vim.notify(('[rust_analyzer] cmd failed with code %d: %s\n%s'):format(output.code, cmd, output.stderr)) + end) + end + end) + end + ''; + + capabilities = { + experimental = { + serverStatusNotification = true; + commands = { + commands = [ + "rust-analyzer.showReferences" + "rust-analyzer.runSingle" + "rust-analyzer.debugSingle" + ]; + }; + }; + }; + }; + }; +}