History pager to only operate on the line at cursor

Multiline search strings are weirdly broken (inserting control characters
in the command line) and probably not very useful anyway.
On the other hand I often want to compose a multi-line command
from single-line commands I ran previously.

Let's support this case by limiting the initial search string to the current
line; and replace only that line.

Alternatively this could operate on jobs (that is, replace a surrounding
"foo | bar") instead of using line boundaries.
This commit is contained in:
Johannes Altmanninger 2024-03-22 09:39:26 +01:00
parent 299fcde808
commit 232483d89a
5 changed files with 52 additions and 15 deletions

View file

@ -41,6 +41,7 @@ Notable improvements and fixes
- New function ``fish_should_add_to_history`` can be overridden to decide whether a command should be added to the history (:issue:`10302`).
- :kbd:`Control-C` during command input no longer prints ``^C`` and a new prompt but merely clears the command line. This restores the behavior from version 2.2. To revert to the old behavior use ``bind \cc __fish_cancel_commandline`` (:issue:`10213`).
- The :kbd:`Control-R` history search now uses glob syntax (:issue:`10131`).
- The :kbd:`Control-R` history search now operates only on the line at cursor, making it easier to quickly compose a multi-line commandline by recalling previous commands.
Deprecations and removed features
---------------------------------

View file

@ -109,8 +109,8 @@ bitflags! {
const DONT_SORT = 1 << 5;
/// This completion looks to have the same string as an existing argument.
const DUPLICATES_ARGUMENT = 1 << 6;
/// This completes not just a token but replaces the entire commandline.
const REPLACES_COMMANDLINE = 1 << 7;
/// This completes not just a token but replaces an entire line.
const REPLACES_LINE = 1 << 7;
}
}
@ -193,8 +193,8 @@ impl Completion {
}
/// Returns whether this replaces the entire commandline.
pub fn replaces_commandline(&self) -> bool {
self.flags.contains(CompleteFlags::REPLACES_COMMANDLINE)
pub fn replaces_line(&self) -> bool {
self.flags.contains(CompleteFlags::REPLACES_LINE)
}
/// Returns the completion's match rank. Lower ranks are better completions.

View file

@ -139,6 +139,23 @@ impl EditableLine {
self.text.char_at(idx)
}
pub fn line_at_cursor(&self) -> &wstr {
let start = self.text[0..self.position()]
.as_char_slice()
.iter()
.rposition(|&c| c == '\n')
.map(|newline| newline + 1)
.unwrap_or(0);
let end = self.text[self.position()..]
.as_char_slice()
.iter()
.position(|&c| c == '\n')
.map(|pos| self.position() + pos)
.unwrap_or(self.len());
// Remove any traililng newline
self.text[start..end].trim_matches('\n')
}
pub fn clear(&mut self) {
self.undo_history.clear();
if self.is_empty() {

View file

@ -1190,7 +1190,7 @@ fn process_completions_into_infos(lst: &[Completion]) -> Vec<PagerComp> {
EscapeFlags::NO_PRINTABLES | EscapeFlags::NO_QUOTED | EscapeFlags::SYMBOLIC,
),
));
if comp.replaces_commandline()
if comp.replaces_line()
// HACK We want to render a full shell command, with syntax highlighting. Above we
// escape nonprintables, which might make the rendered command longer than the original
// completion. In that case we get wrong colors. However this should only happen in

View file

@ -2423,7 +2423,7 @@ impl ReaderData {
let search_string = if !self.history_search.active()
|| self.history_search.search_string().is_empty()
{
parse_util_escape_wildcards(self.command_line.text())
parse_util_escape_wildcards(self.command_line.line_at_cursor())
} else {
// If we have an actual history search already going, reuse that term
// - this is if the user looks around a bit and decides to switch to the pager.
@ -4138,9 +4138,7 @@ fn history_pager_search(
item.str().to_owned(),
L!("").to_owned(),
StringFuzzyMatch::exact_match(),
CompleteFlags::REPLACES_COMMANDLINE
| CompleteFlags::DONT_ESCAPE
| CompleteFlags::DONT_SORT,
CompleteFlags::REPLACES_LINE | CompleteFlags::DONT_ESCAPE | CompleteFlags::DONT_SORT,
));
next_match_found = search.go_to_next_match(direction);
@ -4877,6 +4875,28 @@ fn unescaped_quote(s: &wstr, pos: usize) -> Option<char> {
result
}
fn replace_line_at_cursor(
text: &wstr,
inout_cursor_pos: &mut usize,
replacement: &wstr,
) -> WString {
let cursor = *inout_cursor_pos;
let start = text[0..cursor]
.as_char_slice()
.iter()
.rposition(|&c| c == '\n')
.map(|newline| newline + 1)
.unwrap_or(0);
let end = text[cursor..]
.as_char_slice()
.iter()
.position(|&c| c == '\n')
.map(|pos| cursor + pos)
.unwrap_or(text.len());
*inout_cursor_pos = start + replacement.len();
text[..start].to_owned() + replacement + &text[end..]
}
/// Apply a completion string. Exposed for testing only.
///
/// Insert the string in the given command line at the given cursor position. The function checks if
@ -4900,8 +4920,8 @@ pub fn completion_apply_to_command_line(
append_only: bool,
) -> WString {
let add_space = !flags.contains(CompleteFlags::NO_SPACE);
let do_replace = flags.contains(CompleteFlags::REPLACES_TOKEN);
let do_replace_commandline = flags.contains(CompleteFlags::REPLACES_COMMANDLINE);
let do_replace_token = flags.contains(CompleteFlags::REPLACES_TOKEN);
let do_replace_line = flags.contains(CompleteFlags::REPLACES_LINE);
let do_escape = !flags.contains(CompleteFlags::DONT_ESCAPE);
let no_tilde = flags.contains(CompleteFlags::DONT_ESCAPE_TILDES);
@ -4909,13 +4929,12 @@ pub fn completion_apply_to_command_line(
let mut back_into_trailing_quote = false;
let have_space_after_token = command_line.char_at(cursor_pos) == ' ';
if do_replace_commandline {
if do_replace_line {
assert!(!do_escape, "unsupported completion flag");
*inout_cursor_pos = val_str.len();
return val_str.to_owned();
return replace_line_at_cursor(command_line, inout_cursor_pos, val_str);
}
if do_replace {
if do_replace_token {
let mut move_cursor;
let mut range = 0..0;
parse_util_token_extent(command_line, cursor_pos, &mut range, None);