Skip to content

docs/lib: generate function docs from RFC145 doc-comments #3049

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 19, 2025
12 changes: 10 additions & 2 deletions docs/default.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
helpers,
system,
nixvim,
nixpkgs,
nuschtosSearch,
}:
Expand Down Expand Up @@ -107,7 +108,14 @@ lib.fix (

gfm-alerts-to-admonitions = pkgs.python3.pkgs.callPackage ./gfm-alerts-to-admonitions { };

man-docs = pkgs.callPackage ./man { inherit options-json; };
man-docs = pkgs.callPackage ./man {
inherit options-json;
inherit (self) lib-docs;
};

lib-docs = pkgs.callPackage ./lib {
inherit nixvim lib;
};
}
// lib.optionalAttrs (!pkgs.stdenv.isDarwin) {
# NuschtOS/search does not seem to work on darwin
Expand All @@ -122,7 +130,7 @@ lib.fix (
# > sandbox-exec: pattern serialization length 69298 exceeds maximum (65535)
docs = pkgs.callPackage ./mdbook {
inherit evaledModules transformOptions;
inherit (self) search;
inherit (self) search lib-docs;
};
}
)
178 changes: 178 additions & 0 deletions docs/lib/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Generates the documentation for library functions using nixdoc.
# See https://github.com/nix-community/nixdoc
{
lib,
runCommand,
writers,
nixdoc,
nixvim,
pageSpecs ? import ./pages.nix,
}:

let
# Some pages are just menu entries, others have an actual markdown page that
# needs rendering.
shouldRenderPage = page: page ? file || page ? markdown;

# Normalise a page node, recursively normalise its children
elaboratePage =
loc:
{
title ? "",
markdown ? null,
file ? null,
pages ? { },
}@page:
{
name = lib.attrsets.showAttrPath loc;
loc = lib.throwIfNot (
builtins.head loc == "lib"
) "All pages must be within `lib`, unexpected root `${builtins.head loc}`" (builtins.tail loc);
}
// lib.optionalAttrs (shouldRenderPage page) {
inherit
file
title
;
markdown =
if builtins.isString markdown then
builtins.toFile "${lib.strings.replaceStrings [ "/" "-" ] (lib.lists.last loc)}.md" markdown
else
markdown;
outFile = lib.strings.concatStringsSep "/" (loc ++ [ "index.md" ]);
}
// lib.optionalAttrs (page ? pages) {
pages = elaboratePages loc pages;
};
Comment on lines +17 to +46
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we could simplify this by using the module system to evaluate pages.nix.

Most of this "normalisation" could be done by a page submodule.

I'll plan to address this in a follow up PR.


# Recursively normalise page nodes
elaboratePages = prefix: builtins.mapAttrs (name: elaboratePage (prefix ++ [ name ]));

# Collect all page nodes into a list of page entries
collectPages =
pages:
builtins.concatMap (
page:
[ (builtins.removeAttrs page [ "pages" ]) ]
++ lib.optionals (page ? pages) (collectPages page.pages)
) (builtins.attrValues pages);

# Normalised page specs
elaboratedPageSpecs = elaboratePages [ ] pageSpecs;
pageList = collectPages elaboratedPageSpecs;
pagesToRender = builtins.filter (page: page ? outFile) pageList;
pagesWithFunctions = builtins.filter (page: page.file or null != null) pageList;
in

runCommand "nixvim-lib-docs"
{
nativeBuildInputs = [
nixdoc
];

locations = writers.writeJSON "locations.json" (
import ./function-locations.nix {
inherit lib;
rootPath = nixvim;
functionSet = lib.extend nixvim.lib.overlay;
pathsToScan = builtins.catAttrs "loc" pagesWithFunctions;
revision = nixvim.rev or "main";
}
);

passthru.menu = import ./menu.nix {
inherit lib;
pageSpecs = elaboratedPageSpecs;
};

passthru.pages = builtins.listToAttrs (
builtins.map (
{ name, outFile, ... }:
{
inherit name;
value = outFile;
}
) pagesToRender
);
}
''
function docgen {
md_file="$1"
in_file="$2"
name="$3"
out_file="$out/$4"
title="$5"

if [[ -z "$in_file" ]]; then
if [[ -z "$md_file" ]]; then
>&2 echo "No markdown or nix file for $name"
exit 1
fi
elif [[ -f "$in_file/default.nix" ]]; then
in_file+="/default.nix"
elif [[ ! -f "$in_file" ]]; then
>&2 echo "File not found: $in_file"
exit 1
fi

if [[ -n "$in_file" ]]; then
nixdoc \
--file "$in_file" \
--locs "$locations" \
--category "$name" \
--description "REMOVED BY TAIL" \
--prefix "" \
--anchor-prefix "" \
| tail --lines +2 \
> functions.md
fi

default_heading="# $name"
if [[ -n "$title" ]]; then
default_heading+=": $title"
fi

print_heading=true
if [[ -f "$md_file" ]] && [[ "$(head --lines 1 "$md_file")" == '# '* ]]; then
>&2 echo "NOTE: markdown file for $name starts with a <h1> heading. Skipping default heading \"$default_heading\"."
>&2 echo " Found \"$(head --lines 1 "$md_file")\" in: $md_file"
print_heading=false
fi

mkdir -p $(dirname "$out_file")
(
if [[ "$print_heading" = true ]]; then
echo "$default_heading"
echo
fi
if [[ -f "$md_file" ]]; then
cat "$md_file"
echo
fi
if [[ -f functions.md ]]; then
cat functions.md
fi
) > "$out_file"
}

mkdir -p "$out"

