This commit is contained in:
atagen 2026-03-23 15:06:49 +11:00
parent 76656231be
commit adea668355
6 changed files with 76 additions and 27 deletions

View file

@ -586,26 +586,29 @@ let cmd_complete spans user_dir system_dirs =
| Some r -> Some (try_name, r, List.length tokens)
| None ->
find_result (List.rev (List.tl (List.rev tokens))) in
let all_tokens = cmd_name :: (match rest with
| _ :: _ when List.length rest >= 1 ->
(* exclude the partial last token from subcommand lookup *)
List.rev (List.tl (List.rev rest))
let all_tokens = cmd_name :: rest in
let partial_tokens = cmd_name :: (match rest with
| _ :: _ -> List.rev (List.tl (List.rev rest))
| _ -> []) in
let found = find_result all_tokens in
let last_token = match rest with
| [] -> "" | _ -> List.nth rest (List.length rest - 1) in
(* Try full token list first (last token is a complete subcommand),
then fall back to treating the last token as partial *)
let try_both () =
match find_result all_tokens with
| Some _ as r -> (r, "")
| None -> (find_result partial_tokens, last_token) in
let found, partial = try_both () in
(* If not found at all, try on-the-fly resolution for the base command *)
let result = match found with
| Some _ -> found
let result, partial = match found with
| Some _ -> (found, partial)
| None ->
(match find_in_path cmd_name with
| Some path ->
(match resolve_and_cache ~dir:user_dir cmd_name path with
| Some _pairs ->
find_result all_tokens
| None -> None)
| None -> None) in
let partial = match rest with
| [] -> ""
| _ -> List.nth rest (List.length rest - 1) in
| Some _pairs -> try_both ()
| None -> (None, partial))
| None -> (None, partial)) in
(match result with
| None -> print_string "[]\n"
| Some (_matched_name, r, _depth) ->

View file

@ -57,9 +57,8 @@
imports = [ ./nix/module.nix ];
programs.inshellah.package = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
programs.inshellah.snippet = ''
let cache_home = if ('XDG_CACHE_HOME' in $env) { "--dir " + $env.XDG_CACHE_HOME } else if ('HOME' in $env) { "--dir " + $env.HOME + '/.cache/inshellah' } else { "" }
let inshellah_complete = {|spans|
${lib.getExe config.programs.inshellah.package} complete "$spans" $cache_home --system-dir /run/current-system/sw/${config.programs.inshellah.completionsPath}
${lib.getExe config.programs.inshellah.package} complete ...$spans --system-dir /run/current-system/sw/${config.programs.inshellah.completionsPath} | from json
}
$env.config.completions.external = {
enable: true

View file

@ -455,6 +455,41 @@ let extract_entries lines =
else (name, best)
) ("none", []) candidates |> snd
(* --- NAME section description extraction --- *)
let extract_name_description contents =
let lines = String.split_on_char '\n' contents in
let classified = List.map classify_line lines in
let rec find = function
| [] -> None
| Macro ("SH", args) :: rest
when String.uppercase_ascii (String.trim args) = "NAME" ->
collect rest []
| _ :: rest -> find rest
and collect lines acc =
match lines with
| Macro ("SH", _) :: _ | [] -> finish acc
| Text s :: rest -> collect rest (s :: acc)
| Macro (("B" | "BI" | "BR" | "I" | "IR"), args) :: rest ->
let s = strip_inline_macro_args args |> strip_groff_escapes |> String.trim in
collect rest (if String.length s > 0 then s :: acc else acc)
| Macro ("Nm", args) :: rest ->
let s = strip_groff_escapes args |> String.trim in
collect rest (if String.length s > 0 then s :: acc else acc)
| Macro ("Nd", args) :: rest ->
let s = strip_groff_escapes args |> String.trim in
collect rest (if String.length s > 0 then ("\\- " ^ s) :: acc else acc)
| _ :: rest -> collect rest acc
and finish acc =
let full = String.concat " " (List.rev acc) |> String.trim in
(* NAME lines look like: "git-add \- Add file contents to the index" *)
let sep = Str.regexp {| *\\- *\| +- +|} in
match Str.bounded_split sep full 2 with
| [_; desc] -> Some (String.trim desc)
| _ -> None
in
find classified
(* --- SYNOPSIS command name extraction --- *)
let extract_synopsis_command_lines lines =
@ -661,7 +696,7 @@ let parse_mdoc_lines lines =
) ([], [])
|> snd |> List.rev
in
{ entries = List.rev entries; subcommands = []; positionals }
{ entries = List.rev entries; subcommands = []; positionals; description = "" }
(* --- Top-level API --- *)
@ -672,12 +707,15 @@ let parse_manpage_lines lines =
let options_section = extract_options_section lines in
let entries = extract_entries options_section in
let positionals = extract_synopsis_positionals_lines lines in
{ entries; subcommands = []; positionals }
{ entries; subcommands = []; positionals; description = "" }
end
let parse_manpage_string contents =
let lines = String.split_on_char '\n' contents in
parse_manpage_lines lines
let result = parse_manpage_lines lines in
let description = match extract_name_description contents with
| Some d -> d | None -> "" in
{ result with description }
let read_manpage_file path =
if Filename.check_suffix path ".gz" then begin

