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; [ sane.programs.docsets.config.pkgs = with pkgs; [
# packages which ship docsets natively: # packages which ship docsets natively:
docsets.nix-builtins
docsets.nixpkgs-lib docsets.nixpkgs-lib
docsets.rust-std docsets.rust-std
] ++ lib.map ] ++ lib.map

View File

@@ -4,43 +4,70 @@
import argparse import argparse
import json import json
import logging import logging
import re
import sqlite3 import sqlite3
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from pathlib import Path from pathlib import Path
logger = logging.getLogger(__name__) 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 @dataclass
class Items: class Items:
functions: list[str] items: dict[str, Item]
sections: list[str]
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: def read_items(locations_json: Path) -> Items:
# TODO: this seems to yield some items which don't have documentation? like `lib.gvariant.type` # 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: with open(locations_json, "r") as f:
functions = json.load(f).keys() for attrpath, val in json.load(f).items():
if isinstance(val, dict) and "type" in val:
sections = set(parent_attrpath(f) for f in functions) # `nix __dump-language` output is structured and only constants can have a type
while True: type = ItemType.CONSTANT
expanded_sections = sections.union(parent_attrpath(s) for s in sections)
if expanded_sections == sections:
break
else: 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 item = Item(attrpath=attrpath, type=type)
sections = [ s for s in sections if s ] items.add(item)
# for some cases (lib.gvariant.type), both the section and its members show up in `locations.json` while any(items.add(i.parent()) for i in items.all() if i.parent()):
functions = [ f for f in functions if f not in sections ] pass
return Items(functions=sort_attrpaths(functions), sections=sort_attrpaths(sections)) return items
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 ]
def parent_attrpath(p: str) -> str: def parent_attrpath(p: str) -> str:
""" `lib.foo.bar` -> `lib.foo` """ """ `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 TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT);")
db.execute("CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path);") 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})") logger.debug(f"register ({attrpath}, {kind}, {path})")
db.execute("INSERT INTO searchIndex(name, type, path) values (?, ?, ?);", (attrpath, kind, path)) db.execute("INSERT INTO searchIndex(name, type, path) values (?, ?, ?);", (attrpath, kind, path))
def register_function(db, attrpath: str) -> None: def insert_item(db, item: Item) -> None:
register_item(db, attrpath, kind="Function", path=f"manual.html#function-library-{attrpath}") # TODO: stop hardcoding the .html filename; maybe even the anchor prefix
if item.attrpath.startswith("lib."):
def register_section(db, attrpath: str) -> None: if item.type == ItemType.SECTION:
library = chomp_attrpath(attrpath) library = chomp_attrpath(item.attrpath)
if library == "": 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" path = "manual.html#sec-functions-library"
else: else:
path = f"manual.html#sec-functions-library-{library}" path = f"language/builtins.html#builtins-{item.attrpath}"
register_item(db, attrpath, kind="Property", path=path)
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: def main() -> None:
logging.basicConfig() logging.basicConfig()
@@ -90,11 +132,8 @@ def main() -> None:
db = conn.cursor() db = conn.cursor()
init_db(db) init_db(db)
for section in items.sections: for item in items.all():
register_section(db, section) insert_item(db, item)
for fn in items.functions:
register_function(db, fn)
conn.commit() conn.commit()
conn.close() 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` # docsets are usually distributed as .tgz, but compression is actually optional at least for tools like `dasht`
installPhase = '' installPhase = ''
mkdir -p $out/share/docsets mkdir -p $out/share/docsets
cp -R nixpkgs-lib.docset $out/share/docsets/nixpkgs-lib.docset cp -R *.docset $out/share/docsets/
''; '';
passthru = { passthru = {