${lib.concatMapStringsSep "\n" (
{
name,
file,
markdown,
outFile,
title ? "",
...
}:
lib.escapeShellArgs [
"docgen"
"${lib.optionalString (markdown != null) markdown}" # md_file
"${lib.optionalString (file != null) file}" # in_file
name # name
outFile # out_file
title # title
]
) pagesToRender}
''
85 changes: 85 additions & 0 deletions docs/lib/function-locations.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Generates an attrset of "function name" → "markdown location",
# for use with nixdoc's `--locs` option.
#
# {
# "lib.nixvim.foo.bar" = "[lib/foo.nix:123](https://github.com/nix-community/nixvim/blob/«rev»/lib/foo.nix#L123) in `<nixvim>`";
# }
{
rootPath,
lib,
functionSet,
pathsToScan,
revision,
functionSetName ? "lib",
url ? "https://github.com/nix-community/nixvim/blob",
}:
let
rootPathString = toString rootPath;
urlPrefix = "${url}/${revision}";

sanitizeId = builtins.replaceStrings [ "'" ] [ "-prime" ];

# Like `isAttrs`, but returns `false` if `v` throws
tryIsAttrs = v: (builtins.tryEval (builtins.isAttrs v)).value;

# Collect position entries from an attrset
# `prefix` is used in the human-readable name,
# and for determining whether to recurse into attrs
collectPositionEntriesInSet =
prefix: set:
builtins.concatMap (
name:
[
{
name = lib.showAttrPath (
builtins.concatMap lib.toList [
functionSetName
prefix
name
]
);
location = builtins.unsafeGetAttrPos name set;
}
]
++ lib.optionals (prefix == [ ] && tryIsAttrs set.${name}) (
collectPositionEntriesInSet (prefix ++ [ name ]) set.${name}
)
) (builtins.attrNames set);

# Collect position entries from each `pathsToScan` in `set`
collectPositionEntriesFromPaths =
set:
builtins.concatMap (loc: collectPositionEntriesInSet loc (lib.getAttrFromPath loc set)) pathsToScan;

# Remove the tree root (usually the top-level store path)
removeNixvimPrefix = lib.flip lib.pipe [
(lib.strings.removePrefix rootPathString)
(lib.strings.removePrefix "/")
];

# Create a name-value-pair for use with `listToAttrs`
entryToNameValuePair =
{ name, location }:
{
name = sanitizeId name;
value =
let
file = removeNixvimPrefix location.file;
line = builtins.toString location.line;
text = "${file}:${line}";
target = "${urlPrefix}/${file}#L${line}";
in
"[${text}](${target}) in `<nixvim>`";
};
in
lib.pipe functionSet [
# Get the entries
collectPositionEntriesFromPaths
# Only include entries that have a location
(builtins.filter (entry: entry.location != null))
# No need to include out-of-tree entries
(builtins.filter (entry: lib.strings.hasPrefix rootPathString entry.location.file))
# Convert entries to attrset
(builtins.map entryToNameValuePair)
builtins.listToAttrs
]
46 changes: 0 additions & 46 deletions docs/user-guide/helpers.md → docs/lib/index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# Helpers

## Accessing Nixvim's functions

If Nixvim is built using the standalone method, you can access our "helpers" as part of the `lib` module arg:
Expand Down Expand Up @@ -67,47 +65,3 @@ This can be achieved using the lib overlay, available via the `<nixvim>.lib.over
};
}
```

## Common helper functions

A certain number of helpers are defined that can be useful:

- `helpers.emptyTable`: An empty lua table `{}` that will be included in the final lua configuration.
This is equivalent to `{__empty = {};}`. This form can allow to do `option.__empty = {}`.

- `helpers.mkRaw str`: Write the string `str` as raw lua in the final lua configuration.
This is equivalent to `{__raw = "lua code";}`. This form can allow to do `option.__raw = "lua code"`.

- `helpers.toLuaObject obj`: Create a string representation of the Nix object. Useful to define your own plugins.

- `helpers.listToUnkeyedAttrs list`: Transforms a list to an "unkeyed" attribute set.

This allows to define mixed table/list in lua:

```nix
(listToUnkeyedAttrs ["a" "b"]) // {foo = "bar";}
```

Resulting in the following lua:

```lua
{"a", "b", [foo] = "bar"}
```

- `helpers.enableExceptInTests`: Evaluates to `true`, except in `mkTestDerivationFromNixvimModule`
where it evaluates to `false`. This allows to skip instantiating plugins that can't be run in tests.

- `helpers.toRawKeys attrs`: Convert the keys of the given `attrs` to raw lua.
```nix
toRawKeys {foo = 1; bar = "hi";}
```
will translate in lua to:
```lua
{[foo] = 1, [bar] = 2,}
```
Otherwise, the keys of a regular `attrs` will be interpreted as lua string:
```lua
{['foo'] = 1, ['bar'] = 2,}
-- which is the same as
{foo = 1, bar = 2,}
```
31 changes: 31 additions & 0 deletions docs/lib/menu.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
lib,
pageSpecs,
indentSize ? " ",
}:
let
pageToLines =
indent: parentName:
{
name,
outFile ? "",
pages ? { },
...
}:
let
menuName = lib.strings.removePrefix (parentName + ".") name;
children = builtins.attrValues pages;
# Only add node to the menu if it has content or multiple children
useNodeInMenu = outFile != "" || builtins.length children > 1;
parentOfChildren = if useNodeInMenu then name else parentName;
in
lib.optional useNodeInMenu "${indent}- [${menuName}](${outFile})"
++ lib.optionals (children != [ ]) (
builtins.concatMap (pageToLines (indent + indentSize) parentOfChildren) children
);
in
lib.pipe pageSpecs [
builtins.attrValues
(builtins.concatMap (pageToLines "" ""))
lib.concatLines
]
Loading