flake-modules -> flake

This commit is contained in:
Gaetan Lepage 2025-01-19 12:56:24 +01:00
parent cf647bc045
commit 998bae9dac
17 changed files with 1 additions and 1 deletions

109
flake/dev/default.nix Normal file
View file

@ -0,0 +1,109 @@
{ lib, inputs, ... }:
{
imports =
[
./devshell.nix
./list-plugins
]
++ lib.optional (inputs.git-hooks ? flakeModule) inputs.git-hooks.flakeModule
++ lib.optional (inputs.treefmt-nix ? flakeModule) inputs.treefmt-nix.flakeModule;
perSystem =
{
lib,
pkgs,
system,
...
}:
lib.optionalAttrs (inputs.treefmt-nix ? flakeModule) {
treefmt.config = {
projectRootFile = "flake.nix";
flakeCheck = true;
programs = {
isort.enable = true;
nixfmt = {
enable = true;
package = pkgs.nixfmt-rfc-style;
};
prettier = {
enable = true;
excludes = [ "**.md" ];
};
ruff = {
check = true;
format = true;
};
statix.enable = true;
stylua.enable = true;
shfmt.enable = true;
# FIXME: re-enable on darwin, currently broken: taplo with options '[format]' failed to apply: exit status 101
taplo.enable = pkgs.stdenv.isLinux;
};
settings = {
global.excludes = [
".editorconfig"
".envrc"
".git-blame-ignore-revs"
".gitignore"
"LICENSE"
"flake.lock"
"**.md"
"**.scm"
"**.svg"
"**/man/*.5"
# Those files are generated by pytest-regression, which then `diff`s them.
# Formatting them will make the tests fail.
"docs/gfm-alerts-to-admonitions/tests/**/*.yml"
];
formatter.ruff-format.options = [ "--isolated" ];
};
};
}
// lib.optionalAttrs (inputs.git-hooks ? flakeModule) {
pre-commit = {
# We have a treefmt check already, so this is redundant.
# We also can't run the test if it includes running `nix build`,
# since the nix CLI can't build within a derivation builder.
check.enable = false;
settings.hooks = {
deadnix = {
enable = true;
settings = {
noLambdaArg = true;
noLambdaPatternNames = true;
edit = true;
};
};
treefmt.enable = true;
typos = {
enable = true;
excludes = [ "generated/*" ];
};
maintainers = {
enable = true;
name = "maintainers";
description = "Check maintainers when it is modified.";
files = "^lib/maintainers[.]nix$";
package = pkgs.nix;
entry = "nix build --no-link --print-build-logs";
args = [ ".#checks.${system}.maintainers" ];
pass_filenames = false;
};
plugins-by-name = {
enable = true;
name = "plugins-by-name";
description = "Check `plugins/by-name` when it's modified.";
files = "^(?:tests/test-sources/)?plugins/by-name/";
package = pkgs.nix;
entry = "nix build --no-link --print-build-logs";
args = [ ".#checks.${system}.plugins-by-name" ];
pass_filenames = false;
};
};
};
};
}

122
flake/dev/devshell.nix Normal file
View file

