diff --git a/bin/main.ml b/bin/main.ml index 99234be..66d753a 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -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) -> diff --git a/flake.nix b/flake.nix index c3a8a00..f24111d 100644 --- a/flake.nix +++ b/flake.nix @@ -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 diff --git a/lib/manpage.ml b/lib/manpage.ml index 552c246..ff48b68 100644 --- a/lib/manpage.ml +++ b/lib/manpage.ml @@ -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 diff --git a/lib/nushell.ml b/lib/nushell.ml index d976806..2f11916 100644 --- a/lib/nushell.ml +++ b/lib/nushell.ml @@ -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 = "" } diff --git a/lib/parser.ml b/lib/parser.ml index 3fcc807..a28e8b6 100644 --- a/lib/parser.ml +++ b/lib/parser.ml @@ -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 diff --git a/lib/store.ml b/lib/store.ml index 2c51e48..298044f 100644 --- a/lib/store.ml +++ b/lib/store.ml @@ -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;