modules/lsp: add native LSP feature toggles

Expose `completion`, `semanticTokens`, `documentColor`,
`linkedEditingRange`, `onTypeFormatting`, and `inlineCompletion`.

Each feature uses `enable` to opt into Nixvim management and `activate`
as the value passed to `vim.lsp.<feature>.enable()`. `activate` defaults
to true, preserving the existing `inlayHints` and `codelens` output.
Set `enable = true` and `activate = false` to disable semantic tokens
or document color, which Neovim enables by default.

A shared table generates the global feature options and Lua.
`documentColor` also accepts `settings` for its `opts` argument.
`enabledByDefault` limits disable guidance to Neovim defaults, while
`settingsExample` carries the document color example.

Because `vim.lsp.completion.enable()` takes a client id and buffer,
completion runs from `LspAttach` after `onAttach`. Neovim snapshots
`completionProvider.triggerCharacters` when completion is enabled, so
users must extend that list first. See `:h lsp-autocompletion`.
`supports_method` receives `bufnr` so dynamic registration checks the
attached buffer. User `onAttach` code runs in a function so `return`
cannot skip Nixvim setup.

`client/unregisterCapability` is not handled, so withdrawn support
leaves completion state until the client detaches. If `onAttach` calls
`completion.enable()` first, its buffer options remain because Neovim
keeps the options from the first call.
This commit is contained in:
Austin Horstman
2026-09-18 14:13:14 +00:00
parent afcfb8c1dc
commit 56839cde1c
3 changed files with 277 additions and 28 deletions
+87 -25
View File
@@ -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 {{{
+64 -3
View File
@@ -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)
+126
View File
@@ -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 = {