From 9dd1261a4602a20cc9c6e8a450493e909c20f585 Mon Sep 17 00:00:00 2001 From: atagen Date: Sun, 29 Mar 2026 19:48:57 +1100 Subject: [PATCH] use relative /share path to look for manuals when resolving on-the-fly --- bin/main.ml | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/bin/main.ml b/bin/main.ml index 4ca64b5..b72a456 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -1087,15 +1087,27 @@ let cmd_complete spans user_dir system_dirs mandirs = match lookup dirs try_name with | Some r -> Some (try_name, r, List.length prefix) | None -> None) in - let all_tokens = cmd_name :: rest in + (* strip flag tokens (--user, -a, etc.) from intermediate positions. + * flags are not part of the subcommand path and should not affect + * lookup. e.g. "systemctl --user start" should look up "systemctl start". + * the last token (partial) is NOT stripped — it may be a flag the + * user is typing (e.g. "--u") which needs fuzzy matching. *) + let strip_intermediate_flags tokens = + match List.rev tokens with + | last :: rev_rest -> + List.filter (fun t -> + String.length t = 0 || t.[0] <> '-') (List.rev rev_rest) + @ [last] + | [] -> [] in + let all_tokens = strip_intermediate_flags (cmd_name :: rest) in let last_token = match rest with | [] -> "" | _ -> List.nth rest (List.length rest - 1) 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)) + else match all_tokens with + | _ :: _ -> List.rev (List.tl (List.rev all_tokens)) | _ -> [cmd_name] in let resolve tokens partial = match find_result tokens with @@ -1112,7 +1124,34 @@ let cmd_complete spans user_dir system_dirs mandirs = (* no match, or only a parent matched — try on-the-fly resolution *) (match find_in_path cmd_name with | Some path -> - (match resolve_and_cache ~dir:user_dir ~mandirs cmd_name path with + (* derive sibling share/man from the binary's location. + * e.g. /nix/store/.../bin/foo → /nix/store/.../share/man + * this lets on-the-fly resolution find manpages for commands + * not in the indexed prefixes. also resolves through nix + * wrappers to find the real binary's manpage location. *) + let mandir_of_bin p = + let bindir = Filename.dirname p in + let prefix = Filename.dirname bindir in + Filename.concat (Filename.concat prefix "share") "man" in + let bin_mandirs = + let direct = mandir_of_bin path in + (* also check the canonical path after resolving symlinks. + * e.g. /run/current-system/sw/bin/foo is a symlink to + * /nix/store/xxx/bin/foo — check /nix/store/xxx/share/man *) + let via_realpath = + try let real = Unix.realpath path in + if real <> path then [mandir_of_bin real] else [] + with Unix.Unix_error _ -> [] in + let via_wrapper = + match nix_script_wrapper_target path with + | Some target -> [mandir_of_bin target] + | None -> + match nix_wrapper_target path with + | Some target -> [mandir_of_bin target] + | None -> [] in + List.filter is_dir (direct :: via_realpath @ via_wrapper) in + let all_mandirs = bin_mandirs @ mandirs in + (match resolve_and_cache ~dir:user_dir ~mandirs:all_mandirs cmd_name path with | Some _pairs -> resolve lookup_tokens last_token | None -> (found, partial)) | None -> (found, partial)) in