Autosuggestions in multi-line command lines

If I run

	$ command A
	$ command B
	$ command C

and find myself wanting to re-run the same sequence of commands
multiple times, I like to join them into a single command:

	$ command A &&
	    command B &&
	    command C

When composing this mega-commandline, history search can recall the
first one; the others I usually inserted with a combination of ctrl-k,
ctrl-x or the ctrl-r (since 232483d89a (History pager to only operate
on the line at cursor, 2024-03-22), which is motivated by exactly
this use case).

It's irritating that autosuggestions are missing, so try adding them.

Today, only single-line commands from history are suggested. In
future, we should perhaps also suggest any line from a multi-line
command from history.
This commit is contained in:
Johannes Altmanninger
2024-12-15 17:27:00 +01:00
parent 532abaddae
commit 1c4e5cadf2
6 changed files with 460 additions and 173 deletions

View File

@@ -12,6 +12,7 @@ Scripting improvements
Interactive improvements Interactive improvements
------------------------ ------------------------
- Autosuggestions are now also provided in multi-line command lines. Like `ctrl-r`, autosuggestions operate only on the current line.
New or improved bindings New or improved bindings
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -342,7 +342,7 @@ fn cursor_position_after_edit(edit: &Edit) -> usize {
cursor.saturating_sub(removed) cursor.saturating_sub(removed)
} }
fn range_of_line_at_cursor(buffer: &wstr, cursor: usize) -> Range<usize> { pub fn range_of_line_at_cursor(buffer: &wstr, cursor: usize) -> Range<usize> {
let start = buffer[0..cursor] let start = buffer[0..cursor]
.as_char_slice() .as_char_slice()
.iter() .iter()

View File

@@ -58,8 +58,7 @@
complete, complete_load, sort_and_prioritize, CompleteFlags, Completion, CompletionList, complete, complete_load, sort_and_prioritize, CompleteFlags, Completion, CompletionList,
CompletionRequestOptions, CompletionRequestOptions,
}; };
use crate::editable_line::line_at_cursor; use crate::editable_line::{line_at_cursor, range_of_line_at_cursor, Edit, EditableLine};
use crate::editable_line::{Edit, EditableLine};
use crate::env::{EnvMode, Environment, Statuses}; use crate::env::{EnvMode, Environment, Statuses};
use crate::exec::exec_subshell; use crate::exec::exec_subshell;
use crate::expand::{expand_string, expand_tilde, ExpandFlags, ExpandResultCode}; use crate::expand::{expand_string, expand_tilde, ExpandFlags, ExpandResultCode};
@@ -100,6 +99,7 @@
use crate::parse_constants::SourceRange; use crate::parse_constants::SourceRange;
use crate::parse_constants::{ParseTreeFlags, ParserTestErrorBits}; use crate::parse_constants::{ParseTreeFlags, ParserTestErrorBits};
use crate::parse_tree::ParsedSource; use crate::parse_tree::ParsedSource;
use crate::parse_util::parse_util_process_extent;
use crate::parse_util::MaybeParentheses; use crate::parse_util::MaybeParentheses;
use crate::parse_util::SPACES_PER_INDENT; use crate::parse_util::SPACES_PER_INDENT;
use crate::parse_util::{ use crate::parse_util::{
@@ -133,7 +133,8 @@
use crate::wchar::prelude::*; use crate::wchar::prelude::*;
use crate::wcstringutil::string_prefixes_string_maybe_case_insensitive; use crate::wcstringutil::string_prefixes_string_maybe_case_insensitive;
use crate::wcstringutil::{ use crate::wcstringutil::{
count_preceding_backslashes, join_strings, string_prefixes_string, StringFuzzyMatch, count_preceding_backslashes, join_strings, string_prefixes_string,
string_prefixes_string_case_insensitive, StringFuzzyMatch,
}; };
use crate::wildcard::wildcard_has; use crate::wildcard::wildcard_has;
use crate::wutil::{fstat, perror}; use crate::wutil::{fstat, perror};
@@ -983,7 +984,8 @@ pub fn reader_showing_suggestion(parser: &Parser) -> bool {
let reader = Reader { parser, data }; let reader = Reader { parser, data };
let suggestion = &reader.autosuggestion.text; let suggestion = &reader.autosuggestion.text;
let is_single_space = suggestion.ends_with(L!(" ")) let is_single_space = suggestion.ends_with(L!(" "))
&& reader.command_line.text() == suggestion[..suggestion.len() - 1]; && line_at_cursor(reader.command_line.text(), reader.command_line.position())
== suggestion[..suggestion.len() - 1];
!suggestion.is_empty() && !is_single_space !suggestion.is_empty() && !is_single_space
} else { } else {
false false
@@ -1322,8 +1324,8 @@ fn apply_commandline_state_changes(&mut self) {
/// Update the cursor position. /// Update the cursor position.
fn update_buff_pos(&mut self, elt: EditableLineTag, mut new_pos: Option<usize>) -> bool { fn update_buff_pos(&mut self, elt: EditableLineTag, mut new_pos: Option<usize>) -> bool {
let el = self.edit_line(elt);
if self.cursor_end_mode == CursorEndMode::Inclusive { if self.cursor_end_mode == CursorEndMode::Inclusive {
let el = self.edit_line(elt);
let mut pos = new_pos.unwrap_or(el.position()); let mut pos = new_pos.unwrap_or(el.position());
if !el.is_empty() && pos == el.len() { if !el.is_empty() && pos == el.len() {
pos = el.len() - 1; pos = el.len() - 1;
@@ -1333,6 +1335,7 @@ fn update_buff_pos(&mut self, elt: EditableLineTag, mut new_pos: Option<usize>)
new_pos = Some(pos); new_pos = Some(pos);
} }
} }
let old_pos = el.position();
if let Some(pos) = new_pos { if let Some(pos) = new_pos {
self.edit_line_mut(elt).set_position(pos); self.edit_line_mut(elt).set_position(pos);
} }
@@ -1340,6 +1343,17 @@ fn update_buff_pos(&mut self, elt: EditableLineTag, mut new_pos: Option<usize>)
if elt != EditableLineTag::Commandline { if elt != EditableLineTag::Commandline {
return true; return true;
} }
// When moving across lines, hold off on autosuggestions until the next insertion.
if let Some(new_pos) = new_pos {
let range = if new_pos <= old_pos {
new_pos..old_pos
} else {
old_pos..new_pos
};
if self.command_line.text()[range].contains('\n') {
self.suppress_autosuggestion = true;
}
}
let buff_pos = self.command_line.position(); let buff_pos = self.command_line.position();
let target_char = if self.cursor_selection_mode == CursorSelectionMode::Inclusive { let target_char = if self.cursor_selection_mode == CursorSelectionMode::Inclusive {
1 1
@@ -1386,36 +1400,50 @@ pub fn mouse_left_click(&mut self, cursor: ViewportPosition, click_position: Vie
/// Given a command line and an autosuggestion, return the string that gets shown to the user. /// Given a command line and an autosuggestion, return the string that gets shown to the user.
/// Exposed for testing purposes only. /// Exposed for testing purposes only.
pub fn combine_command_and_autosuggestion(cmdline: &wstr, autosuggestion: &wstr) -> WString { pub fn combine_command_and_autosuggestion(
cmdline: &wstr,
line_range: Range<usize>,
autosuggestion: &wstr,
) -> WString {
// We want to compute the full line, containing the command line and the autosuggestion They may // We want to compute the full line, containing the command line and the autosuggestion They may
// disagree on whether characters are uppercase or lowercase Here we do something funny: if the // disagree on whether characters are uppercase or lowercase.
// last token of the command line contains any uppercase characters, we use its case. Otherwise let pos = line_range.end;
// we use the case of the autosuggestion. This is an idea from issue #335. let full_line;
let mut full_line; assert!(!autosuggestion.is_empty());
if autosuggestion.len() <= cmdline.len() || cmdline.is_empty() { assert!(autosuggestion.len() >= line_range.len());
// No or useless autosuggestion, or no command line. let available = autosuggestion.len() - line_range.len();
full_line = cmdline.to_owned(); let line = &cmdline[line_range.clone()];
} else if string_prefixes_string(cmdline, autosuggestion) {
// No case disagreements, or no extra characters in the autosuggestion. if !string_prefixes_string(line, autosuggestion) {
full_line = autosuggestion.to_owned();
} else {
// We have an autosuggestion which is not a prefix of the command line, i.e. a case // We have an autosuggestion which is not a prefix of the command line, i.e. a case
// disagreement. Decide whose case we want to use. // disagreement. Decide whose case we want to use.
assert!(string_prefixes_string_case_insensitive(
line,
autosuggestion
));
// Here we do something funny: if the last token of the command line contains any uppercase
// characters, we use its case. Otherwise we use the case of the autosuggestion. This
// is an idea from issue #335.
let mut tok = 0..0; let mut tok = 0..0;
parse_util_token_extent(cmdline, cmdline.len() - 1, &mut tok, None); parse_util_token_extent(cmdline, cmdline.len() - 1, &mut tok, None);
let last_token_contains_uppercase = cmdline[tok].chars().any(|c| c.is_uppercase()); let last_token_contains_uppercase = cmdline[tok].chars().any(|c| c.is_uppercase());
if !last_token_contains_uppercase { if !last_token_contains_uppercase {
// Use the autosuggestion's case. // Use the autosuggestion's case.
full_line = autosuggestion.to_owned(); let start: usize = unsafe {
} else { (line.as_char_slice().first().unwrap() as *const char)
// Use the command line case for its characters, then append the remaining characters in .offset_from(&cmdline.as_char_slice()[0])
// the autosuggestion. Note that we know that autosuggestion.size() > cmdline.size() due }
// to the first test above. .try_into()
full_line = cmdline.to_owned(); .unwrap();
full_line.push_utfstr(&autosuggestion[cmdline.len()..]); full_line = cmdline[..start].to_owned() + autosuggestion + &cmdline[pos..];
return full_line;
} }
} }
full_line // Use the command line case for its characters, then append the remaining characters in
// the autosuggestion.
cmdline[..pos].to_owned()
+ &autosuggestion[autosuggestion.len() - available..]
+ &cmdline[pos..]
} }
impl<'a> Reader<'a> { impl<'a> Reader<'a> {
@@ -1510,17 +1538,35 @@ fn layout_and_repaint_before_execution(&mut self) {
/// `reason` is used in FLOG to explain why. /// `reason` is used in FLOG to explain why.
fn paint_layout(&mut self, reason: &wstr, is_final_rendering: bool) { fn paint_layout(&mut self, reason: &wstr, is_final_rendering: bool) {
FLOGF!(reader_render, "Repainting from %ls", reason); FLOGF!(reader_render, "Repainting from %ls", reason);
let data = &self.data.rendered_layout;
let cmd_line = &self.data.command_line; let cmd_line = &self.data.command_line;
let full_line = if self.conf.in_silent_mode { let (full_line, autosuggested_range) = if self.conf.in_silent_mode {
wstr::from_char_slice(&[get_obfuscation_read_char()]).repeat(cmd_line.len()) (
} else { Cow::Owned(
wstr::from_char_slice(&[get_obfuscation_read_char()]).repeat(cmd_line.len()),
),
0..0,
)
} else if self.is_at_line_with_autosuggestion() {
// Combine the command and autosuggestion into one string. // Combine the command and autosuggestion into one string.
combine_command_and_autosuggestion(cmd_line.text(), &self.autosuggestion.text) let autosuggestion = &self.autosuggestion;
let search_string_range = &autosuggestion.search_string_range;
let autosuggested_start = search_string_range.end;
let autosuggested_end = search_string_range.start + autosuggestion.text.len();
(
Cow::Owned(combine_command_and_autosuggestion(
cmd_line.text(),
autosuggestion.search_string_range.clone(),
&autosuggestion.text,
)),
autosuggested_start..autosuggested_end,
)
} else {
(Cow::Borrowed(cmd_line.text()), 0..0)
}; };
// Copy the colors and extend them with autosuggestion color. // Copy the colors and insert the autosuggestion color.
let data = &self.data.rendered_layout;
let mut colors = data.colors.clone(); let mut colors = data.colors.clone();
// Highlight any history search. // Highlight any history search.
@@ -1546,16 +1592,23 @@ fn paint_layout(&mut self, reason: &wstr, is_final_rendering: bool) {
} }
} }
// Extend our colors with the autosuggestion. let mut indents;
colors.resize( {
full_line.len(), // Extend our colors with the autosuggestion.
HighlightSpec::with_fg(HighlightRole::autosuggestion), let pos = autosuggested_range.start;
); colors.splice(
pos..pos,
vec![
HighlightSpec::with_fg(HighlightRole::autosuggestion);
autosuggested_range.len()
],
);
// Compute the indentation, then extend it with 0s for the autosuggestion. The autosuggestion // Compute the indentation, then extend it with 0s for the autosuggestion. The autosuggestion
// always conceptually has an indent of 0. // always conceptually has an indent of 0.
let mut indents = parse_util_compute_indents(cmd_line.text()); indents = parse_util_compute_indents(cmd_line.text());
indents.resize(full_line.len(), 0); indents.splice(pos..pos, vec![0; autosuggested_range.len()]);
}
let screen = &mut self.data.screen; let screen = &mut self.data.screen;
let pager = &mut self.data.pager; let pager = &mut self.data.pager;
@@ -1565,9 +1618,9 @@ fn paint_layout(&mut self, reason: &wstr, is_final_rendering: bool) {
&(self.data.mode_prompt_buff.clone() + &self.data.left_prompt_buff[..]), &(self.data.mode_prompt_buff.clone() + &self.data.left_prompt_buff[..]),
&self.data.right_prompt_buff, &self.data.right_prompt_buff,
&full_line, &full_line,
cmd_line.len(), autosuggested_range,
&colors, colors,
&indents, indents,
data.position, data.position,
data.pager_search_field_position, data.pager_search_field_position,
self.parser.vars(), self.parser.vars(),
@@ -1682,15 +1735,19 @@ fn try_apply_edit_to_autosuggestion(&mut self, elt: EditableLineTag, edit: &Edit
// text avoid recomputing the autosuggestion. // text avoid recomputing the autosuggestion.
assert!(string_prefixes_string_maybe_case_insensitive( assert!(string_prefixes_string_maybe_case_insensitive(
autosuggestion.icase, autosuggestion.icase,
&self.command_line.text(), &self.command_line.text()[autosuggestion.search_string_range.clone()],
&autosuggestion.text &autosuggestion.text
)); ));
let search_string_range = autosuggestion.search_string_range.clone();
// This is a heuristic with false negatives but that seems fine. // This is a heuristic with false negatives but that seems fine.
let Some(remaining) = autosuggestion.text.get(edit.range.start..) else { let Some(offset) = edit.range.start.checked_sub(search_string_range.start) else {
return false; return false;
}; };
if edit.range.end != self.command_line.len() let Some(remaining) = autosuggestion.text.get(offset..) else {
return false;
};
if edit.range.end != search_string_range.end
|| !string_prefixes_string_maybe_case_insensitive( || !string_prefixes_string_maybe_case_insensitive(
autosuggestion.icase, autosuggestion.icase,
&edit.replacement, &edit.replacement,
@@ -1700,6 +1757,9 @@ fn try_apply_edit_to_autosuggestion(&mut self, elt: EditableLineTag, edit: &Edit
{ {
return false; return false;
} }
self.autosuggestion.search_string_range.end = search_string_range.end
- edit.range.len().min(search_string_range.end)
+ edit.replacement.len();
true true
} }
@@ -2379,10 +2439,9 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
} }
} }
rl::EndOfLine => { rl::EndOfLine => {
let (_elt, el) = self.active_edit_line(); if self.is_at_autosuggestion() {
if self.is_at_end(el) {
self.accept_autosuggestion(AutosuggestionPortion::Count(usize::MAX)); self.accept_autosuggestion(AutosuggestionPortion::Count(usize::MAX));
} else { } else if !self.is_at_end() {
loop { loop {
let position = { let position = {
let (_elt, el) = self.active_edit_line(); let (_elt, el) = self.active_edit_line();
@@ -2860,7 +2919,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
rl::HistoryPagerDelete => { rl::HistoryPagerDelete => {
// Also applies to ordinary history search. // Also applies to ordinary history search.
let is_history_search = !self.history_search.is_at_end(); let is_history_search = !self.history_search.is_at_end();
if is_history_search || !self.autosuggestion.is_empty() { if is_history_search || self.is_at_line_with_autosuggestion() {
self.history.remove(if is_history_search { self.history.remove(if is_history_search {
self.history_search.current_result() self.history_search.current_result()
} else { } else {
@@ -2909,10 +2968,9 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
} }
} }
rl::ForwardChar | rl::ForwardSingleChar => { rl::ForwardChar | rl::ForwardSingleChar => {
let (elt, el) = self.active_edit_line();
if self.is_navigating_pager_contents() { if self.is_navigating_pager_contents() {
self.select_completion_in_direction(SelectionMotion::East, false); self.select_completion_in_direction(SelectionMotion::East, false);
} else if self.is_at_end(el) { } else if self.is_at_autosuggestion() {
self.accept_autosuggestion(AutosuggestionPortion::Count( self.accept_autosuggestion(AutosuggestionPortion::Count(
if c == rl::ForwardSingleChar { if c == rl::ForwardSingleChar {
1 1
@@ -2920,13 +2978,14 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
usize::MAX usize::MAX
}, },
)); ));
} else { } else if !self.is_at_end() {
let (elt, el) = self.active_edit_line();
self.update_buff_pos(elt, Some(el.position() + 1)); self.update_buff_pos(elt, Some(el.position() + 1));
} }
} }
rl::ForwardCharPassive => { rl::ForwardCharPassive => {
let (elt, el) = self.active_edit_line(); if !self.is_at_end() {
if !self.is_at_end(el) { let (elt, el) = self.active_edit_line();
if elt == EditableLineTag::SearchField || !self.is_navigating_pager_contents() { if elt == EditableLineTag::SearchField || !self.is_navigating_pager_contents() {
self.update_buff_pos(elt, Some(el.position() + 1)); self.update_buff_pos(elt, Some(el.position() + 1));
} }
@@ -3016,15 +3075,16 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
); );
} }
rl::ForwardToken => { rl::ForwardToken => {
let (_elt, el) = self.active_edit_line(); if self.is_at_autosuggestion() {
if self.is_at_end(el) {
let Some(new_position) = self.forward_token(true) else { let Some(new_position) = self.forward_token(true) else {
return; return;
}; };
let (_elt, el) = self.active_edit_line();
let search_string_range = range_of_line_at_cursor(el.text(), el.position());
self.accept_autosuggestion(AutosuggestionPortion::Count( self.accept_autosuggestion(AutosuggestionPortion::Count(
new_position - el.len(), new_position - search_string_range.end,
)); ));
} else { } else if !self.is_at_end() {
let Some(new_position) = self.forward_token(false) else { let Some(new_position) = self.forward_token(false) else {
return; return;
}; };
@@ -3068,10 +3128,10 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
} else { } else {
MoveWordStyle::Whitespace MoveWordStyle::Whitespace
}; };
let (elt, el) = self.active_edit_line(); if self.is_at_autosuggestion() {
if self.is_at_end(el) {
self.accept_autosuggestion(AutosuggestionPortion::PerMoveWordStyle(style)); self.accept_autosuggestion(AutosuggestionPortion::PerMoveWordStyle(style));
} else { } else if !self.is_at_end() {
let (elt, _el) = self.active_edit_line();
self.move_word(elt, MoveWordDir::Right, /*erase=*/ false, style, false); self.move_word(elt, MoveWordDir::Right, /*erase=*/ false, style, false);
} }
} }
@@ -3165,14 +3225,16 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
} }
rl::SuppressAutosuggestion => { rl::SuppressAutosuggestion => {
self.suppress_autosuggestion = true; self.suppress_autosuggestion = true;
let success = !self.autosuggestion.is_empty(); let success = self.is_at_line_with_autosuggestion();
self.autosuggestion.clear(); self.autosuggestion.clear();
// Return true if we had a suggestion to clear. // Return true if we had a suggestion to clear.
self.input_data.function_set_status(success); self.input_data.function_set_status(success);
} }
rl::AcceptAutosuggestion => { rl::AcceptAutosuggestion => {
let success = !self.autosuggestion.is_empty(); let success = self.is_at_line_with_autosuggestion();
self.accept_autosuggestion(AutosuggestionPortion::Count(usize::MAX)); if success {
self.accept_autosuggestion(AutosuggestionPortion::Count(usize::MAX));
}
self.input_data.function_set_status(success); self.input_data.function_set_status(success);
} }
rl::TransposeChars => { rl::TransposeChars => {
@@ -3312,7 +3374,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
let mut replacement = WString::new(); let mut replacement = WString::new();
while pos while pos
< if self.cursor_selection_mode == CursorSelectionMode::Inclusive < if self.cursor_selection_mode == CursorSelectionMode::Inclusive
&& self.is_at_end(el) && self.is_at_end()
{ {
el.len() el.len()
} else { } else {
@@ -3606,15 +3668,19 @@ fn backward_token(&mut self) -> Option<usize> {
} }
fn forward_token(&self, autosuggest: bool) -> Option<usize> { fn forward_token(&self, autosuggest: bool) -> Option<usize> {
let (_elt, el) = self.active_edit_line(); let (elt, el) = self.active_edit_line();
let pos = el.position(); let pos = el.position();
let buffer = if autosuggest { let buffer = if autosuggest {
if pos > self.autosuggestion.text.len() { assert!(elt == EditableLineTag::Commandline);
return None; assert!(self.is_at_line_with_autosuggestion());
} let autosuggestion = &self.autosuggestion;
&self.autosuggestion.text Cow::Owned(combine_command_and_autosuggestion(
el.text(),
autosuggestion.search_string_range.clone(),
&autosuggestion.text,
))
} else { } else {
el.text() Cow::Borrowed(el.text())
}; };
if pos == buffer.len() { if pos == buffer.len() {
return None; return None;
@@ -3628,7 +3694,7 @@ fn forward_token(&self, autosuggest: bool) -> Option<usize> {
.count(); .count();
let mut tok = 0..0; let mut tok = 0..0;
parse_util_token_extent(buffer, buff_pos, &mut tok, None); parse_util_token_extent(&buffer, buff_pos, &mut tok, None);
let new_position = if tok.end == pos { pos + 1 } else { tok.end }; let new_position = if tok.end == pos { pos + 1 } else { tok.end };
@@ -4364,9 +4430,12 @@ fn exec_prompt(&mut self) {
#[derive(Default)] #[derive(Default)]
struct Autosuggestion { struct Autosuggestion {
// The text to use, as an extension/replacement of the command line. // The text to use, as an extension/replacement of the current line.
text: WString, text: WString,
// The range within the commandline that was searched. Always a whole line.
search_string_range: Range<usize>,
// Whether the autosuggestion should be case insensitive. // Whether the autosuggestion should be case insensitive.
// This is true for file-generated autosuggestions, but not for history. // This is true for file-generated autosuggestions, but not for history.
icase: bool, icase: bool,
@@ -4387,10 +4456,11 @@ fn is_empty(&self) -> bool {
/// The result of an autosuggestion computation. /// The result of an autosuggestion computation.
#[derive(Default)] #[derive(Default)]
struct AutosuggestionResult { struct AutosuggestionResult {
// The autosuggestion.
autosuggestion: Autosuggestion, autosuggestion: Autosuggestion,
// The string which was searched for. // The commandline this result is based off.
search_string: WString, command_line: WString,
// The list of completions which may need loading. // The list of completions which may need loading.
needs_load: Vec<WString>, needs_load: Vec<WString>,
@@ -4404,20 +4474,34 @@ fn deref(&self) -> &Self::Target {
} }
impl AutosuggestionResult { impl AutosuggestionResult {
fn new(text: WString, search_string: WString, icase: bool) -> Self { fn new(
command_line: WString,
search_string_range: Range<usize>,
text: WString,
icase: bool,
) -> Self {
Self { Self {
autosuggestion: Autosuggestion { text, icase }, autosuggestion: Autosuggestion {
search_string, text,
search_string_range,
icase,
},
command_line,
needs_load: vec![], needs_load: vec![],
} }
} }
/// The line which was searched for.
fn search_string(&self) -> &wstr {
&self.command_line[self.search_string_range.clone()]
}
} }
// Returns a function that can be invoked (potentially // Returns a function that can be invoked (potentially
// on a background thread) to determine the autosuggestion // on a background thread) to determine the autosuggestion
fn get_autosuggestion_performer( fn get_autosuggestion_performer(
parser: &Parser, parser: &Parser,
search_string: WString, command_line: WString,
cursor_pos: usize, cursor_pos: usize,
history: Arc<History>, history: Arc<History>,
) -> impl FnOnce() -> AutosuggestionResult { ) -> impl FnOnce() -> AutosuggestionResult {
@@ -4433,29 +4517,38 @@ fn get_autosuggestion_performer(
} }
// Let's make sure we aren't using the empty string. // Let's make sure we aren't using the empty string.
if search_string.is_empty() { let search_string_range = range_of_line_at_cursor(&command_line, cursor_pos);
let search_string = &command_line[search_string_range.clone()];
let Some(last_char) = search_string.chars().next_back() else {
return nothing; return nothing;
} };
// Search history for a matching item. // Search history for a matching item unless this line is not a continuation line or quoted.
let mut searcher = if range_of_line_at_cursor(
HistorySearch::new_with_type(history, search_string.to_owned(), SearchType::Prefix); &command_line,
while !ctx.check_cancel() && searcher.go_to_next_match(SearchDirection::Backward) { parse_util_process_extent(&command_line, cursor_pos, None).start,
let item = searcher.current_item(); ) == search_string_range
{
let mut searcher =
HistorySearch::new_with_type(history, search_string.to_owned(), SearchType::Prefix);
while !ctx.check_cancel() && searcher.go_to_next_match(SearchDirection::Backward) {
let item = searcher.current_item();
// Skip items with newlines because they make terrible autosuggestions. // Skip items with newlines because they make terrible autosuggestions.
if item.str().contains('\n') { if item.str().contains('\n') {
continue; continue;
} }
if autosuggest_validate_from_history(item, &working_directory, &ctx) { if autosuggest_validate_from_history(item, &working_directory, &ctx) {
// The command autosuggestion was handled specially, so we're done. // The command autosuggestion was handled specially, so we're done.
// History items are case-sensitive, see #3978. // History items are case-sensitive, see #3978.
return AutosuggestionResult::new( return AutosuggestionResult::new(
searcher.current_string().to_owned(), command_line,
search_string.to_owned(), search_string_range,
/*icase=*/ false, searcher.current_string().to_owned(),
); /*icase=*/ false,
);
}
} }
} }
@@ -4467,8 +4560,8 @@ fn get_autosuggestion_performer(
// Here we do something a little funny. If the line ends with a space, and the cursor is not // Here we do something a little funny. If the line ends with a space, and the cursor is not
// at the end, don't use completion autosuggestions. It ends up being pretty weird seeing // at the end, don't use completion autosuggestions. It ends up being pretty weird seeing
// stuff get spammed on the right while you go back to edit a line // stuff get spammed on the right while you go back to edit a line
let last_char = search_string.chars().next_back().unwrap(); let cursor_at_end =
let cursor_at_end = cursor_pos == search_string.len(); cursor_pos == command_line.len() || command_line.as_char_slice()[cursor_pos] == '\n';
if !cursor_at_end && last_char.is_whitespace() { if !cursor_at_end && last_char.is_whitespace() {
return nothing; return nothing;
} }
@@ -4480,25 +4573,28 @@ fn get_autosuggestion_performer(
// Try normal completions. // Try normal completions.
let complete_flags = CompletionRequestOptions::autosuggest(); let complete_flags = CompletionRequestOptions::autosuggest();
let (mut completions, needs_load) = complete(&search_string, complete_flags, &ctx); let (mut completions, needs_load) =
complete(&command_line[..cursor_pos], complete_flags, &ctx);
let full_line = if completions.is_empty() { let suggestion = if completions.is_empty() {
WString::new() WString::new()
} else { } else {
sort_and_prioritize(&mut completions, complete_flags); sort_and_prioritize(&mut completions, complete_flags);
let comp = &completions[0]; let comp = &completions[0];
let mut cursor = cursor_pos; let mut cursor = cursor_pos;
completion_apply_to_command_line( let full_line = completion_apply_to_command_line(
&comp.completion, &comp.completion,
comp.flags, comp.flags,
&search_string, &command_line,
&mut cursor, &mut cursor,
/*append_only=*/ true, /*append_only=*/ true,
) );
line_at_cursor(&full_line, search_string_range.end).to_owned()
}; };
let mut result = AutosuggestionResult::new( let mut result = AutosuggestionResult::new(
full_line, command_line,
search_string.to_owned(), search_string_range.clone(),
suggestion,
true, // normal completions are case-insensitive true, // normal completions are case-insensitive
); );
result.needs_load = needs_load; result.needs_load = needs_load;
@@ -4529,10 +4625,10 @@ fn can_autosuggest(&self) -> bool {
// Called after an autosuggestion has been computed on a background thread. // Called after an autosuggestion has been computed on a background thread.
fn autosuggest_completed(&mut self, result: AutosuggestionResult) { fn autosuggest_completed(&mut self, result: AutosuggestionResult) {
assert_is_main_thread(); assert_is_main_thread();
if result.search_string == self.data.in_flight_autosuggest_request { if result.command_line == self.data.in_flight_autosuggest_request {
self.data.in_flight_autosuggest_request.clear(); self.data.in_flight_autosuggest_request.clear();
} }
if result.search_string != self.command_line.text() { if result.command_line != self.command_line.text() {
// This autosuggestion is stale. // This autosuggestion is stale.
return; return;
} }
@@ -4556,7 +4652,7 @@ fn autosuggest_completed(&mut self, result: AutosuggestionResult) {
&& self.can_autosuggest() && self.can_autosuggest()
&& string_prefixes_string_maybe_case_insensitive( && string_prefixes_string_maybe_case_insensitive(
result.icase, result.icase,
&result.search_string, result.search_string(),
&result.text, &result.text,
) )
{ {
@@ -4578,10 +4674,10 @@ fn update_autosuggestion(&mut self) {
let el = &self.data.command_line; let el = &self.data.command_line;
let autosuggestion = &self.autosuggestion; let autosuggestion = &self.autosuggestion;
if !self.autosuggestion.is_empty() { if self.is_at_line_with_autosuggestion() {
assert!(string_prefixes_string_maybe_case_insensitive( assert!(string_prefixes_string_maybe_case_insensitive(
autosuggestion.icase, autosuggestion.icase,
&el.text(), &el.text()[autosuggestion.search_string_range.clone()],
&autosuggestion.text &autosuggestion.text
)); ));
return; return;
@@ -4612,52 +4708,87 @@ fn update_autosuggestion(&mut self) {
debounce_autosuggestions().perform_with_completion(performer, completion); debounce_autosuggestions().perform_with_completion(performer, completion);
} }
fn is_at_end(&self, el: &EditableLine) -> bool { fn is_at_end(&self) -> bool {
let (_elt, el) = self.active_edit_line();
match self.cursor_end_mode { match self.cursor_end_mode {
CursorEndMode::Exclusive => el.position() == el.len(), CursorEndMode::Exclusive => el.position() == el.len(),
CursorEndMode::Inclusive => el.position() + 1 >= el.len(), CursorEndMode::Inclusive => el.position() + 1 >= el.len(),
} }
} }
fn is_at_autosuggestion(&self) -> bool {
if self.active_edit_line_tag() != EditableLineTag::Commandline {
return false;
}
let autosuggestion = &self.autosuggestion;
if autosuggestion.is_empty() {
return false;
}
let el = &self.command_line;
(match self.cursor_end_mode {
CursorEndMode::Exclusive => el.position(),
CursorEndMode::Inclusive => el.position() + 1,
}) == autosuggestion.search_string_range.end
}
fn is_at_line_with_autosuggestion(&self) -> bool {
if self.active_edit_line_tag() != EditableLineTag::Commandline {
return false;
}
let autosuggestion = &self.autosuggestion;
if autosuggestion.is_empty() {
return false;
}
let el = &self.command_line;
range_of_line_at_cursor(el.text(), el.position()) == autosuggestion.search_string_range
}
// Accept any autosuggestion by replacing the command line with it. If full is true, take the whole // Accept any autosuggestion by replacing the command line with it. If full is true, take the whole
// thing; if it's false, then respect the passed in style. // thing; if it's false, then respect the passed in style.
fn accept_autosuggestion(&mut self, amount: AutosuggestionPortion) { fn accept_autosuggestion(&mut self, amount: AutosuggestionPortion) {
if self.autosuggestion.is_empty() { assert!(self.is_at_line_with_autosuggestion());
return;
}
// Accepting an autosuggestion clears the pager. // Accepting an autosuggestion clears the pager.
self.clear_pager(); self.clear_pager();
let autosuggestion = &self.autosuggestion;
let autosuggestion_text = &autosuggestion.text;
let search_string_range = autosuggestion.search_string_range.clone();
// Accept the autosuggestion. // Accept the autosuggestion.
let (range, replacement) = match amount { let (range, replacement) = match amount {
AutosuggestionPortion::Count(count) => { AutosuggestionPortion::Count(count) => {
let pos = self.command_line.len();
if count == usize::MAX { if count == usize::MAX {
(0..self.command_line.len(), self.autosuggestion.text.clone()) (search_string_range, autosuggestion_text.clone())
} else { } else {
let count = count.min(self.autosuggestion.text.len() - pos); let pos = search_string_range.end;
let available = autosuggestion_text.len() - search_string_range.len();
let count = count.min(available);
if count == 0 { if count == 0 {
return; return;
} }
let start = autosuggestion_text.len() - available;
( (
pos..pos, pos..pos,
self.autosuggestion.text[pos..pos + count].to_owned(), autosuggestion_text[start..start + count].to_owned(),
) )
} }
} }
AutosuggestionPortion::PerMoveWordStyle(style) => { AutosuggestionPortion::PerMoveWordStyle(style) => {
// Accept characters according to the specified style. // Accept characters according to the specified style.
let mut state = MoveWordStateMachine::new(style); let mut state = MoveWordStateMachine::new(style);
let mut want = self.command_line.len(); let have = search_string_range.len();
while want < self.autosuggestion.text.len() { let mut want = have;
let wc = self.autosuggestion.text.as_char_slice()[want]; while want < autosuggestion_text.len() {
let wc = autosuggestion_text.as_char_slice()[want];
if !state.consume_char(wc) { if !state.consume_char(wc) {
break; break;
} }
want += 1; want += 1;
} }
let have = self.command_line.len(); (
(have..have, self.autosuggestion.text[have..want].to_owned()) search_string_range.end..search_string_range.end,
autosuggestion_text[have..want].to_owned(),
)
} }
}; };
self.data self.data

View File

@@ -15,6 +15,7 @@
use std::collections::LinkedList; use std::collections::LinkedList;
use std::ffi::{CStr, CString}; use std::ffi::{CStr, CString};
use std::io::Write; use std::io::Write;
use std::ops::Range;
use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Mutex; use std::sync::Mutex;
use std::time::SystemTime; use std::time::SystemTime;
@@ -255,9 +256,9 @@ pub fn write(
left_prompt: &wstr, left_prompt: &wstr,
right_prompt: &wstr, right_prompt: &wstr,
commandline: &wstr, commandline: &wstr,
explicit_len: usize, autosuggested_range: Range<usize>,
colors: &[HighlightSpec], mut colors: Vec<HighlightSpec>,
indent: &[i32], mut indent: Vec<i32>,
cursor_pos: usize, cursor_pos: usize,
pager_search_field_position: Option<usize>, pager_search_field_position: Option<usize>,
vars: &dyn Environment, vars: &dyn Environment,
@@ -282,17 +283,19 @@ struct ScrolledCursor {
let mut scrolled_cursor: Option<ScrolledCursor> = None; let mut scrolled_cursor: Option<ScrolledCursor> = None;
// Turn the command line into the explicit portion and the autosuggestion. // Turn the command line into the explicit portion and the autosuggestion.
let (explicit_command_line, autosuggestion) = commandline.split_at(explicit_len); let explicit_before_suggestion = &commandline[..autosuggested_range.start];
let autosuggestion = &commandline[autosuggested_range.clone()];
let explicit_after_suggestion = &commandline[autosuggested_range.end..];
// If we are using a dumb terminal, don't try any fancy stuff, just print out the text. // If we are using a dumb terminal, don't try any fancy stuff, just print out the text.
// right_prompt not supported. // right_prompt not supported.
if is_dumb() { if is_dumb() {
let prompt_narrow = wcs2string(left_prompt); let prompt_narrow = wcs2string(left_prompt);
let command_line_narrow = wcs2string(explicit_command_line);
let _ = write_loop(&STDOUT_FILENO, b"\r"); let _ = write_loop(&STDOUT_FILENO, b"\r");
let _ = write_loop(&STDOUT_FILENO, &prompt_narrow); let _ = write_loop(&STDOUT_FILENO, &prompt_narrow);
let _ = write_loop(&STDOUT_FILENO, &command_line_narrow); let _ = write_loop(&STDOUT_FILENO, &wcs2string(explicit_before_suggestion));
let _ = write_loop(&STDOUT_FILENO, &wcs2string(explicit_after_suggestion));
return; return;
} }
@@ -311,10 +314,13 @@ struct ScrolledCursor {
// Compute a layout. // Compute a layout.
let layout = compute_layout( let layout = compute_layout(
get_ellipsis_char(),
screen_width, screen_width,
left_prompt, left_prompt,
right_prompt, right_prompt,
explicit_command_line, explicit_before_suggestion,
&mut colors,
&mut indent,
autosuggestion, autosuggestion,
); );
@@ -345,7 +351,9 @@ struct ScrolledCursor {
let first_line_prompt_space = layout.left_prompt_space; let first_line_prompt_space = layout.left_prompt_space;
// Reconstruct the command line. // Reconstruct the command line.
let effective_commandline = explicit_command_line.to_owned() + &layout.autosuggestion[..]; let effective_commandline = explicit_before_suggestion.to_owned()
+ &layout.autosuggestion[..]
+ explicit_after_suggestion;
// Output the command line. // Output the command line.
let mut i = 0; let mut i = 0;
@@ -375,7 +383,12 @@ struct ScrolledCursor {
break scrolled_cursor.unwrap(); break scrolled_cursor.unwrap();
} }
if !self.desired_append_char( if !self.desired_append_char(
/*offset_in_cmdline=*/ i, /*offset_in_cmdline=*/
if i <= explicit_before_suggestion.len() + layout.autosuggestion.len() {
i.min(explicit_before_suggestion.len())
} else {
i - layout.autosuggestion.len()
},
if is_final_rendering { if is_final_rendering {
usize::MAX usize::MAX
} else { } else {
@@ -1835,16 +1848,17 @@ fn is_dumb() -> bool {
}) })
} }
#[derive(Default)] // Exposed for testing.
struct ScreenLayout { #[derive(Debug, Default, Eq, PartialEq)]
pub(crate) struct ScreenLayout {
// The left prompt that we're going to use. // The left prompt that we're going to use.
left_prompt: WString, pub(crate) left_prompt: WString,
// How much space to leave for it. // How much space to leave for it.
left_prompt_space: usize, pub(crate) left_prompt_space: usize,
// The right prompt. // The right prompt.
right_prompt: WString, pub(crate) right_prompt: WString,
// The autosuggestion. // The autosuggestion.
autosuggestion: WString, pub(crate) autosuggestion: WString,
} }
// Given a vector whose indexes are offsets and whose values are the widths of the string if // Given a vector whose indexes are offsets and whose values are the widths of the string if
@@ -1864,11 +1878,15 @@ fn truncation_offset_for_width(width_by_offset: &[usize], max_width: usize) -> u
i - 1 i - 1
} }
fn compute_layout( // Exposed for testing.
pub(crate) fn compute_layout(
ellipsis_char: char,
screen_width: usize, screen_width: usize,
left_untrunc_prompt: &wstr, left_untrunc_prompt: &wstr,
right_untrunc_prompt: &wstr, right_untrunc_prompt: &wstr,
commandline: &wstr, commandline_before_suggestion: &wstr,
colors: &mut Vec<HighlightSpec>,
indent: &mut Vec<i32>,
autosuggestion_str: &wstr, autosuggestion_str: &wstr,
) -> ScreenLayout { ) -> ScreenLayout {
let mut result = ScreenLayout::default(); let mut result = ScreenLayout::default();
@@ -1901,24 +1919,23 @@ fn compute_layout(
assert!(left_prompt_width + right_prompt_width <= screen_width); assert!(left_prompt_width + right_prompt_width <= screen_width);
// Get the width of the first line, and if there is more than one line. // Get the width of the first line, and if there is more than one line.
let multiline = commandline.contains('\n'); let first_command_line_width: usize = line_at_cursor(commandline_before_suggestion, 0)
let first_command_line_width: usize = line_at_cursor(commandline, 0)
.chars() .chars()
.map(wcwidth_rendered_min_0) .map(wcwidth_rendered_min_0)
.sum(); .sum();
let autosuggestion_line_explicit_width: usize = line_at_cursor(
commandline_before_suggestion,
commandline_before_suggestion.len(),
)
.chars()
.map(wcwidth_rendered_min_0)
.sum();
// If we have more than one line, ensure we have no autosuggestion.
let mut autosuggestion = autosuggestion_str;
let mut autosuggest_total_width = 0; let mut autosuggest_total_width = 0;
let mut autosuggest_truncated_widths = vec![]; let mut autosuggest_truncated_widths = Vec::with_capacity(autosuggestion_str.len());
if multiline { for c in autosuggestion_str.chars() {
autosuggestion = L!(""); autosuggest_truncated_widths.push(autosuggest_total_width);
} else { autosuggest_total_width += wcwidth_rendered_min_0(c);
autosuggest_truncated_widths.reserve(1 + autosuggestion_str.len());
for c in autosuggestion_str.chars() {
autosuggest_truncated_widths.push(autosuggest_total_width);
autosuggest_total_width += wcwidth_rendered_min_0(c);
}
} }
// Here are the layouts we try in turn: // Here are the layouts we try in turn:
@@ -1940,21 +1957,35 @@ fn compute_layout(
// prompt will wrap to the next line. This means that we can't go back to the line that we were // prompt will wrap to the next line. This means that we can't go back to the line that we were
// on, and things turn to chaos very quickly. // on, and things turn to chaos very quickly.
let truncated_autosuggestion = |right_prompt_width: usize| { let mut truncated_autosuggestion = |indent: &mut Vec<i32>, right_prompt_width: usize| {
let width = left_prompt_width + right_prompt_width + first_command_line_width; let width = if let Some(pos) = commandline_before_suggestion
.chars()
.rposition(|c| c == '\n')
{
left_prompt_width
+ usize::try_from(indent[pos]).unwrap() * INDENT_STEP
+ autosuggestion_line_explicit_width
} else {
left_prompt_width + right_prompt_width + first_command_line_width
};
// Need at least two characters to show an autosuggestion. // Need at least two characters to show an autosuggestion.
let available_autosuggest_space = screen_width - width; let available_autosuggest_space = screen_width.saturating_sub(width);
let mut result = WString::new(); let mut result = WString::new();
if available_autosuggest_space > autosuggest_total_width { if available_autosuggest_space > autosuggest_total_width {
result = autosuggestion.to_owned(); result = autosuggestion_str.to_owned();
} else if autosuggest_total_width > 0 && available_autosuggest_space > 2 { } else if autosuggest_total_width > 0 && available_autosuggest_space > 2 {
let truncation_offset = truncation_offset_for_width( let truncation_offset = truncation_offset_for_width(
&autosuggest_truncated_widths, &autosuggest_truncated_widths,
available_autosuggest_space - 2, available_autosuggest_space - 2,
); );
result = autosuggestion[..truncation_offset].to_owned(); result = autosuggestion_str[..truncation_offset].to_owned();
result.push(get_ellipsis_char()); result.push(ellipsis_char);
} }
let suggestion_start = commandline_before_suggestion.len();
let truncation_range =
suggestion_start + result.len()..suggestion_start + autosuggestion_str.len();
colors.drain(truncation_range.clone());
indent.drain(truncation_range);
result result
}; };
@@ -1965,7 +1996,7 @@ fn compute_layout(
result.left_prompt = left_prompt; result.left_prompt = left_prompt;
result.left_prompt_space = left_prompt_width; result.left_prompt_space = left_prompt_width;
result.right_prompt = right_prompt; result.right_prompt = right_prompt;
result.autosuggestion = truncated_autosuggestion(right_prompt_width); result.autosuggestion = truncated_autosuggestion(indent, right_prompt_width);
return result; return result;
} }
@@ -1974,14 +2005,14 @@ fn compute_layout(
if calculated_width <= screen_width { if calculated_width <= screen_width {
result.left_prompt = left_prompt; result.left_prompt = left_prompt;
result.left_prompt_space = left_prompt_width; result.left_prompt_space = left_prompt_width;
result.autosuggestion = truncated_autosuggestion(0); result.autosuggestion = truncated_autosuggestion(indent, 0);
return result; return result;
} }
// Case 5 // Case 5
result.left_prompt = left_prompt; result.left_prompt = left_prompt;
result.left_prompt_space = left_prompt_width; result.left_prompt_space = left_prompt_width;
result.autosuggestion = autosuggestion.to_owned(); result.autosuggestion = autosuggestion_str.to_owned();
result result
} }

View File

@@ -5,30 +5,43 @@
#[test] #[test]
fn test_autosuggestion_combining() { fn test_autosuggestion_combining() {
assert_eq!( assert_eq!(
combine_command_and_autosuggestion(L!("alpha"), L!("alphabeta")), combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("alphabeta")),
L!("alphabeta") L!("alphabeta")
); );
// When the last token contains no capital letters, we use the case of the autosuggestion. // When the last token contains no capital letters, we use the case of the autosuggestion.
assert_eq!( assert_eq!(
combine_command_and_autosuggestion(L!("alpha"), L!("ALPHABETA")), combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHABETA")),
L!("ALPHABETA") L!("ALPHABETA")
); );
// When the last token contains capital letters, we use its case. // When the last token contains capital letters, we use its case.
assert_eq!( assert_eq!(
combine_command_and_autosuggestion(L!("alPha"), L!("alphabeTa")), combine_command_and_autosuggestion(L!("alPha"), 0..5, L!("alphabeTa")),
L!("alPhabeTa") L!("alPhabeTa")
); );
// If autosuggestion is not longer than input, use the input's case. // If autosuggestion is not longer than input, use the input's case.
assert_eq!( assert_eq!(
combine_command_and_autosuggestion(L!("alpha"), L!("ALPHAA")), combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHAA")),
L!("ALPHAA") L!("ALPHAA")
); );
assert_eq!( assert_eq!(
combine_command_and_autosuggestion(L!("alpha"), L!("ALPHA")), combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHA")),
L!("alpha") L!("ALPHA")
);
assert_eq!(
combine_command_and_autosuggestion(L!("al\nbeta"), 0..2, L!("alpha")),
L!("alpha\nbeta").to_owned()
);
assert_eq!(
combine_command_and_autosuggestion(L!("alpha\nbe"), 6..8, L!("beta")),
L!("alpha\nbeta").to_owned()
);
assert_eq!(
combine_command_and_autosuggestion(L!("alpha\nbe\ngamma"), 6..8, L!("beta")),
L!("alpha\nbeta\ngamma").to_owned()
); );
} }

View File

@@ -1,5 +1,7 @@
use crate::common::get_ellipsis_char; use crate::common::get_ellipsis_char;
use crate::screen::{LayoutCache, PromptCacheEntry, PromptLayout}; use crate::highlight::HighlightSpec;
use crate::parse_util::parse_util_compute_indents;
use crate::screen::{compute_layout, LayoutCache, PromptCacheEntry, PromptLayout, ScreenLayout};
use crate::tests::prelude::*; use crate::tests::prelude::*;
use crate::wchar::prelude::*; use crate::wchar::prelude::*;
use crate::wcstringutil::join_strings; use crate::wcstringutil::join_strings;
@@ -245,3 +247,112 @@ fn test_prompt_truncation() {
); );
assert_eq!(trunc, ellipsis()); assert_eq!(trunc, ellipsis());
} }
#[test]
fn test_compute_layout() {
macro_rules! validate {
(
(
$screen_width:expr,
$left_untrunc_prompt:literal,
$right_untrunc_prompt:literal,
$commandline_before_suggestion:literal,
$autosuggestion_str:literal,
$commandline_after_suggestion:literal
)
-> (
$left_prompt:literal,
$left_prompt_space:expr,
$right_prompt:literal,
$autosuggestion:literal $(,)?
)
) => {{
let full_commandline = L!($commandline_before_suggestion).to_owned()
+ L!($autosuggestion_str)
+ L!($commandline_after_suggestion);
let mut colors = vec![HighlightSpec::default(); full_commandline.len()];
let mut indent = parse_util_compute_indents(&full_commandline);
assert_eq!(
compute_layout(
'…',
$screen_width,
L!($left_untrunc_prompt),
L!($right_untrunc_prompt),
L!($commandline_before_suggestion),
&mut colors,
&mut indent,
L!($autosuggestion_str),
),
ScreenLayout {
left_prompt: L!($left_prompt).to_owned(),
left_prompt_space: $left_prompt_space,
right_prompt: L!($right_prompt).to_owned(),
autosuggestion: L!($autosuggestion).to_owned(),
}
);
indent
}};
}
validate!(
(
80, "left>", "<right", "command", " autosuggestion", ""
) -> (
"left>",
5,
"<right",
" autosuggestion",
)
);
validate!(
(
30, "left>", "<right", "command", " autosuggestion", ""
) -> (
"left>",
5,
"<right",
" autosugge…",
)
);
validate!(
(
30, "left>", "<right", "foo\ncommand", " autosuggestion", ""
) -> (
"left>",
5,
"<right",
" autosuggestion",
)
);
validate!(
(
30, "left>", "<right", "foo\ncommand", " autosuggestion TRUNCATED", ""
) -> (
"left>",
5,
"<right",
" autosuggestion …",
)
);
validate!(
(
30, "left>", "<right", "if :\ncommand", " autosuggestion TRUNCATED", ""
) -> (
"left>",
5,
"<right",
" autosuggest…",
)
);
let indent = validate!(
(
30, "left>", "<right", "if :\ncommand", " autosuggestion TRUNCATED", "\nfoo"
) -> (
"left>",
5,
"<right",
" autosuggest…",
)
);
assert_eq!(indent["if :\ncommand autosuggest…\n".len()], 1);
}