mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-06-01 21:21:15 -03:00
reader: add case-insensitive history autosuggest
Resolves issue #3126 To match what I've been able to figure out about the existing design philosophy, case-sensitive matches still always take priority, but case-insensitive history suggestions precede case-insensitive completion suggestions.
This commit is contained in:
@@ -83,7 +83,7 @@
|
||||
};
|
||||
use crate::history::{
|
||||
history_session_id, in_private_mode, History, HistorySearch, PersistenceMode, SearchDirection,
|
||||
SearchType,
|
||||
SearchFlags, SearchType,
|
||||
};
|
||||
use crate::input::init_input;
|
||||
use crate::input_common::{
|
||||
@@ -151,6 +151,7 @@
|
||||
};
|
||||
use crate::wchar::prelude::*;
|
||||
use crate::wcstringutil::string_prefixes_string_maybe_case_insensitive;
|
||||
use crate::wcstringutil::CaseSensitivity;
|
||||
use crate::wcstringutil::{
|
||||
count_preceding_backslashes, join_strings, string_prefixes_string,
|
||||
string_prefixes_string_case_insensitive, StringFuzzyMatch,
|
||||
@@ -4696,7 +4697,6 @@ struct Autosuggestion {
|
||||
search_string_range: Range<usize>,
|
||||
|
||||
// Whether the autosuggestion should be case insensitive.
|
||||
// This is true for file-generated autosuggestions, but not for history.
|
||||
icase: bool,
|
||||
|
||||
// Whether the autosuggestion is a whole match from history.
|
||||
@@ -4796,35 +4796,66 @@ fn get_autosuggestion_performer(
|
||||
tokens.first().map(|tok| tok.offset()).unwrap_or(cursor_pos),
|
||||
) == search_string_range
|
||||
};
|
||||
|
||||
// Only to be used if no case-sensitive suggestions are found.
|
||||
let mut icase_history_result = None;
|
||||
|
||||
if cursor_line_has_process_start {
|
||||
let mut searcher = HistorySearch::new_with_type(
|
||||
let mut searcher = HistorySearch::new_with(
|
||||
history,
|
||||
search_string.to_owned(),
|
||||
SearchType::LinePrefix,
|
||||
SearchFlags::IGNORE_CASE,
|
||||
0,
|
||||
);
|
||||
|
||||
while !ctx.check_cancel() && searcher.go_to_next_match(SearchDirection::Backward) {
|
||||
let item = searcher.current_item();
|
||||
|
||||
// Suggest only a single line each time.
|
||||
let matched_line = item
|
||||
// The history item's may have multiple lines of text.
|
||||
// Only suggest the line that actually contains the search string.
|
||||
|
||||
let lines = item
|
||||
.str()
|
||||
.as_char_slice()
|
||||
.split(|&c| c == '\n')
|
||||
.rev()
|
||||
.find(|line| line.starts_with(search_string.as_char_slice()))
|
||||
.unwrap();
|
||||
.map(wstr::from_char_slice);
|
||||
|
||||
let mut icase = false;
|
||||
let mut matched_line = lines.clone().find(|line| line.starts_with(search_string));
|
||||
|
||||
// Only check for a case-insensitive match if we haven't already found one
|
||||
if matched_line.is_none() && icase_history_result.is_none() {
|
||||
icase = true;
|
||||
matched_line = lines
|
||||
.into_iter()
|
||||
.find(|line| string_prefixes_string_case_insensitive(search_string, line));
|
||||
}
|
||||
|
||||
let Some(matched_line) = matched_line else {
|
||||
assert!(
|
||||
icase_history_result.is_some(),
|
||||
"couldn't find line matching search {search_string:?} in history item {item:?} (did history search yield a bogus result?)"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
if autosuggest_validate_from_history(item, &working_directory, &ctx) {
|
||||
// The command autosuggestion was handled specially, so we're done.
|
||||
// History items are case-sensitive, see #3978.
|
||||
let is_whole = matched_line.len() == item.str().len();
|
||||
return AutosuggestionResult::new(
|
||||
command_line,
|
||||
search_string_range,
|
||||
let result = AutosuggestionResult::new(
|
||||
command_line.clone(),
|
||||
search_string_range.clone(),
|
||||
matched_line.into(),
|
||||
/*icase=*/ false,
|
||||
icase,
|
||||
is_whole,
|
||||
);
|
||||
if icase {
|
||||
icase_history_result = Some(result);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4855,10 +4886,22 @@ fn get_autosuggestion_performer(
|
||||
complete(&command_line[..would_be_cursor], complete_flags, &ctx);
|
||||
|
||||
let suggestion = if completions.is_empty() {
|
||||
// If there are no completions to suggest, fall back to icase history.
|
||||
if let Some(result) = icase_history_result {
|
||||
return result;
|
||||
}
|
||||
WString::new()
|
||||
} else {
|
||||
sort_and_prioritize(&mut completions, complete_flags);
|
||||
let comp = &completions[0];
|
||||
|
||||
// Prefer icase history over smartcase/icase completions.
|
||||
if let (Some(result), CaseSensitivity::Smart | CaseSensitivity::Insensitive) =
|
||||
(icase_history_result, comp.r#match.case_fold)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
let full_line = completion_apply_to_command_line(
|
||||
&OperationContext::background_interruptible(&vars),
|
||||
&comp.completion,
|
||||
|
||||
74
tests/pexpects/autosuggest.py
Normal file
74
tests/pexpects/autosuggest.py
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
from pexpect_helper import SpawnedProc
|
||||
|
||||
sp = SpawnedProc()
|
||||
send, sendline, sleep, expect_prompt = (
|
||||
sp.send,
|
||||
sp.sendline,
|
||||
sp.sleep,
|
||||
sp.expect_prompt,
|
||||
)
|
||||
|
||||
|
||||
def use_suggestion(*, delay=0.1):
|
||||
sleep(delay)
|
||||
send("\033[C")
|
||||
sendline("")
|
||||
|
||||
|
||||
def run(cmd: str):
|
||||
sendline(cmd)
|
||||
expect_prompt()
|
||||
|
||||
|
||||
expect_prompt()
|
||||
|
||||
run("echo Hello")
|
||||
|
||||
# basic samecase history suggestion
|
||||
send("echo He")
|
||||
use_suggestion()
|
||||
expect_prompt(">echo Hello\r\nHello")
|
||||
|
||||
# case-correcting history suggestion
|
||||
send("echo he")
|
||||
use_suggestion()
|
||||
expect_prompt(">echo Hello\r\nHello")
|
||||
|
||||
# prefer samecase history suggestions, even if older
|
||||
run("echo hello")
|
||||
send("echo He")
|
||||
use_suggestion()
|
||||
expect_prompt(">echo Hello\r\nHello")
|
||||
|
||||
# case-correcting command suggestion
|
||||
send("Tru")
|
||||
use_suggestion(delay=2.0)
|
||||
expect_prompt(">true \r\n")
|
||||
|
||||
# the motivating example: prefer icase history suggestions over icase completion suggestions
|
||||
run("mkdir -p Projects/myproject Projects/wrongproject")
|
||||
|
||||
# (prerequisite: without any relevant history, and with more than one subdir, fish can't suggest deeper than Projects/)
|
||||
send("cd pro")
|
||||
use_suggestion(delay=0.5)
|
||||
expect_prompt(">cd Projects/\r\n")
|
||||
|
||||
run("cd ..")
|
||||
|
||||
# (and now the actual test)
|
||||
run("cd Projects/myproject")
|
||||
run("cd ../..")
|
||||
|
||||
send("cd pro")
|
||||
use_suggestion()
|
||||
expect_prompt(">cd Projects/myproject\r\n")
|
||||
|
||||
run("cd ../..")
|
||||
|
||||
# BUT prefer samecase completion suggestions over icase history suggestions
|
||||
run("mkdir problems")
|
||||
|
||||
send("cd pro")
|
||||
use_suggestion(delay=0.5)
|
||||
expect_prompt(">cd problems/\r\n")
|
||||
Reference in New Issue
Block a user