mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-05-23 04:51:16 -03:00
Multi-line autosuggestions
Unlike other shells, fish tries to make it easy to work with multiline commands. Arguably, it's often better to use a full text editor but the shell can feel more convenient. Spreading long commands into multiple lines can improve readability, especially when there is some semantic grouping (loops, pipelines, command substitutions, quoted parts). Note that in Unix shell, every quoted string can span multiple lines, like Python's triple quotes, so the barrier to writing a multiline command is quite low. However these commands are not autosuggested. From1c4e5cadf2 (commitcomment-150853293)> the reason we don't offer multi-line autosuggestion is that they > can cause the command line to "jump" to make room for the second > and third lines, if you're at the bottom of your terminal. This jumping (as done by nushell for example) might be surprising, especially since there is no limit on the height of a command. Let's maybe avoid this jumping by rendering only however many lines from the autosuggestion can fit on the screen without scrolling. The truncation is hinted at by a single ellipsis ("…") after the last suggested character, just like when a single-line autosuggestion is truncated. (We might want to use something else in future.) To implement this, query for the cursor position after every command, so we know the y-position of the shell prompt within the terminal window (whose height we already know). Also, after we register a terminal window resize, query for the cursor position before doing anything else (until we od #12004, only height changes are relevant), to prevent this scenario: 1. move prompt to bottom of terminal 2. reduce terminal height 3. increase terminal height 4. type a command that triggers a multi-line autosuggestion 5. observe that it would fail to truncate properly As a refresher: when we fail to receive a query response, we always wait for 2 seconds, except if the initial query had also failed, seeb907bc775a(Use a low TTY query timeout only if first query failed, 2025-09-25). If the terminal does not support cursor position report (which is unlikely), show at most 1 line worth of autosuggestion. Note that either way, we don't skip multiline commands anymore. This might make the behavior worse on such terminals, which are probably not important enough. Alternatively, we could use no limit for such terminals, that's probably the better fallback behavior. The only reason I didn't do that yet is to stay a little bit closer to historical behavior. Storing the prompt's position simplifies scrollback-push and the mouse click handler, which no longer need to query. Move some associated code to the screen module. Technically we don't need to query for cursor position if the previous command was empty. But for now we do, trading a potential optimization for andother simplification. Disable this feature in pexpect tests for now, since those are still missing some terminal emulation features.
This commit is contained in:
@@ -5,6 +5,7 @@ Notable improvements and fixes
|
||||
------------------------------
|
||||
- New Taiwanese Chinese translation.
|
||||
- :ref:`Transient prompt <transient-prompt>` containing more lines than the final prompt will now be cleared properly (:issue:`11875`).
|
||||
- History-based autosuggestions now include multi-line commands.
|
||||
|
||||
Deprecations and removed features
|
||||
---------------------------------
|
||||
|
||||
@@ -177,7 +177,8 @@ Optional Commands
|
||||
and the second parameter is the column number.
|
||||
Both start at 1.
|
||||
|
||||
This is used by the :ref:`scrollback-push <special-input-functions-scrollback-push>` special input function,
|
||||
This is used for truncating multiline autosuggestions at the screen's bottom edge,
|
||||
by the :ref:`scrollback-push <special-input-functions-scrollback-push>` special input function,
|
||||
and inside terminals that implement the OSC 133 :ref:`click_events <term-compat-osc-133>` feature.
|
||||
* - ``\e[ \x20 q``
|
||||
- Se
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
use crate::job_group::MaybeJobId;
|
||||
use crate::parser::{Block, Parser};
|
||||
use crate::proc::Pid;
|
||||
use crate::reader::reader_update_termsize;
|
||||
use crate::signal::{Signal, signal_check_cancel, signal_handle};
|
||||
use crate::termsize::termsize_update;
|
||||
use crate::wchar::prelude::*;
|
||||
|
||||
pub enum event_type_t {
|
||||
@@ -549,7 +549,7 @@ pub fn fire_delayed(parser: &Parser) {
|
||||
// HACK: The only variables we change in response to a *signal* are $COLUMNS and $LINES.
|
||||
// Do that now.
|
||||
if sig == libc::SIGWINCH {
|
||||
termsize_update(parser);
|
||||
reader_update_termsize(parser)
|
||||
}
|
||||
let event = Event {
|
||||
desc: EventDescription::Signal { signal: sig },
|
||||
|
||||
@@ -324,6 +324,8 @@ pub enum ImplicitEvent {
|
||||
FocusOut,
|
||||
/// Mouse left click.
|
||||
MouseLeft(ViewportPosition),
|
||||
/// Window height changed.
|
||||
WindowHeight,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -695,20 +697,23 @@ pub fn function_set_status(&mut self, status: bool) {
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub enum CursorPositionQueryKind {
|
||||
MouseLeft(ViewportPosition),
|
||||
ScrollbackPush,
|
||||
pub enum CursorPositionQueryReason {
|
||||
NewPrompt,
|
||||
WindowHeightChange,
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct CursorPositionQuery {
|
||||
pub kind: CursorPositionQueryKind,
|
||||
pub reason: CursorPositionQueryReason,
|
||||
pub result: Option<ViewportPosition>,
|
||||
}
|
||||
|
||||
impl CursorPositionQuery {
|
||||
pub fn new(kind: CursorPositionQueryKind) -> Self {
|
||||
Self { kind, result: None }
|
||||
pub fn new(reason: CursorPositionQueryReason) -> Self {
|
||||
Self {
|
||||
reason,
|
||||
result: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,12 +86,13 @@
|
||||
History, HistorySearch, PersistenceMode, SearchDirection, SearchFlags, SearchType,
|
||||
history_session_id, in_private_mode,
|
||||
};
|
||||
use crate::input_common::CursorPositionQueryReason;
|
||||
use crate::input_common::InputEventQueue;
|
||||
use crate::input_common::InputEventQueuer;
|
||||
use crate::input_common::QueryResponse;
|
||||
use crate::input_common::{
|
||||
CharEvent, CharInputStyle, CursorPositionQuery, CursorPositionQueryKind, ImplicitEvent,
|
||||
InputData, QueryResultEvent, ReadlineCmd, TerminalQuery, stop_query,
|
||||
CharEvent, CharInputStyle, CursorPositionQuery, ImplicitEvent, InputData, QueryResultEvent,
|
||||
ReadlineCmd, TerminalQuery, stop_query,
|
||||
};
|
||||
use crate::io::IoChain;
|
||||
use crate::key::ViewportPosition;
|
||||
@@ -1099,6 +1100,19 @@ pub fn reader_schedule_prompt_repaint() {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reader_update_termsize(parser: &Parser) {
|
||||
let last = termsize_last();
|
||||
let new = termsize_update(parser);
|
||||
if new.height() == last.height() {
|
||||
return;
|
||||
}
|
||||
let Some(data) = current_data() else {
|
||||
return;
|
||||
};
|
||||
let mut data = Reader { parser, data };
|
||||
data.push_front(CharEvent::Implicit(ImplicitEvent::WindowHeight));
|
||||
}
|
||||
|
||||
pub fn reader_execute_readline_cmd(parser: &Parser, ch: CharEvent) {
|
||||
if parser.scope().readonly_commandline {
|
||||
return;
|
||||
@@ -1562,17 +1576,13 @@ fn update_buff_pos(&mut self, elt: EditableLineTag, mut new_pos: Option<usize>)
|
||||
true
|
||||
}
|
||||
|
||||
pub fn mouse_left_click(&mut self, viewport_cursor_y: usize, click_position: ViewportPosition) {
|
||||
pub fn mouse_left_click(&mut self, click_position: ViewportPosition) {
|
||||
FLOGF!(
|
||||
reader,
|
||||
"Received left mouse click at %u. Cursor is at %u",
|
||||
"Received left mouse click at %u",
|
||||
format!("{:?}", click_position),
|
||||
viewport_cursor_y,
|
||||
);
|
||||
match self
|
||||
.screen
|
||||
.offset_in_cmdline_given_cursor(click_position, viewport_cursor_y)
|
||||
{
|
||||
match self.screen.offset_in_cmdline_given_cursor(click_position) {
|
||||
CharOffset::Cmd(new_pos) | CharOffset::Pointer(new_pos) => {
|
||||
let (elt, _el) = self.active_edit_line();
|
||||
self.update_buff_pos(elt, Some(new_pos));
|
||||
@@ -1637,13 +1647,22 @@ fn combine_command_and_autosuggestion(
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
pub fn request_cursor_position(&mut self, q: CursorPositionQuery) {
|
||||
pub fn request_cursor_position(&mut self, reason: CursorPositionQueryReason) {
|
||||
if self
|
||||
.vars()
|
||||
.get_unless_empty(L!("FISH_TEST_NO_CURSOR_POSITION_QUERY"))
|
||||
.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if !querying_allowed(self.vars()) {
|
||||
return;
|
||||
}
|
||||
let mut query = self.blocking_query();
|
||||
assert!(query.is_none());
|
||||
*query = Some(TerminalQuery::CursorPosition(q));
|
||||
*query = Some(TerminalQuery::CursorPosition(CursorPositionQuery::new(
|
||||
reason,
|
||||
)));
|
||||
{
|
||||
let mut out = Outputter::stdoutput().borrow_mut();
|
||||
out.begin_buffering();
|
||||
@@ -1821,15 +1840,15 @@ fn paint_layout(&mut self, reason: &wstr, is_final_rendering: bool) {
|
||||
],
|
||||
);
|
||||
|
||||
// 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.splice(pos..pos, vec![0; autosuggested_range.len()]);
|
||||
// Compute the indentation.
|
||||
let indents = parse_util_compute_indents(&full_line);
|
||||
|
||||
let screen = &mut self.data.screen;
|
||||
let pager = &mut self.data.pager;
|
||||
let current_page_rendering = &mut self.data.current_page_rendering;
|
||||
let curr_termsize = termsize_last();
|
||||
screen.write(
|
||||
curr_termsize,
|
||||
// Prepend the mode prompt to the left prompt.
|
||||
&(self.data.mode_prompt_buff.clone() + &self.data.left_prompt_buff[..]),
|
||||
&self.data.right_prompt_buff,
|
||||
@@ -1844,6 +1863,7 @@ fn paint_layout(&mut self, reason: &wstr, is_final_rendering: bool) {
|
||||
current_page_rendering,
|
||||
is_final_rendering,
|
||||
);
|
||||
screen.autoscroll(curr_termsize.height())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2333,6 +2353,8 @@ fn readline(
|
||||
// Start out as initially dirty.
|
||||
self.force_exec_prompt_and_repaint = true;
|
||||
|
||||
self.request_cursor_position(CursorPositionQueryReason::NewPrompt);
|
||||
|
||||
while !check_exit_loop_maybe_warning(Some(self)) {
|
||||
// Enable tty protocols while we read input.
|
||||
tty.enable_tty_protocols();
|
||||
@@ -2506,7 +2528,7 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
|
||||
self.rls_mut().complete_did_insert = false;
|
||||
}
|
||||
// Perhaps update the termsize. This is cheap if it has not changed.
|
||||
self.update_termsize();
|
||||
reader_update_termsize(self.parser);
|
||||
|
||||
// Repaint as needed.
|
||||
self.color_suggest_repaint_now();
|
||||
@@ -2625,9 +2647,11 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
|
||||
}
|
||||
MouseLeft(position) => {
|
||||
FLOG!(reader, "Mouse left click", position);
|
||||
self.request_cursor_position(CursorPositionQuery::new(
|
||||
CursorPositionQueryKind::MouseLeft(position),
|
||||
));
|
||||
self.mouse_left_click(position);
|
||||
}
|
||||
WindowHeight => {
|
||||
FLOG!(reader, "Handling window height change");
|
||||
self.request_cursor_position(CursorPositionQueryReason::WindowHeightChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2651,20 +2675,20 @@ fn handle_char_event(&mut self, injected_event: Option<CharEvent>) -> ControlFlo
|
||||
) => {
|
||||
let cursor_pos_query = cursor_pos_query.clone();
|
||||
drop(maybe_query);
|
||||
use CursorPositionQueryKind::*;
|
||||
let cursor_pos = cursor_pos_query.result;
|
||||
match cursor_pos_query.kind {
|
||||
MouseLeft(click_position) => {
|
||||
if let Some(cursor_pos) = cursor_pos {
|
||||
self.mouse_left_click(cursor_pos.y, click_position);
|
||||
}
|
||||
}
|
||||
ScrollbackPush => {
|
||||
if let Some(cursor_pos) = cursor_pos {
|
||||
self.screen.push_to_scrollback(cursor_pos.y);
|
||||
}
|
||||
}
|
||||
use CursorPositionQueryReason::*;
|
||||
let reason = cursor_pos_query.reason;
|
||||
let whence = match reason {
|
||||
NewPrompt => "cursor position query on new prompt",
|
||||
WindowHeightChange => "cursor position query on window height change",
|
||||
};
|
||||
let y = cursor_pos.map(|cursor_pos| match reason {
|
||||
NewPrompt => cursor_pos.y,
|
||||
WindowHeightChange => {
|
||||
self.screen.command_line_y_given_cursor_y(cursor_pos.y)
|
||||
}
|
||||
});
|
||||
self.screen.set_position_in_viewport(whence, y);
|
||||
self.blocking_query()
|
||||
}
|
||||
// Rogue reply
|
||||
@@ -2740,7 +2764,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
|
||||
}
|
||||
rl::EndOfLine => {
|
||||
if self.is_at_autosuggestion() {
|
||||
self.accept_autosuggestion(AutosuggestionPortion::Count(usize::MAX));
|
||||
self.accept_autosuggestion(AutosuggestionPortion::Line);
|
||||
} else if !self.is_at_end() {
|
||||
loop {
|
||||
let position = {
|
||||
@@ -3959,24 +3983,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
|
||||
self.clear_screen_and_repaint();
|
||||
}
|
||||
rl::ScrollbackPush => {
|
||||
let query = self.blocking_query();
|
||||
let Some(query) = &*query else {
|
||||
drop(query);
|
||||
self.request_cursor_position(CursorPositionQuery::new(
|
||||
CursorPositionQueryKind::ScrollbackPush,
|
||||
));
|
||||
return;
|
||||
};
|
||||
match query {
|
||||
TerminalQuery::Initial => panic!(),
|
||||
TerminalQuery::CursorPosition(_) => {
|
||||
// TODO: re-queue it I guess.
|
||||
FLOG!(
|
||||
reader,
|
||||
"Ignoring scrollback-push received while still waiting for Cursor Position Report"
|
||||
);
|
||||
}
|
||||
}
|
||||
self.screen.push_to_scrollback();
|
||||
}
|
||||
rl::SelfInsert | rl::SelfInsertNotFirst | rl::FuncAnd | rl::FuncOr => {
|
||||
// This can be reached via `commandline -f and` etc
|
||||
@@ -3996,6 +4003,8 @@ fn clear_screen_and_repaint(&mut self) {
|
||||
Outputter::stdoutput()
|
||||
.borrow_mut()
|
||||
.write_command(ClearScreen);
|
||||
self.screen
|
||||
.set_position_in_viewport("screen clear", Some(0));
|
||||
self.screen.reset_line(/*repaint_prompt=*/ true);
|
||||
self.layout_and_repaint(L!("readline"));
|
||||
|
||||
@@ -4104,9 +4113,6 @@ fn handle_execute(&mut self) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Delete any autosuggestion.
|
||||
self.autosuggestion.clear();
|
||||
|
||||
// The user may have hit return with pager contents, but while not navigating them.
|
||||
// Clear the pager in that event.
|
||||
self.clear_pager();
|
||||
@@ -4153,6 +4159,9 @@ fn handle_execute(&mut self) -> bool {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
// Delete any autosuggestion.
|
||||
self.autosuggestion.clear();
|
||||
|
||||
self.add_to_history();
|
||||
self.rls_mut().finished = true;
|
||||
self.command_line.pending_position = Some(self.command_line.position());
|
||||
@@ -4225,11 +4234,6 @@ fn selection_is_at_top(&self) -> bool {
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
/// Called to update the termsize, including $COLUMNS and $LINES, as necessary.
|
||||
fn update_termsize(&mut self) {
|
||||
termsize_update(self.parser);
|
||||
}
|
||||
|
||||
/// Flash the screen. This function changes the color of the current line momentarily.
|
||||
fn flash(&mut self, mut flash_range: Range<usize>) {
|
||||
// Multiple flashes may be enqueued by keypress repeat events and can pile up to cause a
|
||||
@@ -4743,7 +4747,7 @@ fn exec_prompt(&mut self, full_prompt: bool, final_prompt: bool) {
|
||||
|
||||
// Update the termsize now.
|
||||
// This allows prompts to react to $COLUMNS.
|
||||
self.update_termsize();
|
||||
reader_update_termsize(self.parser);
|
||||
|
||||
self.mode_prompt_buff.clear();
|
||||
if function::exists(MODE_PROMPT_FUNCTION_NAME, self.parser) {
|
||||
@@ -4893,31 +4897,36 @@ fn get_autosuggestion_performer(
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// Let's make sure we aren't using the empty string.
|
||||
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 unless this line is not a continuation line or quoted.
|
||||
let cursor_line_has_process_start = {
|
||||
let mut tokens = vec![];
|
||||
parse_util_process_extent(&command_line, cursor_pos, Some(&mut tokens));
|
||||
range_of_line_at_cursor(
|
||||
&command_line,
|
||||
tokens.first().map(|tok| tok.offset()).unwrap_or(cursor_pos),
|
||||
) == search_string_range
|
||||
};
|
||||
|
||||
// Only to be used if no case-sensitive suggestions are found.
|
||||
let mut icase_history_result = None;
|
||||
|
||||
if cursor_line_has_process_start {
|
||||
let line_range = range_of_line_at_cursor(&command_line, cursor_pos);
|
||||
// Search history for a matching item unless this line is not a continuation line or quoted.
|
||||
for (search_type, range) in [
|
||||
(SearchType::Prefix, 0..command_line.len()),
|
||||
(SearchType::LinePrefix, line_range.clone()),
|
||||
] {
|
||||
if range.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let search_string = &command_line[range.clone()];
|
||||
if search_type == SearchType::LinePrefix {
|
||||
let cursor_line_has_process_start = {
|
||||
let mut tokens = vec![];
|
||||
parse_util_process_extent(&command_line, cursor_pos, Some(&mut tokens));
|
||||
range_of_line_at_cursor(
|
||||
&command_line,
|
||||
tokens.first().map(|tok| tok.offset()).unwrap_or(cursor_pos),
|
||||
) == range
|
||||
};
|
||||
if !cursor_line_has_process_start {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let mut searcher = HistorySearch::new_with(
|
||||
history,
|
||||
history.clone(),
|
||||
search_string.to_owned(),
|
||||
SearchType::LinePrefix,
|
||||
search_type,
|
||||
SearchFlags::IGNORE_CASE,
|
||||
0,
|
||||
);
|
||||
@@ -4925,28 +4934,44 @@ fn get_autosuggestion_performer(
|
||||
while !ctx.check_cancel() && searcher.go_to_next_match(SearchDirection::Backward) {
|
||||
let item = searcher.current_item();
|
||||
|
||||
// The history item's may have multiple lines of text.
|
||||
// Only suggest the line that actually contains the search string.
|
||||
let (matched_part, icase) = if search_type == SearchType::Prefix {
|
||||
let mut matched_part =
|
||||
item.str().starts_with(search_string).then_some(item.str());
|
||||
let mut icase = false;
|
||||
// Only check for a case-insensitive match if we haven't already found one
|
||||
if matched_part.is_none() && icase_history_result.is_none() {
|
||||
icase = true;
|
||||
matched_part =
|
||||
string_prefixes_string_case_insensitive(search_string, item.str())
|
||||
.then_some(item.str());
|
||||
}
|
||||
|
||||
let lines = item
|
||||
.str()
|
||||
.as_char_slice()
|
||||
.split(|&c| c == '\n')
|
||||
.rev()
|
||||
.map(wstr::from_char_slice);
|
||||
(matched_part, icase)
|
||||
} else {
|
||||
// The history items may have multiple lines of text.
|
||||
// Only suggest the line that actually contains the search string.
|
||||
let lines = item
|
||||
.str()
|
||||
.as_char_slice()
|
||||
.split(|&c| c == '\n')
|
||||
.rev()
|
||||
.map(wstr::from_char_slice);
|
||||
|
||||
let mut icase = false;
|
||||
let mut matched_line = lines.clone().find(|line| line.starts_with(search_string));
|
||||
let mut icase = false;
|
||||
let mut matched_part =
|
||||
lines.clone().find(|line| line.starts_with(search_string));
|
||||
|
||||
// Only check for a case-insensitive match if we haven't already found one
|
||||
if matched_line.is_none() && icase_history_result.is_none() {
|
||||
icase = true;
|
||||
matched_line = lines
|
||||
.into_iter()
|
||||
.find(|line| string_prefixes_string_case_insensitive(search_string, line));
|
||||
}
|
||||
// Only check for a case-insensitive match if we haven't already found one
|
||||
if matched_part.is_none() && icase_history_result.is_none() {
|
||||
icase = true;
|
||||
matched_part = lines.into_iter().find(|line| {
|
||||
string_prefixes_string_case_insensitive(search_string, line)
|
||||
});
|
||||
}
|
||||
|
||||
let Some(matched_line) = matched_line else {
|
||||
(matched_part, icase)
|
||||
};
|
||||
let Some(matched_part) = matched_part else {
|
||||
assert!(
|
||||
icase_history_result.is_some(),
|
||||
"couldn't find line matching search {search_string:?} in history item {item:?} (did history search yield a bogus result?)"
|
||||
@@ -4956,11 +4981,11 @@ fn get_autosuggestion_performer(
|
||||
|
||||
if autosuggest_validate_from_history(item, &working_directory, &ctx) {
|
||||
// The command autosuggestion was handled specially, so we're done.
|
||||
let is_whole = matched_line.len() == item.str().len();
|
||||
let is_whole = matched_part.len() == item.str().len();
|
||||
let result = AutosuggestionResult::new(
|
||||
command_line.clone(),
|
||||
search_string_range.clone(),
|
||||
matched_line.into(),
|
||||
range.clone(),
|
||||
matched_part.into(),
|
||||
icase,
|
||||
is_whole,
|
||||
);
|
||||
@@ -4978,6 +5003,11 @@ fn get_autosuggestion_performer(
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let Some(last_char) = command_line[line_range.clone()].chars().next_back() else {
|
||||
// Let's make sure we aren't using the empty string.
|
||||
return nothing;
|
||||
};
|
||||
|
||||
// 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
|
||||
@@ -4994,7 +5024,7 @@ fn get_autosuggestion_performer(
|
||||
|
||||
// Try normal completions.
|
||||
let complete_flags = CompletionRequestOptions::autosuggest();
|
||||
let mut would_be_cursor = search_string_range.end;
|
||||
let mut would_be_cursor = line_range.end;
|
||||
let (mut completions, needs_load) =
|
||||
complete(&command_line[..would_be_cursor], complete_flags, &ctx);
|
||||
|
||||
@@ -5028,7 +5058,7 @@ fn get_autosuggestion_performer(
|
||||
};
|
||||
let mut result = AutosuggestionResult::new(
|
||||
command_line,
|
||||
search_string_range,
|
||||
line_range,
|
||||
suggestion,
|
||||
true, // normal completions are case-insensitive
|
||||
/*is_whole_item_from_history=*/ false,
|
||||
@@ -5040,6 +5070,7 @@ fn get_autosuggestion_performer(
|
||||
|
||||
enum AutosuggestionPortion {
|
||||
Count(usize),
|
||||
Line,
|
||||
PerMoveWordStyle(MoveWordStyle),
|
||||
}
|
||||
|
||||
@@ -5169,7 +5200,12 @@ fn is_at_line_with_autosuggestion(&self) -> bool {
|
||||
return false;
|
||||
}
|
||||
let el = &self.command_line;
|
||||
range_of_line_at_cursor(el.text(), el.position()) == autosuggestion.search_string_range
|
||||
let search_string_range = &autosuggestion.search_string_range;
|
||||
// Single-line autosuggestion
|
||||
range_of_line_at_cursor(el.text(), el.position()) == *search_string_range || {
|
||||
// Multi-line autosuggestion
|
||||
search_string_range.start == 0 && el.position() <= search_string_range.end
|
||||
}
|
||||
}
|
||||
|
||||
// Accept any autosuggestion by replacing the command line with it. If full is true, take the whole
|
||||
@@ -5202,6 +5238,20 @@ fn accept_autosuggestion(&mut self, amount: AutosuggestionPortion) {
|
||||
)
|
||||
}
|
||||
}
|
||||
AutosuggestionPortion::Line => {
|
||||
let suggested = &autosuggestion_text[search_string_range.len()..];
|
||||
let line_end = suggested
|
||||
.chars()
|
||||
.position(|c| c == '\n')
|
||||
.unwrap_or(suggested.len());
|
||||
if line_end == 0 {
|
||||
return;
|
||||
}
|
||||
(
|
||||
search_string_range.end..search_string_range.end,
|
||||
suggested[..line_end].to_owned(),
|
||||
)
|
||||
}
|
||||
AutosuggestionPortion::PerMoveWordStyle(style) => {
|
||||
// Accept characters according to the specified style.
|
||||
let mut state = MoveWordStateMachine::new(style);
|
||||
|
||||
200
src/screen.rs
200
src/screen.rs
@@ -11,6 +11,7 @@
|
||||
use crate::editable_line::line_at_cursor;
|
||||
use crate::key::ViewportPosition;
|
||||
use crate::pager::{PAGER_MIN_HEIGHT, PageRendering, Pager};
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::LinkedList;
|
||||
use std::io::Write;
|
||||
@@ -36,7 +37,7 @@
|
||||
CursorUp, EnterDimMode, ExitAttributeMode, Osc133PromptStart, ScrollContentUp,
|
||||
};
|
||||
use crate::terminal::{BufferedOutputter, CardinalDirection, Output, Outputter, use_terminfo};
|
||||
use crate::termsize::{Termsize, termsize_last};
|
||||
use crate::termsize::Termsize;
|
||||
use crate::wchar::prelude::*;
|
||||
use crate::wcstringutil::{fish_wcwidth_visible, string_prefixes_string};
|
||||
use crate::wutil::fstat;
|
||||
@@ -221,6 +222,8 @@ pub struct Screen {
|
||||
/// Receiver for our output.
|
||||
outp: &'static RefCell<Outputter>,
|
||||
|
||||
/// Vertical offset of the screen contents within the terminal window.
|
||||
viewport_y: Option<usize>,
|
||||
/// The internal representation of the desired screen contents.
|
||||
desired: ScreenData,
|
||||
/// The internal representation of the actual screen contents.
|
||||
@@ -253,6 +256,7 @@ fn default() -> Self {
|
||||
autosuggestion_is_truncated: Default::default(),
|
||||
scrolled: Default::default(),
|
||||
outp: Outputter::stdoutput(),
|
||||
viewport_y: Default::default(),
|
||||
desired: Default::default(),
|
||||
actual: Default::default(),
|
||||
actual_left_prompt: Default::default(),
|
||||
@@ -281,6 +285,7 @@ impl Screen {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn write(
|
||||
&mut self,
|
||||
curr_termsize: Termsize,
|
||||
left_prompt: &wstr,
|
||||
right_prompt: &wstr,
|
||||
commandline: &wstr,
|
||||
@@ -294,7 +299,6 @@ pub fn write(
|
||||
page_rendering: &mut PageRendering,
|
||||
is_final_rendering: bool,
|
||||
) {
|
||||
let curr_termsize = termsize_last();
|
||||
let screen_width = curr_termsize.width();
|
||||
let screen_height = curr_termsize.height();
|
||||
static REPAINTS: AtomicU32 = AtomicU32::new(0);
|
||||
@@ -339,6 +343,8 @@ struct ScrolledCursor {
|
||||
let layout = compute_layout(
|
||||
get_ellipsis_char(),
|
||||
screen_width,
|
||||
screen_height,
|
||||
self.viewport_y,
|
||||
left_prompt,
|
||||
right_prompt,
|
||||
explicit_before_suggestion,
|
||||
@@ -557,8 +563,13 @@ pub fn reset_line(&mut self, repaint_prompt: bool /* = false */) {
|
||||
self.save_status();
|
||||
}
|
||||
|
||||
pub fn push_to_scrollback(&mut self, viewport_cursor_y: usize) {
|
||||
pub fn push_to_scrollback(&mut self) {
|
||||
let Some(viewport_cursor_y) = self.viewport_y else {
|
||||
return;
|
||||
};
|
||||
FLOG!(reader, "Pushing to scrollback");
|
||||
let lines_to_scroll = self.command_line_y_given_cursor_y(viewport_cursor_y);
|
||||
self.set_position_in_viewport("scrollback-push", Some(0));
|
||||
if lines_to_scroll == 0 {
|
||||
return;
|
||||
}
|
||||
@@ -569,7 +580,37 @@ pub fn push_to_scrollback(&mut self, viewport_cursor_y: usize) {
|
||||
out.write_command(CursorMove(CardinalDirection::Up, lines_to_scroll));
|
||||
}
|
||||
|
||||
fn command_line_y_given_cursor_y(&mut self, viewport_cursor_y: usize) -> usize {
|
||||
pub fn set_position_in_viewport(&mut self, whence: &str, viewport_y: Option<usize>) {
|
||||
FLOGF!(
|
||||
reader,
|
||||
"Setting screen y to %s due to %s",
|
||||
viewport_y.map_or("<none>".to_string(), |y| format!("{y}")),
|
||||
whence,
|
||||
);
|
||||
self.viewport_y = viewport_y;
|
||||
}
|
||||
|
||||
pub fn autoscroll(&mut self, screen_height: usize) {
|
||||
let Some(viewport_y) = self.viewport_y else {
|
||||
return;
|
||||
};
|
||||
let actual_lines = self.actual.line_count();
|
||||
let remaining_vertical_space = screen_height.saturating_sub(actual_lines);
|
||||
if viewport_y > remaining_vertical_space {
|
||||
FLOGF!(
|
||||
reader,
|
||||
"printing %u lines at y=%u would exceed window height (%u); \
|
||||
assuming the extra lines have been pushed to scrollback, setting screen y to %d",
|
||||
actual_lines,
|
||||
viewport_y,
|
||||
screen_height,
|
||||
remaining_vertical_space,
|
||||
);
|
||||
self.set_position_in_viewport("autoscroll", Some(remaining_vertical_space));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command_line_y_given_cursor_y(&mut self, viewport_cursor_y: usize) -> usize {
|
||||
let prompt_y = viewport_cursor_y.checked_sub(self.actual.cursor.y);
|
||||
prompt_y.unwrap_or_else(|| {
|
||||
FLOG!(
|
||||
@@ -586,9 +627,11 @@ fn command_line_y_given_cursor_y(&mut self, viewport_cursor_y: usize) -> usize {
|
||||
pub fn offset_in_cmdline_given_cursor(
|
||||
&mut self,
|
||||
viewport_position: ViewportPosition,
|
||||
viewport_cursor_y: usize,
|
||||
) -> CharOffset {
|
||||
let viewport_prompt_y = self.command_line_y_given_cursor_y(viewport_cursor_y);
|
||||
let Some(viewport_y) = self.viewport_y else {
|
||||
return CharOffset::None;
|
||||
};
|
||||
let viewport_prompt_y = self.command_line_y_given_cursor_y(viewport_y);
|
||||
let y = viewport_position
|
||||
.y
|
||||
.checked_sub(viewport_prompt_y)
|
||||
@@ -1925,6 +1968,8 @@ fn truncation_offset_for_width(str: &wstr, max_width: usize) -> usize {
|
||||
fn compute_layout(
|
||||
ellipsis_char: char,
|
||||
screen_width: usize,
|
||||
screen_height: usize,
|
||||
screen_viewport_y: Option<usize>,
|
||||
left_untrunc_prompt: &wstr,
|
||||
right_untrunc_prompt: &wstr,
|
||||
commandline_before_suggestion: &wstr,
|
||||
@@ -1962,7 +2007,6 @@ fn compute_layout(
|
||||
.chars()
|
||||
.map(wcwidth_rendered_min_0)
|
||||
.sum();
|
||||
let autosuggest_total_width = autosuggestion_str.chars().map(wcwidth_rendered_min_0).sum();
|
||||
|
||||
// Here are the layouts we try:
|
||||
// 1. Right prompt visible.
|
||||
@@ -1995,34 +2039,126 @@ fn compute_layout(
|
||||
// Now we should definitely fit.
|
||||
assert!(left_prompt_width + right_prompt_width <= screen_width);
|
||||
|
||||
// Calculate space available for autosuggestion.
|
||||
let width = (left_prompt_width
|
||||
+ autosuggestion_line_explicit_width
|
||||
// Truncate each logical line from the autosuggestion to fit on a single screen line.
|
||||
// In future, we should truncate only once, at the end (#12004).
|
||||
let cursor_y = left_prompt_layout.line_starts.len() - 1
|
||||
+ commandline_before_suggestion
|
||||
.chars()
|
||||
.rposition(|c| c == '\n')
|
||||
.map_or(right_prompt_width, |pos| {
|
||||
usize::try_from(indent[pos]).unwrap() * INDENT_STEP
|
||||
}))
|
||||
% screen_width;
|
||||
let available_autosuggest_space = screen_width - width;
|
||||
.filter(|&c| c == '\n')
|
||||
.count();
|
||||
|
||||
// Truncate the autosuggestion to fit on the line.
|
||||
let mut autosuggestion = WString::new();
|
||||
if available_autosuggest_space >= autosuggest_total_width {
|
||||
autosuggestion = autosuggestion_str.to_owned();
|
||||
} else if autosuggest_total_width > 0 {
|
||||
let truncation_offset =
|
||||
truncation_offset_for_width(autosuggestion_str, available_autosuggest_space - 1);
|
||||
autosuggestion = autosuggestion_str[..truncation_offset].to_owned();
|
||||
autosuggestion.push(ellipsis_char);
|
||||
struct SuggestionLine<'a> {
|
||||
available_autosuggest_space: usize,
|
||||
autosuggestion_line: &'a wstr,
|
||||
autosuggest_total_width: usize,
|
||||
}
|
||||
let mut suggestion_lines = vec![];
|
||||
|
||||
let mut available_vertical_space = screen_viewport_y.map_or(
|
||||
1, // Cursor position report not implemented in the terminal?
|
||||
|screen_viewport_y| screen_height.saturating_sub(screen_viewport_y + cursor_y),
|
||||
);
|
||||
let mut truncated_vertically = false;
|
||||
let mut suggestion_start = commandline_before_suggestion.len();
|
||||
for (line, autosuggestion_line) in autosuggestion_str
|
||||
.as_char_slice()
|
||||
.split(|&c| c == '\n')
|
||||
.enumerate()
|
||||
{
|
||||
let autosuggestion_line = wstr::from_char_slice(autosuggestion_line);
|
||||
let autosuggest_total_width = autosuggestion_line
|
||||
.chars()
|
||||
.map(wcwidth_rendered_min_0)
|
||||
.sum();
|
||||
|
||||
// Calculate space available for autosuggestion.
|
||||
let indent_width = |pos| usize::try_from(indent[pos]).unwrap() * INDENT_STEP;
|
||||
let width = left_prompt_width
|
||||
+ if line == 0 {
|
||||
autosuggestion_line_explicit_width
|
||||
+ commandline_before_suggestion
|
||||
.chars()
|
||||
.rposition(|c| c == '\n')
|
||||
.map_or(right_prompt_width, indent_width)
|
||||
} else {
|
||||
indent_width(suggestion_start - "\n".len())
|
||||
};
|
||||
let available_horizontal_space = screen_width - (width % screen_width);
|
||||
|
||||
let suggestion_line_height =
|
||||
if width >= screen_width || autosuggest_total_width >= available_horizontal_space {
|
||||
// As per the comment above, we truncate and autosuggestion lines that would wrap.
|
||||
// We truncate them at the very end of the screen, so they (barely) soft wrap,
|
||||
// and take up two screen lines.
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
match available_vertical_space.checked_sub(suggestion_line_height) {
|
||||
Some(lines) => available_vertical_space = lines,
|
||||
None => {
|
||||
truncated_vertically = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
suggestion_lines.push(SuggestionLine {
|
||||
available_autosuggest_space: available_horizontal_space,
|
||||
autosuggestion_line,
|
||||
autosuggest_total_width,
|
||||
});
|
||||
|
||||
suggestion_start += autosuggestion_line.len() + "\n".len();
|
||||
}
|
||||
|
||||
let suggestion_start = commandline_before_suggestion.len();
|
||||
let truncation_range =
|
||||
suggestion_start + autosuggestion.len()..suggestion_start + autosuggestion_str.len();
|
||||
colors.drain(truncation_range.clone());
|
||||
indent.drain(truncation_range);
|
||||
let mut autosuggestion = WString::new();
|
||||
let mut erased = 0;
|
||||
let mut suggestion_start = commandline_before_suggestion.len();
|
||||
for (
|
||||
line,
|
||||
&SuggestionLine {
|
||||
available_autosuggest_space,
|
||||
autosuggestion_line,
|
||||
autosuggest_total_width,
|
||||
},
|
||||
) in suggestion_lines.iter().enumerate()
|
||||
{
|
||||
let truncated_suggestion_line;
|
||||
let mut vertical_truncation_marker = None;
|
||||
if autosuggest_total_width > 0 && autosuggest_total_width >= available_autosuggest_space {
|
||||
// horizontal truncation
|
||||
let truncation_offset =
|
||||
truncation_offset_for_width(autosuggestion_line, available_autosuggest_space - 1);
|
||||
truncated_suggestion_line = Cow::Owned(
|
||||
autosuggestion_line[..truncation_offset].to_owned()
|
||||
+ wstr::from_char_slice(&[ellipsis_char]),
|
||||
);
|
||||
} else if truncated_vertically && line == suggestion_lines.len() - 1 {
|
||||
// vertical truncation
|
||||
truncated_suggestion_line = Cow::Borrowed(autosuggestion_line);
|
||||
vertical_truncation_marker = Some(ellipsis_char);
|
||||
} else {
|
||||
// no truncation
|
||||
assert!(available_autosuggest_space >= autosuggest_total_width);
|
||||
truncated_suggestion_line = Cow::Borrowed(autosuggestion_line);
|
||||
}
|
||||
|
||||
let truncation_range = suggestion_start - erased + truncated_suggestion_line.len()
|
||||
..suggestion_start - erased + autosuggestion_line.len();
|
||||
colors.drain(truncation_range.clone());
|
||||
indent.drain(truncation_range.clone());
|
||||
erased += truncation_range.len();
|
||||
suggestion_start += autosuggestion_line.len() + "\n".len();
|
||||
if line != 0 {
|
||||
autosuggestion.push('\n');
|
||||
}
|
||||
autosuggestion.push_utfstr(&truncated_suggestion_line);
|
||||
if let Some(extra) = vertical_truncation_marker {
|
||||
autosuggestion.push(extra);
|
||||
colors.insert(truncation_range.end, colors[truncation_range.end - 1]);
|
||||
indent.insert(truncation_range.end, indent[truncation_range.end - 1]);
|
||||
}
|
||||
}
|
||||
result.autosuggestion = autosuggestion;
|
||||
|
||||
result
|
||||
@@ -2324,6 +2460,8 @@ macro_rules! validate {
|
||||
compute_layout(
|
||||
'…',
|
||||
$screen_width,
|
||||
/*screen_height=*/ 24,
|
||||
/*screen_viewport_y=*/ Some(0),
|
||||
L!($left_untrunc_prompt),
|
||||
L!($right_untrunc_prompt),
|
||||
L!($commandline_before_suggestion),
|
||||
@@ -2422,7 +2560,7 @@ macro_rules! validate {
|
||||
"left>",
|
||||
5,
|
||||
"",
|
||||
"s",
|
||||
"…",
|
||||
)
|
||||
);
|
||||
validate!(
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#RUN: %fish %s
|
||||
#REQUIRES: command -v tmux
|
||||
|
||||
isolated-tmux-start -C '
|
||||
set -g i 0
|
||||
function fish_prompt
|
||||
set -g i (math $i + 1)
|
||||
printf "$i.%s \n" (seq $i)
|
||||
end
|
||||
history append "\
|
||||
if true
|
||||
echo hello1
|
||||
echo hello2
|
||||
echo hello3
|
||||
echo hello4
|
||||
echo hello5
|
||||
echo hello6
|
||||
echo hello7
|
||||
echo hello8
|
||||
end"
|
||||
'
|
||||
|
||||
isolated-tmux send-keys Enter
|
||||
tmux-sleep
|
||||
isolated-tmux send-keys i
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | string replace -r ^ ^
|
||||
# CHECK: ^1.1
|
||||
# CHECK: ^2.1
|
||||
# CHECK: ^2.2 if true
|
||||
# CHECK: ^ echo hello1
|
||||
# CHECK: ^ echo hello2
|
||||
# CHECK: ^ echo hello3
|
||||
# CHECK: ^ echo hello4
|
||||
# CHECK: ^ echo hello5
|
||||
# CHECK: ^ echo hello6
|
||||
# CHECK: ^ echo hello7…
|
||||
@@ -0,0 +1,36 @@
|
||||
#RUN: %fish %s
|
||||
#REQUIRES: command -v tmux
|
||||
|
||||
isolated-tmux-start -C '
|
||||
tmux resize-window -y 10
|
||||
history append "\
|
||||
if true
|
||||
echo hello1
|
||||
echo hello2
|
||||
echo hello3
|
||||
echo hello4
|
||||
echo hello5
|
||||
end"
|
||||
'
|
||||
|
||||
isolated-tmux \
|
||||
send-keys (for i in (seq 9); echo Enter; end) \; \
|
||||
resize-window -y 5
|
||||
tmux-sleep
|
||||
isolated-tmux \
|
||||
resize-window -y 10
|
||||
tmux-sleep
|
||||
isolated-tmux \
|
||||
send-keys i
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | string replace -r ^ ^
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0> if true…
|
||||
162
tests/checks/tmux-autosuggestion-multiline.fish
Normal file
162
tests/checks/tmux-autosuggestion-multiline.fish
Normal file
@@ -0,0 +1,162 @@
|
||||
#RUN: %fish %s
|
||||
#REQUIRES: command -v tmux
|
||||
|
||||
isolated-tmux-start
|
||||
|
||||
isolated-tmux send-keys \
|
||||
'function fish_prompt; echo "prompt> "; end' Enter \
|
||||
'if true' Enter \
|
||||
"echo $(printf %050d)" Enter \
|
||||
"echo $(printf %0100d)" Enter \
|
||||
'e' 'n' 'd' Enter C-l
|
||||
|
||||
isolated-tmux send-keys 'if'
|
||||
tmux-sleep
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed /if/,/end/s/^/^/
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
# CHECK: ^ end
|
||||
|
||||
# Enter does not invalidate autosuggestion.
|
||||
isolated-tmux send-keys ' true' Enter
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed /if/,/end/s/^/^/
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
# CHECK: ^ end
|
||||
|
||||
# Autosuggestion is also computed after Enter.
|
||||
isolated-tmux send-keys C-u C-u C-u 'if true' Enter
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p \; send-keys C-u C-u C-u C-l | sed /if/,/end/s/^/^/
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
# CHECK: ^ end
|
||||
|
||||
# Test smaller windows; only the lines that fit will be shown.
|
||||
isolated-tmux send-keys 'if' \; resize-window -y 4
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Currently, we take either all or nothing from soft-wrapped suggestion-lines.
|
||||
# The ellipsis means that we'll get more lines.
|
||||
isolated-tmux resize-window -y 3
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Test that truncation also works after the resize.
|
||||
isolated-tmux send-keys C-u if
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Test that we truncate such that the prompt is never pushed up.
|
||||
isolated-tmux resize-window -y 5 \; send-keys C-u Enter if
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt>
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Again, we take all or nothing from a soft-wrapped line.
|
||||
isolated-tmux send-keys C-u Enter if
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt>
|
||||
# CHECK: ^prompt>
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Now try with a multiline prompt.
|
||||
isolated-tmux send-keys C-u 'function fish_prompt; printf "prompt-line%d/2> \n" 1 2; end' Enter C-l Enter if
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt-line1/2>
|
||||
# CHECK: ^prompt-line2/2>
|
||||
# CHECK: ^prompt-line1/2>
|
||||
# CHECK: ^prompt-line2/2> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000…
|
||||
|
||||
isolated-tmux send-keys C-u \; resize-window -y 6 \; send-keys if
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt-line1/2>
|
||||
# CHECK: ^prompt-line2/2>
|
||||
# CHECK: ^prompt-line1/2>
|
||||
# CHECK: ^prompt-line2/2> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
isolated-tmux send-keys C-u \; resize-window -y 7 \; send-keys if
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt-line1/2>
|
||||
# CHECK: ^prompt-line2/2>
|
||||
# CHECK: ^prompt-line1/2>
|
||||
# CHECK: ^prompt-line2/2> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000
|
||||
# CHECK: ^ echo 000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Autosuggestion with a line that barely wraps.
|
||||
isolated-tmux resize-window -x 80 -y 4 \; send-keys C-u \
|
||||
'function fish_prompt; printf "prompt-line1\n> "; end' Enter \
|
||||
b e g i n Enter \
|
||||
# prompt=2 command=2 indent=4
|
||||
": $(printf %072d)" Enter \
|
||||
Enter \
|
||||
Enter \
|
||||
Enter \
|
||||
e n d Enter C-l b e g i n
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt-line1
|
||||
# CHECK: ^> begin
|
||||
# CHECK: ^ : 00000000000000000000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Autosuggestions on a soft-wrapped commandline don't push the prompt.
|
||||
isolated-tmux resize-window -x 6 -y 4 \; send-keys C-u \
|
||||
'function fish_prompt; printf "> "; end' Enter \
|
||||
'echo l1 \\' Enter 'indented line continuation' Enter \
|
||||
C-l Enter 'e'
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^>
|
||||
# CHECK: ^> ech…
|
||||
# CHECK: ^
|
||||
# CHECK: ^
|
||||
|
||||
isolated-tmux resize-window -x 6 -y 4 \; send-keys C-u \
|
||||
'function fish_prompt; printf "> "; end' Enter \
|
||||
'echo wrapped \\' Enter \
|
||||
'l1 \\' Enter \
|
||||
'l2 \\' Enter \
|
||||
'l3' Enter \
|
||||
Enter Enter \
|
||||
'echo'
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^>
|
||||
# CHECK: ^>
|
||||
# CHECK: ^> echo
|
||||
# CHECK: ^ wrap…
|
||||
@@ -10,10 +10,10 @@ isolated-tmux-start -C '
|
||||
printf "> full prompt > "
|
||||
end
|
||||
end
|
||||
bind ctrl-j "set transient true; commandline -f repaint execute"
|
||||
bind ctrl-x "set transient true; commandline -f repaint execute"
|
||||
'
|
||||
|
||||
isolated-tmux send-keys 'echo foo' C-j
|
||||
isolated-tmux send-keys 'echo foo' C-x
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p
|
||||
# CHECK: > echo foo
|
||||
|
||||
@@ -179,6 +179,8 @@ class SpawnedProc(object):
|
||||
self.colorize = sys.stdout.isatty() or env.get("FISH_FORCE_COLOR", "0") == "1"
|
||||
self.messages = []
|
||||
self.start_time = None
|
||||
if "FISH_PEXPECT_TESTS_RUNNING" not in env:
|
||||
env["FISH_PEXPECT_TESTS_RUNNING"] = "1"
|
||||
self.spawn = pexpect.spawn(
|
||||
exe_path, env=env, encoding="utf-8", timeout=timeout, **kwargs
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
|
||||
env = os.environ.copy()
|
||||
env["TERM"] = "not-dumb"
|
||||
env["FISH_TEST_NO_CURSOR_POSITION_QUERY"] = ""
|
||||
|
||||
sp = SpawnedProc(env=env, scroll_content_up_supported=True)
|
||||
sendline, expect_prompt = sp.sendline, sp.expect_prompt
|
||||
@@ -16,7 +17,8 @@ sp.send_cursor_position_report(y=10, x=5)
|
||||
sp.send_primary_device_attribute()
|
||||
sp.expect_str("\x1b[9S\x1b[9A")
|
||||
|
||||
sp.send(control("l"))
|
||||
sp.send("\r")
|
||||
sp.send_cursor_position_report(y=15, x=5)
|
||||
sp.send_primary_device_attribute()
|
||||
sp.send(control("l"))
|
||||
sp.expect_str("\x1b[14S\x1b[14A")
|
||||
|
||||
Reference in New Issue
Block a user