From fadf0f2e5bb484e9ae7f8c03f6c0b25aa6994adb Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Sat, 2 Dec 2023 18:20:40 +0100 Subject: [PATCH] Port editable_line_t --- fish-rust/build.rs | 1 + fish-rust/src/editable_line.rs | 426 +++++++++++++++++++++++++++ fish-rust/src/highlight.rs | 11 + fish-rust/src/lib.rs | 1 + fish-rust/src/tests/editable_line.rs | 60 ++++ fish-rust/src/tests/mod.rs | 2 + src/editable_line.h | 18 ++ src/fish_tests.cpp | 68 +---- src/pager.cpp | 20 +- src/pager.h | 11 +- src/reader.cpp | 373 +++++++---------------- src/reader.h | 112 +------ 12 files changed, 660 insertions(+), 443 deletions(-) create mode 100644 fish-rust/src/editable_line.rs create mode 100644 fish-rust/src/tests/editable_line.rs create mode 100644 src/editable_line.h diff --git a/fish-rust/build.rs b/fish-rust/build.rs index c407dd2b5..eb816f545 100644 --- a/fish-rust/build.rs +++ b/fish-rust/build.rs @@ -69,6 +69,7 @@ fn main() { "fish-rust/src/builtins/shared.rs", "fish-rust/src/common.rs", "fish-rust/src/complete.rs", + "fish-rust/src/editable_line.rs", "fish-rust/src/env_dispatch.rs", "fish-rust/src/env/env_ffi.rs", "fish-rust/src/event.rs", diff --git a/fish-rust/src/editable_line.rs b/fish-rust/src/editable_line.rs new file mode 100644 index 000000000..f4427aeb6 --- /dev/null +++ b/fish-rust/src/editable_line.rs @@ -0,0 +1,426 @@ +use std::pin::Pin; + +use cxx::{CxxWString, UniquePtr}; + +use crate::future::IsSomeAnd; +use crate::highlight::{HighlightSpec, HighlightSpecListFFI}; +use crate::wchar::prelude::*; +use crate::wchar_ffi::{WCharFromFFI, WCharToFFI}; + +/// An edit action that can be undone. +#[derive(Clone, Eq, PartialEq)] +pub struct Edit { + /// When undoing the edit we use this to restore the previous cursor position. + pub cursor_position_before_edit: usize, + + /// The span of text that is replaced by this edit. + pub range: std::ops::Range, + + /// The strings that are removed and added by this edit, respectively. + pub old: WString, + pub replacement: WString, + + /// edit_t is only for contiguous changes, so to restore a group of arbitrary changes to the + /// command line we need to have a group id as forcibly coalescing changes is not enough. + group_id: Option, +} + +impl Edit { + pub fn new(range: std::ops::Range, replacement: WString) -> Self { + Self { + cursor_position_before_edit: 0, + range, + old: WString::new(), + replacement, + group_id: None, + } + } +} + +/// Modify a string and its syntax highlighting according to the given edit. +/// Currently exposed for testing only. +pub fn apply_edit(target: &mut WString, colors: &mut Vec, edit: &Edit) { + let range = &edit.range; + target.replace_range(range.clone(), &edit.replacement); + + // Now do the same to highlighting. + let last_color = edit + .range + .start + .checked_sub(1) + .map(|i| colors[i]) + .unwrap_or_default(); + colors.splice( + range.clone(), + std::iter::repeat(last_color).take(edit.replacement.len()), + ); +} + +/// The history of all edits to some command line. +#[derive(Clone, Default)] +pub struct UndoHistory { + /// The stack of edits that can be undone or redone atomically. + pub edits: Vec, + + /// The position in the undo stack that corresponds to the current + /// state of the input line. + /// Invariants: + /// edits_applied - 1 is the index of the next edit to undo. + /// edits_applied is the index of the next edit to redo. + /// + /// For example, if nothing was undone, edits_applied is edits.size(). + /// If every single edit was undone, edits_applied is 0. + pub edits_applied: usize, + + /// Whether we allow the next edit to be grouped together with the + /// last one. + may_coalesce: bool, + + /// Whether to be more aggressive in coalescing edits. Ideally, it would be "force coalesce" + /// with guaranteed atomicity but as `edit_t` is strictly for contiguous changes, that guarantee + /// can't be made at this time. + try_coalesce: bool, +} + +impl UndoHistory { + /// Empty the history. + pub fn clear(&mut self) { + self.edits.clear(); + self.edits_applied = 0; + self.may_coalesce = false; + } +} + +/// Helper class for storing a command line. +#[derive(Clone, Default)] +pub struct EditableLine { + /// The command line. + text: WString, + /// Syntax highlighting. + colors: Vec, + /// The current position of the cursor in the command line. + position: usize, + + /// The history of all edits. + undo_history: UndoHistory, + /// The nesting level for atomic edits, so that recursive invocations of start_edit_group() + /// are not ended by one end_edit_group() call. + edit_group_level: Option, + /// Monotonically increasing edit group, ignored when edit_group_level_ is -1. Allowed to wrap. + edit_group_id: usize, +} + +impl EditableLine { + pub fn text(&self) -> &wstr { + &self.text + } + + pub fn colors(&self) -> &[HighlightSpec] { + &self.colors + } + pub fn set_colors(&mut self, colors: Vec) { + assert_eq!(colors.len(), self.len()); + self.colors = colors; + } + + pub fn position(&self) -> usize { + self.position + } + pub fn set_position(&mut self, position: usize) { + self.position = position; + } + + // Gets the length of the text. + pub fn len(&self) -> usize { + self.text.len() + } + + pub fn is_empty(&self) -> bool { + self.text.is_empty() + } + + pub fn at(&self, idx: usize) -> char { + self.text.char_at(idx) + } + + pub fn clear(&mut self) { + self.undo_history.clear(); + if self.is_empty() { + return; + } + let len = self.len(); + apply_edit( + &mut self.text, + &mut self.colors, + &Edit::new(0..len, L!("").to_owned()), + ); + self.set_position(0); + } + + /// Modify the commandline according to @edit. Most modifications to the + /// text should pass through this function. + pub fn push_edit(&mut self, mut edit: Edit, allow_coalesce: bool) { + let range = &edit.range; + let is_insertion = range.is_empty(); + // Coalescing insertion does not create a new undo entry but adds to the last insertion. + if allow_coalesce && is_insertion && self.want_to_coalesce_insertion_of(&edit.replacement) { + assert!(range.start == self.position()); + let last_edit = self.undo_history.edits.last_mut().unwrap(); + last_edit.replacement.push_utfstr(&edit.replacement); + apply_edit(&mut self.text, &mut self.colors, &edit); + self.set_position(self.position() + edit.replacement.len()); + + assert!(self.undo_history.may_coalesce); + return; + } + + // Assign a new group id or propagate the old one if we're in a logical grouping of edits + if self.edit_group_level.is_some() { + edit.group_id = Some(self.edit_group_id); + } + + let edit_does_nothing = range.is_empty() && edit.replacement.is_empty(); + if edit_does_nothing { + return; + } + if self.undo_history.edits_applied != self.undo_history.edits.len() { + // After undoing some edits, the user is making a new edit; + // we are about to create a new edit branch. + // Discard all edits that were undone because we only support + // linear undo/redo, they will be unreachable. + self.undo_history + .edits + .truncate(self.undo_history.edits_applied); + } + edit.cursor_position_before_edit = self.position(); + edit.old = self.text[range.clone()].to_owned(); + apply_edit(&mut self.text, &mut self.colors, &edit); + self.set_position(cursor_position_after_edit(&edit)); + assert_eq!( + self.undo_history.edits_applied, + self.undo_history.edits.len() + ); + self.undo_history.may_coalesce = + is_insertion && (self.undo_history.try_coalesce || edit.replacement.len() == 1); + self.undo_history.edits_applied += 1; + self.undo_history.edits.push(edit); + } + + /// Undo the most recent edit that was not yet undone. Returns true on success. + pub fn undo(&mut self) -> bool { + let mut did_undo = false; + let mut last_group_id = None; + while self.undo_history.edits_applied != 0 { + let edit = &self.undo_history.edits[self.undo_history.edits_applied - 1]; + if did_undo + && edit + .group_id + .is_none_or(|group_id| Some(group_id) != last_group_id) + { + // We've restored all the edits in this logical undo group + break; + } + last_group_id = edit.group_id; + self.undo_history.edits_applied -= 1; + let range = &edit.range; + let mut inverse = Edit::new( + range.start..range.start + edit.replacement.len(), + L!("").to_owned(), + ); + inverse.replacement = edit.old.clone(); + let old_position = edit.cursor_position_before_edit; + apply_edit(&mut self.text, &mut self.colors, &inverse); + self.set_position(old_position); + did_undo = true; + } + + self.end_edit_group(); + self.undo_history.may_coalesce = false; + did_undo + } + + /// Redo the most recent undo. Returns true on success. + pub fn redo(&mut self) -> bool { + let mut did_redo = false; + + let mut last_group_id = None; + while let Some(edit) = self.undo_history.edits.get(self.undo_history.edits_applied) { + if did_redo + && edit + .group_id + .is_none_or(|group_id| Some(group_id) != last_group_id) + { + // We've restored all the edits in this logical undo group + break; + } + last_group_id = edit.group_id; + self.undo_history.edits_applied += 1; + apply_edit(&mut self.text, &mut self.colors, edit); + self.set_position(cursor_position_after_edit(edit)); + did_redo = true; + } + + self.end_edit_group(); + did_redo + } + + /// Start a logical grouping of command line edits that should be undone/redone together. + pub fn begin_edit_group(&mut self) { + if self.edit_group_level.is_some() { + return; + } + self.edit_group_level = Some(55 + 1); + // Indicate that the next change must trigger the creation of a new history item + self.undo_history.may_coalesce = false; + // Indicate that future changes should be coalesced into the same edit if possible. + self.undo_history.try_coalesce = true; + // Assign a logical edit group id to future edits in this group + self.edit_group_id += 1; + } + + /// End a logical grouping of command line edits that should be undone/redone together. + pub fn end_edit_group(&mut self) { + let Some(edit_group_level) = self.edit_group_level.as_mut() else { + // Clamp the minimum value to -1 to prevent unbalanced end_edit_group() calls from breaking + // everything. + return; + }; + + *edit_group_level -= 1; + + if *edit_group_level == 55 { + self.undo_history.try_coalesce = false; + self.undo_history.may_coalesce = false; + } + } + + /// Whether we want to append this string to the previous edit. + fn want_to_coalesce_insertion_of(&self, s: &wstr) -> bool { + // The previous edit must support coalescing. + if !self.undo_history.may_coalesce { + return false; + } + // Only consolidate single character inserts. + if s.len() != 1 { + return false; + } + // Make an undo group after every space. + if s.as_char_slice()[0] == ' ' && !self.undo_history.try_coalesce { + return false; + } + let last_edit = self.undo_history.edits.last().unwrap(); + // Don't add to the last edit if it deleted something. + if !last_edit.range.is_empty() { + return false; + } + // Must not have moved the cursor! + if cursor_position_after_edit(last_edit) != self.position() { + return false; + } + true + } +} + +/// Returns the number of characters left of the cursor that are removed by the +/// deletion in the given edit. +fn chars_deleted_left_of_cursor(edit: &Edit) -> usize { + if edit.cursor_position_before_edit > edit.range.start { + return std::cmp::min( + edit.range.len(), + edit.cursor_position_before_edit - edit.range.start, + ); + } + 0 +} + +/// Compute the position of the cursor after the given edit. +fn cursor_position_after_edit(edit: &Edit) -> usize { + let cursor = edit.cursor_position_before_edit + edit.replacement.len(); + let removed = chars_deleted_left_of_cursor(edit); + cursor.saturating_sub(removed) +} + +#[cxx::bridge] +mod editable_line_ffi { + extern "C++" { + include!("editable_line.h"); + include!("highlight.h"); + pub type HighlightSpec = crate::highlight::HighlightSpec; + pub type HighlightSpecListFFI = crate::highlight::HighlightSpecListFFI; + } + extern "Rust" { + type Edit; + fn new_edit(start: usize, end: usize, replacement: &CxxWString) -> Box; + #[cxx_name = "apply_edit"] + fn apply_edit_ffi( + target: &CxxWString, + mut colors: Pin<&mut HighlightSpecListFFI>, + edit: Box, + ) -> UniquePtr; + } + extern "Rust" { + type UndoHistory; + } + extern "Rust" { + type EditableLine; + fn new_editable_line() -> Box; + fn empty(&self) -> bool; + #[cxx_name = "text"] + fn text_ffi(&self) -> UniquePtr; + #[cxx_name = "clone"] + fn clone_ffi(&self) -> Box; + fn position(&self) -> usize; + fn set_position(&mut self, position: usize); + fn clear(&mut self); + fn undo(&mut self) -> bool; + fn redo(&mut self) -> bool; + fn size(&self) -> usize; + #[cxx_name = "push_edit"] + fn push_edit_ffi(&mut self, edit: Box, allow_coalesce: bool); + fn begin_edit_group(&mut self); + fn end_edit_group(&mut self); + #[cxx_name = "at"] + fn at_ffi(&self, index: usize) -> u32; + #[cxx_name = "set_colors"] + fn set_colors_ffi(&mut self, colors: &HighlightSpecListFFI); + } +} +fn new_edit(start: usize, end: usize, replacement: &CxxWString) -> Box { + Box::new(Edit::new(start..end, replacement.from_ffi())) +} +fn new_editable_line() -> Box { + Box::default() +} +impl EditableLine { + fn empty(&self) -> bool { + self.is_empty() + } + fn text_ffi(&self) -> UniquePtr { + self.text().to_ffi() + } + fn clone_ffi(&self) -> Box { + Box::new(self.clone()) + } + fn size(&self) -> usize { + self.len() + } + #[allow(clippy::boxed_local)] + fn push_edit_ffi(&mut self, edit: Box, allow_coalesce: bool) { + self.push_edit(*edit, allow_coalesce); + } + fn at_ffi(&self, index: usize) -> u32 { + self.at(index) as _ + } + fn set_colors_ffi(&mut self, colors: &HighlightSpecListFFI) { + self.set_colors(colors.0.clone()) + } +} +fn apply_edit_ffi( + target: &CxxWString, + mut colors: Pin<&mut HighlightSpecListFFI>, + edit: Box, +) -> UniquePtr { + let mut target = target.from_ffi(); + apply_edit(&mut target, &mut colors.0, &edit); + target.to_ffi() +} diff --git a/fish-rust/src/highlight.rs b/fish-rust/src/highlight.rs index e40010166..8b6bb82bf 100644 --- a/fish-rust/src/highlight.rs +++ b/fish-rust/src/highlight.rs @@ -11,6 +11,7 @@ EXPAND_RESERVED_BASE, EXPAND_RESERVED_END, }; use crate::compat::_PC_CASE_SENSITIVE; +use crate::editable_line::EditableLine; use crate::env::{EnvStackRefFFI, Environment}; use crate::expand::{ expand_one, expand_tilde, expand_to_command_and_args, ExpandFlags, ExpandResultCode, @@ -1681,6 +1682,7 @@ pub struct HighlightSpec { #[cxx_name = "clone"] fn clone_ffi(self: &HighlightSpec) -> Box; fn new_highlight_spec() -> Box; + fn editable_line_colors(editable_line: &EditableLine) -> &[HighlightSpec]; } extern "C++" { @@ -1688,6 +1690,7 @@ pub struct HighlightSpec { include!("history.h"); include!("color.h"); include!("operation_context.h"); + include!("editable_line.h"); type HistoryItem = crate::history::HistoryItem; type OperationContext<'a> = crate::operation_context::OperationContext<'a>; type rgb_color_t = crate::ffi::rgb_color_t; @@ -1695,6 +1698,7 @@ pub struct HighlightSpec { type EnvDynFFI = crate::env::EnvDynFFI; #[cxx_name = "EnvStackRef"] type EnvStackRefFFI = crate::env::EnvStackRefFFI; + type EditableLine = crate::editable_line::EditableLine; } extern "Rust" { #[cxx_name = "autosuggest_validate_from_history"] @@ -1838,3 +1842,10 @@ fn clone_ffi(&self) -> Box { fn new_highlight_spec() -> Box { Box::default() } +unsafe impl cxx::ExternType for EditableLine { + type Id = cxx::type_id!("EditableLine"); + type Kind = cxx::kind::Opaque; +} +fn editable_line_colors(editable_line: &EditableLine) -> &[HighlightSpec] { + editable_line.colors() +} diff --git a/fish-rust/src/lib.rs b/fish-rust/src/lib.rs index 950d38419..7c4cba543 100644 --- a/fish-rust/src/lib.rs +++ b/fish-rust/src/lib.rs @@ -35,6 +35,7 @@ mod compat; mod complete; mod curses; +mod editable_line; mod env; mod env_dispatch; mod env_universal_common; diff --git a/fish-rust/src/tests/editable_line.rs b/fish-rust/src/tests/editable_line.rs new file mode 100644 index 000000000..7b8c66a18 --- /dev/null +++ b/fish-rust/src/tests/editable_line.rs @@ -0,0 +1,60 @@ +use crate::{ + editable_line::{Edit, EditableLine}, + wchar::prelude::*, +}; + +#[test] +fn test_undo() { + let mut line = EditableLine::default(); + + let insert = |line: &EditableLine| line.position()..line.position(); + + assert!(!line.undo()); // nothing to undo + assert!(line.text().is_empty()); + assert_eq!(line.position(), 0); + line.push_edit(Edit::new(0..0, L!("a b c").to_owned()), true); + assert_eq!(line.text(), L!("a b c").to_owned()); + assert_eq!(line.position(), 5); + line.set_position(2); + line.push_edit(Edit::new(2..3, L!("B").to_owned()), true); // replacement right of cursor + assert_eq!(line.text(), L!("a B c").to_owned()); + line.undo(); + assert_eq!(line.text(), L!("a b c").to_owned()); + assert_eq!(line.position(), 2); + line.redo(); + assert_eq!(line.text(), L!("a B c").to_owned()); + assert_eq!(line.position(), 3); + + assert!(!line.redo()); // nothing to redo + + line.push_edit(Edit::new(0..2, L!("").to_owned()), true); // deletion left of cursor + assert_eq!(line.text(), L!("B c").to_owned()); + assert_eq!(line.position(), 1); + line.undo(); + assert_eq!(line.text(), L!("a B c").to_owned()); + assert_eq!(line.position(), 3); + line.redo(); + assert_eq!(line.text(), L!("B c").to_owned()); + assert_eq!(line.position(), 1); + + line.push_edit(Edit::new(0..line.len(), L!("a b c").to_owned()), true); // replacement left and right of cursor + assert_eq!(line.text(), L!("a b c").to_owned()); + assert_eq!(line.position(), 5); + + // Undo coalesced edits + line.clear(); + line.push_edit(Edit::new(insert(&line), L!("a").to_owned()), true); + line.push_edit(Edit::new(insert(&line), L!("b").to_owned()), true); + line.push_edit(Edit::new(insert(&line), L!("c").to_owned()), true); + line.push_edit(Edit::new(insert(&line), L!(" ").to_owned()), true); + line.undo(); + line.undo(); + line.redo(); + assert_eq!(line.text(), L!("abc").to_owned()); + // This removes the space insertion from the history, but does not coalesce with the first edit. + line.push_edit(Edit::new(insert(&line), L!("d").to_owned()), true); + line.push_edit(Edit::new(insert(&line), L!("e").to_owned()), true); + assert_eq!(line.text(), L!("abcde").to_owned()); + line.undo(); + assert_eq!(line.text(), L!("abc").to_owned()); +} diff --git a/fish-rust/src/tests/mod.rs b/fish-rust/src/tests/mod.rs index 2cf179658..2b871d78a 100644 --- a/fish-rust/src/tests/mod.rs +++ b/fish-rust/src/tests/mod.rs @@ -3,6 +3,8 @@ #[cfg(test)] mod common; mod complete; +#[cfg(test)] +mod editable_line; mod env; mod env_universal_common; mod expand; diff --git a/src/editable_line.h b/src/editable_line.h new file mode 100644 index 000000000..c0852a295 --- /dev/null +++ b/src/editable_line.h @@ -0,0 +1,18 @@ +#ifndef FISH_EDITABLE_LINE_H +#define FISH_EDITABLE_LINE_H + +struct HighlightSpecListFFI; + +#if INCLUDE_RUST_HEADERS +#include "editable_line.rs.h" +#else +struct Edit; +struct UndoHistory; +struct EditableLine; +#endif + +using edit_t = Edit; +using undo_history_t = UndoHistory; +using editable_line_t = EditableLine; + +#endif diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index d412e9b91..e2fe11b7a 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -1110,7 +1110,17 @@ static void test_abbreviations() { cmdline, cursor_pos.value_or(cmdline.size()), parser_principal_parser()->deref())) { wcstring cmdline_expanded = cmdline; std::vector colors{cmdline_expanded.size()}; - apply_edit(&cmdline_expanded, &colors, edit_t{replacement->range, *replacement->text}); + auto ffi_colors = new_highlight_spec_list(); + for (auto &c : colors) { + ffi_colors->push(c); + } + cmdline_expanded = *apply_edit( + cmdline_expanded, *ffi_colors, + new_edit(replacement->range.start, replacement->range.end(), *replacement->text)); + colors.clear(); + for (size_t i = 0; i < ffi_colors->size(); i++) { + colors.push_back(ffi_colors->at(i)); + } return cmdline_expanded; } return none_t(); @@ -1604,61 +1614,6 @@ static void test_input() { } } -// todo!("port this") -static void test_undo() { - say(L"Testing undo/redo setting and restoring text and cursor position."); - - editable_line_t line; - do_test(!line.undo()); // nothing to undo - do_test(line.text().empty()); - do_test(line.position() == 0); - line.push_edit(edit_t(0, 0, L"a b c"), true); - do_test(line.text() == L"a b c"); - do_test(line.position() == 5); - line.set_position(2); - line.push_edit(edit_t(2, 1, L"B"), true); // replacement right of cursor - do_test(line.text() == L"a B c"); - line.undo(); - do_test(line.text() == L"a b c"); - do_test(line.position() == 2); - line.redo(); - do_test(line.text() == L"a B c"); - do_test(line.position() == 3); - - do_test(!line.redo()); // nothing to redo - - line.push_edit(edit_t(0, 2, L""), true); // deletion left of cursor - do_test(line.text() == L"B c"); - do_test(line.position() == 1); - line.undo(); - do_test(line.text() == L"a B c"); - do_test(line.position() == 3); - line.redo(); - do_test(line.text() == L"B c"); - do_test(line.position() == 1); - - line.push_edit(edit_t(0, line.size(), L"a b c"), true); // replacement left and right of cursor - do_test(line.text() == L"a b c"); - do_test(line.position() == 5); - - say(L"Testing undoing coalesced edits."); - line.clear(); - line.push_edit(edit_t(line.position(), 0, L"a"), true); - line.push_edit(edit_t(line.position(), 0, L"b"), true); - line.push_edit(edit_t(line.position(), 0, L"c"), true); - line.push_edit(edit_t(line.position(), 0, L" "), true); - line.undo(); - line.undo(); - line.redo(); - do_test(line.text() == L"abc"); - // This removes the space insertion from the history, but does not coalesce with the first edit. - line.push_edit(edit_t(line.position(), 0, L"d"), true); - line.push_edit(edit_t(line.position(), 0, L"e"), true); - do_test(line.text() == L"abcde"); - line.undo(); - do_test(line.text() == L"abc"); -} - // todo!("port this") static void test_new_parser_correctness() { say(L"Testing parser correctness"); @@ -2515,7 +2470,6 @@ static const test_t s_tests[]{ {TEST_GROUP("word_motion"), test_word_motion}, {TEST_GROUP("colors"), test_colors}, {TEST_GROUP("input"), test_input}, - {TEST_GROUP("undo"), test_undo}, {TEST_GROUP("completion_insertions"), test_completion_insertions}, {TEST_GROUP("illegal_command_exit_code"), test_illegal_command_exit_code}, {TEST_GROUP("maybe"), test_maybe}, diff --git a/src/pager.cpp b/src/pager.cpp index 23b068480..bf10f1076 100644 --- a/src/pager.cpp +++ b/src/pager.cpp @@ -16,6 +16,7 @@ #include "common.h" #include "complete.h" +#include "editable_line.rs.h" #include "fallback.h" #include "highlight.h" #include "maybe.h" @@ -400,7 +401,7 @@ bool pager_t::completion_info_passes_filter(const comp_t &info) const { // If we have no filter, everything passes. if (!search_field_shown || this->search_field_line.empty()) return true; - const wcstring &needle = this->search_field_line.text(); + const wcstring needle = *this->search_field_line.text(); // Match against the description. if (string_fuzzy_match_string(needle, info.desc)) { @@ -591,7 +592,7 @@ bool pager_t::completion_try_print(size_t cols, const wcstring &prefix, const co } // Add the search field. - wcstring search_field_text = search_field_line.text(); + wcstring search_field_text = *search_field_line.text(); // Append spaces to make it at least the required width. if (search_field_text.size() < PAGER_SEARCH_FIELD_WIDTH) { search_field_text.append(PAGER_SEARCH_FIELD_WIDTH - search_field_text.size(), L' '); @@ -618,7 +619,7 @@ page_rendering_t pager_t::render() const { rendering.term_width = this->available_term_width; rendering.term_height = this->available_term_height; rendering.search_field_shown = this->search_field_shown; - rendering.search_field_line = this->search_field_line; + rendering.search_field_line = this->search_field_line.clone(); for (size_t cols = PAGER_MAX_COLS; cols > 0; cols--) { // Initially empty rendering. @@ -660,10 +661,10 @@ bool pager_t::rendering_needs_update(const page_rendering_t &rendering) const { rendering.term_width != this->available_term_width || // rendering.term_height != this->available_term_height || // rendering.selected_completion_idx != - this->visual_selected_completion_index(rendering.rows, rendering.cols) || // - rendering.search_field_shown != this->search_field_shown || // - rendering.search_field_line.text() != this->search_field_line.text() || // - rendering.search_field_line.position() != this->search_field_line.position() || // + this->visual_selected_completion_index(rendering.rows, rendering.cols) || // + rendering.search_field_shown != this->search_field_shown || // + *rendering.search_field_line->text() != *this->search_field_line.text() || // + rendering.search_field_line->position() != this->search_field_line.position() || // (rendering.remaining_to_disclose > 0 && this->fully_disclosed); } @@ -674,7 +675,7 @@ void pager_t::update_rendering(page_rendering_t *rendering) { } } -pager_t::pager_t() = default; +pager_t::pager_t() : search_field_line_box(new_editable_line()) {} pager_t::~pager_t() = default; bool pager_t::empty() const { return unfiltered_completion_infos.empty(); } @@ -965,4 +966,5 @@ size_t pager_t::cursor_position() const { return result; } -page_rendering_t::page_rendering_t() : screen_data(new_screen_data()) {} +page_rendering_t::page_rendering_t() + : screen_data(new_screen_data()), search_field_line(new_editable_line()) {} diff --git a/src/pager.h b/src/pager.h index 4125f4d52..9087cec7e 100644 --- a/src/pager.h +++ b/src/pager.h @@ -34,7 +34,9 @@ class page_rendering_t { size_t remaining_to_disclose{0}; bool search_field_shown{false}; - editable_line_t search_field_line{}; +#if INCLUDE_RUST_HEADERS + rust::Box search_field_line; +#endif // Returns a rendering with invalid data, useful to indicate "no rendering". page_rendering_t(); @@ -156,8 +158,11 @@ class pager_t { bool selected, page_rendering_t *rendering) const; public: - // The text of the search field. - editable_line_t search_field_line; +// The text of the search field. +#if INCLUDE_RUST_HEADERS + rust::Box search_field_line_box; + editable_line_t &search_field_line = *search_field_line_box; +#endif // Extra text to display at the bottom of the pager. wcstring extra_progress_text{}; diff --git a/src/reader.cpp b/src/reader.cpp index 508dae30e..fdd07cb63 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -210,183 +210,6 @@ static debounce_t &debounce_history_pager() { return *res; } -bool edit_t::operator==(const edit_t &other) const { - return cursor_position_before_edit == other.cursor_position_before_edit && - offset == other.offset && length == other.length && old == other.old && - replacement == other.replacement; -} - -void undo_history_t::clear() { - edits.clear(); - edits_applied = 0; - may_coalesce = false; -} - -void apply_edit(wcstring *target, std::vector *colors, const edit_t &edit) { - size_t offset = edit.offset; - target->replace(offset, edit.length, edit.replacement); - - // Now do the same to highlighting. - auto it = colors->begin() + offset; - colors->erase(it, it + edit.length); - highlight_spec_t last_color = offset == 0 ? highlight_spec_t{} : colors->at(offset - 1); - colors->insert(it, edit.replacement.size(), last_color); -} - -/// Returns the number of characters left of the cursor that are removed by the -/// deletion in the given edit. -static size_t chars_deleted_left_of_cursor(const edit_t &edit) { - if (edit.cursor_position_before_edit > edit.offset) { - return std::min(edit.length, edit.cursor_position_before_edit - edit.offset); - } - return 0; -} - -/// Compute the position of the cursor after the given edit. -static size_t cursor_position_after_edit(const edit_t &edit) { - size_t cursor = edit.cursor_position_before_edit + edit.replacement.size(); - size_t removed = chars_deleted_left_of_cursor(edit); - return cursor > removed ? cursor - removed : 0; -} - -void editable_line_t::set_colors(std::vector colors) { - assert(colors.size() == size()); - colors_ = std::move(colors); -} - -bool editable_line_t::undo() { - bool did_undo = false; - maybe_t last_group_id{-1}; - while (undo_history_.edits_applied != 0) { - const edit_t &edit = undo_history_.edits.at(undo_history_.edits_applied - 1); - if (did_undo && (!edit.group_id.has_value() || edit.group_id != last_group_id)) { - // We've restored all the edits in this logical undo group - break; - } - last_group_id = edit.group_id; - undo_history_.edits_applied--; - edit_t inverse = edit_t(edit.offset, edit.replacement.size(), L""); - inverse.replacement = edit.old; - size_t old_position = edit.cursor_position_before_edit; - apply_edit(&text_, &colors_, inverse); - set_position(old_position); - did_undo = true; - } - - end_edit_group(); - undo_history_.may_coalesce = false; - return did_undo; -} - -void editable_line_t::clear() { - undo_history_.clear(); - if (empty()) return; - apply_edit(&text_, &colors_, edit_t(0, text_.length(), L"")); - set_position(0); -} - -void editable_line_t::push_edit(edit_t edit, bool allow_coalesce) { - bool is_insertion = edit.length == 0; - /// Coalescing insertion does not create a new undo entry but adds to the last insertion. - if (allow_coalesce && is_insertion && want_to_coalesce_insertion_of(edit.replacement)) { - assert(edit.offset == position()); - edit_t &last_edit = undo_history_.edits.back(); - last_edit.replacement.append(edit.replacement); - apply_edit(&text_, &colors_, edit); - set_position(position() + edit.replacement.size()); - - assert(undo_history_.may_coalesce); - return; - } - - // Assign a new group id or propagate the old one if we're in a logical grouping of edits - if (edit_group_level_ != -1) { - edit.group_id = edit_group_id_; - } - - bool edit_does_nothing = edit.length == 0 && edit.replacement.empty(); - if (edit_does_nothing) return; - if (undo_history_.edits_applied != undo_history_.edits.size()) { - // After undoing some edits, the user is making a new edit; - // we are about to create a new edit branch. - // Discard all edits that were undone because we only support - // linear undo/redo, they will be unreachable. - undo_history_.edits.erase(undo_history_.edits.begin() + undo_history_.edits_applied, - undo_history_.edits.end()); - } - edit.cursor_position_before_edit = position(); - edit.old = text_.substr(edit.offset, edit.length); - apply_edit(&text_, &colors_, edit); - set_position(cursor_position_after_edit(edit)); - assert(undo_history_.edits_applied == undo_history_.edits.size()); - undo_history_.may_coalesce = - is_insertion && (undo_history_.try_coalesce || edit.replacement.size() == 1); - undo_history_.edits_applied++; - undo_history_.edits.emplace_back(std::move(edit)); -} - -bool editable_line_t::redo() { - bool did_redo = false; - - maybe_t last_group_id{-1}; - while (undo_history_.edits_applied < undo_history_.edits.size()) { - const edit_t &edit = undo_history_.edits.at(undo_history_.edits_applied); - if (did_redo && (!edit.group_id.has_value() || edit.group_id != last_group_id)) { - // We've restored all the edits in this logical undo group - break; - } - last_group_id = edit.group_id; - undo_history_.edits_applied++; - apply_edit(&text_, &colors_, edit); - set_position(cursor_position_after_edit(edit)); - did_redo = true; - } - - end_edit_group(); - return did_redo; -} - -void editable_line_t::begin_edit_group() { - if (++edit_group_level_ == 0) { - // Indicate that the next change must trigger the creation of a new history item - undo_history_.may_coalesce = false; - // Indicate that future changes should be coalesced into the same edit if possible. - undo_history_.try_coalesce = true; - // Assign a logical edit group id to future edits in this group - edit_group_id_ += 1; - } -} - -void editable_line_t::end_edit_group() { - if (edit_group_level_ == -1) { - // Clamp the minimum value to -1 to prevent unbalanced end_edit_group() calls from breaking - // everything. - return; - } - - if (--edit_group_level_ == -1) { - undo_history_.try_coalesce = false; - undo_history_.may_coalesce = false; - } -} - -/// Whether we want to append this string to the previous edit. -bool editable_line_t::want_to_coalesce_insertion_of(const wcstring &str) const { - // The previous edit must support coalescing. - if (!undo_history_.may_coalesce) return false; - // Only consolidate single character inserts. - if (str.size() != 1) return false; - // Make an undo group after every space. - if (str.at(0) == L' ' && !undo_history_.try_coalesce) return false; - assert(!undo_history_.edits.empty()); - const edit_t &last_edit = undo_history_.edits.back(); - // Don't add to the last edit if it deleted something. - if (last_edit.length != 0) return false; - // Must not have moved the cursor! - if (cursor_position_after_edit(last_edit) != position()) return false; - return true; -} - // Make the search case-insensitive unless we have an uppercase character. static history_search_flags_t smartcase_flags(const wcstring &query) { return query == wcstolower(query) ? history_search_ignore_case : 0; @@ -714,7 +537,8 @@ class reader_data_t : public std::enable_shared_from_this { /// The parser being used. rust::Box parser_ref; /// String containing the whole current commandline. - editable_line_t command_line; + rust::Box command_line_box; + editable_line_t &command_line; /// Whether the most recent modification to the command line was done by either history search /// or a pager selection change. When this is true and another transient change is made, the /// old transient change will be removed from the undo history. @@ -859,6 +683,8 @@ class reader_data_t : public std::enable_shared_from_this { reader_data_t(rust::Box parser, HistorySharedPtr &hist, reader_config_t &&conf) : conf(std::move(conf)), parser_ref(std::move(parser)), + command_line_box(new_editable_line()), + command_line(*command_line_box), screen(new_screen()), inputter(parser_ref->deref(), conf.in), history(hist.clone()) {} @@ -872,7 +698,7 @@ class reader_data_t : public std::enable_shared_from_this { void erase_substring(editable_line_t *el, size_t offset, size_t length); /// Replace the text of length @length at @offset by @replacement. void replace_substring(editable_line_t *el, size_t offset, size_t length, wcstring replacement); - void push_edit(editable_line_t *el, edit_t edit); + void push_edit(editable_line_t *el, rust::Box &&edit); /// Insert the character into the command line buffer and print it to the screen using syntax /// highlighting, etc. @@ -1162,7 +988,7 @@ bool reader_data_t::is_repaint_needed(const std::vector *mcolo bool focused_on_pager = active_edit_line() == &pager.search_field_line; const layout_data_t &last = this->rendered_layout; return check(force_exec_prompt_and_repaint, L"forced") || - check(command_line.text() != last.text, L"text") || + check(*command_line.text() != last.text, L"text") || check(mcolors && *mcolors != last.colors, L"highlight") || check(selection != last.selection, L"selection") || check(focused_on_pager != last.focused_on_pager, L"focus") || @@ -1179,8 +1005,10 @@ bool reader_data_t::is_repaint_needed(const std::vector *mcolo layout_data_t reader_data_t::make_layout_data() const { layout_data_t result{}; bool focused_on_pager = active_edit_line() == &pager.search_field_line; - result.text = command_line.text(); - result.colors = command_line.colors(); + result.text = *command_line.text(); + for (auto &color : editable_line_colors(command_line)) { + result.colors.push_back(color); + } assert(result.text.size() == result.colors.size()); result.position = focused_on_pager ? pager.cursor_position() : command_line.position(); result.selection = selection; @@ -1200,10 +1028,10 @@ void reader_data_t::paint_layout(const wchar_t *reason) { wcstring full_line; if (conf.in_silent_mode) { - full_line = wcstring(cmd_line->text().length(), get_obfuscation_read_char()); + full_line = wcstring(cmd_line->text()->length(), get_obfuscation_read_char()); } else { // Combine the command and autosuggestion into one string. - full_line = combine_command_and_autosuggestion(cmd_line->text(), autosuggestion.text); + full_line = combine_command_and_autosuggestion(*cmd_line->text(), autosuggestion.text); } // Copy the colors and extend them with autosuggestion color. @@ -1237,7 +1065,7 @@ void reader_data_t::paint_layout(const wchar_t *reason) { // Compute the indentation, then extend it with 0s for the autosuggestion. The autosuggestion // always conceptually has an indent of 0. - std::vector indents = parse_util_compute_indents(cmd_line->text()); + std::vector indents = parse_util_compute_indents(*cmd_line->text()); indents.resize(full_line.size(), 0); auto ffi_colors = new_highlight_spec_list(); @@ -1250,7 +1078,8 @@ void reader_data_t::paint_layout(const wchar_t *reason) { /// Internal helper function for handling killing parts of text. void reader_data_t::kill(editable_line_t *el, size_t begin_idx, size_t length, int mode, int newv) { - const wchar_t *begin = el->text().c_str() + begin_idx; + wcstring text = *el->text(); + const wchar_t *begin = text.c_str() + begin_idx; if (newv) { kill_item = wcstring(begin, length); kill_add(kill_item); @@ -1356,14 +1185,14 @@ void reader_data_t::fill_history_pager(history_pager_invocation_t why, old_pager_index = pager.selected_completion_index(); break; } - const wcstring &search_term = pager.search_field_line.text(); + const wcstring search_term = *pager.search_field_line.text(); auto shared_this = this->shared_from_this(); std::function func = [=]() { return history_pager_search(**shared_this->history, direction, index, search_term); }; std::function completion = [=](const history_pager_result_t &result) { - if (search_term != shared_this->pager.search_field_line.text()) + if (search_term != *shared_this->pager.search_field_line.text()) return; // Stale request. if (result.matched_commands->empty() && why == history_pager_invocation_t::Advance) { // No more matches, keep the existing ones and flash. @@ -1412,7 +1241,7 @@ void reader_data_t::pager_selection_changed() { } // Only update if something changed, to avoid useless edits in the undo history. - if (new_cmd_line != command_line.text()) { + if (new_cmd_line != *command_line.text()) { set_buffer_maintaining_pager(new_cmd_line, cursor_pos, true /* transient */); } } @@ -1548,8 +1377,9 @@ bool reader_data_t::expand_abbreviation_at_cursor(size_t cursor_backtrack) { this->update_commandline_state(); size_t cursor_pos = el->position() - std::min(el->position(), cursor_backtrack); if (auto replacement = - reader_expand_abbreviation_at_cursor(el->text(), cursor_pos, this->parser())) { - push_edit(el, edit_t{replacement->range, *replacement->text}); + reader_expand_abbreviation_at_cursor(*el->text(), cursor_pos, this->parser())) { + push_edit(el, new_edit(replacement->range.start, replacement->range.end(), + *replacement->text)); if (replacement->has_cursor) { update_buff_pos(el, replacement->cursor); } else { @@ -1832,7 +1662,7 @@ void reader_data_t::delete_char(bool backward) { int width; do { pos--; - width = fish_wcwidth(el->text().at(pos)); + width = fish_wcwidth(el->text()->at(pos)); } while (width == 0 && pos > 0); erase_substring(el, pos, pos_end - pos); update_buff_pos(el); @@ -1844,7 +1674,7 @@ void reader_data_t::delete_char(bool backward) { /// Returns true if the string changed. void reader_data_t::insert_string(editable_line_t *el, const wcstring &str) { if (!str.empty()) { - el->push_edit(edit_t(el->position(), 0, str), + el->push_edit(new_edit(el->position(), el->position(), str), !history_search.active() /* allow_coalesce */); } @@ -1855,18 +1685,18 @@ void reader_data_t::insert_string(editable_line_t *el, const wcstring &str) { maybe_refilter_pager(el); } -void reader_data_t::push_edit(editable_line_t *el, edit_t edit) { +void reader_data_t::push_edit(editable_line_t *el, rust::Box &&edit) { el->push_edit(std::move(edit), false /* allow_coalesce */); maybe_refilter_pager(el); } void reader_data_t::erase_substring(editable_line_t *el, size_t offset, size_t length) { - push_edit(el, edit_t(offset, length, L"")); + push_edit(el, new_edit(offset, offset + length, L"")); } void reader_data_t::replace_substring(editable_line_t *el, size_t offset, size_t length, wcstring replacement) { - push_edit(el, edit_t(offset, length, std::move(replacement))); + push_edit(el, new_edit(offset, offset + length, std::move(replacement))); } /// Insert the string in the given command line at the given cursor position. The function checks if @@ -2003,7 +1833,7 @@ void reader_data_t::completion_insert(const wcstring &val, size_t token_end, if (el->position() != token_end) update_buff_pos(el, token_end); size_t cursor = el->position(); - wcstring new_command_line = completion_apply_to_command_line(val, flags, el->text(), &cursor, + wcstring new_command_line = completion_apply_to_command_line(val, flags, *el->text(), &cursor, false /* not append only */); set_buffer_maintaining_pager(new_command_line, cursor); } @@ -2094,7 +1924,7 @@ bool reader_data_t::can_autosuggest() const { const editable_line_t *el = active_edit_line(); const wchar_t *whitespace = L" \t\r\n\v"; return conf.autosuggest_ok && !suppress_autosuggestion && history_search.is_at_end() && - el == &command_line && el->text().find_first_not_of(whitespace) != wcstring::npos; + el == &command_line && el->text()->find_first_not_of(whitespace) != wcstring::npos; } // Called after an autosuggestion has been computed on a background thread. @@ -2103,7 +1933,7 @@ void reader_data_t::autosuggest_completed(autosuggestion_t result) { if (result.search_string == in_flight_autosuggest_request) { in_flight_autosuggest_request.clear(); } - if (result.search_string != command_line.text()) { + if (result.search_string != *command_line.text()) { // This autosuggestion is stale. return; } @@ -2144,22 +1974,22 @@ void reader_data_t::update_autosuggestion() { // This is also the main mechanism by which readline commands that don't change the command line // text avoid recomputing the autosuggestion. const editable_line_t &el = command_line; - if (autosuggestion.text.size() > el.text().size() && + if (autosuggestion.text.size() > el.text()->size() && (autosuggestion.icase - ? string_prefixes_string_case_insensitive(el.text(), autosuggestion.text) - : string_prefixes_string(el.text(), autosuggestion.text))) { + ? string_prefixes_string_case_insensitive(*el.text(), autosuggestion.text) + : string_prefixes_string(*el.text(), autosuggestion.text))) { return; } // Do nothing if we've already kicked off this autosuggest request. - if (el.text() == in_flight_autosuggest_request) return; - in_flight_autosuggest_request = el.text(); + if (*el.text() == in_flight_autosuggest_request) return; + in_flight_autosuggest_request = *el.text(); // Clear the autosuggestion and kick it off in the background. FLOG(reader_render, L"Autosuggesting"); autosuggestion.clear(); std::function performer = - get_autosuggestion_performer(parser(), el.text(), el.position(), **history); + get_autosuggestion_performer(parser(), *el.text(), el.position(), **history); auto shared_this = this->shared_from_this(); std::function completion = [shared_this](autosuggestion_t result) { shared_this->autosuggest_completed(std::move(result)); @@ -2311,7 +2141,7 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok bool success = false; const editable_line_t *el = &command_line; - const wcstring tok(el->text(), token_begin, token_end - token_begin); + const wcstring tok(*el->text(), token_begin, token_end - token_begin); // Check trivial cases. size_t size = comp.size(); @@ -2431,7 +2261,7 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok // a prefix; don't insert a space after it. if (prefix_is_partial_completion) flags |= COMPLETE_NO_SPACE; completion_insert(common_prefix, token_end, flags); - cycle_command_line = command_line.text(); + cycle_command_line = *command_line.text(); cycle_cursor_pos = command_line.position(); } } @@ -2665,7 +2495,7 @@ static void reader_interactive_destroy() { /// Set the specified string as the current buffer. void reader_data_t::set_command_line_and_position(editable_line_t *el, wcstring &&new_str, size_t pos) { - push_edit(el, edit_t(0, el->size(), std::move(new_str))); + push_edit(el, new_edit(0, el->size(), std::move(new_str))); el->set_position(pos); update_buff_pos(el, pos); } @@ -2685,7 +2515,8 @@ void reader_data_t::replace_current_token(wcstring &&new_token) { // Find current token. editable_line_t *el = active_edit_line(); - const wchar_t *buff = el->text().c_str(); + auto text = *el->text(); + const wchar_t *buff = text.c_str(); parse_util_token_extent(buff, el->position(), &begin, &end, nullptr, nullptr); if (!begin || !end) return; @@ -2730,7 +2561,8 @@ void reader_data_t::move_word(editable_line_t *el, bool move_right, bool erase, // When moving left, a value of 1 means the character at index 0. auto state = new_move_word_state_machine(style); - const wchar_t *const command_line = el->text().c_str(); + auto text = *el->text(); + const wchar_t *const command_line = text.c_str(); const size_t start_buff_pos = el->position(); size_t buff_pos = el->position(); @@ -2851,10 +2683,14 @@ static parser_test_error_bits_t reader_shell_test(const parser_t &parser, const void reader_data_t::highlight_complete(highlight_result_t result) { ASSERT_IS_MAIN_THREAD(); in_flight_highlight_request.clear(); - if (result.text == command_line.text()) { + if (result.text == *command_line.text()) { assert(result.colors.size() == command_line.size()); if (this->is_repaint_needed(&result.colors)) { - command_line.set_colors(std::move(result.colors)); + auto ffi_colors = new_highlight_spec_list(); + for (auto &c : result.colors) { + ffi_colors->push(c); + } + command_line.set_colors(*ffi_colors); this->layout_and_repaint(L"highlight"); } } @@ -2863,17 +2699,19 @@ void reader_data_t::highlight_complete(highlight_result_t result) { // Given text and whether IO is allowed, return a function that performs highlighting. The function // may be invoked on a background thread. static std::function get_highlight_performer(const parser_t &parser, - const editable_line_t &el, + const editable_line_t *el, bool io_ok) { // shard_ptr to work around std::function requiring copyable types auto vars = std::make_shared>(parser.vars().snapshot()); uint32_t generation_count = read_generation_count(); + size_t position = el->position(); + wcstring text = *el->text(); return [=]() -> highlight_result_t { - if (el.text().empty()) return {}; + if (text.empty()) return {}; auto ctx = get_bg_context(**vars, generation_count); - std::vector colors(el.text().size(), highlight_spec_t{}); - highlight_shell(el.text(), colors, *ctx, io_ok, std::make_shared(el.position())); - return highlight_result_t{std::move(colors), el.text()}; + std::vector colors(text.size(), highlight_spec_t{}); + highlight_shell(text, colors, *ctx, io_ok, std::make_shared(position)); + return highlight_result_t{std::move(colors), text}; }; } @@ -2883,12 +2721,12 @@ void reader_data_t::super_highlight_me_plenty() { // Do nothing if this text is already in flight. const editable_line_t *el = &command_line; - if (el->text() == in_flight_highlight_request) return; - in_flight_highlight_request = el->text(); + if (*el->text() == in_flight_highlight_request) return; + in_flight_highlight_request = *el->text(); FLOG(reader_render, L"Highlighting"); std::function highlight_performer = - get_highlight_performer(parser(), *el, true /* io_ok */); + get_highlight_performer(parser(), el, true /* io_ok */); auto shared_this = this->shared_from_this(); std::function completion = [shared_this](highlight_result_t result) { shared_this->highlight_complete(std::move(result)); @@ -2908,8 +2746,8 @@ void reader_data_t::finish_highlighting_before_exec() { // 1: The user hit return after highlighting finished, so current highlighting is correct. // 2: The user hit return before highlighting started, so current highlighting is stale. // We can distinguish these based on what we last rendered. - current_highlight_ok = (this->rendered_layout.text == command_line.text()); - } else if (in_flight_highlight_request == command_line.text()) { + current_highlight_ok = (this->rendered_layout.text == *command_line.text()); + } else if (in_flight_highlight_request == *command_line.text()) { // The user hit return while our in-flight highlight request was still processing the text. // Wait for its completion to run, but not forever. namespace sc = std::chrono; @@ -2933,7 +2771,7 @@ void reader_data_t::finish_highlighting_before_exec() { if (!current_highlight_ok) { // We need to do a quick highlight without I/O. auto highlight_no_io = - get_highlight_performer(parser(), command_line, false /* io not ok */); + get_highlight_performer(parser(), &command_line, false /* io not ok */); this->highlight_complete(highlight_no_io()); } } @@ -3124,7 +2962,7 @@ static bool selection_is_at_top(const reader_data_t *data) { void reader_data_t::update_commandline_state() const { auto snapshot = commandline_state_snapshot(); - snapshot->text = this->command_line.text(); + snapshot->text = *this->command_line.text(); snapshot->cursor_pos = this->command_line.position(); if (this->history) { snapshot->history = (*this->history)->clone(); @@ -3139,7 +2977,7 @@ void reader_data_t::update_commandline_state() const { void reader_data_t::apply_commandline_state_changes() { // Only the text and cursor position may be changed. commandline_state_t state = commandline_get_state(); - if (state.text != this->command_line.text() || + if (state.text != *this->command_line.text() || state.cursor_pos != this->command_line.position()) { // The commandline builtin changed our contents. this->clear_pager(); @@ -3155,12 +2993,13 @@ void reader_data_t::compute_and_apply_completions(readline_cmd_t c, readline_loo // Remove a trailing backslash. This may trigger an extra repaint, but this is // rare. - if (is_backslashed(el->text(), el->position())) { + if (is_backslashed(*el->text(), el->position())) { delete_char(); } // Get the string; we have to do this after removing any trailing backslash. - const wchar_t *const buff = el->text().c_str(); + auto text = *el->text(); + const wchar_t *const buff = text.c_str(); // Figure out the extent of the command substitution surrounding the cursor. // This is because we only look at the current command substitution to form @@ -3200,7 +3039,7 @@ void reader_data_t::compute_and_apply_completions(readline_cmd_t c, readline_loo rls.complete_did_insert = false; size_t tok_off = static_cast(token_begin - buff); size_t tok_len = static_cast(token_end - token_begin); - push_edit(el, edit_t{tok_off, tok_len, std::move(*wc_expanded)}); + push_edit(el, new_edit(tok_off, tok_off + tok_len, std::move(*wc_expanded))); return; } @@ -3217,14 +3056,14 @@ void reader_data_t::compute_and_apply_completions(readline_cmd_t c, readline_loo // User-supplied completions may have changed the commandline - prevent buffer // overflow. - if (token_begin > buff + el->text().size()) token_begin = buff + el->text().size(); - if (token_end > buff + el->text().size()) token_end = buff + el->text().size(); + if (token_begin > buff + el->text()->size()) token_begin = buff + el->text()->size(); + if (token_end > buff + el->text()->size()) token_end = buff + el->text()->size(); // Munge our completions. rls.comp->sort_and_prioritize(CompletionRequestOptions()); // Record our cycle_command_line. - cycle_command_line = el->text(); + cycle_command_line = *el->text(); cycle_cursor_pos = token_end - buff; rls.complete_did_insert = handle_completions(*rls.comp, token_begin - buff, token_end - buff); @@ -3470,7 +3309,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // Go to beginning of line. case rl::beginning_of_line: { editable_line_t *el = active_edit_line(); - while (el->position() > 0 && el->text().at(el->position() - 1) != L'\n') { + while (el->position() > 0 && el->text()->at(el->position() - 1) != L'\n') { update_buff_pos(el, el->position() - 1); } break; @@ -3478,7 +3317,8 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat case rl::end_of_line: { editable_line_t *el = active_edit_line(); if (el->position() < el->size()) { - const wchar_t *buff = el->text().c_str(); + auto text = *el->text(); + const wchar_t *buff = text.c_str(); while (buff[el->position()] && buff[el->position()] != L'\n') { update_buff_pos(el, el->position() + 1); } @@ -3611,7 +3451,8 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } case rl::kill_line: { editable_line_t *el = active_edit_line(); - const wchar_t *buff = el->text().c_str(); + auto text = *el->text(); + const wchar_t *buff = text.c_str(); const wchar_t *begin = &buff[el->position()]; const wchar_t *end = begin; @@ -3630,7 +3471,8 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat if (el->position() == 0) { break; } - const wchar_t *buff = el->text().c_str(); + auto text = *el->text(); + const wchar_t *buff = text.c_str(); const wchar_t *end = &buff[el->position()]; const wchar_t *begin = end; @@ -3652,7 +3494,8 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat case rl::kill_inner_line: // Do not kill the following newline { editable_line_t *el = active_edit_line(); - const wchar_t *buff = el->text().c_str(); + auto text = *el->text(); + const wchar_t *buff = text.c_str(); // Back up to the character just past the previous newline, or go to the beginning // of the command line. Note that if the position is on a newline, visually this @@ -3736,7 +3579,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } case rl::execute: { if (!this->handle_execute(rls)) { - event_fire_generic(parser(), L"fish_posterror", {command_line.text()}); + event_fire_generic(parser(), L"fish_posterror", {*command_line.text()}); screen->reset_abandoning_line(termsize_last().width); } break; @@ -3763,7 +3606,8 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat if (mode == reader_history_search_t::token) { // Searching by token. const wchar_t *begin, *end; - const wchar_t *buff = el->text().c_str(); + auto text = *el->text(); + const wchar_t *buff = text.c_str(); parse_util_token_extent(buff, el->position(), &begin, &end, nullptr, nullptr); if (begin) { wcstring token(begin, end); @@ -3775,7 +3619,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } } else { // Searching by line. - history_search.reset_to_mode(el->text(), **history, mode, 0); + history_search.reset_to_mode(*el->text(), **history, mode, 0); // Skip the autosuggestion in the history unless it was truncated. const wcstring &suggest = autosuggestion.text; @@ -3815,7 +3659,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } // Record our cycle_command_line. - cycle_command_line = command_line.text(); + cycle_command_line = *command_line.text(); cycle_cursor_pos = command_line.position(); this->history_pager_active = true; @@ -3826,7 +3670,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat pager.set_prefix(MB_CUR_MAX > 1 ? L"► " : L"> ", false /* highlight */); // Update the search field, which triggers the actual history search. if (!history_search.active() || history_search.search_string().empty()) { - insert_string(&pager.search_field_line, command_line.text()); + insert_string(&pager.search_field_line, *command_line.text()); } else { // If we have an actual history search already going, reuse that term // - this is if the user looks around a bit and decides to switch to the pager. @@ -3989,7 +3833,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } else { // Not navigating the pager contents. editable_line_t *el = active_edit_line(); - int line_old = parse_util_get_line_from_offset(el->text(), el->position()); + int line_old = parse_util_get_line_from_offset(*el->text(), el->position()); int line_new; if (c == rl::up_line) @@ -3997,12 +3841,12 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat else line_new = line_old + 1; - int line_count = parse_util_lineno(el->text(), el->size()) - 1; + int line_count = parse_util_lineno(*el->text(), el->size()) - 1; if (line_new >= 0 && line_new <= line_count) { - auto indents = parse_util_compute_indents(el->text()); - size_t base_pos_new = parse_util_get_offset_from_line(el->text(), line_new); - size_t base_pos_old = parse_util_get_offset_from_line(el->text(), line_old); + auto indents = parse_util_compute_indents(*el->text()); + size_t base_pos_new = parse_util_get_offset_from_line(*el->text(), line_new); + size_t base_pos_old = parse_util_get_offset_from_line(*el->text(), line_old); assert(base_pos_new != static_cast(-1) && base_pos_old != static_cast(-1)); @@ -4011,7 +3855,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat size_t line_offset_old = el->position() - base_pos_old; size_t total_offset_new = parse_util_get_offset( - el->text(), line_new, line_offset_old - 4 * (indent_new - indent_old)); + *el->text(), line_new, line_offset_old - 4 * (indent_new - indent_old)); update_buff_pos(el, total_offset_new); } } @@ -4043,7 +3887,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // Drag the character before the cursor forward over the character at the cursor, // moving the cursor forward as well. if (el->position() > 0) { - wcstring local_cmd = el->text(); + wcstring local_cmd = *el->text(); std::swap(local_cmd.at(el->position()), local_cmd.at(el->position() - 1)); set_command_line_and_position(el, std::move(local_cmd), el->position() + 1); } @@ -4052,7 +3896,8 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat case rl::transpose_words: { editable_line_t *el = active_edit_line(); size_t len = el->size(); - const wchar_t *buff = el->text().c_str(); + auto text = *el->text(); + const wchar_t *buff = text.c_str(); const wchar_t *tok_begin, *tok_end, *prev_begin, *prev_end; // If we are not in a token, look for one ahead. @@ -4095,7 +3940,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // Check that the cursor is on a character if (buff_pos < el->size()) { - wchar_t chr = el->text().at(buff_pos); + wchar_t chr = el->text()->at(buff_pos); wcstring replacement; // Toggle the case of the current character @@ -4128,7 +3973,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // Loop through the selected characters and toggle their case. for (size_t pos = start; pos < start + len && pos < el->size(); pos++) { - wchar_t chr = el->text().at(pos); + wchar_t chr = el->text()->at(pos); // Toggle the case of the current character. bool make_uppercase = iswlower(chr); @@ -4163,7 +4008,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat false); wcstring replacement; for (; pos < el->position(); pos++) { - wchar_t chr = el->text().at(pos); + wchar_t chr = el->text()->at(pos); // We always change the case; this decides whether we go uppercase (true) or // lowercase (false). @@ -4221,7 +4066,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } case rl::insert_line_over: { editable_line_t *el = active_edit_line(); - while (el->position() > 0 && el->text().at(el->position() - 1) != L'\n') { + while (el->position() > 0 && el->text()->at(el->position() - 1) != L'\n') { update_buff_pos(el, el->position() - 1); } insert_char(el, L'\n'); @@ -4231,7 +4076,8 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat case rl::insert_line_under: { editable_line_t *el = active_edit_line(); if (el->position() < el->size()) { - const wchar_t *buff = el->text().c_str(); + auto text = *el->text(); + const wchar_t *buff = text.c_str(); while (buff[el->position()] && buff[el->position()] != L'\n') { update_buff_pos(el, el->position() + 1); } @@ -4364,7 +4210,7 @@ void reader_data_t::add_to_history() { } // Historical behavior is to trim trailing spaces, unless escape (#7661). - wcstring text = command_line.text(); + wcstring text = *command_line.text(); while (!text.empty() && text.back() == L' ' && count_preceding_backslashes(text, text.size() - 1) % 2 == 0) { text.pop_back(); @@ -4398,7 +4244,7 @@ parser_test_error_bits_t reader_data_t::expand_for_execute() { // Syntax check before expanding abbreviations. We could consider relaxing this: a string may be // syntactically invalid but become valid after expanding abbreviations. if (conf.syntax_check_ok) { - test_res = reader_shell_test(parser(), el->text()); + test_res = reader_shell_test(parser(), *el->text()); if (test_res & PARSER_TEST_ERROR) return test_res; } @@ -4408,7 +4254,7 @@ parser_test_error_bits_t reader_data_t::expand_for_execute() { // Trigger syntax highlighting as we are likely about to execute this command. this->super_highlight_me_plenty(); if (conf.syntax_check_ok) { - test_res = reader_shell_test(parser(), el->text()); + test_res = reader_shell_test(parser(), *el->text()); } } return test_res; @@ -4422,7 +4268,7 @@ bool reader_data_t::handle_execute(readline_loop_state_t &rls) { if (this->history_pager_active && this->pager.selected_completion_index() == PAGER_SELECTION_NONE) { command_line.push_edit( - edit_t{0, command_line.size(), this->pager.search_field_line.text()}, + new_edit(0, command_line.size(), *this->pager.search_field_line.text()), /* allow_coalesce */ false); command_line.set_position(this->pager.search_field_line.position()); } @@ -4445,13 +4291,14 @@ bool reader_data_t::handle_execute(readline_loop_state_t &rls) { if (el->position() >= el->size()) { // We're at the end of the text and not in a comment (issue #1225). continue_on_next_line = - is_backslashed(el->text(), el->position()) && !text_ends_in_comment(el->text()); + is_backslashed(*el->text(), el->position()) && !text_ends_in_comment(*el->text()); } else { // Allow mid line split if the following character is whitespace (issue #613). - if (is_backslashed(el->text(), el->position()) && iswspace(el->text().at(el->position()))) { + if (is_backslashed(*el->text(), el->position()) && + iswspace(el->text()->at(el->position()))) { continue_on_next_line = true; // Check if the end of the line is backslashed (issue #4467). - } else if (is_backslashed(el->text(), el->size()) && !text_ends_in_comment(el->text())) { + } else if (is_backslashed(*el->text(), el->size()) && !text_ends_in_comment(*el->text())) { // Move the cursor to the end of the line. el->set_position(el->size()); continue_on_next_line = true; @@ -4708,7 +4555,7 @@ bool reader_data_t::jump(jump_direction_t dir, jump_precision_t precision, edita size_t tmp_pos = el->position(); while (tmp_pos--) { - if (el->at(tmp_pos) == target) { + if ((wchar_t)el->at(tmp_pos) == target) { if (precision == jump_precision_t::till) { tmp_pos = std::min(el->size() - 1, tmp_pos + 1); } @@ -4721,7 +4568,7 @@ bool reader_data_t::jump(jump_direction_t dir, jump_precision_t precision, edita } case jump_direction_t::forward: { for (size_t tmp_pos = el->position() + 1; tmp_pos < el->size(); tmp_pos++) { - if (el->at(tmp_pos) == target) { + if ((wchar_t)el->at(tmp_pos) == target) { if (precision == jump_precision_t::till && tmp_pos) { tmp_pos--; } diff --git a/src/reader.h b/src/reader.h index 29b73312b..434c9f9ef 100644 --- a/src/reader.h +++ b/src/reader.h @@ -25,117 +25,7 @@ #include "reader.rs.h" #endif -/// An edit action that can be undone. -struct edit_t { - /// When undoing the edit we use this to restore the previous cursor position. - size_t cursor_position_before_edit = 0; - - /// The span of text that is replaced by this edit. - size_t offset, length; - - /// The strings that are removed and added by this edit, respectively. - wcstring old, replacement; - - /// edit_t is only for contiguous changes, so to restore a group of arbitrary changes to the - /// command line we need to have a group id as forcibly coalescing changes is not enough. - maybe_t group_id; - - explicit edit_t(size_t offset, size_t length, wcstring replacement) - : offset(offset), length(length), replacement(std::move(replacement)) {} - - explicit edit_t(source_range_t range, wcstring replacement) - : edit_t(range.start, range.length, std::move(replacement)) {} - - /// Used for testing. - bool operator==(const edit_t &other) const; -}; - -/// Modify a string and its syntax highlighting according to the given edit. -/// Currently exposed for testing only. -void apply_edit(wcstring *target, std::vector *colors, const edit_t &edit); - -/// The history of all edits to some command line. -struct undo_history_t { - /// The stack of edits that can be undone or redone atomically. - std::vector edits; - - /// The position in the undo stack that corresponds to the current - /// state of the input line. - /// Invariants: - /// edits_applied - 1 is the index of the next edit to undo. - /// edits_applied is the index of the next edit to redo. - /// - /// For example, if nothing was undone, edits_applied is edits.size(). - /// If every single edit was undone, edits_applied is 0. - size_t edits_applied = 0; - - /// Whether we allow the next edit to be grouped together with the - /// last one. - bool may_coalesce = false; - - /// Whether to be more aggressive in coalescing edits. Ideally, it would be "force coalesce" - /// with guaranteed atomicity but as `edit_t` is strictly for contiguous changes, that guarantee - /// can't be made at this time. - bool try_coalesce = false; - - /// Empty the history. - void clear(); -}; - -/// Helper class for storing a command line. -class editable_line_t { - public: - const wcstring &text() const { return text_; } - - const std::vector &colors() const { return colors_; } - void set_colors(std::vector colors); - - size_t position() const { return position_; } - void set_position(size_t position) { position_ = position; } - - // Gets the length of the text. - size_t size() const { return text().size(); } - - bool empty() const { return text().empty(); } - - wchar_t at(size_t idx) const { return text().at(idx); } - - void clear(); - - /// Modify the commandline according to @edit. Most modifications to the - /// text should pass through this function. - void push_edit(edit_t edit, bool allow_coalesce); - - /// Undo the most recent edit that was not yet undone. Returns true on success. - bool undo(); - - /// Redo the most recent undo. Returns true on success. - bool redo(); - - /// Start a logical grouping of command line edits that should be undone/redone together. - void begin_edit_group(); - /// End a logical grouping of command line edits that should be undone/redone together. - void end_edit_group(); - - private: - /// Whether we want to append this string to the previous edit. - bool want_to_coalesce_insertion_of(const wcstring &str) const; - - /// The command line. - wcstring text_; - /// Syntax highlighting. - std::vector colors_; - /// The current position of the cursor in the command line. - size_t position_ = 0; - - /// The history of all edits. - undo_history_t undo_history_; - /// The nesting level for atomic edits, so that recursive invocations of start_edit_group() - /// are not ended by one end_edit_group() call. - int32_t edit_group_level_ = -1; - /// Monotonically increasing edit group, ignored when edit_group_level_ is -1. Allowed to wrap. - uint32_t edit_group_id_ = -1; -}; +#include "editable_line.h" int reader_read_ffi(const void *parser, int fd, const void *io_chain); /// Read commands from \c fd until encountering EOF.