mirror of
https://github.com/fish-shell/fish-shell.git
synced 2025-03-20 17:45:15 +08:00
Autosuggestions in multi-line command lines
Some checks are pending
make test / ubuntu (push) Waiting to run
make test / ubuntu-32bit-static-pcre2 (push) Waiting to run
make test / ubuntu-asan (push) Waiting to run
make test / macos (push) Waiting to run
Rust checks / rustfmt (push) Waiting to run
Rust checks / clippy (push) Waiting to run
Some checks are pending
make test / ubuntu (push) Waiting to run
make test / ubuntu-32bit-static-pcre2 (push) Waiting to run
make test / ubuntu-asan (push) Waiting to run
make test / macos (push) Waiting to run
Rust checks / rustfmt (push) Waiting to run
Rust checks / clippy (push) Waiting to run
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:
parent
532abaddae
commit
1c4e5cadf2
@ -12,6 +12,7 @@ Scripting 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
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -342,7 +342,7 @@ fn cursor_position_after_edit(edit: &Edit) -> usize {
|
||||
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]
|
||||
.as_char_slice()
|
||||
.iter()
|
||||
|
383
src/reader.rs
383
src/reader.rs
@ -58,8 +58,7 @@ use crate::complete::{
|
||||
complete, complete_load, sort_and_prioritize, CompleteFlags, Completion, CompletionList,
|
||||
CompletionRequestOptions,
|
||||
};
|
||||
use crate::editable_line::line_at_cursor;
|
||||
use crate::editable_line::{Edit, EditableLine};
|
||||
use crate::editable_line::{line_at_cursor, range_of_line_at_cursor, Edit, EditableLine};
|
||||
use crate::env::{EnvMode, Environment, Statuses};
|
||||
use crate::exec::exec_subshell;
|
||||
use crate::expand::{expand_string, expand_tilde, ExpandFlags, ExpandResultCode};
|
||||
@ -100,6 +99,7 @@ use crate::panic::AT_EXIT;
|
||||
use crate::parse_constants::SourceRange;
|
||||
use crate::parse_constants::{ParseTreeFlags, ParserTestErrorBits};
|
||||
use crate::parse_tree::ParsedSource;
|
||||
use crate::parse_util::parse_util_process_extent;
|
||||
use crate::parse_util::MaybeParentheses;
|
||||
use crate::parse_util::SPACES_PER_INDENT;
|
||||
use crate::parse_util::{
|
||||
@ -133,7 +133,8 @@ use crate::tokenizer::{
|
||||
use crate::wchar::prelude::*;
|
||||
use crate::wcstringutil::string_prefixes_string_maybe_case_insensitive;
|
||||
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::wutil::{fstat, perror};
|
||||
@ -983,7 +984,8 @@ pub fn reader_showing_suggestion(parser: &Parser) -> bool {
|
||||
let reader = Reader { parser, data };
|
||||
let suggestion = &reader.autosuggestion.text;
|
||||
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
|
||||
} else {
|
||||
false
|
||||
@ -1322,8 +1324,8 @@ impl ReaderData {
|
||||
|
||||
/// Update the cursor position.
|
||||
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 {
|
||||
let el = self.edit_line(elt);
|
||||
let mut pos = new_pos.unwrap_or(el.position());
|
||||
if !el.is_empty() && pos == el.len() {
|
||||
pos = el.len() - 1;
|
||||
@ -1333,6 +1335,7 @@ impl ReaderData {
|
||||
new_pos = Some(pos);
|
||||
}
|
||||
}
|
||||
let old_pos = el.position();
|
||||
if let Some(pos) = new_pos {
|
||||
self.edit_line_mut(elt).set_position(pos);
|
||||
}
|
||||
@ -1340,6 +1343,17 @@ impl ReaderData {
|
||||
if elt != EditableLineTag::Commandline {
|
||||
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 target_char = if self.cursor_selection_mode == CursorSelectionMode::Inclusive {
|
||||
1
|
||||
@ -1386,36 +1400,50 @@ impl ReaderData {
|
||||
|
||||
/// Given a command line and an autosuggestion, return the string that gets shown to the user.
|
||||
/// 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
|
||||
// disagree on whether characters are uppercase or lowercase 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 full_line;
|
||||
if autosuggestion.len() <= cmdline.len() || cmdline.is_empty() {
|
||||
// No or useless autosuggestion, or no command line.
|
||||
full_line = cmdline.to_owned();
|
||||
} else if string_prefixes_string(cmdline, autosuggestion) {
|
||||
// No case disagreements, or no extra characters in the autosuggestion.
|
||||
full_line = autosuggestion.to_owned();
|
||||
} else {
|
||||
// disagree on whether characters are uppercase or lowercase.
|
||||
let pos = line_range.end;
|
||||
let full_line;
|
||||
assert!(!autosuggestion.is_empty());
|
||||
assert!(autosuggestion.len() >= line_range.len());
|
||||
let available = autosuggestion.len() - line_range.len();
|
||||
let line = &cmdline[line_range.clone()];
|
||||
|
||||
if !string_prefixes_string(line, autosuggestion) {
|
||||
// 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.
|
||||
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;
|
||||
parse_util_token_extent(cmdline, cmdline.len() - 1, &mut tok, None);
|
||||
let last_token_contains_uppercase = cmdline[tok].chars().any(|c| c.is_uppercase());
|
||||
if !last_token_contains_uppercase {
|
||||
// Use the autosuggestion's case.
|
||||
full_line = autosuggestion.to_owned();
|
||||
} else {
|
||||
// Use the command line case for its characters, then append the remaining characters in
|
||||
// the autosuggestion. Note that we know that autosuggestion.size() > cmdline.size() due
|
||||
// to the first test above.
|
||||
full_line = cmdline.to_owned();
|
||||
full_line.push_utfstr(&autosuggestion[cmdline.len()..]);
|
||||
let start: usize = unsafe {
|
||||
(line.as_char_slice().first().unwrap() as *const char)
|
||||
.offset_from(&cmdline.as_char_slice()[0])
|
||||
}
|
||||
.try_into()
|
||||
.unwrap();
|
||||
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> {
|
||||
@ -1510,17 +1538,35 @@ impl<'a> Reader<'a> {
|
||||
/// `reason` is used in FLOG to explain why.
|
||||
fn paint_layout(&mut self, reason: &wstr, is_final_rendering: bool) {
|
||||
FLOGF!(reader_render, "Repainting from %ls", reason);
|
||||
let data = &self.data.rendered_layout;
|
||||
let cmd_line = &self.data.command_line;
|
||||
|
||||
let full_line = if self.conf.in_silent_mode {
|
||||
wstr::from_char_slice(&[get_obfuscation_read_char()]).repeat(cmd_line.len())
|
||||
} else {
|
||||
let (full_line, autosuggested_range) = if self.conf.in_silent_mode {
|
||||
(
|
||||
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_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();
|
||||
|
||||
// Highlight any history search.
|
||||
@ -1546,16 +1592,23 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Extend our colors with the autosuggestion.
|
||||
colors.resize(
|
||||
full_line.len(),
|
||||
HighlightSpec::with_fg(HighlightRole::autosuggestion),
|
||||
);
|
||||
let mut indents;
|
||||
{
|
||||
// Extend our colors with the 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
|
||||
// always conceptually has an indent of 0.
|
||||
let mut indents = parse_util_compute_indents(cmd_line.text());
|
||||
indents.resize(full_line.len(), 0);
|
||||
// Compute the indentation, then extend it with 0s for the autosuggestion. The autosuggestion
|
||||
// always conceptually has an indent of 0.
|
||||
indents = parse_util_compute_indents(cmd_line.text());
|
||||
indents.splice(pos..pos, vec![0; autosuggested_range.len()]);
|
||||
}
|
||||
|
||||
let screen = &mut self.data.screen;
|
||||
let pager = &mut self.data.pager;
|
||||
@ -1565,9 +1618,9 @@ impl<'a> Reader<'a> {
|
||||
&(self.data.mode_prompt_buff.clone() + &self.data.left_prompt_buff[..]),
|
||||
&self.data.right_prompt_buff,
|
||||
&full_line,
|
||||
cmd_line.len(),
|
||||
&colors,
|
||||
&indents,
|
||||
autosuggested_range,
|
||||
colors,
|
||||
indents,
|
||||
data.position,
|
||||
data.pager_search_field_position,
|
||||
self.parser.vars(),
|
||||
@ -1682,15 +1735,19 @@ impl ReaderData {
|
||||
// text avoid recomputing the autosuggestion.
|
||||
assert!(string_prefixes_string_maybe_case_insensitive(
|
||||
autosuggestion.icase,
|
||||
&self.command_line.text(),
|
||||
&self.command_line.text()[autosuggestion.search_string_range.clone()],
|
||||
&autosuggestion.text
|
||||
));
|
||||
let search_string_range = autosuggestion.search_string_range.clone();
|
||||
|
||||
// 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;
|
||||
};
|
||||
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(
|
||||
autosuggestion.icase,
|
||||
&edit.replacement,
|
||||
@ -1700,6 +1757,9 @@ impl ReaderData {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.autosuggestion.search_string_range.end = search_string_range.end
|
||||
- edit.range.len().min(search_string_range.end)
|
||||
+ edit.replacement.len();
|
||||
true
|
||||
}
|
||||
|
||||
@ -2379,10 +2439,9 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
}
|
||||
rl::EndOfLine => {
|
||||
let (_elt, el) = self.active_edit_line();
|
||||
if self.is_at_end(el) {
|
||||
if self.is_at_autosuggestion() {
|
||||
self.accept_autosuggestion(AutosuggestionPortion::Count(usize::MAX));
|
||||
} else {
|
||||
} else if !self.is_at_end() {
|
||||
loop {
|
||||
let position = {
|
||||
let (_elt, el) = self.active_edit_line();
|
||||
@ -2860,7 +2919,7 @@ impl<'a> Reader<'a> {
|
||||
rl::HistoryPagerDelete => {
|
||||
// Also applies to ordinary history search.
|
||||
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_search.current_result()
|
||||
} else {
|
||||
@ -2909,10 +2968,9 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
}
|
||||
rl::ForwardChar | rl::ForwardSingleChar => {
|
||||
let (elt, el) = self.active_edit_line();
|
||||
if self.is_navigating_pager_contents() {
|
||||
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(
|
||||
if c == rl::ForwardSingleChar {
|
||||
1
|
||||
@ -2920,13 +2978,14 @@ impl<'a> Reader<'a> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
rl::ForwardCharPassive => {
|
||||
let (elt, el) = self.active_edit_line();
|
||||
if !self.is_at_end(el) {
|
||||
if !self.is_at_end() {
|
||||
let (elt, el) = self.active_edit_line();
|
||||
if elt == EditableLineTag::SearchField || !self.is_navigating_pager_contents() {
|
||||
self.update_buff_pos(elt, Some(el.position() + 1));
|
||||
}
|
||||
@ -3016,15 +3075,16 @@ impl<'a> Reader<'a> {
|
||||
);
|
||||
}
|
||||
rl::ForwardToken => {
|
||||
let (_elt, el) = self.active_edit_line();
|
||||
if self.is_at_end(el) {
|
||||
if self.is_at_autosuggestion() {
|
||||
let Some(new_position) = self.forward_token(true) else {
|
||||
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(
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
@ -3068,10 +3128,10 @@ impl<'a> Reader<'a> {
|
||||
} else {
|
||||
MoveWordStyle::Whitespace
|
||||
};
|
||||
let (elt, el) = self.active_edit_line();
|
||||
if self.is_at_end(el) {
|
||||
if self.is_at_autosuggestion() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -3165,14 +3225,16 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
rl::SuppressAutosuggestion => {
|
||||
self.suppress_autosuggestion = true;
|
||||
let success = !self.autosuggestion.is_empty();
|
||||
let success = self.is_at_line_with_autosuggestion();
|
||||
self.autosuggestion.clear();
|
||||
// Return true if we had a suggestion to clear.
|
||||
self.input_data.function_set_status(success);
|
||||
}
|
||||
rl::AcceptAutosuggestion => {
|
||||
let success = !self.autosuggestion.is_empty();
|
||||
self.accept_autosuggestion(AutosuggestionPortion::Count(usize::MAX));
|
||||
let success = self.is_at_line_with_autosuggestion();
|
||||
if success {
|
||||
self.accept_autosuggestion(AutosuggestionPortion::Count(usize::MAX));
|
||||
}
|
||||
self.input_data.function_set_status(success);
|
||||
}
|
||||
rl::TransposeChars => {
|
||||
@ -3312,7 +3374,7 @@ impl<'a> Reader<'a> {
|
||||
let mut replacement = WString::new();
|
||||
while pos
|
||||
< if self.cursor_selection_mode == CursorSelectionMode::Inclusive
|
||||
&& self.is_at_end(el)
|
||||
&& self.is_at_end()
|
||||
{
|
||||
el.len()
|
||||
} else {
|
||||
@ -3606,15 +3668,19 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
|
||||
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 buffer = if autosuggest {
|
||||
if pos > self.autosuggestion.text.len() {
|
||||
return None;
|
||||
}
|
||||
&self.autosuggestion.text
|
||||
assert!(elt == EditableLineTag::Commandline);
|
||||
assert!(self.is_at_line_with_autosuggestion());
|
||||
let autosuggestion = &self.autosuggestion;
|
||||
Cow::Owned(combine_command_and_autosuggestion(
|
||||
el.text(),
|
||||
autosuggestion.search_string_range.clone(),
|
||||
&autosuggestion.text,
|
||||
))
|
||||
} else {
|
||||
el.text()
|
||||
Cow::Borrowed(el.text())
|
||||
};
|
||||
if pos == buffer.len() {
|
||||
return None;
|
||||
@ -3628,7 +3694,7 @@ impl<'a> Reader<'a> {
|
||||
.count();
|
||||
|
||||
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 };
|
||||
|
||||
@ -4364,9 +4430,12 @@ impl<'a> Reader<'a> {
|
||||
|
||||
#[derive(Default)]
|
||||
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,
|
||||
|
||||
// The range within the commandline that was searched. Always a whole line.
|
||||
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,
|
||||
@ -4387,10 +4456,11 @@ impl Autosuggestion {
|
||||
/// The result of an autosuggestion computation.
|
||||
#[derive(Default)]
|
||||
struct AutosuggestionResult {
|
||||
// The autosuggestion.
|
||||
autosuggestion: Autosuggestion,
|
||||
|
||||
// The string which was searched for.
|
||||
search_string: WString,
|
||||
// The commandline this result is based off.
|
||||
command_line: WString,
|
||||
|
||||
// The list of completions which may need loading.
|
||||
needs_load: Vec<WString>,
|
||||
@ -4404,20 +4474,34 @@ impl std::ops::Deref for 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 {
|
||||
autosuggestion: Autosuggestion { text, icase },
|
||||
search_string,
|
||||
autosuggestion: Autosuggestion {
|
||||
text,
|
||||
search_string_range,
|
||||
icase,
|
||||
},
|
||||
command_line,
|
||||
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
|
||||
// on a background thread) to determine the autosuggestion
|
||||
fn get_autosuggestion_performer(
|
||||
parser: &Parser,
|
||||
search_string: WString,
|
||||
command_line: WString,
|
||||
cursor_pos: usize,
|
||||
history: Arc<History>,
|
||||
) -> impl FnOnce() -> AutosuggestionResult {
|
||||
@ -4433,29 +4517,38 @@ fn get_autosuggestion_performer(
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
|
||||
// Search history for a matching item.
|
||||
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();
|
||||
// Search history for a matching item unless this line is not a continuation line or quoted.
|
||||
if range_of_line_at_cursor(
|
||||
&command_line,
|
||||
parse_util_process_extent(&command_line, cursor_pos, None).start,
|
||||
) == 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.
|
||||
if item.str().contains('\n') {
|
||||
continue;
|
||||
}
|
||||
// Skip items with newlines because they make terrible autosuggestions.
|
||||
if item.str().contains('\n') {
|
||||
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.
|
||||
return AutosuggestionResult::new(
|
||||
searcher.current_string().to_owned(),
|
||||
search_string.to_owned(),
|
||||
/*icase=*/ false,
|
||||
);
|
||||
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.
|
||||
return AutosuggestionResult::new(
|
||||
command_line,
|
||||
search_string_range,
|
||||
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
|
||||
// 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
|
||||
let last_char = search_string.chars().next_back().unwrap();
|
||||
let cursor_at_end = cursor_pos == search_string.len();
|
||||
let cursor_at_end =
|
||||
cursor_pos == command_line.len() || command_line.as_char_slice()[cursor_pos] == '\n';
|
||||
if !cursor_at_end && last_char.is_whitespace() {
|
||||
return nothing;
|
||||
}
|
||||
@ -4480,25 +4573,28 @@ fn get_autosuggestion_performer(
|
||||
|
||||
// Try normal completions.
|
||||
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()
|
||||
} else {
|
||||
sort_and_prioritize(&mut completions, complete_flags);
|
||||
let comp = &completions[0];
|
||||
let mut cursor = cursor_pos;
|
||||
completion_apply_to_command_line(
|
||||
let full_line = completion_apply_to_command_line(
|
||||
&comp.completion,
|
||||
comp.flags,
|
||||
&search_string,
|
||||
&command_line,
|
||||
&mut cursor,
|
||||
/*append_only=*/ true,
|
||||
)
|
||||
);
|
||||
line_at_cursor(&full_line, search_string_range.end).to_owned()
|
||||
};
|
||||
let mut result = AutosuggestionResult::new(
|
||||
full_line,
|
||||
search_string.to_owned(),
|
||||
command_line,
|
||||
search_string_range.clone(),
|
||||
suggestion,
|
||||
true, // normal completions are case-insensitive
|
||||
);
|
||||
result.needs_load = needs_load;
|
||||
@ -4529,10 +4625,10 @@ impl<'a> Reader<'a> {
|
||||
// Called after an autosuggestion has been computed on a background thread.
|
||||
fn autosuggest_completed(&mut self, result: AutosuggestionResult) {
|
||||
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();
|
||||
}
|
||||
if result.search_string != self.command_line.text() {
|
||||
if result.command_line != self.command_line.text() {
|
||||
// This autosuggestion is stale.
|
||||
return;
|
||||
}
|
||||
@ -4556,7 +4652,7 @@ impl<'a> Reader<'a> {
|
||||
&& self.can_autosuggest()
|
||||
&& string_prefixes_string_maybe_case_insensitive(
|
||||
result.icase,
|
||||
&result.search_string,
|
||||
result.search_string(),
|
||||
&result.text,
|
||||
)
|
||||
{
|
||||
@ -4578,10 +4674,10 @@ impl<'a> Reader<'a> {
|
||||
|
||||
let el = &self.data.command_line;
|
||||
let autosuggestion = &self.autosuggestion;
|
||||
if !self.autosuggestion.is_empty() {
|
||||
if self.is_at_line_with_autosuggestion() {
|
||||
assert!(string_prefixes_string_maybe_case_insensitive(
|
||||
autosuggestion.icase,
|
||||
&el.text(),
|
||||
&el.text()[autosuggestion.search_string_range.clone()],
|
||||
&autosuggestion.text
|
||||
));
|
||||
return;
|
||||
@ -4612,52 +4708,87 @@ impl<'a> Reader<'a> {
|
||||
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 {
|
||||
CursorEndMode::Exclusive => el.position() == 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
|
||||
// thing; if it's false, then respect the passed in style.
|
||||
fn accept_autosuggestion(&mut self, amount: AutosuggestionPortion) {
|
||||
if self.autosuggestion.is_empty() {
|
||||
return;
|
||||
}
|
||||
assert!(self.is_at_line_with_autosuggestion());
|
||||
|
||||
// Accepting an autosuggestion clears the 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.
|
||||
let (range, replacement) = match amount {
|
||||
AutosuggestionPortion::Count(count) => {
|
||||
let pos = self.command_line.len();
|
||||
if count == usize::MAX {
|
||||
(0..self.command_line.len(), self.autosuggestion.text.clone())
|
||||
(search_string_range, autosuggestion_text.clone())
|
||||
} 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 {
|
||||
return;
|
||||
}
|
||||
let start = autosuggestion_text.len() - available;
|
||||
(
|
||||
pos..pos,
|
||||
self.autosuggestion.text[pos..pos + count].to_owned(),
|
||||
autosuggestion_text[start..start + count].to_owned(),
|
||||
)
|
||||
}
|
||||
}
|
||||
AutosuggestionPortion::PerMoveWordStyle(style) => {
|
||||
// Accept characters according to the specified style.
|
||||
let mut state = MoveWordStateMachine::new(style);
|
||||
let mut want = self.command_line.len();
|
||||
while want < self.autosuggestion.text.len() {
|
||||
let wc = self.autosuggestion.text.as_char_slice()[want];
|
||||
let have = search_string_range.len();
|
||||
let mut want = have;
|
||||
while want < autosuggestion_text.len() {
|
||||
let wc = autosuggestion_text.as_char_slice()[want];
|
||||
if !state.consume_char(wc) {
|
||||
break;
|
||||
}
|
||||
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
|
||||
|
109
src/screen.rs
109
src/screen.rs
@ -15,6 +15,7 @@ use std::cell::RefCell;
|
||||
use std::collections::LinkedList;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::io::Write;
|
||||
use std::ops::Range;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::time::SystemTime;
|
||||
@ -255,9 +256,9 @@ impl Screen {
|
||||
left_prompt: &wstr,
|
||||
right_prompt: &wstr,
|
||||
commandline: &wstr,
|
||||
explicit_len: usize,
|
||||
colors: &[HighlightSpec],
|
||||
indent: &[i32],
|
||||
autosuggested_range: Range<usize>,
|
||||
mut colors: Vec<HighlightSpec>,
|
||||
mut indent: Vec<i32>,
|
||||
cursor_pos: usize,
|
||||
pager_search_field_position: Option<usize>,
|
||||
vars: &dyn Environment,
|
||||
@ -282,17 +283,19 @@ impl Screen {
|
||||
let mut scrolled_cursor: Option<ScrolledCursor> = None;
|
||||
|
||||
// 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.
|
||||
// right_prompt not supported.
|
||||
if is_dumb() {
|
||||
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, &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;
|
||||
}
|
||||
@ -311,10 +314,13 @@ impl Screen {
|
||||
|
||||
// Compute a layout.
|
||||
let layout = compute_layout(
|
||||
get_ellipsis_char(),
|
||||
screen_width,
|
||||
left_prompt,
|
||||
right_prompt,
|
||||
explicit_command_line,
|
||||
explicit_before_suggestion,
|
||||
&mut colors,
|
||||
&mut indent,
|
||||
autosuggestion,
|
||||
);
|
||||
|
||||
@ -345,7 +351,9 @@ impl Screen {
|
||||
let first_line_prompt_space = layout.left_prompt_space;
|
||||
|
||||
// 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.
|
||||
let mut i = 0;
|
||||
@ -375,7 +383,12 @@ impl Screen {
|
||||
break scrolled_cursor.unwrap();
|
||||
}
|
||||
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 {
|
||||
usize::MAX
|
||||
} else {
|
||||
@ -1835,16 +1848,17 @@ fn is_dumb() -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ScreenLayout {
|
||||
// Exposed for testing.
|
||||
#[derive(Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) struct ScreenLayout {
|
||||
// The left prompt that we're going to use.
|
||||
left_prompt: WString,
|
||||
pub(crate) left_prompt: WString,
|
||||
// How much space to leave for it.
|
||||
left_prompt_space: usize,
|
||||
pub(crate) left_prompt_space: usize,
|
||||
// The right prompt.
|
||||
right_prompt: WString,
|
||||
pub(crate) right_prompt: WString,
|
||||
// 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
|
||||
@ -1864,11 +1878,15 @@ fn truncation_offset_for_width(width_by_offset: &[usize], max_width: usize) -> u
|
||||
i - 1
|
||||
}
|
||||
|
||||
fn compute_layout(
|
||||
// Exposed for testing.
|
||||
pub(crate) fn compute_layout(
|
||||
ellipsis_char: char,
|
||||
screen_width: usize,
|
||||
left_untrunc_prompt: &wstr,
|
||||
right_untrunc_prompt: &wstr,
|
||||
commandline: &wstr,
|
||||
commandline_before_suggestion: &wstr,
|
||||
colors: &mut Vec<HighlightSpec>,
|
||||
indent: &mut Vec<i32>,
|
||||
autosuggestion_str: &wstr,
|
||||
) -> ScreenLayout {
|
||||
let mut result = ScreenLayout::default();
|
||||
@ -1901,24 +1919,23 @@ fn compute_layout(
|
||||
assert!(left_prompt_width + right_prompt_width <= screen_width);
|
||||
|
||||
// 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, 0)
|
||||
let first_command_line_width: usize = line_at_cursor(commandline_before_suggestion, 0)
|
||||
.chars()
|
||||
.map(wcwidth_rendered_min_0)
|
||||
.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_truncated_widths = vec![];
|
||||
if multiline {
|
||||
autosuggestion = L!("");
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
let mut autosuggest_truncated_widths = Vec::with_capacity(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:
|
||||
@ -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
|
||||
// on, and things turn to chaos very quickly.
|
||||
|
||||
let truncated_autosuggestion = |right_prompt_width: usize| {
|
||||
let width = left_prompt_width + right_prompt_width + first_command_line_width;
|
||||
let mut truncated_autosuggestion = |indent: &mut Vec<i32>, right_prompt_width: usize| {
|
||||
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.
|
||||
let available_autosuggest_space = screen_width - width;
|
||||
let available_autosuggest_space = screen_width.saturating_sub(width);
|
||||
let mut result = WString::new();
|
||||
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 {
|
||||
let truncation_offset = truncation_offset_for_width(
|
||||
&autosuggest_truncated_widths,
|
||||
available_autosuggest_space - 2,
|
||||
);
|
||||
result = autosuggestion[..truncation_offset].to_owned();
|
||||
result.push(get_ellipsis_char());
|
||||
result = autosuggestion_str[..truncation_offset].to_owned();
|
||||
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
|
||||
};
|
||||
|
||||
@ -1965,7 +1996,7 @@ fn compute_layout(
|
||||
result.left_prompt = left_prompt;
|
||||
result.left_prompt_space = left_prompt_width;
|
||||
result.right_prompt = right_prompt;
|
||||
result.autosuggestion = truncated_autosuggestion(right_prompt_width);
|
||||
result.autosuggestion = truncated_autosuggestion(indent, right_prompt_width);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -1974,14 +2005,14 @@ fn compute_layout(
|
||||
if calculated_width <= screen_width {
|
||||
result.left_prompt = left_prompt;
|
||||
result.left_prompt_space = left_prompt_width;
|
||||
result.autosuggestion = truncated_autosuggestion(0);
|
||||
result.autosuggestion = truncated_autosuggestion(indent, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Case 5
|
||||
result.left_prompt = left_prompt;
|
||||
result.left_prompt_space = left_prompt_width;
|
||||
result.autosuggestion = autosuggestion.to_owned();
|
||||
result.autosuggestion = autosuggestion_str.to_owned();
|
||||
result
|
||||
}
|
||||
|
||||
|
@ -5,30 +5,43 @@ use crate::wchar::prelude::*;
|
||||
#[test]
|
||||
fn test_autosuggestion_combining() {
|
||||
assert_eq!(
|
||||
combine_command_and_autosuggestion(L!("alpha"), L!("alphabeta")),
|
||||
combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("alphabeta")),
|
||||
L!("alphabeta")
|
||||
);
|
||||
|
||||
// When the last token contains no capital letters, we use the case of the autosuggestion.
|
||||
assert_eq!(
|
||||
combine_command_and_autosuggestion(L!("alpha"), L!("ALPHABETA")),
|
||||
combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHABETA")),
|
||||
L!("ALPHABETA")
|
||||
);
|
||||
|
||||
// When the last token contains capital letters, we use its case.
|
||||
assert_eq!(
|
||||
combine_command_and_autosuggestion(L!("alPha"), L!("alphabeTa")),
|
||||
combine_command_and_autosuggestion(L!("alPha"), 0..5, L!("alphabeTa")),
|
||||
L!("alPhabeTa")
|
||||
);
|
||||
|
||||
// If autosuggestion is not longer than input, use the input's case.
|
||||
assert_eq!(
|
||||
combine_command_and_autosuggestion(L!("alpha"), L!("ALPHAA")),
|
||||
combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHAA")),
|
||||
L!("ALPHAA")
|
||||
);
|
||||
assert_eq!(
|
||||
combine_command_and_autosuggestion(L!("alpha"), L!("ALPHA")),
|
||||
L!("alpha")
|
||||
combine_command_and_autosuggestion(L!("alpha"), 0..5, 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()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
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::wchar::prelude::*;
|
||||
use crate::wcstringutil::join_strings;
|
||||
@ -245,3 +247,112 @@ fn test_prompt_truncation() {
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user