just do everything i guess
This commit is contained in:
parent
1e7b7d1614
commit
cadc5f98d4
5 changed files with 274 additions and 21 deletions
|
|
@ -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"
|
||||
|
|
|
|||
184
lib/store.ml
184
lib/store.ml
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue