Port editable_line_t

This commit is contained in:
Johannes Altmanninger 2023-12-02 18:20:40 +01:00
parent 020b990c81
commit fadf0f2e5b
12 changed files with 660 additions and 443 deletions

View File

@ -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",

View File

@ -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<usize>,
/// 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<usize>,
}
impl Edit {
pub fn new(range: std::ops::Range<usize>, 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<HighlightSpec>, 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<Edit>,
/// 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<HighlightSpec>,
/// 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<usize>,
/// 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<HighlightSpec>) {
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<Edit>;
#[cxx_name = "apply_edit"]
fn apply_edit_ffi(
target: &CxxWString,
mut colors: Pin<&mut HighlightSpecListFFI>,
edit: Box<Edit>,
) -> UniquePtr<CxxWString>;
}
extern "Rust" {
type UndoHistory;
}
extern "Rust" {
type EditableLine;
fn new_editable_line() -> Box<EditableLine>;
fn empty(&self) -> bool;
#[cxx_name = "text"]
fn text_ffi(&self) -> UniquePtr<CxxWString>;
#[cxx_name = "clone"]
fn clone_ffi(&self) -> Box<EditableLine>;
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<Edit>, 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<Edit> {
Box::new(Edit::new(start..end, replacement.from_ffi()))
}
fn new_editable_line() -> Box<EditableLine> {
Box::default()
}
impl EditableLine {
fn empty(&self) -> bool {
self.is_empty()
}
fn text_ffi(&self) -> UniquePtr<CxxWString> {
self.text().to_ffi()
}
fn clone_ffi(&self) -> Box<Self> {
Box::new(self.clone())
}
fn size(&self) -> usize {
self.len()
}
#[allow(clippy::boxed_local)]
fn push_edit_ffi(&mut self, edit: Box<Edit>, 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<Edit>,
) -> UniquePtr<CxxWString> {
let mut target = target.from_ffi();
apply_edit(&mut target, &mut colors.0, &edit);
target.to_ffi()
}

View File

@ -11,6 +11,7 @@ use crate::common::{
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 @@ mod highlight_ffi {
#[cxx_name = "clone"]
fn clone_ffi(self: &HighlightSpec) -> Box<HighlightSpec>;
fn new_highlight_spec() -> Box<HighlightSpec>;
fn editable_line_colors(editable_line: &EditableLine) -> &[HighlightSpec];
}
extern "C++" {
@ -1688,6 +1690,7 @@ mod highlight_ffi {
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 @@ mod highlight_ffi {
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 @@ impl HighlightSpec {
fn new_highlight_spec() -> Box<HighlightSpec> {
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()
}

View File

@ -35,6 +35,7 @@ mod color;
mod compat;
mod complete;
mod curses;
mod editable_line;
mod env;
mod env_dispatch;
mod env_universal_common;

View File

@ -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());
}

View File

@ -3,6 +3,8 @@ use crate::wchar::prelude::*;
#[cfg(test)]
mod common;
mod complete;
#[cfg(test)]
mod editable_line;
mod env;
mod env_universal_common;
mod expand;

18
src/editable_line.h Normal file
View File

@ -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

View File

@ -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<highlight_spec_t> 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},

View File

@ -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()) {}

View File

@ -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<editable_line_t> 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<editable_line_t> 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{};

View File

@ -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<highlight_spec_t> *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<highlight_spec_t> colors) {
assert(colors.size() == size());
colors_ = std::move(colors);
}
bool editable_line_t::undo() {
bool did_undo = false;
maybe_t<int> 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<int> 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<reader_data_t> {
/// The parser being used.
rust::Box<ParserRef> parser_ref;
/// String containing the whole current commandline.
editable_line_t command_line;
rust::Box<editable_line_t> 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> {
reader_data_t(rust::Box<ParserRef> 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<reader_data_t> {
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> &&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<highlight_spec_t> *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<highlight_spec_t> *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<int> indents = parse_util_compute_indents(cmd_line->text());
std::vector<int> 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<history_pager_result_t()> func = [=]() {
return history_pager_search(**shared_this->history, direction, index, search_term);
};
std::function<void(const history_pager_result_t &)> 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> &&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<autosuggestion_t()> 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<void(autosuggestion_t)> 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<highlight_result_t(void)> 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<rust::Box<::EnvDyn>>(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<highlight_spec_t> colors(el.text().size(), highlight_spec_t{});
highlight_shell(el.text(), colors, *ctx, io_ok, std::make_shared<size_t>(el.position()));
return highlight_result_t{std::move(colors), el.text()};
std::vector<highlight_spec_t> colors(text.size(), highlight_spec_t{});
highlight_shell(text, colors, *ctx, io_ok, std::make_shared<size_t>(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_result_t()> 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<void(highlight_result_t)> 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<size_t>(token_begin - buff);
size_t tok_len = static_cast<size_t>(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<size_t>(-1) &&
base_pos_old != static_cast<size_t>(-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--;
}

View File

@ -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<int> 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<highlight_spec_t> *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<edit_t> 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<highlight_spec_t> &colors() const { return colors_; }
void set_colors(std::vector<highlight_spec_t> 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<highlight_spec_t> 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.