Compare commits

...

5 Commits

Author SHA1 Message Date
Johannes Altmanninger
5eb7687a64 tests/checks/tmux-complete4.fish: fix for macOS sed 2026-04-17 03:22:56 +08:00
Johannes Altmanninger
8d6426295e complete: automatically resolve REPLACES_TOKEN flag
This flag is implied by matches that require replacements.  Reflect that
in the Completion::new, reducing the number of places where we raise the
flag.  This slightly simplifies tasks like proving the parent commit.

There are other scenarios (e.g. wildcards) where we currently set
the flag additionally.
2026-04-17 01:31:29 +08:00
Johannes Altmanninger
85e76ba356 Fix option substr completions not being filtered out
Commit 3546ffa3ef (reader handle_completions(): remove dead filtering code,
2026-01-02) gives a proof of correctness that still makes sense;
The first lemma ("if will_replace_token") is trivially true, so no need to
assert it.
The second lemma ("if !will_replace_token") is violated in some edge cases:
we claim that given a token "-c", the option completion "--clip" is an exact match,
which is not true, it's a substring match.

Fix that, asserting the claim.
2026-04-17 01:31:29 +08:00
Johannes Altmanninger
fee4288122 complete: reuse string fuzzy match when completing ~$USER
If we get to this code path, we'll only get completions for user
names, so technically the full StringFuzzyMatch with its ranking of
samecase/smartcase/icase (only showing the best) might be overkill,
but it seems like a good idea to treat this the same way as other
completions.

The occasion for this commit is to correct a wrong
StringFuzzyMatch::exact_match() in the icase branch; which will be
important for a following commit.  Add a test for that.
2026-04-17 01:28:54 +08:00
Johannes Altmanninger
413246a93d reader handle_completions(): move loop-invariant code 2026-04-17 01:28:02 +08:00
5 changed files with 71 additions and 45 deletions

View File

