diff --git a/modules/lsp/servers/default.nix b/modules/lsp/servers/default.nix index 9f0ba63d..3ea31130 100644 --- a/modules/lsp/servers/default.nix +++ b/modules/lsp/servers/default.nix @@ -10,6 +10,7 @@ let inherit (lib.nixvim) toLuaObject; cfg = config.lsp; + oldCfg = config.plugins.lsp; # Import `server.nix` and apply args # For convenience, we set a default here for args.pkgs @@ -156,13 +157,37 @@ in let luaName = toLuaObject server.name; luaSettings = toLuaObject server.settings; + wrap = server.__settingsWrapper or lib.id; in [ - (lib.mkIf (server.settings != { }) "vim.lsp.config(${luaName}, ${luaSettings})") + (lib.mkIf (server.settings != { }) "vim.lsp.config(${luaName}, ${wrap luaSettings})") (lib.mkIf (server.activate or false) "vim.lsp.enable(${luaName})") ]; in - lib.mkMerge (builtins.concatMap mkServerConfig enabledServers); + lib.mkMerge ( + # Implement the legacy settings wrapping and capabilities mutation when `plugins.lsp` is enabled + lib.optional oldCfg.enable '' + local __lspCapabilities = function() + local capabilities = vim.lsp.protocol.make_client_capabilities() + + ${oldCfg.capabilities} + + return capabilities + end + + local __setup = ${lib.foldr lib.id "{ capabilities = __lspCapabilities() }" oldCfg.setupWrappers} + + local __wrapSettings = function(settings) + if settings == nil then + settings = __setup + else + settings = vim.tbl_extend("keep", settings, __setup) + end + return settings + end + '' + ++ builtins.concatMap mkServerConfig enabledServers + ); # Propagate per-server warnings warnings = lib.mkIf (serverWarnings != [ ]) serverWarnings; diff --git a/plugins/lsp/default.nix b/plugins/lsp/default.nix index 5927c970..2130c17b 100644 --- a/plugins/lsp/default.nix +++ b/plugins/lsp/default.nix @@ -1,4 +1,9 @@ -{ lib, config, ... }: +{ + lib, + config, + options, + ... +}: let inherit (lib) mkOption types; in @@ -11,7 +16,10 @@ lib.nixvim.plugins.mkNeovimPlugin { maintainers = [ lib.maintainers.HeitorAugustoLN ]; - imports = [ ./language-servers ]; + imports = [ + ./language-servers + (lib.mkRemovedOptionModule [ "plugins" "lsp" "enabledServers" ] "Internal option has been removed.") + ]; extraOptions = { keymaps = { @@ -77,28 +85,6 @@ lib.nixvim.plugins.mkNeovimPlugin { }; }; - enabledServers = mkOption { - type = - with types; - listOf (submodule { - options = { - name = mkOption { - type = str; - description = "The server's name"; - }; - - extraOptions = mkOption { - type = attrsOf anything; - description = "Extra options for the server"; - }; - }; - }); - description = "A list of enabled LSP servers. Don't use this directly."; - default = [ ]; - internal = true; - visible = false; - }; - inlayHints = mkOption { type = types.bool; default = false; @@ -136,13 +122,33 @@ lib.nixvim.plugins.mkNeovimPlugin { preConfig = mkOption { type = types.lines; description = "Code to be run before loading the LSP. Useful for requiring plugins"; - default = ""; + # When `plugins.lsp` is enabled, definitions are aliased to `lsp.luaConfig.pre`; so read that final value here. + # This is slightly complicated because `lsp.luaConfig.pre` is nullable, unlike this option. + # The other half of this two-way alias is below in `extraConfig`. + apply = + value: + if config.plugins.lsp.enable then + if config.lsp.luaConfig.pre == null then "" else config.lsp.luaConfig.pre + else if options.plugins.lsp.preConfig.isDefined then + value + else + ""; }; postConfig = mkOption { type = types.lines; description = "Code to be run after loading the LSP. This is an internal option"; - default = ""; + # When `plugins.lsp` is enabled, definitions are aliased to `lsp.luaConfig.post`; so read that final value here. + # This is slightly complicated because `lsp.luaConfig.post` is nullable, unlike this option. + # The other half of this two-way alias is below in `extraConfig`. + apply = + value: + if config.plugins.lsp.enable then + if config.lsp.luaConfig.post == null then "" else config.lsp.luaConfig.post + else if options.plugins.lsp.postConfig.isDefined then + value + else + ""; }; }; @@ -195,46 +201,8 @@ lib.nixvim.plugins.mkNeovimPlugin { lsp = { onAttach = lib.modules.mkAliasAndWrapDefsWithPriority lib.id opts.onAttach; inlayHints.enable = lib.modules.mkAliasAndWrapDefsWithPriority lib.id opts.inlayHints; + luaConfig.pre = lib.modules.mkAliasAndWrapDefsWithPriority lib.id opts.preConfig; + luaConfig.post = lib.modules.mkAliasAndWrapDefsWithPriority lib.id opts.postConfig; }; - - plugins.lsp.luaConfig.content = - let - runWrappers = - wrappers: s: - if wrappers == [ ] then s else (builtins.head wrappers) (runWrappers (builtins.tail wrappers) s); - in - '' - -- nvim-lspconfig {{{ - do - ${cfg.preConfig} - - local __lspServers = ${lib.nixvim.toLuaObject cfg.enabledServers} - - local __lspCapabilities = function() - capabilities = vim.lsp.protocol.make_client_capabilities() - - ${cfg.capabilities} - - return capabilities - end - - local __setup = ${runWrappers cfg.setupWrappers "{ capabilities = __lspCapabilities() }"} - - for i, server in ipairs(__lspServers) do - local options = ${runWrappers cfg.setupWrappers "server.extraOptions"} - - if options == nil then - options = __setup - else - options = vim.tbl_extend("keep", options, __setup) - end - - require("lspconfig")[server.name].setup(options) - end - - ${cfg.postConfig} - end - -- }}} - ''; }; } diff --git a/plugins/lsp/language-servers/_mk-lsp.nix b/plugins/lsp/language-servers/_mk-lsp.nix index 0e1d6fea..738d7f62 100644 --- a/plugins/lsp/language-servers/_mk-lsp.nix +++ b/plugins/lsp/language-servers/_mk-lsp.nix @@ -20,7 +20,6 @@ }@args: # returns a module { - pkgs, config, options, lib, @@ -51,9 +50,41 @@ in plugins.lsp.servers.${name} = { enable = lib.mkEnableOption description; + # alias to lsp.servers.${name}.package package = - lib.nixvim.mkMaybeUnpackagedOption "plugins.lsp.servers.${name}.package" pkgs name - package; + let + getSubOptions = opt: opt.type.getSubOptions opt.loc; + serverOpts = getSubOptions options.lsp.servers; + opt = lib.pipe serverOpts [ + (lib.getAttr serverName) + getSubOptions + (lib.getAttr "package") + ]; + pkg = config.lsp.servers.${serverName}.package; + self = options.plugins.lsp.servers.${name}.package; + in + if serverOpts ? ${serverName} then + lib.mkOption ( + { + inherit (opt) type default description; + apply = + v: + if enabled then + pkg + else if self.isDefined then + v + else + opt.default or v; + } + // lib.optionalAttrs (opt ? example) { inherit (opt) example; } + // lib.optionalAttrs (opt ? defaultText) { inherit (opt) defaultText; } + ) + else + # If there's no explicit option, that means there isn't a known package, so the server uses freeformType + lib.nixvim.mkUnpackagedOption options.plugins.lsp.servers.${name}.package name + // { + apply = v: if enabled then config.lsp.servers.${serverName}.packageFallback else v; + }; packageFallback = mkOption { type = types.bool; @@ -63,6 +94,7 @@ in This can be useful if you want local versions of the language server (e.g. from a devshell) to override the nixvim version. ''; + apply = v: if enabled then config.lsp.servers.${serverName}.packageFallback else v; }; cmd = mkOption { @@ -159,16 +191,40 @@ in }; config = lib.mkIf enabled { - extraPackages = lib.optional (!cfg.packageFallback) cfg.package; - extraPackagesAfter = lib.optional cfg.packageFallback cfg.package; + # The server submodule is using `shorthandOnlyDefinesConfig`, + # so define a "longhand" function module. + lsp.servers.${serverName} = _: { + # Top-secret internal option that only exists when the server is enabled via the old API. + # The new API checks if this attr exists and uses it to wrap the server's settings string. + options.__settingsWrapper = lib.mkOption { + type = lib.types.functionTo lib.types.str; + description = '' + This internal option exists to preserve the old `plugins.lsp` behaviour. - plugins.lsp.enabledServers = [ - { - name = serverName; - extraOptions = { - inherit (cfg) cmd filetypes autostart; - root_markers = cfg.rootMarkers; - on_attach = lib.nixvim.ifNonNull' cfg.onAttach ( + > [!IMPORTANT] + > This option should not be used by end-users! + > It will be removed when `plugins.lsp` is dropped. + ''; + readOnly = true; + internal = true; + visible = false; + }; + + # Propagate definitions to the new lsp module + config = { + enable = true; + package = lib.mkIf (opts.package.highestPrio < 1500) ( + lib.modules.mkAliasAndWrapDefsWithPriority lib.id opts.package + ); + packageFallback = lib.modules.mkAliasAndWrapDefsWithPriority lib.id opts.packageFallback; + __settingsWrapper = + settings: "__wrapSettings(${lib.foldr lib.id settings config.plugins.lsp.setupWrappers})"; + settings = { + autostart = lib.mkIf (cfg.autostart != null) cfg.autostart; + cmd = lib.mkIf (cfg.cmd != null) cfg.cmd; + filetypes = lib.mkIf (cfg.filetypes != null) cfg.filetypes; + root_markers = lib.mkIf (cfg.rootMarkers != null) cfg.rootMarkers; + on_attach = lib.mkIf (cfg.onAttach != null) ( lib.nixvim.mkRaw '' function(client, bufnr) ${lib.optionalString (!cfg.onAttach.override) config.plugins.lsp.onAttach} @@ -184,8 +240,8 @@ in }; } // cfg.extraOptions; - } - ]; + }; + }; }; imports = diff --git a/tests/test-sources/plugins/lsp/_lsp.nix b/tests/test-sources/plugins/lsp/_lsp.nix index cd30548e..248d0db2 100644 --- a/tests/test-sources/plugins/lsp/_lsp.nix +++ b/tests/test-sources/plugins/lsp/_lsp.nix @@ -173,25 +173,13 @@ "example" ]; }; - enabledServers = lib.mkAfter [ - { - name = "second"; - extraOptions.settings = lib.mkIf false { - should.be = "missing"; - }; - } - { - name = "third"; - extraOptions.settings = lib.mkIf true { - should.be = "present"; - }; - } - ]; }; }; assertions = let + enabledServers = builtins.filter (server: server.enable) (builtins.attrValues config.lsp.servers); + toLua = lib.nixvim.lua.toLua' { removeNullAttrValues = true; removeEmptyAttrValues = true; @@ -200,39 +188,35 @@ multiline = true; }; - print = lib.generators.toPretty { - multiline = true; - }; + serverCount = builtins.length enabledServers; + expectedCount = 2; - serverCount = builtins.length config.plugins.lsp.enabledServers; - expectedCount = 3; + baseServer = builtins.elemAt enabledServers 0; - nilServer = builtins.head config.plugins.lsp.enabledServers; - nilSettings = toLua nilServer.extraOptions.settings; + nilServer = builtins.elemAt enabledServers 1; + nilSettings = toLua nilServer.settings.settings; expectedNilSettings = toLua { nil.formatting.command = [ "real" "example" ]; }; - - secondServer = builtins.elemAt config.plugins.lsp.enabledServers 1; - expectedSecondServer = { - name = "second"; - extraOptions = { }; - }; - - thirdServer = builtins.elemAt config.plugins.lsp.enabledServers 2; - expectedThirdServer = { - name = "third"; - extraOptions.settings.should.be = "present"; - }; in [ { assertion = serverCount == expectedCount; message = "Expected ${toString expectedCount} enabled LSP server!"; } + { + assertion = baseServer.name == "*"; + message = '' + baseServer's `name` does not match expected value. + + Expected: "*" + + Actual: ${baseServer.name} + ''; + } { assertion = nilSettings == expectedNilSettings; message = '' @@ -243,26 +227,6 @@ Actual: ${nilSettings} ''; } - { - assertion = secondServer == expectedSecondServer; - message = '' - `secondServer` does not match expected value. - - Expected: ${print expectedSecondServer} - - Actual: ${print secondServer} - ''; - } - { - assertion = secondServer == expectedSecondServer; - message = '' - `thirdServer` does not match expected value. - - Expected: ${print expectedThirdServer} - - Actual: ${print thirdServer} - ''; - } ]; };