just do everything i guess

This commit is contained in:
atagen 2026-03-29 16:40:36 +11:00
parent 1e7b7d1614
commit 54000cfafe
5 changed files with 268 additions and 16 deletions

View file

@ -868,7 +868,15 @@ let cmd_index bindirs mandirs ignorelist help_only dir =
let subdir = Filename.concat mandir (Printf.sprintf "man%d" section) in
if is_dir subdir then begin
let files = Sys.readdir subdir in
Array.sort String.compare files;
(* sort by filename length first, then alphabetically.
* this ensures parent manpages (e.g. nix-env.1.gz) are
* processed before subpage manpages (nix-env-install.1.gz)
* so the parent's data isn't overwritten by a subpage
* whose synopsis also extracts the parent command name. *)
Array.sort (fun a b ->
let la = String.length a and lb = String.length b in
if la <> lb then compare la lb
else String.compare a b) files;
Array.iter (fun file ->
let base_cmd = cmd_name_of_manpage file in
if SSet.mem base_cmd help_only then ()
@ -879,7 +887,12 @@ let cmd_index bindirs mandirs ignorelist help_only dir =
write_result ~dir ~source:"manpage" cmd result;
done_cmds := SSet.add cmd !done_cmds;
incr result_count
end;
end else if cmd <> base_cmd then
(* a subpage manpage (e.g. nix-env-install.1) extracted
* a command name that was already indexed (e.g. "nix-env").
* warn so the user can investigate. *)
Printf.eprintf "warning: %s extracted cmd \"%s\" (already indexed), skipping\n"
file cmd;
List.iter (fun (sub_cmd, sub_result) ->
if not (SSet.mem sub_cmd !done_cmds) then begin
write_result ~dir ~source:"manpage" sub_cmd sub_result;
@ -1041,7 +1054,10 @@ let find_real_command is_command args =
* this ensures file completions appear with full nushell UX. when the user
* IS typing a flag (partial starts with "-"), we return our flag candidates. *)
let cmd_complete spans user_dir system_dirs mandirs =
let dirs = user_dir :: system_dirs in
(* system dirs are searched first — they're built at index time from
* manpages and are authoritative. user dir is an on-the-fly cache
* that should only be used as fallback for commands not in any system dir. *)
let dirs = system_dirs @ [user_dir] in
(* if the command line starts with a privilege-escalation wrapper, scan past
* it to find the real command. we identify the command by checking the store
* and $PATH this avoids needing per-command option tables which are fragile

View file

@ -84,19 +84,24 @@
$entries
}
"systemctl" => {
if ($spans | length) < 2 { null } else {
if ($spans | length) < 3 { null } else {
let kw = $spans | last
^systemctl list-units --all --no-pager --plain --full $"($kw)*"
| detect columns
| drop 7
| headers
| each { |r| {value: $r.UNIT, description: ($r.DESCRIPTION | default "")} }
let scope = if ("--user" in $spans) { [--user] } else { [] }
^systemctl ...$scope list-units --all --no-pager --plain --full --no-legend $"($kw)*"
| lines
| each { |l|
let parsed = $l | parse -r '(?P<unit>\S+)\s+\S+\s+\S+\s+\S+\s+(?P<desc>.*)'
if ($parsed | length) > 0 {
{value: $parsed.0.unit, description: ($parsed.0.desc | str trim)}
}
} | compact
}
}
_ => { null }
}
} else { null }
$completions | append $additional
let result = ($completions | default []) | append ($additional | default []) | compact
if ($result | is-empty) { null } else { $result }
}
$env.config.completions.external = {enable: true, max_results: 200, completer: $inshellah_complete}
'';

View file

@ -655,6 +655,15 @@ let extract_name_description contents =
* we hit something that looks like an argument (starts with [, <, -, etc.). *)
let extract_synopsis_command_lines lines =
(* replace italic text (\fI...\fR) with angle-bracketed placeholders
* before classification strips the font info. italic in groff indicates
* a parameter/placeholder (e.g. \fIoperation\fR), not a command word.
* the angle brackets cause extract_cmd to stop at these tokens since
* '<' is in its stop set. without this, "nix-env \fIoperation\fR"
* would be parsed as command "nix-env operation" instead of "nix-env". *)
let lines = List.map (fun line ->
Str.global_replace (Str.regexp {|\\fI\([^\\]*\)\\f[RP]|}) {|<\1>|} line
) lines in
let classified = List.map classify_line lines in
let is_synopsis name =
String.uppercase_ascii (String.trim name) = "SYNOPSIS"

View file

