filter and sort completions
This commit is contained in:
parent
144d72b223
commit
d4e41a8c2f
1 changed files with 67 additions and 45 deletions
112
bin/main.ml
112
bin/main.ml
|
|
@ -560,26 +560,38 @@ let completion_json value desc =
|
|||
Printf.sprintf "{\"value\":\"%s\",\"description\":\"%s\"}"
|
||||
(escape_json value) (escape_json desc)
|
||||
|
||||
let flag_completions prefix entries =
|
||||
List.filter_map (fun (e : entry) ->
|
||||
let desc = match e.param with
|
||||
| Some (Mandatory p) -> if e.desc <> "" then e.desc ^ " <" ^ p ^ ">" else "<" ^ p ^ ">"
|
||||
| Some (Optional p) -> if e.desc <> "" then e.desc ^ " [" ^ p ^ "]" else "[" ^ p ^ "]"
|
||||
| None -> e.desc in
|
||||
match e.switch with
|
||||
| Long l ->
|
||||
let flag = "--" ^ l in
|
||||
if String.starts_with ~prefix flag then Some (completion_json flag desc) else None
|
||||
| Short c ->
|
||||
let flag = Printf.sprintf "-%c" c in
|
||||
if String.starts_with ~prefix flag then Some (completion_json flag desc) else None
|
||||
| Both (c, l) ->
|
||||
let long = "--" ^ l in
|
||||
let short = Printf.sprintf "-%c" c in
|
||||
if String.starts_with ~prefix long then Some (completion_json long desc)
|
||||
else if String.starts_with ~prefix short then Some (completion_json short desc)
|
||||
else None
|
||||
) entries
|
||||
(* Fuzzy matching: returns a score > 0 if needle is a subsequence of haystack.
|
||||
Higher scores = better match. Scoring:
|
||||
- Exact match: 1000
|
||||
- Prefix match: 900 + length bonus
|
||||
- Subsequence with word-boundary alignment: bonus per boundary hit
|
||||
- Plain subsequence: base score from match density *)
|
||||
let fuzzy_score needle haystack =
|
||||
let nlen = String.length needle and hlen = String.length haystack in
|
||||
if nlen = 0 then 1
|
||||
else if nlen > hlen then 0
|
||||
else if needle = haystack then 1000
|
||||
else
|
||||
let needle = String.lowercase_ascii needle
|
||||
and haystack_lc = String.lowercase_ascii haystack in
|
||||
if String.starts_with ~prefix:needle haystack_lc then
|
||||
900 + (nlen * 100 / hlen)
|
||||
else
|
||||
let is_boundary hi =
|
||||
hi = 0 || haystack.[hi - 1] = '-' || haystack.[hi - 1] = '_'
|
||||
|| (haystack.[hi - 1] >= 'a' && haystack.[hi - 1] <= 'z'
|
||||
&& haystack.[hi] >= 'A' && haystack.[hi] <= 'Z') in
|
||||
(* Walk haystack matching needle chars as a subsequence *)
|
||||
let ni, score, _, _ =
|
||||
String.fold_left (fun (ni, score, hi, prev_match) c ->
|
||||
if ni >= nlen then (ni, score, hi + 1, prev_match)
|
||||
else if c = needle.[ni] then
|
||||
let bonus = (if is_boundary hi then 50 else 10)
|
||||
+ (if prev_match = hi - 1 then 20 else 0) in
|
||||
(ni + 1, score + bonus, hi + 1, hi)
|
||||
else (ni, score, hi + 1, prev_match)
|
||||
) (0, 0, 0, -1) haystack_lc in
|
||||
if ni = nlen then score else 0
|
||||
|
||||
let cmd_complete spans user_dir system_dirs =
|
||||
match spans with
|
||||
|
|
@ -599,18 +611,20 @@ let cmd_complete spans user_dir system_dirs =
|
|||
| Some r -> Some (try_name, r, List.length prefix)
|
||||
| None -> None) in
|
||||
let all_tokens = cmd_name :: rest in
|
||||
let partial_tokens = match rest with
|
||||
| _ :: _ -> cmd_name :: List.rev (List.tl (List.rev rest))
|
||||
| _ -> [cmd_name] 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
|
||||
(* Only treat the last token as a completed subcommand when nushell
|
||||
sends a trailing empty token (cursor is after a space).
|
||||
Otherwise the user is still typing and we treat it as partial. *)
|
||||
let lookup_tokens = if last_token = "" then all_tokens
|
||||
else match rest with
|
||||
| _ :: _ -> cmd_name :: List.rev (List.tl (List.rev rest))
|
||||
| _ -> [cmd_name] in
|
||||
let resolve tokens partial =
|
||||
match find_result tokens with
|
||||
| Some _ as found -> (found, partial)
|
||||
| None -> (None, partial) in
|
||||
let found, partial = resolve lookup_tokens last_token in
|
||||
(* If not found at all, try on-the-fly resolution for the base command *)
|
||||
let result, partial = match found with
|
||||
| Some _ -> (found, partial)
|
||||
|
|
@ -618,26 +632,34 @@ let cmd_complete spans user_dir system_dirs =
|
|||
(match find_in_path cmd_name with
|
||||
| Some path ->
|
||||
(match resolve_and_cache ~dir:user_dir cmd_name path with
|
||||
| Some _pairs -> try_both ()
|
||||
| Some _pairs -> resolve lookup_tokens last_token
|
||||
| None -> (None, partial))
|
||||
| None -> (None, partial)) in
|
||||
let candidates = match result with
|
||||
| None -> []
|
||||
| Some (_matched_name, r, _depth) ->
|
||||
if String.starts_with ~prefix:"-" partial then
|
||||
flag_completions partial r.entries
|
||||
else
|
||||
let subs = match r.subcommands with
|
||||
| _ :: _ -> r.subcommands
|
||||
| [] -> subcommands_of dirs _matched_name in
|
||||
let sub_candidates = List.filter_map (fun (sc : subcommand) ->
|
||||
if partial = "" || String.starts_with ~prefix:partial sc.name then
|
||||
Some (completion_json sc.name sc.desc)
|
||||
else None
|
||||
) subs in
|
||||
if partial = "" || sub_candidates = [] then
|
||||
sub_candidates @ flag_completions partial r.entries
|
||||
else sub_candidates in
|
||||
let subs = match r.subcommands with
|
||||
| _ :: _ -> r.subcommands
|
||||
| [] -> subcommands_of dirs _matched_name in
|
||||
let sub_candidates = List.filter_map (fun (sc : subcommand) ->
|
||||
let s = fuzzy_score partial sc.name in
|
||||
if s > 0 then Some (s, completion_json sc.name sc.desc) else None
|
||||
) subs in
|
||||
let flag_candidates = List.filter_map (fun (e : entry) ->
|
||||
let desc = match e.param with
|
||||
| Some (Mandatory p) -> if e.desc <> "" then e.desc ^ " <" ^ p ^ ">" else "<" ^ p ^ ">"
|
||||
| Some (Optional p) -> if e.desc <> "" then e.desc ^ " [" ^ p ^ "]" else "[" ^ p ^ "]"
|
||||
| None -> e.desc in
|
||||
let flag = match e.switch with
|
||||
| Long l -> "--" ^ l
|
||||
| Short c -> Printf.sprintf "-%c" c
|
||||
| Both (_, l) -> "--" ^ l in
|
||||
let s = fuzzy_score partial flag in
|
||||
if s > 0 then Some (s, completion_json flag desc) else None
|
||||
) r.entries in
|
||||
let scored = sub_candidates @ flag_candidates in
|
||||
List.sort (fun (a, _) (b, _) -> compare b a) scored
|
||||
|> List.map snd in
|
||||
Printf.printf "[%s]\n" (String.concat "," candidates)
|
||||
|
||||
let cmd_query cmd dirs =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue