odd — complete: reference completion#
Status: implemented (
lib/complete.ml,odd complete). The shared resolve/load substrate was extracted intoRefs(lib/refs.ml) rather than left indoc.ml;DocandCompleteare 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:
- 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. - 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 fromRefs.scan(/odoc,/odoc.model,/ocaml-compiler, …), filtered byfrag./<root>/<sub>/…/<frag>→ entries of a directory under the root: the first segment resolves to the root's dir (fromscan'spage_roots/lib_roots); any further segments are literal sub-directories beneath it (/odoc/deprecated/→odoc/odoc/deprecated). The directory's entries are offered:.odocfiles' 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.odoclindex /Accessible_paths), and - members of the open modules (
Stdlib) — soprin→print_endline,print_string, … andLi→List,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 ofPARTIAL(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
doccan't render a bare label reference even though it resolves — rendering the section a label names would be a separatedocenhancement. - Ambiguous roots. A library name occurring in several packages appears once
per occurrence in root completion; dedupe by name (as
docalready tolerates the ambiguity at resolve time).