1
Fork 0
satellite/modules/common/neovim.nix
2023-12-10 12:55:54 +01:00

762 lines
25 KiB
Nix
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# This module provides personalised helpers for managing plugins
# using lazy.nvim and a set of custom runtime primitives.
{ pkgs, lib, config, ... }:
let
inherit (lib) types;
cfg = config.satellite.neovim;
# {{{ Custom types
myTypes = {
oneOrMany = t: types.either t (types.listOf t);
zeroOrMore = t: types.nullOr (myTypes.oneOrMany t);
# {{{ Lua code
luaCode = types.nullOr (types.oneOf [
types.str
types.path
myTypes.luaLiteral
]);
luaLiteral = types.submodule {
options.__luaEncoderTag = lib.mkOption {
type = types.enum [ "lua" ];
};
options.value = lib.mkOption {
type = types.str;
};
};
luaValue = types.nullOr (types.oneOf [
types.str
types.number
types.bool
(types.attrsOf myTypes.luaValue)
(types.listOf myTypes.luaValue)
]);
# }}}
# {{{ Lazy key
lazyKey = types.oneOf [
types.str
(types.submodule
{
options.mapping = lib.mkOption {
type = types.str;
description = "The lhs of the neovim mapping";
};
options.action = lib.mkOption {
default = null;
type = types.nullOr (types.oneOf [
types.str
myTypes.luaLiteral
]);
description = "The rhs of the neovim mapping";
};
options.ft = lib.mkOption {
default = null;
type = myTypes.zeroOrMore types.str;
description = "Filetypes on which this keybind should take effect";
};
options.mode = lib.mkOption {
default = null;
type = types.nullOr types.str;
description = "The vim modes the mapping should take effect in";
};
options.desc = lib.mkOption {
default = null;
type = types.nullOr types.str;
description = "Description for the current keymapping";
};
})
];
# }}}
# {{{ Tempest key
tempestKey = types.submodule {
options = {
mapping = lib.mkOption {
example = "<leader>a";
type = types.str;
description = "The lhs of the neovim mapping";
};
action = lib.mkOption {
example = "<C-^>";
type = types.either types.str myTypes.luaLiteral;
description = "The rhs of the neovim mapping";
};
bufnr = lib.mkOption {
default = null;
example = true;
type = types.nullOr
(types.oneOf [
types.bool
types.integer
myTypes.luaLiteral
]);
description = ''
The index of the buffer to apply local keymaps to. Can be set to
`true` to refer to the current buffer
'';
};
mode = lib.mkOption {
default = null;
example = "nov";
type = types.nullOr types.str;
description = "The vim modes the mapping should take effect in";
};
silent = lib.mkOption {
default = null;
example = true;
type = types.nullOr types.bool;
description = "Whether the logs emitted by the keymap should be supressed";
};
expr = lib.mkOption {
default = null;
example = true;
type = types.nullOr types.bool;
description = "If set to `true`, the mapping is treated as an action factory";
};
desc = lib.mkOption {
default = null;
type = types.nullOr types.str;
description = "Description for the current keymapping";
};
};
};
# }}}
# {{{ Tempest autocmd
tempestAutocmd = types.submodule {
options = {
event = lib.mkOption {
example = "InsertEnter";
type = myTypes.oneOrMany types.str;
description = "Events to bind autocmd to";
};
pattern = lib.mkOption {
example = "Cargo.toml";
type = myTypes.oneOrMany types.str;
description = "File name patterns to run autocmd on";
};
group = lib.mkOption {
example = "CargoCmpSource";
type = types.str;
description = "Name of the group to create and assign autocmd to";
};
callback = lib.mkOption {
example.vim.opt.cmdheight = 1;
type = types.oneOf [
myTypes.tempestConfiguration
myTypes.luaCode
];
description = ''
Code to run when the respctive event occurs. Will pass the event
object as context, which might be used for things like assigning
a buffer number to local keymaps automatically.
'';
};
};
};
# }}}
# {{{ Tempest configuration
tempestConfiguration = types.submodule {
options = {
vim = lib.mkOption {
default = null;
type = myTypes.luaValue;
example.opt.cmdheight = 0;
description = "Values to assign to the `vim` lua global object";
};
keys = lib.mkOption {
default = null;
type = myTypes.zeroOrMore myTypes.tempestKey;
description = ''
Arbitrary key mappings to create. The keymappings might
automatically be buffer specific depending on the context. For
instance, keymappings created inside autocmds will be local unless
otherwise specified.
'';
};
autocmds = lib.mkOption {
default = null;
type = myTypes.zeroOrMore myTypes.tempestAutocmd;
description = "Arbitrary autocmds to create";
};
setup = lib.mkOption {
default = null;
type = types.nullOr (types.attrsOf myTypes.luaValue);
example.lualine.opts.theme = "auto";
description = ''
Key-pair mappings for options to pass to .setup functions imported
from different modules
'';
};
callback = lib.mkOption {
default = null;
type = types.nullOr myTypes.luaCode;
description = "Arbitrary code to run after everything else has been configured";
};
};
};
# }}}
# {{{ Lazy module
lazyModule = lib.fix (lazyModule: types.submodule ({ name ? null, ... }: {
options = {
package = lib.mkOption {
type = types.oneOf [
types.package
types.str
];
description = "Package to configure the module around";
example = "nvim-telescope/telescope.nvim";
};
name = lib.mkOption {
default = name;
type = types.nullOr types.str;
description = "Custom name to use for the module";
example = "lualine";
};
main = lib.mkOption {
default = null;
type = types.nullOr types.str;
description = "The name of the lua entrypoint for the plugin (usually auto-detected)";
example = "lualine";
};
version = lib.mkOption {
default = null;
type = types.nullOr types.str;
description = "Pin the package to a certain version (useful for non-nix managed packages)";
};
tag = lib.mkOption {
default = null;
type = types.nullOr types.str;
description = "Pin the package to a certain git tag (useful for non-nix managed packages)";
};
lazy = lib.mkOption {
default = null;
type = types.nullOr types.bool;
description = "Specifies whether this module should be lazy-loaded";
};
dependencies.lua = lib.mkOption {
default = [ ];
type = types.listOf (types.either types.str lazyModule);
description = "Lazy.nvim module dependencies";
};
dependencies.nix = lib.mkOption {
default = [ ];
type = types.listOf types.package;
description = "Nix packages to give nvim access to";
};
cond = lib.mkOption {
default = null;
type = myTypes.luaCode;
description = "Condition based on which to enable/disbale loading the package";
};
env.blacklist = lib.mkOption {
default = [ ];
type = types.listOf (types.enum [ "firenvim" "vscode" "neovide" ]);
description = "Environments to blacklist plugin on";
};
setup = lib.mkOption {
default = null;
type = types.nullOr (types.oneOf [
myTypes.tempestConfiguration
myTypes.luaCode
types.bool
]);
description = ''
Lua function (or module) to use for configuring the package.
Used instead of the canonically named `config` because said name has a special meaning in nix
'';
};
event = lib.mkOption {
default = null;
type = myTypes.zeroOrMore types.str;
description = "Event on which the module should be lazy loaded";
};
ft = lib.mkOption {
default = null;
type = myTypes.zeroOrMore types.str;
description = "Filetypes on which the module should be lazy loaded";
};
cmd = lib.mkOption {
default = null;
type = myTypes.zeroOrMore types.str;
description = "Comands on which to load this plugin";
};
init = lib.mkOption {
default = null;
type = myTypes.luaCode;
description = "Lua function (or module) to run right away (even if the package is not yet loaded)";
};
passthrough = lib.mkOption {
default = null;
type = myTypes.luaCode;
description = "Attach additional things to the lazy module";
};
opts = lib.mkOption {
default = null;
type = myTypes.luaValue;
description = "Custom data to pass to the plugin .setup function";
};
keys = lib.mkOption {
default = null;
type = myTypes.zeroOrMore myTypes.lazyKey;
description = "Keybinds to lazy-load the module on";
};
};
}));
# }}}
};
# }}}
# {{{ Lua encoders
# We provide a custom set of helpers for generating lua code for nix.enable
#
# An encoder is a function from some nix value to a string containing lua code.
# This object provides combinators for writing such encoders.
luaEncoders = {
# {{{ "Raw" helpers
mkRawLuaObject = chunks:
''
{
${lib.concatStringsSep "," (lib.filter (s: s != "") chunks)}
}
'';
# }}}
# {{{ General helpers
identity = given: given;
# `const` is mostly useful together with `bind`. See the lua encoder for
# lazy modules for example usage.
const = code: _: code;
# Conceptually, this is the monadic bind operation for encoders.
# This implementation is isomoprhic to that of the reader monad in haskell.
bind = encoder: given: encoder given given;
# This is probably the most useful combinnator defined in this entire object.
# Most of the combinators in the other categories are based on this.
conditional = predicate: caseTrue: caseFalse:
luaEncoders.bind (given: if predicate given then caseTrue else caseFalse);
# This is simply left-composition of functions
map = f: encoder: given: encoder (f given);
# This is simply right-composition of functions
postmap = f: encoder: given: f (encoder given);
# This is mostly useful for debugging
trace = message: luaEncoders.map (f: lib.traceSeq message (lib.traceVal f));
fail = mkMessage: v: builtins.throw (mkMessage v);
# }}}
# {{{ Base types
string = given: ''"${lib.escape ["\"" "\\"] (toString given)}"'';
bool = bool: if bool then "true" else "false";
number = toString;
nil = _: "nil";
stringOr = luaEncoders.conditional lib.isString luaEncoders.string;
boolOr = luaEncoders.conditional lib.isBool luaEncoders.bool;
numberOr = luaEncoders.conditional (e: lib.isFloat e || lib.isInt e) luaEncoders.number;
nullOr = luaEncoders.conditional (e: e == null) luaEncoders.nil;
# We pipe a combinator which always fail through a bunch of
# `(thing)or : encoder -> encoder` functions, building up a combinator which
# can handle more and more kinds of values, until we eventually build up
# something that should be able to handle everything we throw at it.
anything = lib.pipe (luaEncoders.fail (v: "Cannot figure out how to encode value ${builtins.toJSON v}")) [
(luaEncoders.attrsetOfOr luaEncoders.anything)
(luaEncoders.listOfOr luaEncoders.anything)
luaEncoders.nullOr
luaEncoders.boolOr
luaEncoders.numberOr
luaEncoders.stringOr
luaEncoders.luaCodeOr # Lua code expressions have priority over attrsets
];
# }}}
# {{{ Lua code
# Tagged lua code can be combined with other combinators without worrying
# about conflicts regarding how strings are interpreted.
luaCodeOr =
luaEncoders.conditional (e: lib.isAttrs e && (e.__luaEncoderTag or null) == "lua")
(obj: obj.value);
# This is the most rudimentary (and currently only) way of handling paths.
luaImportOr = tag:
luaEncoders.conditional lib.isPath
(path: "dofile(${luaEncoders.string path}).${tag}");
# Accepts both tagged and untagged strings of lua code.
luaString = luaEncoders.luaCodeOr luaEncoders.identity;
# This simply combines the above combinators into one.
luaCode = tag: luaEncoders.luaImportOr tag luaEncoders.luaString;
# }}}
# {{{ Lists
listOf = encoder: list:
luaEncoders.mkRawLuaObject (lib.lists.map encoder list);
listOfOr = encoder:
luaEncoders.conditional
lib.isList
(luaEncoders.listOf encoder);
# Returns nil when given empty lists
tryNonemptyList = encoder: luaEncoders.conditional
(l: l == [ ])
luaEncoders.nil
(luaEncoders.listOf encoder);
oneOrMany = encoder: luaEncoders.listOfOr encoder encoder;
# Can encode:
# - zero values as nil
# - one value as itself
# - multiple values as a list
zeroOrMany = encoder: luaEncoders.nullOr (luaEncoders.oneOrMany encoder);
# Coerces non list values to lists of one element.
oneOrManyAsList = encoder: luaEncoders.map
(given: if lib.isList given then given else [ given ])
(luaEncoders.listOf encoder);
# Coerces lists of one element to said element.
listAsOneOrMany = encoder:
luaEncoders.map
(l: if lib.length l == 1 then lib.head l else l)
(luaEncoders.oneOrMany encoder);
# }}}
# {{{ Attrsets
attrsetOf = encoder: object:
luaEncoders.mkRawLuaObject (lib.mapAttrsToList
(name: value:
let result = encoder value;
in
lib.optionalString (result != "nil")
"${name} = ${result}"
)
object
);
attrsetOfOr = of: luaEncoders.conditional lib.isAttrs (luaEncoders.attrsetOf of);
# This is the most general combinator provided in this section.
#
# We accept:
# - a `noNils` flag which will automatically remove any nil properties
# - order of props that should be interpreted as list elements
# - spec of props that should be interpreted as list elements
# - record of props that should be interpreted as attribute props
attrset = noNils: listOrder: spec: attrset:
let
shouldKeep = given:
if noNils then
given != "nil"
else
true;
listChunks = lib.lists.map
(attr:
let result = spec.${attr} (attrset.${attr} or null);
in
lib.optionalString (shouldKeep result) result
)
listOrder;
objectChunks = lib.mapAttrsToList
(attr: encoder:
let result = encoder (attrset.${attr} or null);
in
lib.optionalString (!(lib.elem attr listOrder) && shouldKeep result)
"${attr} = ${result}"
)
spec;
in
luaEncoders.mkRawLuaObject (listChunks ++ objectChunks);
# }}}
};
e = luaEncoders;
# }}}
# {{{ Helpers
# Format and write a lua file to disk
writeLuaFile = path: name: text:
let
directory = "lua/${path}";
destination = "${directory}/${name}.lua";
unformatted = pkgs.writeText "raw-lua-${name}" text;
in
pkgs.runCommand "formatted-lua-${name}" { } ''
mkdir -p $out/${directory}
cp --no-preserve=mode ${unformatted} $out/${destination}
${lib.getExe pkgs.stylua} --config-path ${cfg.styluaConfig} $out/${destination}
'';
# }}}
in
{
# {{{ Option declaration
options.satellite.neovim = {
lazy = lib.mkOption {
default = { };
description = "Record of plugins to install using lazy.nvim";
type = types.attrsOf myTypes.lazyModule;
};
# {{{ Generated
generated = {
lazy = lib.mkOption {
type = types.attrsOf (types.submodule {
options = {
raw = lib.mkOption {
type = types.lines;
description = "The lua script generated using the other options";
};
module = lib.mkOption {
type = types.package;
description = "The lua script generated using the other options";
};
};
});
description = "Attrset containing every module generated from the lazy configuration";
};
all = lib.mkOption {
default = { };
type = types.package;
description = "Derivation building all the given nix modules";
};
dependencies = lib.mkOption {
default = [ ];
type = types.listOf types.package;
description = "List of packages to give neovim access to";
};
};
# }}}
# {{{ Lua generation lib
lib = {
# {{{ Basic lua generators
lua = lib.mkOption {
default = value: { inherit value; __luaEncoderTag = "lua"; };
type = types.functionTo myTypes.luaLiteral;
description = "include some raw lua code inside module configuration";
};
import = lib.mkOption {
default = path: tag: cfg.lib.lua "dofile(${e.string path}).${tag}";
type = types.functionTo (types.functionTo myTypes.luaLiteral);
description = "import some identifier from some module";
};
# }}}
# {{{ Encoders
encode = lib.mkOption {
default = luaEncoders.anything;
type = types.functionTo types.str;
description = "Encode a nix value to a lua string";
};
encodeTempestConfiguration = lib.mkOption {
default = given:
e.attrset true [ ]
{
vim = e.anything;
callback = e.nullOr e.luaString;
setup = e.nullOr (e.attrsetOf e.anything);
keys = e.zeroOrMany (e.attrset true [ ] {
mapping = e.string;
action = e.luaCodeOr e.string;
desc = e.nullOr e.string;
expr = e.nullOr e.bool;
mode = e.nullOr e.string;
silent = e.nullOr e.bool;
buffer = e.nullOr (e.luaCodeOr (e.boolOr e.number));
});
autocmds = e.zeroOrMany (e.attrset true [ ] {
event = e.oneOrMany e.string;
pattern = e.oneOrMany e.string;
group = e.string;
callback = e.conditional lib.isAttrs
cfg.lib.encodeTempestConfiguration
e.luaString;
});
}
given;
type = types.functionTo types.str;
description = "Generate a lua object for passing to my own lua runtime for configuration";
};
# }}}
# {{{ Thunks
# This version of `nlib.thunk` is required in ceratain cases because
# of issues with `types.oneOf [types.submodule ..., types.submodule]` not
# working as intended atm.
thunkString = lib.mkOption {
default = given: /* lua */ ''
function() ${e.luaString given} end
'';
type = types.functionTo types.str;
description = "Wrap a lua expression into a lua function as a string";
};
thunk = lib.mkOption {
default = given: cfg.lib.lua (cfg.lib.thunkString given);
type = types.functionTo myTypes.luaLiteral;
description = "Wrap a lua expression into a lua function";
};
contextThunk = lib.mkOption {
default = given: cfg.lib.lua /* lua */ ''
function(context) ${e.luaString given} end
'';
type = types.functionTo myTypes.luaLiteral;
description = "Wrap a lua expression into a lua function taking an argument named `context`";
};
# }}}
# {{{ Language server on attach
languageServerOnAttach = lib.mkOption {
default = given: cfg.lib.lua /* lua */ ''
function(client, bufnr)
require(${e.string cfg.runtime.tempest}).configure(${cfg.lib.encodeTempestConfiguration given},
{ client = client; bufnr = bufnr; })
require(${e.string cfg.runtime.languageServerOnAttach}).on_attach(client, bufnr)
end
'';
type = types.functionTo myTypes.luaCode;
description = "Attach a language server and run some additional code";
};
# }}}
};
# }}}
# {{{ Neovim runtime module paths
runtime = {
env = lib.mkOption {
type = types.str;
example = "my.helpers.env";
description = "Module to import env flags from";
};
tempest = lib.mkOption {
type = types.str;
example = "my.runtime.tempest";
description = "Module to import the tempest runtime from";
};
languageServerOnAttach = lib.mkOption {
type = types.str;
example = "my.runtime.lspconfig";
description = "Module to import langauge server .on_attach function from";
};
};
# }}}
styluaConfig = lib.mkOption {
type = types.path;
description = "Config to use for formatting lua modules";
};
};
# }}}
# {{{ Config generation
# {{{ Lazy module generation
config.satellite.neovim.generated.lazy =
let
# {{{ Lazy key encoder
lazyKeyEncoder =
e.stringOr (e.attrset true [ "mapping" "action" ] {
mapping = e.string;
action = e.nullOr (e.luaCodeOr e.string);
mode = e.nullOr
(e.map
lib.strings.stringToCharacters
(e.listAsOneOrMany e.string));
desc = e.nullOr e.string;
ft = e.zeroOrMany e.string;
});
# }}}
# {{{ Lazy spec encoder
lazyObjectEncoder = e.bind
(opts: e.attrset true [ "package" ]
{
package = e.string;
name = e.nullOr e.string;
main = e.nullOr e.string;
tag = e.nullOr e.string;
version = e.nullOr e.string;
dependencies = e.map (d: d.lua) (e.tryNonemptyList (e.stringOr lazyObjectEncoder));
lazy = e.nullOr e.bool;
cond =
if opts.env.blacklist != [ ] then
assert lib.asserts.assertMsg (opts.cond == null)
"env.blacklist overrides plugin condition";
e.const /* lua */ ''
require(${e.string cfg.runtime.env}).blacklist(${e.listOf e.string opts.env.blacklist})
''
else
e.nullOr (e.luaCode "cond");
config = _:
let
wrap = given: /* lua */''
function(lazy, opts)
require(${e.string cfg.runtime.tempest}).configure(${given},
{ lazy = lazy; opts = opts; })
end
'';
in
e.conditional lib.isAttrs
(e.postmap wrap cfg.lib.encodeTempestConfiguration)
(e.nullOr (e.boolOr (e.luaCode "config")))
opts.setup;
init = e.nullOr (e.luaCode "init");
event = e.zeroOrMany e.string;
cmd = e.zeroOrMany e.string;
ft = e.zeroOrMany e.string;
keys = e.nullOr (e.oneOrManyAsList lazyKeyEncoder);
passthrough = e.anything;
opts = e.anything;
});
# }}}
makeLazyScript = opts: ''
-- This file was generated using nix ^~^
return ${lazyObjectEncoder opts}
'';
in
lib.attrsets.mapAttrs
(name: opts: rec {
raw = makeLazyScript opts;
module = writeLuaFile "nix/plugins" name raw;
})
cfg.lazy;
config.satellite.neovim.generated.all =
pkgs.symlinkJoin {
name = "lazy-nvim-modules";
paths = lib.attrsets.mapAttrsToList (_: m: m.module) cfg.generated.lazy;
};
# }}}
config.satellite.neovim.generated.dependencies =
lib.pipe cfg.lazy
[
(lib.attrsets.mapAttrsToList (_: m: m.dependencies.nix))
lib.lists.flatten
];
# }}}
}