Odoc docs
0

Configure Feed

Select the types of activity you want to include in your feed.

odd / doc / DESIGN_COMPLETE.md
9.9 kB

odd — complete: reference completion#

Status: implemented (lib/complete.ml, odd complete). The shared resolve/load substrate was extracted into Refs (lib/refs.ml) rather than left in doc.ml; Doc and Complete are thin layers over it. Deviations from the design below: names containing __ (wrapped internal modules) are filtered as odoc-hidden; relative/current-package path forms remain out of scope. Section-label completion is implemented — for both modules (Module.label-/section-, from the top-comment and standalone comments) and pages (/pkg/index.) — offering both explicit and auto-generated labels.

A CLI subcommand that, given a partial odoc reference, prints the possible completions — one per line, each a full reference string the user could type next.

$ odd complete /o
/ocaml-compiler
/odoc
/odoc.model
...
$ odd complete List.m
List.map
List.mapi
List.map2
List.mem
...
$ odd complete module-List.type-
module-List.type-t

It is the same machinery as doc, stopped one step earlier: doc resolves a complete reference to an item and renders it; complete resolves the reference's parent context and then lists the children of that context that match the final, incomplete component.

Anatomy of the input#

Every reference is [path-prefix] dotted-component*, where:

  • the path prefix is everything up to and including the last / (/, /odoc.model/, /pkg/sub/); inside it . is literal, so dotted library names live here;
  • each dotted component is [kind-]identifier — e.g. List, module-List, type-t, instance-variable-x. The kind tag is the recognised prefix before the last - (kinds may themselves contain -: module-type, class-type).

complete splits the input into stem + final component, where the final component is what we're completing:

input              stem            kind     name-prefix   what we complete
-----              ----            ----     -----------   ----------------
List.m             "List."         —        "m"           members of List
module-List.type-  "module-List."  type     ""            types in List
Odoc_model.Pa      "Odoc_model."   —        "Pa"          members of Odoc_model
List               ""              —        "List"        top-level names
/o                 "/"             —        "o"           root names
/odoc.model/       "/odoc.model/"  —        ""            units in odoc.model
/odoc.model/Foo.b  "/odoc.model/Foo." —     "b"           members of Foo (in odoc.model)

We cannot reuse odoc's Reference.tokenize for this: it raises on an empty identifier, which is exactly the interesting case (List., type-, /odoc.model/). So complete does its own split:

  1. Find the last top-level separator (. or /, respecting (...)/"..." like the tokenizer). The character there decides the shape (see below); everything up to and including it is the stem, everything after is the final component.
  2. In the final component, peel a leading kind tag: if the text before the last - is a recognised kind, that's (kind, name-prefix); otherwise the whole thing is the name-prefix. (Restricting to recognised kinds avoids mis-splitting operator names like (-).)

Output is always stem ^ [kind-] ^ candidate — we preserve verbatim what the user typed (including Stdlib-relative bare names and any kind tag) and append the matched child name.

Three context shapes#

1. Dotted member completion (the main case)#

Final separator is . (or there's a non-empty stem with no separator). The context is the stem minus the trailing ., a complete, resolvable reference (List, module-List, Odoc_model.Paths). Resolve it exactly as doc does — Ref_tools.resolve_reference with Stdlib open and the -L/-P roots — to a canonical identifier, then list the direct children of the thing it names:

  • module / unit / module-type → the items of its signature;
  • class / class-type → the items of its class signature (methods, instance variables);
  • type → its constructors and fields;
  • page → its section labels.

Each child yields a (name, kind). Filter by the name-prefix, and by the kind tag when one was given. Emit stem ^ [kind-] ^ name.

Resolving the context reuses doc end-to-end; the only new primitive is "children of a resolved identifier" (see below).

2. Path completion#

Final separator is / and the final component has no .. We complete a path segment, decided by how deep we are (split the stem on /):

  • /<frag>root names: the page-root (package) and lib-root (library) names from Refs.scan (/odoc, /odoc.model, /ocaml-compiler, …), filtered by frag.
  • /<root>/<sub>/…/<frag>entries of a directory under the root: the first segment resolves to the root's dir (from scan's page_roots/lib_roots); any further segments are literal sub-directories beneath it (/odoc/deprecated/odoc/odoc/deprecated). The directory's entries are offered: .odoc files' names (capitalised units; page- → page name; impl-/asset- skipped) and sub-directories, hidden (__) names filtered. So pages nested under a package (/odoc/deprecated/index) complete at any depth.

