diff --git a/modules/lsp/default.nix b/modules/lsp/default.nix index 0ec2a7ef..d2f6df90 100644 --- a/modules/lsp/default.nix +++ b/modules/lsp/default.nix @@ -1,6 +1,89 @@ -{ lib, config, ... }: +{ + lib, + config, + options, + ... +}: let cfg = config.lsp; + opts = options.lsp; + + features = { + inlayHints.luaName = "inlay_hint"; + codelens.luaName = "codelens"; + semanticTokens = { + luaName = "semantic_tokens"; + enabledByDefault = true; + }; + documentColor = { + luaName = "document_color"; + enabledByDefault = true; + hasSettings = true; + settingsExample = { + style = "virtual"; + }; + }; + linkedEditingRange.luaName = "linked_editing_range"; + onTypeFormatting.luaName = "on_type_formatting"; + inlineCompletion.luaName = "inline_completion"; + }; + + mkFeatureOptions = + name: + { + luaName, + enabledByDefault ? false, + hasSettings ? false, + settingsExample ? null, + }: + { + enable = lib.mkEnableOption null // { + description = '' + Whether Nixvim manages `vim.lsp.${luaName}`. + + See [`:h lsp-${luaName}`](https://neovim.io/doc/user/lsp/#lsp-${luaName}) + ''; + }; + + activate = lib.mkOption { + type = lib.types.bool; + default = true; + example = false; + description = '' + Value passed to `vim.lsp.${luaName}.enable()`. + '' + + lib.optionalString enabledByDefault '' + + Neovim enables this feature by default. To disable it, set + `${opts.${name}.enable}` to `true` and this option to `false`. + ''; + }; + } + // lib.optionalAttrs hasSettings { + settings = lib.nixvim.mkSettingsOption { + description = '' + Options passed as the `opts` argument to + `vim.lsp.${luaName}.enable()`. Nixvim passes `nil` for `filter`. Use + `${opts.luaConfig}.content` to filter by buffer or client. + ''; + example = settingsExample; + }; + }; + + mkFeatureLua = + name: + { + luaName, + ... + }: + let + featureCfg = cfg.${name}; + # The `opts` parameter follows `filter`, so pass `nil` as the second argument. + optsArg = lib.optionalString ( + featureCfg ? settings && featureCfg.settings != { } + ) ", nil, ${lib.nixvim.toLuaObject featureCfg.settings}"; + in + lib.mkIf featureCfg.enable "vim.lsp.${luaName}.enable(${lib.boolToString featureCfg.activate}${optsArg})"; in { options.lsp = { @@ -11,26 +94,8 @@ in Lua code configuring LSP. ''; }; - - inlayHints = { - enable = lib.mkEnableOption null // { - description = '' - Whether to enable inlay hints globally. - - See [`:h lsp-inlay_hint`](https://neovim.io/doc/user/lsp/#lsp-inlay_hint) - ''; - }; - }; - codelens = { - enable = lib.mkEnableOption null // { - description = '' - Whether to enable codelens globally. - - See [`:h lsp-codelens`](https://neovim.io/doc/user/lsp/#lsp-codelens) - ''; - }; - }; - }; + } + // lib.mapAttrs mkFeatureOptions features; imports = [ ./servers @@ -39,10 +104,7 @@ in ]; config = { - lsp.luaConfig.content = lib.mkMerge [ - (lib.mkIf cfg.inlayHints.enable "vim.lsp.inlay_hint.enable(true)") - (lib.mkIf cfg.codelens.enable "vim.lsp.codelens.enable(true)") - ]; + lsp.luaConfig.content = lib.mkMerge (lib.mapAttrsToList mkFeatureLua features); extraConfigLua = lib.mkIf (cfg.luaConfig.content != "") '' -- LSP {{{ diff --git a/modules/lsp/on-attach.nix b/modules/lsp/on-attach.nix index 20abb34c..715feb93 100644 --- a/modules/lsp/on-attach.nix +++ b/modules/lsp/on-attach.nix @@ -1,9 +1,69 @@ -{ lib, config, ... }: +{ + lib, + config, + options, + ... +}: let cfg = config.lsp; + opts = options.lsp; + + # Isolate user code so `return` cannot skip the completion setup that follows. + userOnAttachLua = lib.optionalString (cfg.onAttach != "") '' + local function __nixvim_user_on_attach() + ${cfg.onAttach} + end + + __nixvim_user_on_attach() + ''; + + # Completion needs a client and buffer, so configure it per attached buffer. + # Neovim snapshots `completionProvider.triggerCharacters` when completion is + # enabled. Run this after `onAttach` so users can extend the list first. See + # `:h lsp-attach` and `:h lsp-autocompletion`. + # Pass `bufnr` because dynamic registration can target a non-current buffer. + completionLua = lib.optionalString cfg.completion.enable '' + if client:supports_method('textDocument/completion', bufnr) then + vim.lsp.completion.enable(${lib.boolToString cfg.completion.activate}, client.id, bufnr${ + lib.optionalString ( + cfg.completion.settings != { } + ) ", ${lib.nixvim.toLuaObject cfg.completion.settings}" + }) + end + ''; in { options.lsp = { + completion = { + enable = lib.mkEnableOption null // { + description = '' + Whether Nixvim manages `vim.lsp.completion` for attached clients that + support `textDocument/completion`. + + See [`:h lsp-completion`](https://neovim.io/doc/user/lsp/#lsp-completion) + ''; + }; + + activate = lib.mkOption { + type = lib.types.bool; + default = true; + example = false; + description = '' + Value passed to `vim.lsp.completion.enable()`. To disable completion, + set `${opts.completion.enable}` to `true` and this option to `false`. + ''; + }; + + settings = lib.nixvim.mkSettingsOption { + description = '' + Options passed to `vim.lsp.completion.enable()`. + ''; + example = { + autotrigger = true; + }; + }; + }; + onAttach = lib.mkOption { type = lib.types.lines; description = '' @@ -22,7 +82,7 @@ in }; }; - config = lib.mkIf (cfg.onAttach != "") { + config = lib.mkIf (cfg.onAttach != "" || cfg.completion.enable) { autoGroups.nixvim_lsp_on_attach.clear = false; autoCmd = [ @@ -61,7 +121,8 @@ in extraConfigLua = '' local function __nixvim_lsp_on_attach(client, bufnr, event) - ${cfg.onAttach} + ${userOnAttachLua} + ${completionLua} end vim.lsp.handlers["client/registerCapability"] = (function(overridden) diff --git a/tests/test-sources/modules/lsp.nix b/tests/test-sources/modules/lsp.nix index fd1ec926..c1048b8c 100644 --- a/tests/test-sources/modules/lsp.nix +++ b/tests/test-sources/modules/lsp.nix @@ -1,5 +1,131 @@ { pkgs }: { + feature-enable-only = + { config, lib, ... }: + let + globalLua = config.lsp.luaConfig.content; + expectedLuaNames = { + inlayHints = "inlay_hint"; + codelens = "codelens"; + semanticTokens = "semantic_tokens"; + documentColor = "document_color"; + linkedEditingRange = "linked_editing_range"; + onTypeFormatting = "on_type_formatting"; + inlineCompletion = "inline_completion"; + }; + in + { + lsp = lib.mapAttrs (_: _: { enable = true; }) expectedLuaNames; + + assertions = lib.mapAttrsToList (option: luaName: { + assertion = lib.hasInfix "vim.lsp.${luaName}.enable(true)" globalLua; + message = "Expected `lsp.${option}.enable` to emit `vim.lsp.${luaName}.enable(true)`."; + }) expectedLuaNames; + }; + + feature-forced-off = + { config, lib, ... }: + { + lsp.semanticTokens = { + enable = true; + activate = false; + }; + + assertions = [ + { + assertion = lib.hasInfix "vim.lsp.semantic_tokens.enable(false)" config.lsp.luaConfig.content; + message = "Expected `activate = false` to emit a disabling call."; + } + ]; + }; + + feature-disabled-emits-nothing = + { config, lib, ... }: + { + lsp.documentColor = { + enable = false; + settings.style = "virtual"; + }; + + assertions = [ + { + assertion = !lib.hasInfix "vim.lsp.document_color" config.lsp.luaConfig.content; + message = "Expected no document color Lua while `enable` is off."; + } + ]; + }; + + document-color-settings = + { config, lib, ... }: + { + lsp.documentColor = { + enable = true; + settings.style = "virtual"; + }; + + assertions = [ + { + assertion = lib.hasInfix ''vim.lsp.document_color.enable(true, nil, { style = "virtual" })'' config.lsp.luaConfig.content; + message = "Expected document color settings as the third argument."; + } + ]; + }; + + completion-survives-onattach-return = + { config, lib, ... }: + let + attachLua = config.extraConfigLua; + in + { + lsp = { + completion.enable = true; + onAttach = '' + if client.name ~= "lua_ls" then + return + end + ''; + }; + + assertions = [ + { + assertion = lib.hasInfix "__nixvim_user_on_attach()" attachLua; + message = "Expected the user's `onAttach` body to run in its own function."; + } + { + assertion = + let + afterUserCall = lib.last (lib.splitString "__nixvim_user_on_attach()" attachLua); + in + lib.hasInfix "vim.lsp.completion.enable" afterUserCall; + message = "Expected the completion call to follow the user's `onAttach`, outside it."; + } + ]; + }; + + completion-activate = + { config, lib, ... }: + let + attachLua = config.extraConfigLua; + in + { + lsp.completion = { + enable = true; + activate = false; + settings.autotrigger = true; + }; + + assertions = [ + { + assertion = lib.hasInfix "vim.lsp.completion.enable(false, client.id, bufnr, { autotrigger = true })" attachLua; + message = "Expected `activate` and `settings` to reach the completion call."; + } + { + assertion = lib.hasInfix "client:supports_method('textDocument/completion', bufnr)" attachLua; + message = "Expected the capability check to pass `bufnr`."; + } + ]; + }; + example = { lsp.servers = { "*".config = {