docsets.nix-builtins: init

This commit is contained in:
2024-11-05 01:41:53 +00:00
parent 97dc226414
commit 1a09d9abe9
5 changed files with 147 additions and 36 deletions

View File

@@ -60,6 +60,7 @@ in {
];
sane.programs.docsets.config.pkgs = with pkgs; [
# packages which ship docsets natively:
docsets.nix-builtins
docsets.nixpkgs-lib
docsets.rust-std
] ++ lib.map

View File

@@ -4,43 +4,70 @@
import argparse
import json
import logging
import re
import sqlite3
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
logger = logging.getLogger(__name__)
class ItemType(Enum):
# these loosely correspond to docset types: <https://kapeli.com/docsets#supportedentrytypes>
SECTION = 1
CONSTANT = 2
FUNCTION = 3
@dataclass(unsafe_hash=True, order=True)
class Item:
attrpath: str
type: ItemType
def parent(self) -> "Item | None":
pp = parent_attrpath(self.attrpath)
if pp:
return Item(attrpath=pp, type=ItemType.SECTION)
@dataclass
class Items:
functions: list[str]
sections: list[str]
items: dict[str, Item]
def add(self, it: Item) -> bool:
""" returns True iff the item was not previously known """
if it.attrpath in self.items and self.items[it.attrpath].type.value <= it.type.value:
return False # we already have the item, and the new item is no more specific
self.items[it.attrpath] = it
return True
def sections(self) -> list[Item]:
return sorted(i for i in self.items.values() if i.type == ItemType.SECTION)
def leaves(self) -> list[Item]:
return sorted(i for i in self.items.values() if i.type != ItemType.SECTION)
def all(self) -> list[Item]:
return self.sections() + self.leaves()
def read_items(locations_json: Path) -> Items:
# TODO: this seems to yield some items which don't have documentation? like `lib.gvariant.type`
items = Items({})
with open(locations_json, "r") as f:
functions = json.load(f).keys()
sections = set(parent_attrpath(f) for f in functions)
while True:
expanded_sections = sections.union(parent_attrpath(s) for s in sections)
if expanded_sections == sections:
break
for attrpath, val in json.load(f).items():
if isinstance(val, dict) and "type" in val:
# `nix __dump-language` output is structured and only constants can have a type
type = ItemType.CONSTANT
else:
sections = expanded_sections
# lacking additional information, assume it's a function
type = ItemType.FUNCTION
# the above algorithm includes a nameless root section `""`: remove that
sections = [ s for s in sections if s ]
item = Item(attrpath=attrpath, type=type)
items.add(item)
# for some cases (lib.gvariant.type), both the section and its members show up in `locations.json`
functions = [ f for f in functions if f not in sections ]
while any(items.add(i.parent()) for i in items.all() if i.parent()):
pass
return Items(functions=sort_attrpaths(functions), sections=sort_attrpaths(sections))
def sort_attrpaths(ps: list[str]) -> list[str]:
ps_expanded = sorted(p.split(".") for p in ps)
return [ ".".join(p) for p in ps_expanded ]
return items
def parent_attrpath(p: str) -> str:
""" `lib.foo.bar` -> `lib.foo` """
@@ -56,20 +83,35 @@ def init_db(db) -> None:
db.execute("CREATE TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT);")
db.execute("CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path);")
def register_item(db, attrpath: str, kind: str, path: str) -> None:
def insert_row(db, attrpath: str, kind: str, path: str) -> None:
logger.debug(f"register ({attrpath}, {kind}, {path})")
db.execute("INSERT INTO searchIndex(name, type, path) values (?, ?, ?);", (attrpath, kind, path))
def register_function(db, attrpath: str) -> None:
register_item(db, attrpath, kind="Function", path=f"manual.html#function-library-{attrpath}")
def register_section(db, attrpath: str) -> None:
library = chomp_attrpath(attrpath)
if library == "":
def insert_item(db, item: Item) -> None:
# TODO: stop hardcoding the .html filename; maybe even the anchor prefix
if item.attrpath.startswith("lib."):
if item.type == ItemType.SECTION:
library = chomp_attrpath(item.attrpath)
path = f"manual.html#sec-functions-library-{library}"
elif item.type == ItemType.CONSTANT:
assert False, f"ItemType.CONSTANT should not exist in nixpkgs.lib; item: {item}"
elif item.type == ItemType.FUNCTION:
path=f"manual.html#function-library-{item.attrpath}"
else:
assert False, f"unexpected item type for item {item}"
elif item.attrpath == "lib":
path = "manual.html#sec-functions-library"
else:
path = f"manual.html#sec-functions-library-{library}"
register_item(db, attrpath, kind="Property", path=path)
path = f"language/builtins.html#builtins-{item.attrpath}"
if item.type == ItemType.SECTION:
kind = "Property" # TODO: should it be `namespace`?
elif item.type == ItemType.CONSTANT:
kind = "Constant"
elif item.type == ItemType.FUNCTION:
kind = "Function"
insert_row(db, item.attrpath, kind=kind, path=path)
def main() -> None:
logging.basicConfig()
@@ -90,11 +132,8 @@ def main() -> None:
db = conn.cursor()
init_db(db)
for section in items.sections:
register_section(db, section)
for fn in items.functions:
register_function(db, fn)
for item in items.all():
insert_item(db, item)
conn.commit()
conn.close()

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>nix-builtins</string>
<key>CFBundleName</key>
<string>nix.builtins</string>
<key>DocSetPlatformFamily</key>
<string>nix</string>
<key>isDashDocset</key>
<true/>
<key>dashIndexFilePath</key>
<string>builtins.html</string>
</dict>
</plist>

View File

@@ -0,0 +1,55 @@
{
docsets,
jq,
nix,
runCommandLocal,
stdenv,
}:
let
# nix has logic to build an attrset of all the items which make it into `nix.doc`
# this is a json dictionary with each entry like:
# "abort": { "args": [ "s" ], "doc": "Abort Nix expression evaluation and print the error message *s*." }
builtins-locations = runCommandLocal "nix-locations.json" {
nativeBuildInputs = [ jq nix ];
} ''
mkdir $out
nix __dump-language | jq . > $out/locations.json
'';
docset = stdenv.mkDerivation {
pname = "nix-builtins";
version = nix.version;
nativeBuildInputs = [ docsets.make-docset-index ];
unpackPhase = ''
cp ${./Info.plist} Info.plist
cp ${builtins-locations}/locations.json locations.json
cp -R ${nix.doc}/share/doc/nix/manual nix-manual
'';
buildPhase = ''
runHook preBuild
mkdir -p nix-builtins.docset/Contents/Resources/
cp Info.plist nix-builtins.docset/Contents/
cp -R nix-manual nix-builtins.docset/Contents/Resources/Documents
make-docset-index --verbose locations.json --output nix-builtins.docset/Contents/Resources/docSet.dsidx
runHook postBuild
'';
# docsets are usually distributed as .tgz, but compression is actually optional at least for tools like `dasht`
installPhase = ''
mkdir -p $out/share/docsets
cp -R *.docset $out/share/docsets/
'';
passthru = {
inherit builtins-locations;
};
};
in
docset

View File

@@ -45,7 +45,7 @@ let
# docsets are usually distributed as .tgz, but compression is actually optional at least for tools like `dasht`
installPhase = ''
mkdir -p $out/share/docsets
cp -R nixpkgs-lib.docset $out/share/docsets/nixpkgs-lib.docset
cp -R *.docset $out/share/docsets/
'';
passthru = {