When the final component does contain a . (/odoc.model/Foo.ba), the last separator is that ., so it falls through to case 1 with a path-qualified context (/odoc.model/Foo), which Ref_tools already resolves (we added the -L/-P roots for exactly this).

Relative (./…) and current-package (//…) path forms are out of scope for the first cut — complete has no "current unit", so they have no anchor (the same reason doc doesn't handle them).

3. Top-level completion#

No separator at all (List, prin). Candidates are the union of:

  • compilation-unit names across the switch (the capitalised basenames in Refs.scan's .odocl index / Accessible_paths), and
  • members of the open modules (Stdlib) — so prinprint_endline, print_string, … and LiList, ListLabels.

Filtered by the prefix and optional kind tag. This is the broadest scope; it is also the least load-bearing (the examples are all qualified), so it can be delivered last.

The new primitive: children of a resolved identifier#

doc already does resolve → load owning .odocl → locate the item by identifier. Today that walk (search_sig &c.) returns the item's doc comment. Factor it so the locate step returns the item itself, then:

type scope =
  | Sig of Lang.Signature.t          (* module / unit / module-type *)
  | Class_sig of Lang.ClassSignature.t
  | Type of Lang.TypeDecl.t          (* constructors + fields *)
  | Page of Lang.Comment.docs        (* section labels *)

val children : scope -> (string * kind) list   (* direct children, with kinds *)

kind is the reference kind string ("module", "val", "type", "constructor", "method", …), mapping 1:1 to Lang.Signature.item / ClassSignature.item / type representation / labels. Getting a module's scope reuses doc's module_decl_sig / modtype_expr_sig (signature out of a Module.decl / ModuleType.expr); the root unit's scope is its content signature.

Listing is direct children only (no recursion) — completion never descends past the component being typed. Names are de-duplicated when no kind tag is given (a type t and a value t collapse to one t; add a kind tag to disambiguate).

CLI shape#

odd complete [--prefix DIR] PARTIAL
  • Prints candidates one per line, sorted, on stdout. Each is a complete reference string (stem preserved + completed component).
  • Always exits 0; no matches is empty output, not an error (a completer asking about a dead end is normal).
  • A --kind-style filter is unnecessary: the kind tag is part of PARTIAL (module-List.type-), which is also what the user is mid-typing.

Shell glue (bash/zsh complete -C, a zsh _odd function) is a thin wrapper over this and is left to a follow-up; the command is the engine.

Performance#

Member completion loads one .odocl (tens of ms for a big unit like Stdlib); path/root completion is directory listings (cheap). Fine for one-shot use; if wired into a tight interactive loop, a cross-invocation cache of the name→.odocl index and of hot units (e.g. Stdlib) would be the lever. Not needed for v1.

Scope summary#

Form v1 Notes
X.Y.frag, kind-X.kind-frag yes the core case; reuses doc
/frag (root names) yes from scan roots
/root/…/frag (entries under a root, any depth) yes directory listing
/root/Unit.frag (path + dotted) yes path-qualified resolve + children
bare frag (top-level) last units + Stdlib members; broad
./…, //… (relative paths) no no "current unit" in complete
operator / quoted names in the fragment best-effort kind split guards on known kinds

Open questions#

  • Top-level breadth. Enumerating every unit in the switch for a 1–2 char prefix can be a long list. Acceptable for a completer (the shell narrows it), but we might cap or require ≥1 char.
  • Labels. Section-label completion is implemented (modules and pages), offering both explicit and auto-generated labels. A label has no doc comment of its own, so doc can't render a bare label reference even though it resolves — rendering the section a label names would be a separate doc enhancement.
  • Ambiguous roots. A library name occurring in several packages appears once per occurrence in root completion; dedupe by name (as doc already tolerates the ambiguity at resolve time).