@ -387,18 +387,190 @@ let find_file dirs command =
else None
) dirs
(* look up a command and deserialize its help_result from JSON.
* only searches for .json files (not .nu, since those can't be deserialized
* back into help_result). returns None if not found or parse fails. *)
(* parse a nushell .nu file to extract a help_result for a specific command.
* .nu files contain `export extern "cmd" [ ... ]` blocks with flag definitions.
* this parser extracts flags, positionals, subcommands, and descriptions
* from the nushell extern syntax so the completer can use native completions.
*
* nushell extern parameter syntax:
* --flag(-s): type # description Both(s, "flag") with param
* --flag: type # description Long "flag" with param
* --flag # description Long "flag" no param
* -s # description Short 's'
* name: type # description positional
* name?: type optional positional
* ...name: type variadic positional
*)
let parse_nu_completions target_cmd contents =
let lines = String.split_on_char '\n' contents in
(* extract the description comment preceding an export extern block *)
let current_desc = ref "" in
(* collect all extern blocks: (cmd_name, entries, positionals, description) *)
let blocks = ref [] in
let in_block = ref false in
let block_cmd = ref "" in
let block_entries = ref [] in
let block_positionals = ref [] in
let block_desc = ref "" in
let finish_block () =
if !in_block then begin
blocks := (!block_cmd, List.rev !block_entries,
List.rev !block_positionals, !block_desc) :: !blocks;
in_block := false
end in
List.iter (fun line ->
let trimmed = String.trim line in
if not !in_block then begin
(* look for description comments and export extern lines *)
if String.length trimmed > 2 && trimmed.[0] = '#' && trimmed.[1] = ' ' then
current_desc := String.trim (String.sub trimmed 2 (String.length trimmed - 2))
else if String.length trimmed > 15
&& (try ignore (Str.search_forward
(Str.regexp_string "export extern") trimmed 0); true
with Not_found -> false) then begin
(* extract command name from: export extern "cmd name" [ or export extern cmd [ *)
let re_quoted = Str.regexp {|export extern "\([^"]*\)"|} in
let re_bare = Str.regexp {|export extern \([a-zA-Z0-9_-]+\)|} in
let cmd_opt =
if try ignore (Str.search_forward re_quoted trimmed 0); true
with Not_found -> false
then Some (Str.matched_group 1 trimmed)
else if try ignore (Str.search_forward re_bare trimmed 0); true
with Not_found -> false
then Some (Str.matched_group 1 trimmed)
else None in
if cmd_opt <> None then begin
let cmd = match cmd_opt with Some c -> c | None -> "" in
in_block := true;
block_cmd := cmd;
block_entries := [];
block_positionals := [];
block_desc := !current_desc;
current_desc := ""
end
end else
current_desc := ""
end else begin
(* inside an extern block — parse flag/positional lines *)
if String.length trimmed > 0 && trimmed.[0] = ']' then
finish_block ()
else begin
(* extract description from # comment *)
let param_part, desc =
match String.split_on_char '#' trimmed with
| before :: rest ->
(String.trim before,
String.trim (String.concat "#" rest))
| _ -> (trimmed, "")
in
if String.length param_part > 1 then begin
if param_part.[0] = '-' && param_part.[1] = '-' then begin
(* long flag: --flag(-s): type or --flag: type or --flag *)
let re_both = Str.regexp {|--\([a-zA-Z0-9-]+\)(-\([a-zA-Z0-9]\))\(: *\([a-zA-Z]+\)\)?|} in
let re_long = Str.regexp {|--\([a-zA-Z0-9-]+\)\(: *\([a-zA-Z]+\)\)?|} in
if try ignore (Str.search_forward re_both param_part 0); true
with Not_found -> false then begin
let long = Str.matched_group 1 param_part in
let short = (Str.matched_group 2 param_part).[0] in
let param = try Some (Mandatory (Str.matched_group 4 param_part))
with Not_found | Invalid_argument _ -> None in
block_entries := { switch = Both (short, long); param; desc } :: !block_entries
end else if try ignore (Str.search_forward re_long param_part 0); true
with Not_found -> false then begin
let long = Str.matched_group 1 param_part in
let param = try Some (Mandatory (Str.matched_group 3 param_part))
with Not_found | Invalid_argument _ -> None in
block_entries := { switch = Long long; param; desc } :: !block_entries
end
end else if param_part.[0] = '-' then begin
(* short flag: -s *)
if String.length param_part >= 2 then
let c = param_part.[1] in
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') then
block_entries := { switch = Short c; param = None; desc } :: !block_entries
end else begin
(* positional: name: type or name?: type or ...name: type *)
let variadic = String.starts_with ~prefix:"..." param_part in
let part = if variadic then String.sub param_part 3 (String.length param_part - 3)
else param_part in
let optional = try let q = String.index part '?' in q > 0
with Not_found -> false in
let name = match String.index_opt part ':' with
| Some i -> String.trim (String.sub part 0 i)
| None -> match String.index_opt part '?' with
| Some i -> String.trim (String.sub part 0 i)
| None -> String.trim part in
let name = String.map (function '-' -> '_' | c -> c) name in
if String.length name > 0 && name.[0] <> '-' then
block_positionals := { pos_name = name; optional = optional || variadic;
variadic } :: !block_positionals
end
end
end
end
) lines;
finish_block ();
let blocks = List.rev !blocks in
(* find the block matching the target command *)
let target = target_cmd in
match List.find_opt (fun (cmd, _, _, _) -> cmd = target) blocks with
| Some (_, entries, positionals, description) ->
(* collect subcommands from other blocks that are children of this command *)
let prefix = target ^ " " in
let subcommands = List.filter_map (fun (cmd, _, _, desc) ->
if String.starts_with ~prefix cmd then
let sub_name = String.sub cmd (String.length prefix)
(String.length cmd - String.length prefix) in
(* only immediate subcommands (no further spaces) *)
if not (String.contains sub_name ' ') && String.length sub_name > 0
then Some { name = sub_name; desc }
else None
else None
) blocks in
{ entries; subcommands; positionals; description }
| None ->
(* target not found — return empty result *)
{ entries = []; subcommands = []; positionals = []; description = "" }
(* look up a command and deserialize its help_result.
* searches for .json files first, then falls back to .nu files
* (parsing the nushell extern syntax to extract completion data).
* for subcommands like "rbw get", also checks the parent's .nu file
* (e.g. rbw.nu) since clap-generated .nu files contain all extern
* blocks in a single file. *)
let lookup dirs command =
let base_name = filename_of_command command in
(* also try the root command's .nu file for subcommand lookups.
* "rbw get" -> try rbw.nu and look for the "rbw get" extern block. *)
let parent_base = match String.index_opt command ' ' with
| Some i -> Some (filename_of_command (String.sub command 0 i))
| None -> None in
List.find_map (fun directory ->
let path = Filename.concat directory (base_name ^ ".json") in
match read_file path with
let json_path = Filename.concat directory (base_name ^ ".json") in
match read_file json_path with
| Some data ->
(try Some (help_result_of_json (parse_json data))
with _ -> None)
| None -> None
| None ->
let nu_path = Filename.concat directory (base_name ^ ".nu") in
(match read_file nu_path with
| Some data ->
(try Some (parse_nu_completions command data)
with _ -> None)
| None ->
(* try parent's .nu file for subcommand blocks *)
match parent_base with
| Some pb ->
let parent_nu = Filename.concat directory (pb ^ ".nu") in
(match read_file parent_nu with
| Some data ->
(try
let r = parse_nu_completions command data in
if r.entries <> [] || r.subcommands <> [] || r.positionals <> []
then Some r else None
with _ -> None)
| None -> None)
| None -> None)
) dirs
(* look up a command's raw data (JSON or .nu source) without parsing.

View file

@ -500,6 +500,52 @@ Options:
* here we just verify the parser extracts it correctly. *)
check "has entries too" (List.length r.entries >= 2)
let test_nu_file_parsing () =
Printf.printf "\n== .nu file parsing ==\n";
let nu_source = {|module completions {
# Unofficial CLI tool
export extern mytool [
--help(-h) # Print help
--version(-V) # Print version
]
# List all items
export extern "mytool list" [
--raw # Output as JSON
--format(-f): string # Output format
--help(-h) # Print help
name?: string # Filter by name
]
}
use completions *
|} in
let r = Inshellah.Store.parse_nu_completions "mytool" nu_source in
check "has entries" (List.length r.entries = 2);
check "has subcommands" (List.length r.subcommands >= 1);
let list_sc = List.find_opt (fun (sc : subcommand) -> sc.name = "list") r.subcommands in
check "has list subcommand" (list_sc <> None);
check "description" (r.description = "Unofficial CLI tool");
(* test subcommand lookup *)
let r2 = Inshellah.Store.parse_nu_completions "mytool list" nu_source in
check "list has entries" (List.length r2.entries = 3);
let has_format = List.exists (fun (e : entry) ->
e.switch = Both ('f', "format")) r2.entries in
check "list has --format(-f)" has_format;
check "list has positional" (List.length r2.positionals >= 1)
let test_italic_synopsis () =
Printf.printf "\n== Italic in SYNOPSIS ==\n";
let groff = {|.SH Synopsis
.LP
\f(CRnix-env\fR \fIoperation\fR [\fIoptions\fR] [\fIarguments\fR]
.SH Description
|} in
let cmd = extract_synopsis_command groff in
check "no phantom operation" (cmd = Some "nix-env")
let test_font_boundary_spacing () =
Printf.printf "\n== Font boundary spacing ==\n";
(* \fB--max-results\fR\fIcount\fR should become "--max-results count" *)
@ -556,5 +602,9 @@ let () =
test_commands_section_subcommands ();
test_self_listing_detection ();
Printf.printf "\nRunning .nu and synopsis tests...\n";
test_nu_file_parsing ();
test_italic_synopsis ();
Printf.printf "\n=== Results: %d passed, %d failed ===\n" !passes !failures;
if !failures > 0 then exit 1