switch from sqlite to json

This commit is contained in:
atagen 2026-03-23 12:17:45 +11:00
parent 17967da43e
commit f2d0a42fd7
5 changed files with 191 additions and 226 deletions

View file

@ -10,19 +10,17 @@ let usage () =
{|inshellah - nushell completions engine
Usage:
inshellah index PREFIX... [--db PATH]
Index completions into a SQLite database.
inshellah index PREFIX... [--dir PATH]
Index completions into a directory of JSON/nu files.
PREFIX is a directory containing bin/ and share/man/.
Default db: $XDG_CACHE_HOME/inshellah/completions.db
inshellah complete CMD [ARGS...] [--db PATH]
Default dir: $XDG_CACHE_HOME/inshellah
inshellah complete CMD [ARGS...] [--dir PATH] [--system-dir PATH]
Nushell custom completer. Outputs JSON completion candidates.
Falls back to --help resolution if command is not in the database.
inshellah query CMD [--db PATH]
Print stored completion data for CMD as JSON.
inshellah clear [CMD...] [--db PATH]
Clear the database, or remove specific commands.
inshellah dump [--db PATH]
Show stats and commands in the database.
Falls back to --help resolution if command is not indexed.
inshellah query CMD [--dir PATH] [--system-dir PATH]
Print stored completion data for CMD.
inshellah dump [--dir PATH] [--system-dir PATH]
List indexed commands.
inshellah manpage FILE Parse a manpage and emit nushell extern
inshellah manpage-dir DIR Batch-process manpages under DIR
@ -390,9 +388,8 @@ let help_resolve_par ?(timeout=200) cmd rest name =
Each child handles one binary completely (including subcommand resolution)
and marshals results back via pipe. Children use help_resolve_par
which forks per subcommand for parallelism. *)
let cmd_index bindirs mandirs ignorelist db_path =
let db = init db_path in
begin_transaction db;
let cmd_index bindirs mandirs ignorelist dir =
ensure_dir dir;
let done_cmds = ref SSet.empty in
let n_results = ref 0 in
let index_bindir bindir mandir =
@ -418,12 +415,12 @@ let cmd_index bindirs mandirs ignorelist db_path =
try Marshal.from_string data 0 with _ -> `None in
(match result with
| `Native src ->
upsert_raw db ~source:"native" name src;
write_native ~dir name src;
incr n_results
| `Parsed pairs ->
List.iter (fun (cmd_name, r) ->
if not (SSet.mem cmd_name !done_cmds) then begin
upsert db ~source:"help" cmd_name r;
write_result ~dir ~source:"help" cmd_name r;
done_cmds := SSet.add cmd_name !done_cmds;
incr n_results
end
@ -504,7 +501,7 @@ let cmd_index bindirs mandirs ignorelist db_path =
| None -> ()
| Some (cmd, result) ->
if not (SSet.mem cmd !done_cmds) then begin
upsert db ~source:"manpage" cmd result;
write_result ~dir ~source:"manpage" cmd result;
done_cmds := SSet.add cmd !done_cmds;
incr n_results
end
@ -513,23 +510,16 @@ let cmd_index bindirs mandirs ignorelist db_path =
) command_sections
end in
List.iter2 index_bindir bindirs mandirs;
commit db;
Printf.printf "indexed %d commands into %s\n" !n_results db_path;
close db
Printf.printf "indexed %d commands into %s\n" !n_results dir
let cmd_dump db_path =
let db = init db_path in
let (count, sources) = stats db in
Printf.printf "database: %s\n" db_path;
Printf.printf "commands: %d (from %d sources)\n" count sources;
let cmds = all_commands db in
let cmd_dump dirs =
let cmds = all_commands dirs in
Printf.printf "%d commands\n" (List.length cmds);
List.iter (fun cmd ->
match lookup db cmd with
| None -> ()
| Some (_data, source) ->
Printf.printf " %-40s [%s]\n" cmd source
) cmds;
close db
let src = match file_type_of dirs cmd with
| Some s -> s | None -> "?" in
Printf.printf " %-40s [%s]\n" cmd src
) cmds
let find_in_path name =
try
@ -543,10 +533,11 @@ let find_in_path name =
go dirs
with Not_found -> None
let resolve_and_cache db name path =
let resolve_and_cache ~dir name path =
let pairs = help_resolve_par ~timeout:200 path [] name in
if pairs <> [] then begin
List.iter (fun (cmd_name, r) -> upsert db cmd_name r) pairs;
ensure_dir dir;
List.iter (fun (cmd_name, r) -> write_result ~dir cmd_name r) pairs;
Some pairs
end else None
@ -580,18 +571,18 @@ let flag_completions prefix entries =
) entries;
List.rev !candidates
let cmd_complete spans db_path =
let cmd_complete spans user_dir system_dirs =
match spans with
| [] -> print_string "[]\n"
| cmd_name :: rest ->
let db = init db_path in
let dirs = user_dir :: system_dirs in
(* Try longest subcommand match first: "git add" before "git" *)
let rec find_result tokens =
match tokens with
| [] -> None
| _ ->
let try_name = String.concat " " tokens in
match lookup_result db try_name with
match lookup dirs try_name with
| Some r -> Some (try_name, r, List.length tokens)
| None ->
find_result (List.rev (List.tl (List.rev tokens))) in
@ -607,9 +598,8 @@ let cmd_complete spans db_path =
| None ->
(match find_in_path cmd_name with
| Some path ->
(match resolve_and_cache db cmd_name path with
(match resolve_and_cache ~dir:user_dir cmd_name path with
| Some _pairs ->
(* Look up again after caching *)
find_result all_tokens
| None -> None)
| None -> None) in
@ -620,50 +610,25 @@ let cmd_complete spans db_path =
| None -> print_string "[]\n"
| Some (_matched_name, r, _depth) ->
let candidates = ref [] in
(* Flag completions when partial starts with - *)
if String.starts_with ~prefix:"-" partial then
candidates := flag_completions partial r.entries
else begin
(* Subcommand completions *)
List.iter (fun (sc : subcommand) ->
if partial = "" || String.starts_with ~prefix:partial sc.name then
candidates := completion_json sc.name sc.desc :: !candidates
) r.subcommands;
candidates := List.rev !candidates;
(* Also offer flags if no subcommand prefix or few subcommand matches *)
if partial = "" || !candidates = [] then
candidates := !candidates @ flag_completions partial r.entries
end;
Printf.printf "[%s]\n" (String.concat "," !candidates));
close db
Printf.printf "[%s]\n" (String.concat "," !candidates))
let cmd_query cmd db_path =
let db = init db_path in
(match lookup db cmd with
| None ->
Printf.eprintf "not found: %s\n" cmd; close db; exit 1
| Some (data, source) ->
Printf.printf "# source: %s\n%s\n" source data);
close db
let cmd_clear cmds db_path =
let db = init db_path in
(match cmds with
| [] ->
(match Sqlite3.exec db "DELETE FROM completions" with
| Sqlite3.Rc.OK ->
Printf.printf "cleared all commands from %s\n" db_path
| rc ->
Printf.eprintf "error: %s\n" (Sqlite3.Rc.to_string rc); exit 1)
| _ ->
List.iter (fun cmd ->
if has_command db cmd then begin
delete db cmd;
Printf.printf "removed %s\n" cmd
end else
Printf.eprintf "not found: %s\n" cmd
) cmds);
close db
let cmd_query cmd dirs =
match lookup_raw dirs cmd with
| None ->
Printf.eprintf "not found: %s\n" cmd; exit 1
| Some data ->
print_string data; print_newline ()
let load_ignorelist path =
try
@ -679,47 +644,40 @@ let load_ignorelist path =
with _ -> SSet.empty
let parse_index_args args =
let rec go prefixes db ignore = function
| [] -> (List.rev prefixes, db, ignore)
| "--db" :: path :: rest -> go prefixes path ignore rest
| "--ignore" :: path :: rest -> go prefixes db (SSet.union ignore (load_ignorelist path)) rest
| dir :: rest -> go (dir :: prefixes) db ignore rest in
go [] (default_db_path ()) SSet.empty args
let rec go prefixes dir ignore = function
| [] -> (List.rev prefixes, dir, ignore)
| "--dir" :: path :: rest -> go prefixes path ignore rest
| "--ignore" :: path :: rest -> go prefixes dir (SSet.union ignore (load_ignorelist path)) rest
| prefix :: rest -> go (prefix :: prefixes) dir ignore rest in
go [] (default_store_path ()) SSet.empty args
let parse_dir_args args =
let rec go user_dir system_dirs rest_args = function
| [] -> (user_dir, system_dirs, List.rev rest_args)
| "--dir" :: path :: rest -> go path system_dirs rest_args rest
| "--system-dir" :: path :: rest -> go user_dir (path :: system_dirs) rest_args rest
| arg :: rest -> go user_dir system_dirs (arg :: rest_args) rest in
go (default_store_path ()) [] [] args
let () =
match Array.to_list Sys.argv |> List.tl with
| "index" :: rest ->
let (prefixes, db_path, ignorelist) = parse_index_args rest in
let (prefixes, dir, ignorelist) = parse_index_args rest in
if prefixes = [] then (Printf.eprintf "error: index requires at least one prefix dir\n"; exit 1);
let bindirs = List.map (fun p -> Filename.concat p "bin") prefixes in
let mandirs = List.map (fun p -> Filename.concat p "share/man") prefixes in
cmd_index bindirs mandirs ignorelist db_path
cmd_index bindirs mandirs ignorelist dir
| "complete" :: rest ->
let rec parse_complete_args spans db = function
| [] -> (List.rev spans, db)
| "--db" :: path :: rest -> parse_complete_args spans path rest
| arg :: rest -> parse_complete_args (arg :: spans) db rest in
let (spans, db_path) = parse_complete_args [] (default_db_path ()) rest in
cmd_complete spans db_path
let (user_dir, system_dirs, spans) = parse_dir_args rest in
cmd_complete spans user_dir system_dirs
| "query" :: rest ->
let (cmd, db_path) = match rest with
| [cmd] -> (cmd, default_db_path ())
| [cmd; "--db"; path] -> (cmd, path)
| _ -> Printf.eprintf "error: query CMD [--db PATH]\n"; exit 1 in
cmd_query cmd db_path
| "clear" :: rest ->
let rec parse_clear_args cmds db = function
| [] -> (List.rev cmds, db)
| "--db" :: path :: rest -> parse_clear_args cmds path rest
| cmd :: rest -> parse_clear_args (cmd :: cmds) db rest in
let (cmds, db_path) = parse_clear_args [] (default_db_path ()) rest in
cmd_clear cmds db_path
let (user_dir, system_dirs, args) = parse_dir_args rest in
(match args with
| [cmd] -> cmd_query cmd (user_dir :: system_dirs)
| _ -> Printf.eprintf "error: query CMD [--dir PATH] [--system-dir PATH]\n"; exit 1)
| "dump" :: rest ->
let db_path = match rest with
| ["--db"; path] -> path
| [] -> default_db_path ()
| _ -> Printf.eprintf "error: dump [--db PATH]\n"; exit 1 in
cmd_dump db_path
let (user_dir, system_dirs, _) = parse_dir_args rest in
cmd_dump (user_dir :: system_dirs)
| ["manpage"; file] -> cmd_manpage file
| ["manpage-dir"; dir] -> cmd_manpage_dir dir
| _ -> usage ()