@@ -32,8 +32,7 @@
use fish_util::wcsfilecmp;
use fish_wcstringutil::{
StringFuzzyMatch, string_fuzzy_match_string, string_prefixes_string,
string_prefixes_string_case_insensitive, string_suffixes_string_case_insensitive,
strip_executable_suffix,
string_suffixes_string_case_insensitive, strip_executable_suffix,
};
use fish_widestring::{WExt as _, charptr2wcstring};
use std::{
@@ -159,7 +158,10 @@ pub fn new(
r#match: StringFuzzyMatch, /* = exact_match */
flags: CompleteFlags,
) -> Self {
let flags = resolve_auto_space(&completion, flags);
let mut flags = resolve_auto_space(&completion, flags);
if r#match.requires_full_replacement() {
flags |= CompleteFlags::REPLACES_TOKEN;
}
Self {
completion,
description,
@@ -1483,14 +1485,12 @@ fn complete_param_for_command(
continue;
};
let mut offset = 0;
let mut flags = CompleteFlags::empty();
if r#match.requires_full_replacement() {
flags = CompleteFlags::REPLACES_TOKEN;
let offset = if r#match.requires_full_replacement() {
0
} else {
offset = s.len();
}
s.len()
};
let completion = whole_opt.slice_from(offset);
// does this switch have any known arguments
let has_arg = !o.comp.is_empty();
@@ -1502,14 +1502,14 @@ fn complete_param_for_command(
// a completion. By default we avoid using '=' and instead rely on '--switch
// switch-arg', since it is more commonly supported by homebrew getopt-like
// functions.
let completion = sprintf!("%s=", whole_opt.slice_from(offset));
let completion = sprintf!("%s=", completion);
// Append a long-style option with a mandatory trailing equal sign
if !self.completions.add(Completion::new(
completion,
o.desc.localize().to_owned(),
StringFuzzyMatch::exact_match(),
flags | CompleteFlags::NO_SPACE,
r#match,
CompleteFlags::NO_SPACE,
)) {
return false;
}
@@ -1517,10 +1517,10 @@ fn complete_param_for_command(
// Append a long-style option
if !self.completions.add(Completion::new(
whole_opt.slice_from(offset).to_owned(),
completion.to_owned(),
o.desc.localize().to_owned(),
StringFuzzyMatch::exact_match(),
flags,
r#match,
CompleteFlags::empty(),
)) {
return false;
}
@@ -1666,7 +1666,7 @@ fn complete_variable(&mut self, s: &wstr, start_offset: usize) -> bool {
// Take only the suffix.
env_name.slice_from(varlen).to_owned()
} else {
flags |= CompleteFlags::REPLACES_TOKEN | CompleteFlags::DONT_ESCAPE;
flags |= CompleteFlags::DONT_ESCAPE;
whole_var.slice_to(start_offset).to_owned() + env_name.as_utfstr()
};
@@ -1823,30 +1823,23 @@ fn getpwent_name() -> Option<WString> {
break;
}
if string_prefixes_string(user_name, &pw_name) {
if let Some(r#match) = StringFuzzyMatch::try_create(user_name, &pw_name, true) {
let desc = wgettext_fmt!(COMPLETE_USER_DESC, &pw_name);
// Append a user name.
// TODO: propagate overflow?
let mut flags = CompleteFlags::NO_SPACE;
if r#match.requires_full_replacement() {
flags |= CompleteFlags::DONT_ESCAPE;
}
let _ = self.completions.add(Completion::new(
pw_name.slice_from(name_len).to_owned(),
if r#match.requires_full_replacement() {
sprintf!("~%s", &pw_name)
} else {
pw_name.slice_from(name_len).to_owned()
},
desc,
StringFuzzyMatch::exact_match(),
CompleteFlags::NO_SPACE,
));
result = true;
} else if string_prefixes_string_case_insensitive(user_name, &pw_name) {
let name = sprintf!("~%s", &pw_name);
let desc = wgettext_fmt!(COMPLETE_USER_DESC, &pw_name);
// Append a user name
// TODO: propagate overflow?
let _ = self.completions.add(Completion::new(
name,
desc,
StringFuzzyMatch::exact_match(),
CompleteFlags::REPLACES_TOKEN
| CompleteFlags::DONT_ESCAPE
| CompleteFlags::NO_SPACE,
r#match,
flags,
));
result = true;
}

View File

@@ -7026,9 +7026,11 @@ fn handle_completions(&mut self, token_range: Range<usize>, mut comp: Vec<Comple
comp.retain(|c| !c.replaces_token() || reader_can_replace(&tok, c.flags));
for c in &mut comp {
if !will_replace_token && c.replaces_token() {
c.flags |= CompleteFlags::SUPPRESS_PAGER_PREFIX;
if !will_replace_token {
for c in &mut comp {
if c.replaces_token() {
c.flags |= CompleteFlags::SUPPRESS_PAGER_PREFIX;
}
}
}

View File

@@ -149,16 +149,11 @@ fn wildcard_complete_internal(
// Note: out_completion may be empty if the completion really is empty, e.g. tab-completing
// 'foo' when a file 'foo' exists.
let local_flags = if full_replacement {
flags | CompleteFlags::REPLACES_TOKEN
} else {
flags
};
if !out.add(Completion::new(
out_completion.to_owned(),
out_desc,
m,
local_flags,
flags,
)) {
return WildcardResult::Overflow;
}

View File

@@ -727,3 +727,9 @@ complete -c foo -a "foo\\"
complete -C
# CHECKERR: complete: Can not get commandline in non-interactive mode
if string match -rq -- '^[a-z]+$' $USER
set -l first_letter_wrong_case (string sub -l 1 -- $USER | string upper)
string match -rq -- "$USER\t.*" (complete -C "echo ~$first_letter_wrong_case")
or echo "`complete -C'echo ~$first_letter_wrong_case'` did not yield $USER"
end

View File

@@ -0,0 +1,30 @@
#RUN: %fish %s
#REQUIRES: command -v tmux
#REQUIRES: uname -r | grep -qv Microsoft
isolated-tmux-start -C '
complete : -s c -l clip
complete : -s q -l qrcode
set -g fish_autosuggestion_enabled 0
'
touch somefile1
touch somefile2
isolated-tmux send-keys C-l ': -c'
function tab
isolated-tmux send-keys Tab
tmux-sleep
isolated-tmux capture-pane -p | awk '/./ { print "[" $0 "]" }'
end
tab
# CHECK: [prompt 0> : -cq]
tab
# CHECK: [prompt 0> : -cq somefile]
# CHECK: [somefile1 somefile2]
tab
# CHECK: [prompt 0> : -cq somefile1]
# CHECK: [somefile1 somefile2]