View file

@ -160,4 +160,4 @@ let generate_module cmd_name result =
Printf.sprintf "module %s {\n%s}\n\nuse %s *\n" m (extern_of cmd_name result) m
let generate_extern_from_entries cmd_name entries =
generate_extern cmd_name { entries; subcommands = []; positionals = [] }
generate_extern cmd_name { entries; subcommands = []; positionals = []; description = "" }

View file

@ -59,7 +59,7 @@ type param = Mandatory of string | Optional of string
type entry = { switch : switch; param : param option; desc : string }
type subcommand = { name : string; desc : string }
type positional = { pos_name : string; optional : bool; variadic : bool }
type help_result = { entries : entry list; subcommands : subcommand list; positionals : positional list }
type help_result = { entries : entry list; subcommands : subcommand list; positionals : positional list; description : string }
(* --- Low-level combinators --- *)
@ -313,7 +313,7 @@ let help_parser =
) []
|> List.rev_map snd
in
{ entries; subcommands; positionals = [] })
{ entries; subcommands; positionals = []; description = "" })
let skip_command_prefix s =
let len = String.length s in

View file

@ -68,8 +68,9 @@ let json_list f items =
"[" ^ String.concat "," (List.map f items) ^ "]"
let json_of_help_result ?(source="help") r =
Printf.sprintf "{\"source\":%s,\"entries\":%s,\"subcommands\":%s,\"positionals\":%s}"
Printf.sprintf "{\"source\":%s,\"description\":%s,\"entries\":%s,\"subcommands\":%s,\"positionals\":%s}"
(json_string source)
(json_string r.description)
(json_list json_entry_of r.entries)
(json_list json_subcommand_of r.subcommands)
(json_list json_positional_of r.positionals)
@ -237,7 +238,8 @@ let positional_of_json j =
let help_result_of_json j =
{ entries = List.map entry_of_json (json_to_list (json_get "entries" j));
subcommands = List.map subcommand_of_json (json_to_list (json_get "subcommands" j));
positionals = List.map positional_of_json (json_to_list (json_get "positionals" j)) }
positionals = List.map positional_of_json (json_to_list (json_get "positionals" j));
description = json_to_string (json_get "description" j) }
(* --- Filesystem operations --- *)
@ -317,9 +319,9 @@ let subcommands_of dirs command =
if Sys.file_exists dir && Sys.is_directory dir then
Array.iter (fun f ->
if String.starts_with ~prefix f then
let is_json = Filename.check_suffix f ".json" in
let base =
if Filename.check_suffix f ".json" then
Some (Filename.chop_suffix f ".json")
if is_json then Some (Filename.chop_suffix f ".json")
else if Filename.check_suffix f ".nu" then
Some (Filename.chop_suffix f ".nu")
else None in
@ -330,7 +332,14 @@ let subcommands_of dirs command =
if not (String.contains rest '_') && String.length rest > 0 then
let name = rest in
if not (SMap.mem name !subs) then
subs := SMap.add name { name; desc = "" } !subs
let desc = if is_json then
match read_file (Filename.concat dir f) with
| Some data ->
(try json_to_string (json_get "description" (parse_json data))
with _ -> "")
| None -> ""
else "" in
subs := SMap.add name { name; desc } !subs
| None -> ()
) (Sys.readdir dir)
) dirs;