@ -0,0 +1,122 @@
{ lib, inputs, ... }:
{
imports = lib.optional (inputs.devshell ? flakeModule) inputs.devshell.flakeModule;
perSystem =
{
lib,
pkgs,
config,
self',
system,
...
}:
lib.optionalAttrs (inputs.devshell ? flakeModule) {
devshells.default = {
devshell.startup.pre-commit.text = config.pre-commit.installationScript;
commands =
let
# Thanks to this, the user can choose to use `nix-output-monitor` (`nom`) instead of plain `nix`
nix = ''$([ "$\{NIXVIM_NOM:-0}" = '1' ] && echo ${pkgs.lib.getExe pkgs.nix-output-monitor} || echo nix)'';
in
[
{
name = "checks";
help = "Run all nixvim checks";
command = ''
echo "=> Running all nixvim checks..."
${nix} flake check "$@"
'';
}
{
name = "tests";
help = "Run nixvim tests";
command =
let
launchTest = pkgs.writeShellApplication {
name = "launch-tests";
runtimeInputs = with pkgs; [
getopt
jq
fzf
];
text = builtins.readFile ./launch-test.sh;
};
tests =
let
checks' = self'.checks;
names = builtins.filter (n: builtins.match "test-.*" n != null) (builtins.attrNames checks');
in
builtins.listToAttrs (
builtins.concatMap (
checkName:
map (testName: {
name = testName;
value = "${checkName}.passthru.entries.${testName}";
}) (builtins.attrNames checks'.${checkName}.passthru.entries)
) names
);
in
''
export NIXVIM_SYSTEM=${system}
export NIXVIM_NIX_COMMAND=${nix}
export NIXVIM_TESTS=${pkgs.writers.writeJSON "tests.json" tests}
${lib.getExe launchTest} "$@"
'';
}
{
name = "test-lib";
help = "Run nixvim library tests";
command = ''
echo "=> Running nixvim library tests for the '${system}' architecture..."
${nix} build .#checks.${system}.lib-tests "$@"
'';
}
{
name = "format";
help = "Format the entire codebase";
command = "nix fmt";
}
{
name = "docs";
help = "Build nixvim documentation";
command = ''
echo "=> Building nixvim documentation..."
${nix} build .#docs "$@"
'';
}
{
name = "serve-docs";
help = "Build and serve documentation locally";
command = ''
echo -e "=> Building nixvim documentation...\n"
doc_derivation=$(${nix} build .#docs --no-link --print-out-paths)
echo -e "\n=> Documentation successfully built ('$doc_derivation')"
echo -e "\n=> You can then open your browser to view the doc\n"
(cd "$doc_derivation"/share/doc && ${pkgs.lib.getExe pkgs.python3} ${./server.py})
'';
}
{
name = "locate-lsp-packages";
command = ''${pkgs.python3.interpreter} ${./locate-lsp-packages.py}'';
help = "Locate (with nix-index) LSP servers in nixpkgs";
}
{
name = "new-plugin";
command = ''${pkgs.python3.interpreter} ${./new-plugin.py} "$@"'';
help = "Create a new plugin";
}
];
};
};
}

115
flake/dev/launch-test.sh Executable file
View file

@ -0,0 +1,115 @@
#!/usr/bin/env bash
: "${NIXVIM_NIX_COMMAND:=nix}"
if [[ -z ${NIXVIM_SYSTEM+x} ]]; then
NIXVIM_SYSTEM=$(nix eval --raw --impure --expr "builtins.currentSystem")
fi
help() {
cat <<EOF
Usage: tests [OPTIONS] [tests...] -- [NIX OPTIONS...]
If tests are passed on the command line only these will be launched
All arguments after '--' starting with '-' will be passed to 'nix build'.
For example to debug a failing test you can append '-- --show-trace'.
Options:
-h, --help: Display this help message and exit
-l, --list: Display the list of tests and exit
-s, --system <system>: Launch checks for "<system>" instead of "${NIXVIM_SYSTEM}".
-i, --interactive: Pick interactively the tests. Can't be supplied if tests where passed.
EOF
}
if ! OPTS=$(getopt -o "hlis:" -l "help,list,interactive,system:" -- "$@"); then
echo "Invalid options" >&2
help
exit 1
fi
eval set -- "$OPTS"
system=${NIXVIM_SYSTEM}
specified_tests=()
nix_args=()
interactive=false
mk_test_list() {
jq -r 'keys[]' "${NIXVIM_TESTS}"
}
while true; do
case "$1" in
-h | --help)
help
exit 0
;;
-l | --list)
mk_test_list
exit 0
;;
-i | --interactive)
interactive=true
shift
;;
-s | --system)
system=$2
shift 2
;;
--)
shift
for arg in "$@"; do
if [[ $arg == -* ]]; then
nix_args+=("$arg")
else
specified_tests+=("$arg")
fi
done
break
;;
esac
done
get_tests() {
# Convert bash array to jq query
# e.g. (foo bar baz) => ."foo",."bar",."baz"
readarray -t queries < <(
for test in "$@"; do
echo '."'"$test"'"'
done
)
query=$(
IFS=,
echo "${queries[*]}"
)
for test in $(jq -r "${query}" "${NIXVIM_TESTS}"); do
echo "checks.${system}.${test}"
done
}
run_tests() {
readarray -t test_list < <(get_tests "$@")
if ! "${NIXVIM_NIX_COMMAND}" build "${nix_args[@]}" --no-link --file . "${test_list[@]}"; then
echo "Test failure" >&2
exit 1
fi
}
if [[ $interactive == true && ${#specified_tests[@]} -ne 0 ]]; then
echo "Can't use --interactive with tests on the command line" >&2
exit 1
fi
if [[ $interactive == true ]]; then
test_name=$(mk_test_list | fzf) || exit $?
specified_tests+=("$test_name")
fi
if [[ ${#specified_tests[@]} -eq 0 ]]; then
readarray -t complete_test_list < <(mk_test_list)
run_tests "${complete_test_list[@]}"
else
echo "Running ${#specified_tests[@]} tests: ${specified_tests[*]}" >&2
run_tests "${specified_tests[@]}"
fi

View file

@ -0,0 +1,46 @@
{ inputs, self, ... }:
{
perSystem =
{
self',
config,
lib,
inputs',
system,
pkgs,
...
}:
let
package = pkgs.writers.writePython3Bin "list-plugins" {
# Disable flake8 checks that are incompatible with the ruff ones
flakeIgnore = [
# line too long
"E501"
# line break before binary operator
"W503"
];
} (builtins.readFile ./list-plugins.py);
in
{
packages.list-plugins = package;
checks.list-plugins-test =
pkgs.runCommand "list-plugins-test"
{
nativeBuildInputs = [ package ];
}
''
list-plugins --root-path ${self} > $out
'';
}
// lib.optionalAttrs (inputs.devshell ? flakeModule) {
devshells.default.commands = [
{
name = "list-plugins";
command = ''${lib.getExe package} "$@"'';
help = "List plugins and get implementation infos";
}
];
};
}

View file

@ -0,0 +1,282 @@
import glob
import os
import re
from argparse import ArgumentParser, RawTextHelpFormatter
from dataclasses import dataclass
from enum import Enum
from typing import Optional
# Ignore files that are not plugin definitions
EXCLUDES: list[str] = [
# Patterns
"TEMPLATE.nix",
"deprecations.nix",
"helpers.nix",
"renamed-options",
"settings-options.nix",
# Specific files
"colorschemes/base16/theme-list.nix",
"plugins/by-name/blink-cmp/provider-config.nix",
"plugins/by-name/dap/dapHelpers.nix",
"plugins/by-name/efmls-configs/packages.nix",
"plugins/by-name/gitsigns/options.nix",
"plugins/by-name/hydra/hydras-option.nix",
"plugins/by-name/hydra/settings-options.nix",
"plugins/by-name/neogit/options.nix",
"plugins/by-name/neotest/adapters-list.nix",
"plugins/by-name/neotest/adapters.nix",
"plugins/by-name/neotest/options.nix",
"plugins/by-name/none-ls/_mk-source-plugin.nix",
"plugins/by-name/none-ls/packages.nix",
"plugins/by-name/none-ls/prettier.nix",
"plugins/by-name/none-ls/prettierd.nix",
"plugins/by-name/none-ls/settings.nix",
"plugins/by-name/none-ls/sources.nix",
"plugins/by-name/openscad/fuzzy-finder-plugin-option.nix",
"plugins/by-name/rustaceanvim/renamed-options.nix",
"plugins/by-name/rustaceanvim/settings-options.nix",
"plugins/by-name/startify/options.nix",
"plugins/by-name/telescope/extensions/_mk-extension.nix",
"plugins/by-name/telescope/extensions/default.nix",
"plugins/cmp/auto-enable.nix",
"plugins/cmp/options/",
"plugins/cmp/sources/cmp-fish.nix",
"plugins/cmp/sources/default.nix",
"plugins/default.nix",
"plugins/deprecation.nix",
"plugins/lsp/language-servers/",
"plugins/lsp/lsp-packages.nix",
]
class Kind(Enum):
NEOVIM = 1
VIM = 2
MISC = 3
class State(Enum):
UNKNOWN = ""
NEW = ""
OLD = ""
KNOWN_PATHS: dict[
str,
tuple[
State, # If the implem is "legacy" or up to date
Kind, # Vim / Neovim / misc
bool, # Has deprecation warnings
],
] = {
"plugins/by-name/chadtree/default.nix": (State.OLD, Kind.NEOVIM, False),
"plugins/by-name/coq-thirdparty/default.nix": (State.OLD, Kind.NEOVIM, False),
"plugins/by-name/dap/default.nix": (State.OLD, Kind.NEOVIM, False),
"plugins/by-name/leap/default.nix": (State.OLD, Kind.NEOVIM, False),
"plugins/by-name/lint/default.nix": (State.OLD, Kind.NEOVIM, False),
"plugins/by-name/lspkind/default.nix": (State.OLD, Kind.NEOVIM, False),
"plugins/by-name/nix-develop/default.nix": (State.OLD, Kind.NEOVIM, False),
"plugins/by-name/rainbow-delimiters/default.nix": (State.OLD, Kind.NEOVIM, False),
"plugins/by-name/treesitter-refactor/default.nix": (State.OLD, Kind.MISC, True),
"plugins/by-name/treesitter-textobjects/default.nix": (
State.OLD,
Kind.NEOVIM,
True,
),
"plugins/colorschemes/base16/default.nix": (State.NEW, Kind.VIM, True),
"plugins/lsp/default.nix": (State.NEW, Kind.MISC, False),
}
for telescope_extension_name, has_depr_warnings in {
"file-browser": True,
"frecency": True,
"fzf-native": True,
"fzy-native": True,
"live-greps-args": False,
"manix": False,
"media-files": True,
"project": False,
"ui-select": False,
"undo": True,
}.items():
KNOWN_PATHS[
f"plugins/by-name/telescope/extensions/{telescope_extension_name}.nix"
] = (
State.NEW,
Kind.MISC,
has_depr_warnings,
)
DEPRECATION_REGEX: list[re.Pattern] = [
re.compile(rf".*{pattern}", re.DOTALL)
for pattern in [
"deprecateExtra",
"mkRemovedOptionModule",
"mkRenamedOptionModule",
"optionsRenamedToSettings",
]
]
@dataclass
class Plugin:
path: str
state: State
kind: Kind
dep_warnings: bool
def __str__(self) -> str:
state_icon: str = self.state.value
kind_icon: str
match self.kind:
case Kind.NEOVIM:
kind_icon = "\033[94m" + ""
case Kind.VIM:
kind_icon = "\033[92m" + ""
case Kind.MISC:
kind_icon = "\033[92m" + "🟢"
case _:
assert False
deprecation_icon: str = "⚠️ " if self.dep_warnings else " "
return (
f"| {kind_icon}\033[0m | {state_icon} | {deprecation_icon} | {self.path}"
)
def print_markdown(self) -> None:
print(f"- [ ] {self.path} ({self.kind.name.lower()})")
def has_deprecation_warnings(string: str) -> bool:
for regex in DEPRECATION_REGEX:
if re.match(regex, string):
return True
return False
def parse_file(path: str) -> Optional[Plugin]:
file_content: str = ""
with open(path, "r") as f:
file_content = f.read()
known_path: str
props: tuple[State, Kind, bool]
for known_path, props in KNOWN_PATHS.items():
if known_path in path:
return Plugin(
path=path,
state=props[0],
kind=props[1],
dep_warnings=props[2],
)
state: State = State.UNKNOWN
kind: Kind
if re.match(
re.compile(r".*mkNeovimPlugin", re.DOTALL),
file_content,
):
kind = Kind.NEOVIM
state = State.NEW
elif re.match(
re.compile(r".*require.+setup", re.DOTALL),
file_content,
):
kind = Kind.NEOVIM
state = State.OLD
elif re.match(
re.compile(r".*mkVimPlugin", re.DOTALL),
file_content,
):
kind = Kind.VIM
state = State.NEW
else:
raise ValueError(
f"I was not able to categorize `{path}`. Consider adding it to `EXCLUDES` or `KNOWN_PATHS`."
)
return Plugin(
path=path,
state=state,
kind=kind,
dep_warnings=has_deprecation_warnings(string=file_content),
)
def _is_excluded(path: str) -> bool:
for exclude_pattern in EXCLUDES:
if exclude_pattern in path:
return False
return True
def main(args) -> None:
pathname: str = os.path.join(args.root_path, "plugins/**/*.nix")
paths: list[str] = glob.glob(pathname=pathname, recursive=True)
filtered_paths: list[str] = list(filter(_is_excluded, paths))
filtered_paths.sort()
if not args.markdown:
print("| Typ | Sty | DW | path")
print(
"|-----|-----|----|--------------------------------------------------------"
)
for plugin_path in filtered_paths:
plugin: Optional[Plugin] = parse_file(path=plugin_path)
if plugin is not None:
if (
(args.kind is None or plugin.kind.name.lower() == args.kind)
and (args.state is None or plugin.state.name.lower() == args.state)
and (not args.deprecation_warnings or plugin.dep_warnings)
):
if args.markdown:
plugin.print_markdown()
else:
print(plugin)
if __name__ == "__main__":
parser: ArgumentParser = ArgumentParser(
description="""
Analyze Nixvim plugin files
Output formats a table showing:
If a plugin is written for Neovim or Vim.
If the plugin has been updated to latest style standards.
If a plugin contains any deprecation warnings.
""",
formatter_class=RawTextHelpFormatter,
)
# TODO: consider automatically localizing the flake's root.
parser.add_argument(
"--root-path",
type=str,
default="./",
help="The path to the root of the nixvim repo",
)
parser.add_argument(
"-k",
"--kind",
choices=[k.name.lower() for k in Kind],
help="Filter plugins by kind (neovim, vim, misc)",
)
parser.add_argument(
"-s",
"--state",
choices=[s.name.lower() for s in State],
help="Filter plugins by state (new, old, unknown)",
)
parser.add_argument(
"-d",
"--deprecation-warnings",
action="store_true",
help="Show only plugins with deprecation warnings",
)
parser.add_argument(
"-m",
"--markdown",
action="store_true",
help="Markdown output",
)
main(parser.parse_args())

126
flake/dev/locate-lsp-packages.py Executable file
View file

@ -0,0 +1,126 @@
#!/usr/bin/env python3
# This script requires nix-locate
import json
import os
import subprocess
from dataclasses import dataclass
def find_project_root(root_identifier: str) -> None | str:
current_path = os.getcwd()
while True:
if root_identifier in os.listdir(current_path):
return current_path
parent_path = os.path.dirname(current_path)
if parent_path == current_path:
return None
os.chdir("..")
current_path = os.getcwd()
@dataclass
class CustomCommand:
package: str
cmd: list[str]
@dataclass
class PackageList:
unpackaged: list[str]
packages: dict[str, str | list[str]]
custom_cmd: dict[str, CustomCommand]
def get_current_package(
current_packages: PackageList, server: str
) -> None | str | list[str]:
if (package := current_packages.packages.get(server)) is not None:
return package
elif (custom_cmd := current_packages.custom_cmd.get(server)) is not None:
return custom_cmd.package
else:
return None
def search_for_package(command: list[str]) -> None | str:
nix_locate = subprocess.run(
[
"nix-locate",
"--top-level",
"--whole-name",
"--at-root",
f"/bin/{command[0]}",
],
capture_output=True,
text=True,
)
if nix_locate.stdout == "":
return None
else:
return nix_locate.stdout.strip()
def main():
repo = find_project_root("flake.nix")
# Extract the list of packages in JSON
current_packages = subprocess.run(
[
"nix",
"eval",
"--impure",
"--raw",
"--expr",
f"builtins.toJSON (import {repo}/plugins/lsp/lsp-packages.nix)",
],
capture_output=True,
text=True,
)
current_packages = json.loads(current_packages.stdout)
current_packages = PackageList(
unpackaged=current_packages["unpackaged"],
packages=current_packages["packages"],
custom_cmd={
server: CustomCommand(**info)
for server, info in current_packages["customCmd"].items()
},
)
with open(f"{repo}/generated/lspconfig-servers.json") as f:
generated_servers = json.load(f)
for info in generated_servers:
server: str = info["name"]
print(f"=== {server} ===")
current_package = get_current_package(current_packages, server)
if current_package is not None:
print(f" Current package: {current_package}")
continue
cmd: list[str] | str | None = info.get("cmd")
if cmd is None:
print(" no upstream command")
continue
if not isinstance(cmd, list):
print(" upstream command is a function")
continue
print(f" upstream command: {cmd}")
if len(cmd) == 0:
continue
possible_packages = search_for_package(cmd)
if possible_packages is None:
print(" no package found for command")
else:
print(" POSSIBLE NEW PACKAGE:")
print(possible_packages)
if __name__ == "__main__":
main()

146
flake/dev/new-plugin.py Normal file
View file

@ -0,0 +1,146 @@
#!/usr/bin/env python3
import os
import re
from argparse import ArgumentParser
# Template for default.nix
# TODO: conditionally include parts of the template based on args
default_nix_template = """{{ lib, ... }}:
lib.nixvim.plugins.mkNeovimPlugin {{
name = "{name}";
packPathName = "{originalName}";
package = "{package}";
maintainers = [ lib.maintainers.YOUR_NAME ]; # TODO
settingsOptions = {{
exampleOption = lib.nixvim.defaultNullOpts.mkBool false ''
Example option for the {name} plugin.
'';
}};
}}
"""
# Template for test file
test_nix_template = """{{
empty = {{
plugins.{name}.enable = true;
}};
}}
"""
def kebab_case(input_string):
"""
Convert a string to kebab-case.
Args:
input_string (str): The input string to convert.
Returns:
str: The converted kebab-case string.
"""
# Replace non-alphanumeric characters with hyphens
input_string = re.sub(r"[\W_]+", "-", input_string).lower()
# Remove leading and trailing standalone 'nvim'
input_string = re.sub(r"(^nvim-|-nvim$|^nvim$)", "", input_string)
return input_string.strip("-")
def create_nix_file(file_path, template, name, originalName, package):
"""
Create a nix file from a template.
Args:
file_path (str): The path to the file to create.
template (str): The template string to use for the file content.
name (str): The name of the plugin.
originalName (str): The original name of the plugin.
package (str): The package name of the plugin.
"""
content = template.format(name=name, originalName=originalName, package=package)
write_to_file(file_path, content)
def create_test_file(file_path, template, name):
"""
Create a test file from a template.
Args:
file_path (str): The path to the file to create.
template (str): The template string to use for the file content.
name (str): The name of the plugin.
"""
content = template.format(name=name)
write_to_file(file_path, content)
def write_to_file(file_path, content: str):
"""
Makes sure directories exist and write content to a file.
Args:
file_path (str): The path to the file to write.
content (str): The content to write to the file.
"""
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w") as f:
f.write(content)
def find_project_root(root_identifier):
current_path = os.getcwd()
while True:
if root_identifier in os.listdir(current_path):
return current_path
parent_path = os.path.dirname(current_path)
if parent_path == current_path:
return None
os.chdir("..")
current_path = os.getcwd()
# TODO: support interactive unmanaged args
def main():
"""
Main function to generate default.nix and test files for a new plugin.
"""
parser = ArgumentParser(
description="Generate default.nix and test files for a new plugin"
)
parser.add_argument(
"originalName", type=str, help="Original name of the new plugin"
)
parser.add_argument("package", type=str, help="Package name of the new plugin")
args = parser.parse_args()
# Calculate name
name = kebab_case(args.originalName)
# Define paths
root_identifier = "flake.nix"
root_dir = find_project_root(root_identifier)
plugin_path = f"{root_dir}/plugins/by-name/{name}/default.nix"
test_path = f"{root_dir}/tests/test-sources/plugins/by-name/{name}/default.nix"
# Create files
create_nix_file(
plugin_path,
default_nix_template,
name,
args.originalName,
args.package,
)
create_test_file(
test_path,
test_nix_template,
name,
)
if __name__ == "__main__":
main()

16
flake/dev/server.py Normal file
View file

@ -0,0 +1,16 @@
import http.server
PORT = 8000
class UncachedHTTPHandler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
self.send_header("Pragma", "no-cache")
self.send_header("Expires", "0")
super().end_headers()
with http.server.HTTPServer(("", PORT), UncachedHTTPHandler) as httpd:
print(f"Serving documentation at http://localhost:{PORT}")
httpd.serve_forever()