From d9a3fb887f0b242eaa77be9a03df9340f7bc7bf1 Mon Sep 17 00:00:00 2001 From: MinusGix Date: Wed, 7 Feb 2024 15:06:40 -0600 Subject: [PATCH] Floem editor (#296) * Initial floem-editor move * Fix handling of shift-movement keybinds * Reexport floem editor core * Put serde under a feature flag * Simplify creation of Editor * Stop using `Rc` in `RwSignal>` * Remove tracing remnant * Cargo fmt * Rename old lapce references * Use View/Widget split * Move editor/ into views/ * Add SimpleStylingBuilder * Add text_editor view * Add TextEditor::update listener * Remove manual impl of view id * Have TextEditor use structures for data * TextDocument into own file; TextEditor into views/ * Add more TextEditor fns; fix read only * Use shorthand 'doc' instead of 'document' * Add Document::edit for applying edits from outside * Rename editor.rs -> mod.rs * Use a range for Document::exec_motion_mode * Add editor to default features --- Cargo.toml | 21 +- editor-core/Cargo.toml | 19 + editor-core/src/buffer/diff.rs | 242 ++ editor-core/src/buffer/mod.rs | 774 +++++++ editor-core/src/buffer/rope_text.rs | 635 +++++ editor-core/src/buffer/test.rs | 168 ++ editor-core/src/char_buffer.rs | 1073 +++++++++ editor-core/src/chars.rs | 34 + editor-core/src/command.rs | 399 ++++ editor-core/src/cursor.rs | 616 +++++ editor-core/src/editor.rs | 1751 ++++++++++++++ editor-core/src/indent.rs | 201 ++ editor-core/src/lib.rs | 15 + editor-core/src/mode.rs | 98 + editor-core/src/movement.rs | 140 ++ editor-core/src/paragraph.rs | 135 ++ editor-core/src/register.rs | 41 + editor-core/src/selection.rs | 764 ++++++ editor-core/src/soft_tab.rs | 237 ++ editor-core/src/util.rs | 129 ++ editor-core/src/word.rs | 701 ++++++ examples/editor/Cargo.toml | 8 + examples/editor/src/main.rs | 57 + src/views/editor/actions.rs | 189 ++ src/views/editor/color.rs | 33 + src/views/editor/command.rs | 41 + src/views/editor/gutter.rs | 123 + src/views/editor/id.rs | 18 + src/views/editor/keypress/key.rs | 816 +++++++ src/views/editor/keypress/mod.rs | 408 ++++ src/views/editor/keypress/press.rs | 160 ++ src/views/editor/layout.rs | 149 ++ src/views/editor/listener.rs | 65 + src/views/editor/mod.rs | 1437 ++++++++++++ src/views/editor/movement.rs | 761 ++++++ src/views/editor/phantom_text.rs | 174 ++ src/views/editor/text.rs | 814 +++++++ src/views/editor/text_document.rs | 298 +++ src/views/editor/view.rs | 1291 +++++++++++ src/views/editor/visual_line.rs | 3352 +++++++++++++++++++++++++++ src/views/mod.rs | 8 + src/views/text_editor.rs | 231 ++ 42 files changed, 18624 insertions(+), 2 deletions(-) create mode 100644 editor-core/Cargo.toml create mode 100644 editor-core/src/buffer/diff.rs create mode 100644 editor-core/src/buffer/mod.rs create mode 100644 editor-core/src/buffer/rope_text.rs create mode 100644 editor-core/src/buffer/test.rs create mode 100644 editor-core/src/char_buffer.rs create mode 100644 editor-core/src/chars.rs create mode 100644 editor-core/src/command.rs create mode 100644 editor-core/src/cursor.rs create mode 100644 editor-core/src/editor.rs create mode 100644 editor-core/src/indent.rs create mode 100644 editor-core/src/lib.rs create mode 100644 editor-core/src/mode.rs create mode 100644 editor-core/src/movement.rs create mode 100644 editor-core/src/paragraph.rs create mode 100644 editor-core/src/register.rs create mode 100644 editor-core/src/selection.rs create mode 100644 editor-core/src/soft_tab.rs create mode 100644 editor-core/src/util.rs create mode 100644 editor-core/src/word.rs create mode 100644 examples/editor/Cargo.toml create mode 100644 examples/editor/src/main.rs create mode 100644 src/views/editor/actions.rs create mode 100644 src/views/editor/color.rs create mode 100644 src/views/editor/command.rs create mode 100644 src/views/editor/gutter.rs create mode 100644 src/views/editor/id.rs create mode 100644 src/views/editor/keypress/key.rs create mode 100644 src/views/editor/keypress/mod.rs create mode 100644 src/views/editor/keypress/press.rs create mode 100644 src/views/editor/layout.rs create mode 100644 src/views/editor/listener.rs create mode 100644 src/views/editor/mod.rs create mode 100644 src/views/editor/movement.rs create mode 100644 src/views/editor/phantom_text.rs create mode 100644 src/views/editor/text.rs create mode 100644 src/views/editor/text_document.rs create mode 100644 src/views/editor/view.rs create mode 100644 src/views/editor/visual_line.rs create mode 100644 src/views/text_editor.rs diff --git a/Cargo.toml b/Cargo.toml index f46bbf45..ade8fa18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["renderer", "vger", "tiny_skia", "reactive", "examples/*"] +members = ["renderer", "vger", "tiny_skia", "reactive", "editor-core", "examples/*"] [workspace.package] license = "MIT" @@ -17,6 +17,12 @@ edition = "2021" rust-version = "1.75" license.workspace = true +[workspace.dependencies] +serde = "1.0" +lapce-xi-rope = { version = "0.3.2", features = ["serde"] } +strum = "0.21.0" +strum_macros = "0.21.1" + [dependencies] sha2 = "0.10.6" bitflags = "2.2.1" @@ -36,14 +42,25 @@ crossbeam-channel = "0.5.6" once_cell = "1.17.1" im = "15.1.0" im-rc = "15.1.0" +serde = { workspace = true, optional = true } +lapce-xi-rope = { workspace = true, optional = true } +strum = { workspace = true, optional = true } +strum_macros = { workspace = true, optional = true } +# TODO: once https://github.com/rust-lang/rust/issues/65991 is stabilized we don't need this +downcast-rs = { version = "1.2.0", optional = true } parking_lot = { version = "0.12.1" } floem_renderer = { path = "renderer", version = "0.1.0" } floem_vger_renderer = { path = "vger", version = "0.1.0" } floem_tiny_skia_renderer = { path = "tiny_skia", version = "0.1.0" } floem_reactive = { path = "reactive", version = "0.1.0" } floem-winit = { version = "0.29.4", features = ["rwh_05"] } +floem-editor-core = { path = "editor-core", version = "0.1.0", optional = true } image = { version = "0.24", features = ["jpeg", "png"] } copypasta = { version = "0.10.0", default-features = false, features = ["wayland", "x11"] } [features] -serde = ["floem-winit/serde"] +default = ["editor"] +# TODO: this is only winit and the editor serde, there are other dependencies that still depend on +# serde +serde = ["floem-winit/serde", "dep:serde"] +editor = ["floem-editor-core", "dep:lapce-xi-rope", "dep:strum", "dep:strum_macros", "dep:downcast-rs"] diff --git a/editor-core/Cargo.toml b/editor-core/Cargo.toml new file mode 100644 index 00000000..8c904e7e --- /dev/null +++ b/editor-core/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "floem-editor-core" +version.workspace = true +edition = "2021" +repository = "https://github.com/lapce/floem" +license.workspace = true + +[dependencies] +serde = { workspace = true, optional = true } +strum.workspace = true +strum_macros.workspace = true + +lapce-xi-rope.workspace = true + +itertools = "0.10.1" +bitflags = "1.3.2" + +[features] +serde = ["dep:serde"] diff --git a/editor-core/src/buffer/diff.rs b/editor-core/src/buffer/diff.rs new file mode 100644 index 00000000..b2b7a139 --- /dev/null +++ b/editor-core/src/buffer/diff.rs @@ -0,0 +1,242 @@ +use std::{ + borrow::Cow, + ops::Range, + sync::{ + atomic::{self, AtomicU64}, + Arc, + }, +}; + +use lapce_xi_rope::Rope; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DiffResult { + Left(T), + Both(T, T), + Right(T), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DiffBothInfo { + pub left: Range, + pub right: Range, + pub skip: Option>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DiffLines { + Left(Range), + Both(DiffBothInfo), + Right(Range), +} + +pub enum DiffExpand { + Up(usize), + Down(usize), + All, +} + +pub fn expand_diff_lines( + diff_lines: &mut [DiffLines], + line: usize, + expand: DiffExpand, + is_right: bool, +) { + for diff_line in diff_lines.iter_mut() { + if let DiffLines::Both(info) = diff_line { + if (is_right && info.right.start == line) || (!is_right && info.left.start == line) { + match expand { + DiffExpand::All => { + info.skip = None; + } + DiffExpand::Up(n) => { + if let Some(skip) = &mut info.skip { + if n >= skip.len() { + info.skip = None; + } else { + skip.start += n; + } + } + } + DiffExpand::Down(n) => { + if let Some(skip) = &mut info.skip { + if n >= skip.len() { + info.skip = None; + } else { + skip.end -= n; + } + } + } + } + break; + } + } + } +} + +pub fn rope_diff( + left_rope: Rope, + right_rope: Rope, + rev: u64, + atomic_rev: Arc, + context_lines: Option, +) -> Option> { + let left_lines = left_rope.lines(..).collect::>>(); + let right_lines = right_rope.lines(..).collect::>>(); + + let left_count = left_lines.len(); + let right_count = right_lines.len(); + let min_count = std::cmp::min(left_count, right_count); + + let leading_equals = left_lines + .iter() + .zip(right_lines.iter()) + .take_while(|p| p.0 == p.1) + .count(); + let trailing_equals = left_lines + .iter() + .rev() + .zip(right_lines.iter().rev()) + .take(min_count - leading_equals) + .take_while(|p| p.0 == p.1) + .count(); + + let left_diff_size = left_count - leading_equals - trailing_equals; + let right_diff_size = right_count - leading_equals - trailing_equals; + + let table: Vec> = { + let mut table = vec![vec![0; right_diff_size + 1]; left_diff_size + 1]; + let left_skip = left_lines.iter().skip(leading_equals).take(left_diff_size); + let right_skip = right_lines + .iter() + .skip(leading_equals) + .take(right_diff_size); + + for (i, l) in left_skip.enumerate() { + for (j, r) in right_skip.clone().enumerate() { + if atomic_rev.load(atomic::Ordering::Acquire) != rev { + return None; + } + table[i + 1][j + 1] = if l == r { + table[i][j] + 1 + } else { + std::cmp::max(table[i][j + 1], table[i + 1][j]) + }; + } + } + + table + }; + + let diff = { + let mut diff = Vec::with_capacity(left_diff_size + right_diff_size); + let mut i = left_diff_size; + let mut j = right_diff_size; + let mut li = left_lines.iter().rev().skip(trailing_equals); + let mut ri = right_lines.iter().skip(trailing_equals); + + loop { + if atomic_rev.load(atomic::Ordering::Acquire) != rev { + return None; + } + if j > 0 && (i == 0 || table[i][j] == table[i][j - 1]) { + j -= 1; + diff.push(DiffResult::Right(ri.next().unwrap())); + } else if i > 0 && (j == 0 || table[i][j] == table[i - 1][j]) { + i -= 1; + diff.push(DiffResult::Left(li.next().unwrap())); + } else if i > 0 && j > 0 { + i -= 1; + j -= 1; + diff.push(DiffResult::Both(li.next().unwrap(), ri.next().unwrap())); + } else { + break; + } + } + + diff + }; + + let mut changes = Vec::new(); + let mut left_line = 0; + let mut right_line = 0; + if leading_equals > 0 { + changes.push(DiffLines::Both(DiffBothInfo { + left: 0..leading_equals, + right: 0..leading_equals, + skip: None, + })) + } + left_line += leading_equals; + right_line += leading_equals; + + for diff in diff.iter().rev() { + if atomic_rev.load(atomic::Ordering::Acquire) != rev { + return None; + } + match diff { + DiffResult::Left(_) => { + match changes.last_mut() { + Some(DiffLines::Left(r)) => r.end = left_line + 1, + _ => changes.push(DiffLines::Left(left_line..left_line + 1)), + } + left_line += 1; + } + DiffResult::Both(_, _) => { + match changes.last_mut() { + Some(DiffLines::Both(info)) => { + info.left.end = left_line + 1; + info.right.end = right_line + 1; + } + _ => changes.push(DiffLines::Both(DiffBothInfo { + left: left_line..left_line + 1, + right: right_line..right_line + 1, + skip: None, + })), + } + left_line += 1; + right_line += 1; + } + DiffResult::Right(_) => { + match changes.last_mut() { + Some(DiffLines::Right(r)) => r.end = right_line + 1, + _ => changes.push(DiffLines::Right(right_line..right_line + 1)), + } + right_line += 1; + } + } + } + + if trailing_equals > 0 { + changes.push(DiffLines::Both(DiffBothInfo { + left: left_count - trailing_equals..left_count, + right: right_count - trailing_equals..right_count, + skip: None, + })); + } + if let Some(context_lines) = context_lines { + if !changes.is_empty() { + let changes_last = changes.len() - 1; + for (i, change) in changes.iter_mut().enumerate() { + if atomic_rev.load(atomic::Ordering::Acquire) != rev { + return None; + } + if let DiffLines::Both(info) = change { + if i == 0 || i == changes_last { + if info.right.len() > context_lines { + if i == 0 { + info.skip = Some(0..info.right.len() - context_lines); + } else { + info.skip = Some(context_lines..info.right.len()); + } + } + } else if info.right.len() > context_lines * 2 { + info.skip = Some(context_lines..info.right.len() - context_lines); + } + } + } + } + } + + Some(changes) +} diff --git a/editor-core/src/buffer/mod.rs b/editor-core/src/buffer/mod.rs new file mode 100644 index 00000000..de7d57d7 --- /dev/null +++ b/editor-core/src/buffer/mod.rs @@ -0,0 +1,774 @@ +use std::{ + borrow::{Borrow, Cow}, + cmp::Ordering, + collections::BTreeSet, + sync::{ + atomic::{self, AtomicU64}, + Arc, + }, +}; + +use lapce_xi_rope::{ + multiset::Subset, + tree::{Node, NodeInfo}, + Delta, DeltaBuilder, DeltaElement, Interval, Rope, RopeDelta, +}; + +use crate::{ + cursor::CursorMode, + editor::EditType, + indent::{auto_detect_indent_style, IndentStyle}, + mode::Mode, + selection::Selection, + word::WordCursor, +}; + +pub mod diff; +pub mod rope_text; + +use rope_text::*; + +#[derive(Clone)] +enum Contents { + Edit { + /// Groups related edits together so that they are undone and re-done + /// together. For example, an auto-indent insertion would be un-done + /// along with the newline that triggered it. + undo_group: usize, + /// The subset of the characters of the union string from after this + /// revision that were added by this revision. + inserts: Subset, + /// The subset of the characters of the union string from after this + /// revision that were deleted by this revision. + deletes: Subset, + }, + Undo { + /// The set of groups toggled between undone and done. + /// Just the `symmetric_difference` (XOR) of the two sets. + toggled_groups: BTreeSet, // set of undo_group id's + /// Used to store a reversible difference between the old + /// and new deletes_from_union + deletes_bitxor: Subset, + }, +} + +#[derive(Clone)] +struct Revision { + num: u64, + max_undo_so_far: usize, + edit: Contents, + cursor_before: Option, + cursor_after: Option, +} + +#[derive(Debug, Clone)] +pub struct InvalLines { + pub start_line: usize, + pub inval_count: usize, + pub new_count: usize, + pub old_text: Rope, +} + +#[derive(Clone)] +pub struct Buffer { + rev_counter: u64, + pristine_rev_id: u64, + atomic_rev: Arc, + + text: Rope, + revs: Vec, + cur_undo: usize, + undos: BTreeSet, + undo_group_id: usize, + live_undos: Vec, + deletes_from_union: Subset, + undone_groups: BTreeSet, + tombstones: Rope, + this_edit_type: EditType, + last_edit_type: EditType, + + indent_style: IndentStyle, + + max_len: usize, + max_len_line: usize, +} + +impl ToString for Buffer { + fn to_string(&self) -> String { + self.text().to_string() + } +} + +impl Buffer { + pub fn new(text: impl Into) -> Self { + let text = text.into(); + let len = text.len(); + Self { + text, + + rev_counter: 1, + pristine_rev_id: 0, + atomic_rev: Arc::new(AtomicU64::new(0)), + + revs: vec![Revision { + num: 0, + max_undo_so_far: 0, + edit: Contents::Undo { + toggled_groups: BTreeSet::new(), + deletes_bitxor: Subset::new(0), + }, + cursor_before: None, + cursor_after: None, + }], + cur_undo: 1, + undos: BTreeSet::new(), + undo_group_id: 1, + live_undos: vec![0], + deletes_from_union: Subset::new(len), + undone_groups: BTreeSet::new(), + tombstones: Rope::default(), + + this_edit_type: EditType::Other, + last_edit_type: EditType::Other, + indent_style: IndentStyle::DEFAULT_INDENT, + + max_len: 0, + max_len_line: 0, + } + } + + /// The current buffer revision + pub fn rev(&self) -> u64 { + self.revs.last().unwrap().num + } + + /// Mark the buffer as pristine (aka 'saved') + pub fn set_pristine(&mut self) { + self.pristine_rev_id = self.rev(); + } + + pub fn is_pristine(&self) -> bool { + self.is_equivalent_revision(self.pristine_rev_id, self.rev()) + } + + pub fn set_cursor_before(&mut self, cursor: CursorMode) { + if let Some(rev) = self.revs.last_mut() { + rev.cursor_before = Some(cursor); + } + } + + pub fn set_cursor_after(&mut self, cursor: CursorMode) { + if let Some(rev) = self.revs.last_mut() { + rev.cursor_after = Some(cursor); + } + } + + fn is_equivalent_revision(&self, base_rev: u64, other_rev: u64) -> bool { + let base_subset = self + .find_rev(base_rev) + .map(|rev_index| self.deletes_from_cur_union_for_index(rev_index)); + let other_subset = self + .find_rev(other_rev) + .map(|rev_index| self.deletes_from_cur_union_for_index(rev_index)); + + base_subset.is_some() && base_subset == other_subset + } + + fn find_rev(&self, rev_id: u64) -> Option { + self.revs + .iter() + .enumerate() + .rev() + .find(|&(_, rev)| rev.num == rev_id) + .map(|(i, _)| i) + } + + pub fn atomic_rev(&self) -> Arc { + self.atomic_rev.clone() + } + + fn get_max_line_len(&self) -> (usize, usize) { + let mut pre_offset = 0; + let mut max_len = 0; + let mut max_len_line = 0; + for line in 0..=self.num_lines() { + let offset = self.offset_of_line(line); + let line_len = offset - pre_offset; + pre_offset = offset; + if line_len > max_len { + max_len = line_len; + max_len_line = line; + } + } + (max_len, max_len_line) + } + + fn update_size(&mut self, inval_lines: &InvalLines) { + if self.max_len_line >= inval_lines.start_line + && self.max_len_line <= inval_lines.start_line + inval_lines.inval_count + { + let (max_len, max_len_line) = self.get_max_line_len(); + self.max_len = max_len; + self.max_len_line = max_len_line; + } else { + let mut max_len = 0; + let mut max_len_line = 0; + for line in inval_lines.start_line..inval_lines.start_line + inval_lines.new_count { + let line_len = self.line_len(line); + if line_len > max_len { + max_len = line_len; + max_len_line = line; + } + } + if max_len > self.max_len { + self.max_len = max_len; + self.max_len_line = max_len_line; + } else if self.max_len_line >= inval_lines.start_line { + self.max_len_line = + self.max_len_line + inval_lines.new_count - inval_lines.inval_count; + } + } + } + + pub fn max_len(&self) -> usize { + self.max_len + } + + pub fn init_content(&mut self, content: Rope) { + if !content.is_empty() { + let delta = Delta::simple_edit(Interval::new(0, 0), content, 0); + let (new_rev, new_text, new_tombstones, new_deletes_from_union) = + self.mk_new_rev(0, delta.clone()); + self.apply_edit( + &delta, + new_rev, + new_text, + new_tombstones, + new_deletes_from_union, + ); + } + self.set_pristine(); + } + + pub fn reload(&mut self, content: Rope, set_pristine: bool) -> (Rope, RopeDelta, InvalLines) { + let len = self.text.len(); + let delta = Delta::simple_edit(Interval::new(0, len), content, len); + self.this_edit_type = EditType::Other; + let (text, delta, inval_lines) = self.add_delta(delta); + if set_pristine { + self.set_pristine(); + } + (text, delta, inval_lines) + } + + pub fn detect_indent(&mut self, default: impl FnOnce() -> IndentStyle) { + self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(default); + } + + pub fn indent_style(&self) -> IndentStyle { + self.indent_style + } + + // TODO: users of this function should often be using Styling::indent_style instead! + pub fn indent_unit(&self) -> &'static str { + self.indent_style.as_str() + } + + pub fn reset_edit_type(&mut self) { + self.last_edit_type = EditType::Other; + } + + /// Apply edits + /// Returns `(Text before delta, delta, invalidated lines)` + pub fn edit<'a, I, E, S>( + &mut self, + edits: I, + edit_type: EditType, + ) -> (Rope, RopeDelta, InvalLines) + where + I: IntoIterator, + E: Borrow<(S, &'a str)>, + S: AsRef, + { + let mut builder = DeltaBuilder::new(self.len()); + let mut interval_rope = Vec::new(); + for edit in edits { + let (selection, content) = edit.borrow(); + let rope = Rope::from(content); + for region in selection.as_ref().regions() { + interval_rope.push((region.min(), region.max(), rope.clone())); + } + } + interval_rope.sort_by(|a, b| { + if a.0 == b.0 && a.1 == b.1 { + Ordering::Equal + } else if a.1 == b.0 { + Ordering::Less + } else { + a.1.cmp(&b.0) + } + }); + for (start, end, rope) in interval_rope.into_iter() { + builder.replace(start..end, rope); + } + let delta = builder.build(); + self.this_edit_type = edit_type; + self.add_delta(delta) + } + + // TODO: don't clone the delta and return it, if the caller needs it then they can clone it + fn add_delta(&mut self, delta: RopeDelta) -> (Rope, RopeDelta, InvalLines) { + let text = self.text.clone(); + + let undo_group = self.calculate_undo_group(); + self.last_edit_type = self.this_edit_type; + + let (new_rev, new_text, new_tombstones, new_deletes_from_union) = + self.mk_new_rev(undo_group, delta.clone()); + + let inval_lines = self.apply_edit( + &delta, + new_rev, + new_text, + new_tombstones, + new_deletes_from_union, + ); + + (text, delta, inval_lines) + } + + fn apply_edit( + &mut self, + delta: &RopeDelta, + new_rev: Revision, + new_text: Rope, + new_tombstones: Rope, + new_deletes_from_union: Subset, + ) -> InvalLines { + self.rev_counter += 1; + + let (iv, newlen) = delta.summary(); + let old_logical_end_line = self.text.line_of_offset(iv.end) + 1; + let old_text = self.text.clone(); + + self.revs.push(new_rev); + self.text = new_text; + self.tombstones = new_tombstones; + self.deletes_from_union = new_deletes_from_union; + + let logical_start_line = self.text.line_of_offset(iv.start); + let new_logical_end_line = self.text.line_of_offset(iv.start + newlen) + 1; + let old_hard_count = old_logical_end_line - logical_start_line; + let new_hard_count = new_logical_end_line - logical_start_line; + + let inval_lines = InvalLines { + start_line: logical_start_line, + inval_count: old_hard_count, + new_count: new_hard_count, + old_text, + }; + self.update_size(&inval_lines); + + inval_lines + } + + fn calculate_undo_group(&mut self) -> usize { + let has_undos = !self.live_undos.is_empty(); + let is_unbroken_group = !self.this_edit_type.breaks_undo_group(self.last_edit_type); + + if has_undos && is_unbroken_group { + *self.live_undos.last().unwrap() + } else { + let undo_group = self.undo_group_id; + self.live_undos.truncate(self.cur_undo); + self.live_undos.push(undo_group); + self.cur_undo += 1; + self.undo_group_id += 1; + undo_group + } + } + + fn mk_new_rev(&self, undo_group: usize, delta: RopeDelta) -> (Revision, Rope, Rope, Subset) { + let (ins_delta, deletes) = delta.factor(); + + let deletes_at_rev = &self.deletes_from_union; + + let union_ins_delta = ins_delta.transform_expand(deletes_at_rev, true); + let mut new_deletes = deletes.transform_expand(deletes_at_rev); + + let new_inserts = union_ins_delta.inserted_subset(); + if !new_inserts.is_empty() { + new_deletes = new_deletes.transform_expand(&new_inserts); + } + let cur_deletes_from_union = &self.deletes_from_union; + let text_ins_delta = union_ins_delta.transform_shrink(cur_deletes_from_union); + let text_with_inserts = text_ins_delta.apply(&self.text); + let rebased_deletes_from_union = cur_deletes_from_union.transform_expand(&new_inserts); + + let undone = self.undone_groups.contains(&undo_group); + let new_deletes_from_union = { + let to_delete = if undone { &new_inserts } else { &new_deletes }; + rebased_deletes_from_union.union(to_delete) + }; + + let (new_text, new_tombstones) = shuffle( + &text_with_inserts, + &self.tombstones, + &rebased_deletes_from_union, + &new_deletes_from_union, + ); + + let head_rev = self.revs.last().unwrap(); + self.atomic_rev + .store(self.rev_counter, atomic::Ordering::Release); + ( + Revision { + num: self.rev_counter, + max_undo_so_far: std::cmp::max(undo_group, head_rev.max_undo_so_far), + edit: Contents::Edit { + undo_group, + inserts: new_inserts, + deletes: new_deletes, + }, + cursor_before: None, + cursor_after: None, + }, + new_text, + new_tombstones, + new_deletes_from_union, + ) + } + + fn deletes_from_union_for_index(&self, rev_index: usize) -> Cow { + self.deletes_from_union_before_index(rev_index + 1, true) + } + + fn deletes_from_cur_union_for_index(&self, rev_index: usize) -> Cow { + let mut deletes_from_union = self.deletes_from_union_for_index(rev_index); + for rev in &self.revs[rev_index + 1..] { + if let Contents::Edit { ref inserts, .. } = rev.edit { + if !inserts.is_empty() { + deletes_from_union = Cow::Owned(deletes_from_union.transform_union(inserts)); + } + } + } + deletes_from_union + } + + fn deletes_from_union_before_index(&self, rev_index: usize, invert_undos: bool) -> Cow { + let mut deletes_from_union = Cow::Borrowed(&self.deletes_from_union); + let mut undone_groups = Cow::Borrowed(&self.undone_groups); + + // invert the changes to deletes_from_union starting in the present and working backwards + for rev in self.revs[rev_index..].iter().rev() { + deletes_from_union = match rev.edit { + Contents::Edit { + ref inserts, + ref deletes, + ref undo_group, + .. + } => { + if undone_groups.contains(undo_group) { + // no need to un-delete undone inserts since we'll just shrink them out + Cow::Owned(deletes_from_union.transform_shrink(inserts)) + } else { + let un_deleted = deletes_from_union.subtract(deletes); + Cow::Owned(un_deleted.transform_shrink(inserts)) + } + } + Contents::Undo { + ref toggled_groups, + ref deletes_bitxor, + } => { + if invert_undos { + let new_undone = undone_groups + .symmetric_difference(toggled_groups) + .cloned() + .collect(); + undone_groups = Cow::Owned(new_undone); + Cow::Owned(deletes_from_union.bitxor(deletes_bitxor)) + } else { + deletes_from_union + } + } + } + } + deletes_from_union + } + + fn find_first_undo_candidate_index(&self, toggled_groups: &BTreeSet) -> usize { + // find the lowest toggled undo group number + if let Some(lowest_group) = toggled_groups.iter().cloned().next() { + for (i, rev) in self.revs.iter().enumerate().rev() { + if rev.max_undo_so_far < lowest_group { + return i + 1; // +1 since we know the one we just found doesn't have it + } + } + 0 + } else { + // no toggled groups, return past end + self.revs.len() + } + } + + fn compute_undo(&self, groups: &BTreeSet) -> (Revision, Subset) { + let toggled_groups = self + .undone_groups + .symmetric_difference(groups) + .cloned() + .collect(); + let first_candidate = self.find_first_undo_candidate_index(&toggled_groups); + // the `false` below: don't invert undos since our first_candidate is based on the current undo set, not past + let mut deletes_from_union = self + .deletes_from_union_before_index(first_candidate, false) + .into_owned(); + + for rev in &self.revs[first_candidate..] { + if let Contents::Edit { + ref undo_group, + ref inserts, + ref deletes, + .. + } = rev.edit + { + if groups.contains(undo_group) { + if !inserts.is_empty() { + deletes_from_union = deletes_from_union.transform_union(inserts); + } + } else { + if !inserts.is_empty() { + deletes_from_union = deletes_from_union.transform_expand(inserts); + } + if !deletes.is_empty() { + deletes_from_union = deletes_from_union.union(deletes); + } + } + } + } + + let cursor_before = self + .revs + .get(first_candidate) + .and_then(|rev| rev.cursor_before.clone()); + + let cursor_after = self + .revs + .get(first_candidate) + .and_then(|rev| match &rev.edit { + Contents::Edit { undo_group, .. } => Some(undo_group), + Contents::Undo { .. } => None, + }) + .and_then(|group| { + let mut cursor = None; + for rev in &self.revs[first_candidate..] { + if let Contents::Edit { ref undo_group, .. } = rev.edit { + if group == undo_group { + cursor = rev.cursor_after.as_ref(); + } else { + break; + } + } + } + cursor.cloned() + }); + + let deletes_bitxor = self.deletes_from_union.bitxor(&deletes_from_union); + let max_undo_so_far = self.revs.last().unwrap().max_undo_so_far; + self.atomic_rev + .store(self.rev_counter, atomic::Ordering::Release); + ( + Revision { + num: self.rev_counter, + max_undo_so_far, + edit: Contents::Undo { + toggled_groups, + deletes_bitxor, + }, + cursor_before, + cursor_after, + }, + deletes_from_union, + ) + } + + fn undo( + &mut self, + groups: BTreeSet, + ) -> ( + Rope, + RopeDelta, + InvalLines, + Option, + Option, + ) { + let text = self.text.clone(); + let (new_rev, new_deletes_from_union) = self.compute_undo(&groups); + let delta = Delta::synthesize( + &self.tombstones, + &self.deletes_from_union, + &new_deletes_from_union, + ); + let new_text = delta.apply(&self.text); + let new_tombstones = shuffle_tombstones( + &self.text, + &self.tombstones, + &self.deletes_from_union, + &new_deletes_from_union, + ); + self.undone_groups = groups; + + let cursor_before = new_rev.cursor_before.clone(); + let cursor_after = new_rev.cursor_after.clone(); + + let inval_lines = self.apply_edit( + &delta, + new_rev, + new_text, + new_tombstones, + new_deletes_from_union, + ); + + (text, delta, inval_lines, cursor_before, cursor_after) + } + + pub fn do_undo(&mut self) -> Option<(Rope, RopeDelta, InvalLines, Option)> { + if self.cur_undo <= 1 { + return None; + } + + self.cur_undo -= 1; + self.undos.insert(self.live_undos[self.cur_undo]); + self.last_edit_type = EditType::Undo; + let (text, delta, inval_lines, cursor_before, _cursor_after) = + self.undo(self.undos.clone()); + + Some((text, delta, inval_lines, cursor_before)) + } + + pub fn do_redo(&mut self) -> Option<(Rope, RopeDelta, InvalLines, Option)> { + if self.cur_undo >= self.live_undos.len() { + return None; + } + + self.undos.remove(&self.live_undos[self.cur_undo]); + self.cur_undo += 1; + self.last_edit_type = EditType::Redo; + let (text, delta, inval_lines, _cursor_before, cursor_after) = + self.undo(self.undos.clone()); + + Some((text, delta, inval_lines, cursor_after)) + } + + pub fn move_word_forward(&self, offset: usize) -> usize { + self.move_n_words_forward(offset, 1) + } + + pub fn move_word_backward(&self, offset: usize, mode: Mode) -> usize { + self.move_n_words_backward(offset, 1, mode) + } + + pub fn char_at_offset(&self, offset: usize) -> Option { + if self.is_empty() { + return None; + } + let offset = offset.min(self.len()); + WordCursor::new(&self.text, offset) + .inner + .peek_next_codepoint() + } +} + +impl RopeText for Buffer { + fn text(&self) -> &Rope { + &self.text + } +} + +fn shuffle_tombstones( + text: &Rope, + tombstones: &Rope, + old_deletes_from_union: &Subset, + new_deletes_from_union: &Subset, +) -> Rope { + // Taking the complement of deletes_from_union leads to an interleaving valid for swapped text and tombstones, + // allowing us to use the same method to insert the text into the tombstones. + let inverse_tombstones_map = old_deletes_from_union.complement(); + let move_delta = Delta::synthesize( + text, + &inverse_tombstones_map, + &new_deletes_from_union.complement(), + ); + move_delta.apply(tombstones) +} + +fn shuffle( + text: &Rope, + tombstones: &Rope, + old_deletes_from_union: &Subset, + new_deletes_from_union: &Subset, +) -> (Rope, Rope) { + // Delta that deletes the right bits from the text + let del_delta = Delta::synthesize(tombstones, old_deletes_from_union, new_deletes_from_union); + let new_text = del_delta.apply(text); + ( + new_text, + shuffle_tombstones( + text, + tombstones, + old_deletes_from_union, + new_deletes_from_union, + ), + ) +} + +pub struct DeltaValueRegion<'a, N: NodeInfo + 'a> { + pub old_offset: usize, + pub new_offset: usize, + pub len: usize, + pub node: &'a Node, +} + +/// Modified version of `xi_rope::delta::InsertsIter` which includes the node +pub struct InsertsValueIter<'a, N: NodeInfo + 'a> { + pos: usize, + last_end: usize, + els_iter: std::slice::Iter<'a, DeltaElement>, +} +impl<'a, N: NodeInfo + 'a> InsertsValueIter<'a, N> { + pub fn new(delta: &'a Delta) -> InsertsValueIter<'a, N> { + InsertsValueIter { + pos: 0, + last_end: 0, + els_iter: delta.els.iter(), + } + } +} +impl<'a, N: NodeInfo> Iterator for InsertsValueIter<'a, N> { + type Item = DeltaValueRegion<'a, N>; + + fn next(&mut self) -> Option { + for elem in &mut self.els_iter { + match *elem { + DeltaElement::Copy(b, e) => { + self.pos += e - b; + self.last_end = e; + } + DeltaElement::Insert(ref n) => { + let result = Some(DeltaValueRegion { + old_offset: self.last_end, + new_offset: self.pos, + len: n.len(), + node: n, + }); + self.pos += n.len(); + self.last_end += n.len(); + return result; + } + } + } + None + } +} + +#[cfg(test)] +mod test; diff --git a/editor-core/src/buffer/rope_text.rs b/editor-core/src/buffer/rope_text.rs new file mode 100644 index 00000000..a6972016 --- /dev/null +++ b/editor-core/src/buffer/rope_text.rs @@ -0,0 +1,635 @@ +use std::{borrow::Cow, ops::Range}; + +use lapce_xi_rope::{interval::IntervalBounds, rope::ChunkIter, Cursor, Rope}; + +use crate::{mode::Mode, paragraph::ParagraphCursor, word::WordCursor}; + +pub trait RopeText { + fn text(&self) -> &Rope; + + fn len(&self) -> usize { + self.text().len() + } + + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// The last line of the held rope + fn last_line(&self) -> usize { + self.line_of_offset(self.len()) + } + + /// Get the offset into the rope of the start of the given line. + /// If the line it out of bounds, then the last offset (the len) is returned. + fn offset_of_line(&self, line: usize) -> usize { + let last_line = self.last_line(); + let line = line.min(last_line + 1); + self.text().offset_of_line(line) + } + + fn offset_line_end(&self, offset: usize, caret: bool) -> usize { + let line = self.line_of_offset(offset); + self.line_end_offset(line, caret) + } + + fn line_of_offset(&self, offset: usize) -> usize { + let offset = offset.min(self.len()); + let offset = self + .text() + .at_or_prev_codepoint_boundary(offset) + .unwrap_or(offset); + + self.text().line_of_offset(offset) + } + + fn offset_to_line_col(&self, offset: usize) -> (usize, usize) { + let offset = offset.min(self.len()); + let line = self.line_of_offset(offset); + let line_start = self.offset_of_line(line); + if offset == line_start { + return (line, 0); + } + + let col = offset - line_start; + (line, col) + } + + fn offset_of_line_col(&self, line: usize, col: usize) -> usize { + let mut pos = 0; + let mut offset = self.offset_of_line(line); + for c in self + .slice_to_cow(offset..self.offset_of_line(line + 1)) + .chars() + { + if c == '\n' { + return offset; + } + + let char_len = c.len_utf8(); + if pos + char_len > col { + return offset; + } + pos += char_len; + offset += char_len; + } + offset + } + + fn line_end_col(&self, line: usize, caret: bool) -> usize { + let line_start = self.offset_of_line(line); + let offset = self.line_end_offset(line, caret); + offset - line_start + } + + /// Get the offset of the end of the line. The caret decides whether it is after the last + /// character, or before it. + /// If the line is out of bounds, then the last offset (the len) is returned. + /// ```rust,ignore + /// let text = Rope::from("hello\nworld"); + /// let text = RopeText::new(&text); + /// assert_eq!(text.line_end_offset(0, false), 4); // "hell|o" + /// assert_eq!(text.line_end_offset(0, true), 5); // "hello|" + /// assert_eq!(text.line_end_offset(1, false), 10); // "worl|d" + /// assert_eq!(text.line_end_offset(1, true), 11); // "world|" + /// // Out of bounds + /// assert_eq!(text.line_end_offset(2, false), 11); // "world|" + /// ``` + fn line_end_offset(&self, line: usize, caret: bool) -> usize { + let mut offset = self.offset_of_line(line + 1); + let mut line_content: &str = &self.line_content(line); + if line_content.ends_with("\r\n") { + offset -= 2; + line_content = &line_content[..line_content.len() - 2]; + } else if line_content.ends_with('\n') { + offset -= 1; + line_content = &line_content[..line_content.len() - 1]; + } + if !caret && !line_content.is_empty() { + offset = self.prev_grapheme_offset(offset, 1, 0); + } + offset + } + + /// Returns the content of the given line. + /// Includes the line ending if it exists. (-> the last line won't have a line ending) + /// Lines past the end of the document will return an empty string. + fn line_content(&self, line: usize) -> Cow<'_, str> { + self.text() + .slice_to_cow(self.offset_of_line(line)..self.offset_of_line(line + 1)) + } + + /// Get the offset of the previous grapheme cluster. + fn prev_grapheme_offset(&self, offset: usize, count: usize, limit: usize) -> usize { + let offset = offset.min(self.len()); + let mut cursor = Cursor::new(self.text(), offset); + let mut new_offset = offset; + for _i in 0..count { + if let Some(prev_offset) = cursor.prev_grapheme() { + if prev_offset < limit { + return new_offset; + } + new_offset = prev_offset; + cursor.set(prev_offset); + } else { + return new_offset; + } + } + new_offset + } + + fn next_grapheme_offset(&self, offset: usize, count: usize, limit: usize) -> usize { + let offset = if offset > self.len() { + self.len() + } else { + offset + }; + let mut cursor = Cursor::new(self.text(), offset); + let mut new_offset = offset; + for _i in 0..count { + if let Some(next_offset) = cursor.next_grapheme() { + if next_offset > limit { + return new_offset; + } + new_offset = next_offset; + cursor.set(next_offset); + } else { + return new_offset; + } + } + new_offset + } + + fn prev_code_boundary(&self, offset: usize) -> usize { + WordCursor::new(self.text(), offset).prev_code_boundary() + } + + fn next_code_boundary(&self, offset: usize) -> usize { + WordCursor::new(self.text(), offset).next_code_boundary() + } + + /// Return the previous and end boundaries of the word under cursor. + fn select_word(&self, offset: usize) -> (usize, usize) { + WordCursor::new(self.text(), offset).select_word() + } + + /// Returns the offset of the first non-blank character on the given line. + /// If the line is one past the last line, then the offset at the end of the rope is returned. + /// If the line is further past that, then it defaults to the last line. + fn first_non_blank_character_on_line(&self, line: usize) -> usize { + let last_line = self.last_line(); + let line = if line > last_line + 1 { + last_line + } else { + line + }; + let line_start_offset = self.text().offset_of_line(line); + WordCursor::new(self.text(), line_start_offset).next_non_blank_char() + } + + fn indent_on_line(&self, line: usize) -> String { + let line_start_offset = self.text().offset_of_line(line); + let word_boundary = WordCursor::new(self.text(), line_start_offset).next_non_blank_char(); + let indent = self.text().slice_to_cow(line_start_offset..word_boundary); + indent.to_string() + } + + /// Get the content of the rope as a Cow string, for 'nice' ranges (small, and at the right + /// offsets) this will be a reference to the rope's data. Otherwise, it allocates a new string. + /// You should be somewhat wary of requesting large parts of the rope, as it will allocate + /// a new string since it isn't contiguous in memory for large chunks. + fn slice_to_cow(&self, range: Range) -> Cow<'_, str> { + self.text() + .slice_to_cow(range.start.min(self.len())..range.end.min(self.len())) + } + + // TODO(minor): Once you can have an `impl Trait` return type in a trait, this could use that. + /// Iterate over (utf8_offset, char) values in the given range + #[allow(clippy::type_complexity)] + /// This uses `iter_chunks` and so does not allocate, compared to `slice_to_cow` which can + fn char_indices_iter<'a, T: IntervalBounds>( + &'a self, + range: T, + ) -> CharIndicesJoin< + std::str::CharIndices<'a>, + std::iter::Map, fn(&str) -> std::str::CharIndices<'_>>, + > { + let iter: ChunkIter<'a> = self.text().iter_chunks(range); + let iter: std::iter::Map, fn(&str) -> std::str::CharIndices<'_>> = + iter.map(str::char_indices); + CharIndicesJoin::new(iter) + } + + /// The number of lines in the file + fn num_lines(&self) -> usize { + self.last_line() + 1 + } + + /// The length of the given line + fn line_len(&self, line: usize) -> usize { + self.offset_of_line(line + 1) - self.offset_of_line(line) + } + + /// Returns `true` if the given line contains no non-whitespace characters. + fn is_line_whitespace(&self, line: usize) -> bool { + let line_start_offset = self.text().offset_of_line(line); + let mut word_cursor = WordCursor::new(self.text(), line_start_offset); + + word_cursor.next_non_blank_char(); + let c = word_cursor.inner.next_codepoint(); + + match c { + None | Some('\n') => true, + Some('\r') => { + let c = word_cursor.inner.next_codepoint(); + c.is_some_and(|c| c == '\n') + } + _ => false, + } + } + + fn move_left(&self, offset: usize, mode: Mode, count: usize) -> usize { + let min_offset = if mode == Mode::Insert { + 0 + } else { + let line = self.line_of_offset(offset); + self.offset_of_line(line) + }; + + self.prev_grapheme_offset(offset, count, min_offset) + } + + fn move_right(&self, offset: usize, mode: Mode, count: usize) -> usize { + let max_offset = if mode == Mode::Insert { + self.len() + } else { + self.offset_line_end(offset, mode != Mode::Normal) + }; + + self.next_grapheme_offset(offset, count, max_offset) + } + + fn find_nth_paragraph(&self, offset: usize, mut count: usize, mut find_next: F) -> usize + where + F: FnMut(&mut ParagraphCursor) -> Option, + { + let mut cursor = ParagraphCursor::new(self.text(), offset); + let mut new_offset = offset; + while count != 0 { + // FIXME: wait for if-let-chain + if let Some(offset) = find_next(&mut cursor) { + new_offset = offset; + } else { + break; + } + count -= 1; + } + new_offset + } + + fn move_n_paragraphs_forward(&self, offset: usize, count: usize) -> usize { + self.find_nth_paragraph(offset, count, |cursor| cursor.next_boundary()) + } + + fn move_n_paragraphs_backward(&self, offset: usize, count: usize) -> usize { + self.find_nth_paragraph(offset, count, |cursor| cursor.prev_boundary()) + } + + /// Find the nth (`count`) word starting at `offset` in either direction + /// depending on `find_next`. + /// + /// A `WordCursor` is created and given to the `find_next` function for the + /// search. The `find_next` function should return None when there is no + /// more word found. Despite the name, `find_next` can search in either + /// direction. + fn find_nth_word(&self, offset: usize, mut count: usize, mut find_next: F) -> usize + where + F: FnMut(&mut WordCursor) -> Option, + { + let mut cursor = WordCursor::new(self.text(), offset); + let mut new_offset = offset; + while count != 0 { + // FIXME: wait for if-let-chain + if let Some(offset) = find_next(&mut cursor) { + new_offset = offset; + } else { + break; + } + count -= 1; + } + new_offset + } + + fn move_n_words_forward(&self, offset: usize, count: usize) -> usize { + self.find_nth_word(offset, count, |cursor| cursor.next_boundary()) + } + + fn move_n_wordends_forward(&self, offset: usize, count: usize, inserting: bool) -> usize { + let mut new_offset = self.find_nth_word(offset, count, |cursor| cursor.end_boundary()); + if !inserting && new_offset != self.len() { + new_offset = self.prev_grapheme_offset(new_offset, 1, 0); + } + new_offset + } + + fn move_n_words_backward(&self, offset: usize, count: usize, mode: Mode) -> usize { + self.find_nth_word(offset, count, |cursor| cursor.prev_boundary(mode)) + } + + fn move_word_backward_deletion(&self, offset: usize) -> usize { + self.find_nth_word(offset, 1, |cursor| cursor.prev_deletion_boundary()) + } +} + +#[derive(Clone)] +pub struct RopeTextVal { + pub text: Rope, +} +impl RopeTextVal { + pub fn new(text: Rope) -> Self { + Self { text } + } +} +impl RopeText for RopeTextVal { + fn text(&self) -> &Rope { + &self.text + } +} +impl From for RopeTextVal { + fn from(text: Rope) -> Self { + Self::new(text) + } +} +#[derive(Copy, Clone)] +pub struct RopeTextRef<'a> { + pub text: &'a Rope, +} +impl<'a> RopeTextRef<'a> { + pub fn new(text: &'a Rope) -> Self { + Self { text } + } +} +impl<'a> RopeText for RopeTextRef<'a> { + fn text(&self) -> &Rope { + self.text + } +} +impl<'a> From<&'a Rope> for RopeTextRef<'a> { + fn from(text: &'a Rope) -> Self { + Self::new(text) + } +} + +/// Joins an iterator of iterators over char indices `(usize, char)` into one +/// as if they were from a single long string +/// Assumes the iterators end after the first `None` value +#[derive(Clone)] +pub struct CharIndicesJoin, O: Iterator> { + /// Our iterator of iterators + main_iter: O, + /// Our current working iterator of indices + current_indices: Option, + /// The amount we should shift future offsets + current_base: usize, + /// The latest base, since we don't know when the `current_indices` iterator will end + latest_base: usize, +} + +impl, O: Iterator> CharIndicesJoin { + pub fn new(main_iter: O) -> CharIndicesJoin { + CharIndicesJoin { + main_iter, + current_indices: None, + current_base: 0, + latest_base: 0, + } + } +} + +impl, O: Iterator> Iterator for CharIndicesJoin { + type Item = (usize, char); + + fn next(&mut self) -> Option { + if let Some(current) = &mut self.current_indices { + if let Some((next_offset, next_ch)) = current.next() { + // Shift by the current base offset, which is the accumulated offset from previous + // iterators, which makes so the offset produced looks like it is from one long str + let next_offset = self.current_base + next_offset; + // Store the latest base offset, because we don't know when the current iterator + // will end (though technically the str iterator impl does) + self.latest_base = next_offset + next_ch.len_utf8(); + return Some((next_offset, next_ch)); + } + } + + // Otherwise, if we didn't return something above, then we get a next iterator + if let Some(next_current) = self.main_iter.next() { + // Update our current working iterator + self.current_indices = Some(next_current); + // Update the current base offset with the previous iterators latest offset base + // This is what we are shifting by + self.current_base = self.latest_base; + + // Get the next item without new current iterator + // As long as main_iter and the iterators it produces aren't infinite then this + // recursion won't be infinite either + // and even the non-recursion version would be infinite if those were infinite + self.next() + } else { + // We didn't get anything from the main iter, so we're completely done. + None + } + } +} + +#[cfg(test)] +mod tests { + use lapce_xi_rope::Rope; + + use super::RopeText; + use crate::buffer::rope_text::RopeTextVal; + + #[test] + fn test_line_content() { + let text = Rope::from(""); + let text = RopeTextVal::new(text); + + assert_eq!(text.line_content(0), ""); + assert_eq!(text.line_content(1), ""); + assert_eq!(text.line_content(2), ""); + + let text = Rope::from("abc\ndef\nghi"); + let text = RopeTextVal::new(text); + + assert_eq!(text.line_content(0), "abc\n"); + assert_eq!(text.line_content(1), "def\n"); + assert_eq!(text.line_content(2), "ghi"); + assert_eq!(text.line_content(3), ""); + assert_eq!(text.line_content(4), ""); + assert_eq!(text.line_content(5), ""); + + let text = Rope::from("abc\r\ndef\r\nghi"); + let text = RopeTextVal::new(text); + + assert_eq!(text.line_content(0), "abc\r\n"); + assert_eq!(text.line_content(1), "def\r\n"); + assert_eq!(text.line_content(2), "ghi"); + assert_eq!(text.line_content(3), ""); + assert_eq!(text.line_content(4), ""); + assert_eq!(text.line_content(5), ""); + } + + #[test] + fn test_offset_of_line() { + let text = Rope::from(""); + let text = RopeTextVal::new(text); + + assert_eq!(text.offset_of_line(0), 0); + assert_eq!(text.offset_of_line(1), 0); + assert_eq!(text.offset_of_line(2), 0); + + let text = Rope::from("abc\ndef\nghi"); + let text = RopeTextVal::new(text); + + assert_eq!(text.offset_of_line(0), 0); + assert_eq!(text.offset_of_line(1), 4); + assert_eq!(text.offset_of_line(2), 8); + assert_eq!(text.offset_of_line(3), text.len()); // 11 + assert_eq!(text.offset_of_line(4), text.len()); + assert_eq!(text.offset_of_line(5), text.len()); + + let text = Rope::from("abc\r\ndef\r\nghi"); + let text = RopeTextVal::new(text); + + assert_eq!(text.offset_of_line(0), 0); + assert_eq!(text.offset_of_line(1), 5); + assert_eq!(text.offset_of_line(2), 10); + assert_eq!(text.offset_of_line(3), text.len()); // 13 + assert_eq!(text.offset_of_line(4), text.len()); + assert_eq!(text.offset_of_line(5), text.len()); + } + + #[test] + fn test_line_end_offset() { + let text = Rope::from(""); + let text = RopeTextVal::new(text); + + assert_eq!(text.line_end_offset(0, false), 0); + assert_eq!(text.line_end_offset(0, true), 0); + assert_eq!(text.line_end_offset(1, false), 0); + assert_eq!(text.line_end_offset(1, true), 0); + assert_eq!(text.line_end_offset(2, false), 0); + assert_eq!(text.line_end_offset(2, true), 0); + + let text = Rope::from("abc\ndef\nghi"); + let text = RopeTextVal::new(text); + + assert_eq!(text.line_end_offset(0, false), 2); + assert_eq!(text.line_end_offset(0, true), 3); + assert_eq!(text.line_end_offset(1, false), 6); + assert_eq!(text.line_end_offset(1, true), 7); + assert_eq!(text.line_end_offset(2, false), 10); + assert_eq!(text.line_end_offset(2, true), text.len()); + assert_eq!(text.line_end_offset(3, false), text.len()); + assert_eq!(text.line_end_offset(3, true), text.len()); + assert_eq!(text.line_end_offset(4, false), text.len()); + assert_eq!(text.line_end_offset(4, true), text.len()); + + // This is equivalent to the doc test for RopeText::line_end_offset + // because you don't seem to be able to do a `use RopeText` in a doc test since it isn't + // public.. + let text = Rope::from("hello\nworld"); + let text = RopeTextVal::new(text); + + assert_eq!(text.line_end_offset(0, false), 4); // "hell|o" + assert_eq!(text.line_end_offset(0, true), 5); // "hello|" + assert_eq!(text.line_end_offset(1, false), 10); // "worl|d" + assert_eq!(text.line_end_offset(1, true), 11); // "world|" + // Out of bounds + assert_eq!(text.line_end_offset(2, false), 11); // "world|" + } + + #[test] + fn test_prev_grapheme_offset() { + let text = Rope::from(""); + let text = RopeTextVal::new(text); + + assert_eq!(text.prev_grapheme_offset(0, 0, 0), 0); + assert_eq!(text.prev_grapheme_offset(0, 1, 0), 0); + assert_eq!(text.prev_grapheme_offset(0, 1, 1), 0); + + let text = Rope::from("abc def ghi"); + let text = RopeTextVal::new(text); + + assert_eq!(text.prev_grapheme_offset(0, 0, 0), 0); + assert_eq!(text.prev_grapheme_offset(0, 1, 0), 0); + assert_eq!(text.prev_grapheme_offset(0, 1, 1), 0); + assert_eq!(text.prev_grapheme_offset(2, 1, 0), 1); + assert_eq!(text.prev_grapheme_offset(2, 1, 1), 1); + } + + #[test] + fn test_first_non_blank_character_on_line() { + let text = Rope::from(""); + let text = RopeTextVal::new(text); + + assert_eq!(text.first_non_blank_character_on_line(0), 0); + assert_eq!(text.first_non_blank_character_on_line(1), 0); + assert_eq!(text.first_non_blank_character_on_line(2), 0); + + let text = Rope::from("abc\ndef\nghi"); + let text = RopeTextVal::new(text); + + assert_eq!(text.first_non_blank_character_on_line(0), 0); + assert_eq!(text.first_non_blank_character_on_line(1), 4); + assert_eq!(text.first_non_blank_character_on_line(2), 8); + assert_eq!(text.first_non_blank_character_on_line(3), 11); + assert_eq!(text.first_non_blank_character_on_line(4), 8); + assert_eq!(text.first_non_blank_character_on_line(5), 8); + + let text = Rope::from("abc\r\ndef\r\nghi"); + let text = RopeTextVal::new(text); + + assert_eq!(text.first_non_blank_character_on_line(0), 0); + assert_eq!(text.first_non_blank_character_on_line(1), 5); + assert_eq!(text.first_non_blank_character_on_line(2), 10); + assert_eq!(text.first_non_blank_character_on_line(3), 13); + assert_eq!(text.first_non_blank_character_on_line(4), 10); + assert_eq!(text.first_non_blank_character_on_line(5), 10); + } + + #[test] + fn test_is_line_whitespace() { + let text = Rope::from(""); + let text = RopeTextVal::new(text); + + assert!(text.is_line_whitespace(0)); + + let text = Rope::from("\n \t\r\t \t \n"); + let text = RopeTextVal::new(text); + + assert!(text.is_line_whitespace(0)); + assert!(!text.is_line_whitespace(1)); + assert!(text.is_line_whitespace(2)); + + let text = Rope::from("qwerty\n\tf\t\r\n00"); + let text = RopeTextVal::new(text); + + assert!(!text.is_line_whitespace(0)); + assert!(!text.is_line_whitespace(1)); + assert!(!text.is_line_whitespace(2)); + + let text = Rope::from(" \r#\n\t \r\n)\t\t\t\t\t\t\t\t"); + let text = RopeTextVal::new(text); + + assert!(!text.is_line_whitespace(0)); + assert!(text.is_line_whitespace(1)); + assert!(!text.is_line_whitespace(2)); + + let text = Rope::from(" \r\n \r"); + let text = RopeTextVal::new(text); + + assert!(text.is_line_whitespace(0)); + assert!(!text.is_line_whitespace(1)); + } +} diff --git a/editor-core/src/buffer/test.rs b/editor-core/src/buffer/test.rs new file mode 100644 index 00000000..b9742b79 --- /dev/null +++ b/editor-core/src/buffer/test.rs @@ -0,0 +1,168 @@ +use super::{Buffer, RopeText}; + +mod editing { + use lapce_xi_rope::Rope; + + use super::*; + use crate::{editor::EditType, selection::Selection}; + + #[test] + fn is_pristine() { + let mut buffer = Buffer::new(""); + buffer.init_content(Rope::from("abc")); + buffer.edit(&[(Selection::caret(0), "d")], EditType::InsertChars); + buffer.edit(&[(Selection::caret(0), "e")], EditType::InsertChars); + buffer.do_undo(); + buffer.do_undo(); + assert!(buffer.is_pristine()); + } +} + +mod motion { + use super::*; + use crate::mode::Mode; + + #[test] + fn cannot_move_in_empty_buffer() { + let buffer = Buffer::new(""); + assert_eq!(buffer.move_word_forward(0), 0); + assert_eq!(buffer.move_n_words_forward(0, 2), 0); + + assert_eq!(buffer.move_word_backward(0, Mode::Insert), 0); + assert_eq!(buffer.move_n_words_backward(0, 2, Mode::Insert), 0); + + assert_eq!(buffer.move_n_wordends_forward(0, 2, false), 0); + assert_eq!(buffer.move_n_wordends_forward(0, 2, true), 0); + } + + #[test] + fn on_word_boundary_in_either_direction() { + let buffer = Buffer::new("one two three four "); + // ->012345678901234567890<- + + // 0 count does not move. + assert_eq!(buffer.move_n_words_forward(0, 0), 0); + assert_eq!(buffer.move_n_words_backward(0, 0, Mode::Insert), 0); + + for offset in 0..4 { + assert_eq!(buffer.move_word_forward(offset), 4); + assert_eq!(buffer.move_word_backward(offset, Mode::Insert), 0); + } + + assert_eq!(buffer.move_word_forward(4), 8); + assert_eq!(buffer.move_word_backward(4, Mode::Insert), 0); + + let end = buffer.len() - 1; + for offset in 0..4 { + assert_eq!(buffer.move_n_words_forward(offset, 2), 8); + } + assert_eq!(buffer.move_n_words_forward(4, 2), 15); + for offset in 0..5 { + assert_eq!( + buffer.move_n_words_backward(end - offset, 2, Mode::Insert), + 8 + ) + } + assert_eq!(buffer.move_n_words_backward(end - 6, 2, Mode::Insert), 4); + + assert_eq!(buffer.move_n_words_forward(0, 2), 8); + assert_eq!(buffer.move_n_words_forward(0, 3), 15); + assert_eq!(buffer.move_n_words_backward(end, 2, Mode::Insert), 8); + assert_eq!(buffer.move_n_words_backward(end, 3, Mode::Insert), 4); + + // FIXME: see #501 for possible issues in WordCursor::next_boundary() + // + // Trying to move beyond the buffer end. The cursor will stay there. + for offset in 0..end { + assert_eq!(buffer.move_n_words_forward(offset, 100), end + 1); + } + + // In the other direction. + for offset in 0..end { + assert_eq!( + buffer.move_n_words_backward(end - offset, 100, Mode::Insert), + 0 + ); + } + } + + mod on_word_end_forward { + use super::*; + + #[test] + fn non_insertion_mode() { + // To save some keystrokes. + fn v(buf: &Buffer, off: usize, n: usize, end: usize) { + assert_eq!(buf.move_n_wordends_forward(off, n, false), end); + } + + let buffer = Buffer::new("one two three four "); + // ->012345678901234567890<- + + // 0 count does not move. + v(&buffer, 0, 0, 0); + + for offset in 0..2 { + v(&buffer, offset, 1, 2); + v(&buffer, offset, 2, 6); + v(&buffer, offset, 3, 12); + } + + let end = buffer.len() - 1; + // Trying to move beyond the buffer end. + for offset in 0..end { + v(&buffer, offset, 100, 21); + } + + v(&buffer, 2, 1, 6); + v(&buffer, 2, 2, 12); + v(&buffer, 2, 3, 18); + v(&buffer, 2, 10, 21); + + let buffer = Buffer::new("one\n\ntwo\n\n\nthree\n\n\n"); + // ->0123 4 5678 9 0 123456 7 8 <- + + v(&buffer, 0, 2, 7); + v(&buffer, 0, 3, 15); + v(&buffer, 0, 4, 19); + } + + #[test] + fn insertion_mode() { + fn v(buf: &Buffer, off: usize, n: usize, end: usize) { + assert_eq!(buf.move_n_wordends_forward(off, n, true), end); + } + + let buffer = Buffer::new("one two three four "); + // ->012345678901234567890<- + + // 0 count does not move. + v(&buffer, 0, 0, 0); + + for offset in 0..2 { + // In Mode::Insert, returns 1 pass the word end. + v(&buffer, offset, 1, 3); + v(&buffer, offset, 2, 7); + v(&buffer, offset, 3, 13); + } + + let end = buffer.len() - 1; + // Trying to move beyond the buffer end. + for offset in 0..end { + v(&buffer, offset, 100, 21); + } + + v(&buffer, 2, 1, 7); + v(&buffer, 2, 2, 13); + v(&buffer, 2, 3, 19); + v(&buffer, 2, 10, 21); + + let buffer = Buffer::new("one\n\ntwo\n\n\nthree\n\n\n"); + // ->0123 4 5678 9 0 123456 7 8 <- + + v(&buffer, 0, 2, 8); + v(&buffer, 0, 3, 16); + v(&buffer, 0, 4, 19); + } + } +} diff --git a/editor-core/src/char_buffer.rs b/editor-core/src/char_buffer.rs new file mode 100644 index 00000000..be2a28b9 --- /dev/null +++ b/editor-core/src/char_buffer.rs @@ -0,0 +1,1073 @@ +extern crate alloc; + +use alloc::{borrow::Cow, rc::Rc, sync::Arc}; +use core::{borrow::Borrow, cmp::Ordering, convert::AsRef, fmt, hash, ops::Deref, str}; + +/// This is a small memory buffer allocated on the stack to store a +/// ‘string slice’ of exactly one character in length. That is, this +/// structure stores the result of converting [`char`] to [`prim@str`] +/// (which can be accessed as [`&str`]). +/// +/// In other words, this struct is a helper for performing `char -> &str` +/// type conversion without heap allocation. +/// +/// # Note +/// +/// In general, it is not recommended to perform `char -> CharBuffer -> char` +/// type conversions, as this may affect performance. +/// +/// [`&str`]: https://doc.rust-lang.org/core/primitive.str.html +/// +/// # Examples +/// +/// ``` +/// use floem_editor_core::char_buffer::CharBuffer; +/// +/// let word = "goodbye"; +/// +/// let mut chars_buf = word.chars().map(CharBuffer::new); +/// +/// assert_eq!("g", chars_buf.next().unwrap().as_ref()); +/// assert_eq!("o", chars_buf.next().unwrap().as_ref()); +/// assert_eq!("o", chars_buf.next().unwrap().as_ref()); +/// assert_eq!("d", chars_buf.next().unwrap().as_ref()); +/// assert_eq!("b", chars_buf.next().unwrap().as_ref()); +/// assert_eq!("y", chars_buf.next().unwrap().as_ref()); +/// assert_eq!("e", chars_buf.next().unwrap().as_ref()); +/// +/// assert_eq!(None, chars_buf.next()); +/// +/// for (char, char_buf) in word.chars().zip(word.chars().map(CharBuffer::new)) { +/// assert_eq!(char.to_string(), char_buf); +/// } +/// ``` +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct CharBuffer { + len: usize, + buf: [u8; 4], +} + +/// The type of error returned when the conversion from [`prim@str`], [`String`] or their borrowed +/// or native forms to [`CharBuffer`] fails. +/// +/// This `structure` is created by various `CharBuffer::try_from` methods (for example, +/// by the [`CharBuffer::try_from<&str>`] method). +/// +/// See its documentation for more. +/// +/// [`CharBuffer::try_from<&str>`]: CharBuffer::try_from<&str> +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct CharBufferTryFromError(()); + +impl CharBuffer { + /// Creates a new `CharBuffer` from the given [`char`]. + /// + /// # Examples + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let char_buf = CharBuffer::new('a'); + /// assert_eq!("a", &char_buf); + /// + /// let string = "Some string"; + /// let char_vec = string.chars().map(CharBuffer::new).collect::>(); + /// assert_eq!( + /// ["S", "o", "m", "e", " ", "s", "t", "r", "i", "n", "g"].as_ref(), + /// &char_vec + /// ); + /// ``` + #[inline] + pub fn new(char: char) -> Self { + let mut buf = [0; 4]; + let len = char.encode_utf8(&mut buf).as_bytes().len(); + Self { len, buf } + } + + /// Converts a `CharBuffer` into an immutable string slice. + /// + /// # Examples + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let char_buf = CharBuffer::from('r'); + /// assert_eq!("r", char_buf.as_str()); + /// ``` + #[inline] + pub fn as_str(&self) -> &str { + self + } + + /// Returns the length of a `&str` stored inside the `CharBuffer`, in bytes, + /// not [`char`]s or graphemes. In other words, it might not be what a human + /// considers the length of the string. + /// + /// # Examples + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let f = CharBuffer::new('f'); + /// assert_eq!(f.len(), 1); + /// + /// let fancy_f = CharBuffer::new('ƒ'); + /// assert_eq!(fancy_f.len(), 2); + /// ``` + #[inline] + pub fn len(&self) -> usize { + self.len + } + + /// Always returns `false` since this structure can only be created from + /// [`char`], which cannot be empty. + /// + /// # Examples + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let c = CharBuffer::new('\0'); + /// assert!(!c.is_empty()); + /// assert_eq!(c.len(), 1); + /// ``` + #[inline] + pub fn is_empty(&self) -> bool { + false + } +} + +impl From for CharBuffer { + /// Creates a new [`CharBuffer`] from the given [`char`]. + /// + /// Calling this function is the same as calling [`new`](CharBuffer::new) + /// function. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let char_buf = CharBuffer::from('a'); + /// assert_eq!("a", &char_buf); + /// + /// let string = "Some string"; + /// let char_vec = string.chars().map(CharBuffer::from).collect::>(); + /// assert_eq!( + /// ["S", "o", "m", "e", " ", "s", "t", "r", "i", "n", "g"].as_ref(), + /// &char_vec + /// ); + /// ``` + #[inline] + fn from(char: char) -> Self { + Self::new(char) + } +} + +impl From<&char> for CharBuffer { + /// Converts a `&char` into a [`CharBuffer`]. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let string = "Some string"; + /// let char_vec = string.chars().collect::>(); + /// assert_eq!( + /// ['S', 'o', 'm', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g'].as_ref(), + /// &char_vec + /// ); + /// + /// let string_vec = char_vec.iter().map(CharBuffer::from).collect::>(); + /// + /// assert_eq!( + /// ["S", "o", "m", "e", " ", "s", "t", "r", "i", "n", "g"].as_ref(), + /// &string_vec + /// ); + /// ```` + #[inline] + fn from(char: &char) -> Self { + Self::new(*char) + } +} + +impl From<&mut char> for CharBuffer { + /// Converts a `&mut char` into a [`CharBuffer`]. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let string = "Some string"; + /// let mut char_vec = string.chars().collect::>(); + /// assert_eq!( + /// ['S', 'o', 'm', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g'].as_ref(), + /// &char_vec + /// ); + /// + /// let string_vec = char_vec + /// .iter_mut() + /// .map(CharBuffer::from) + /// .collect::>(); + /// + /// assert_eq!( + /// ["S", "o", "m", "e", " ", "s", "t", "r", "i", "n", "g"].as_ref(), + /// &string_vec + /// ); + /// ```` + #[inline] + fn from(char: &mut char) -> Self { + Self::new(*char) + } +} + +impl From for char { + /// Creates a new [`char`] from the given reference to [`CharBuffer`]. + /// + /// # Note + /// + /// In general, it is not recommended to perform `char -> CharBuffer -> char` + /// type conversions, as this may affect performance. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let char_buf = CharBuffer::from('a'); + /// let char: char = char_buf.into(); + /// assert_eq!('a', char); + /// + /// let string = "Some string"; + /// + /// // Such type conversions are not recommended, use `char` directly + /// let char_vec = string + /// .chars() + /// .map(CharBuffer::from) + /// .map(char::from) + /// .collect::>(); + /// + /// assert_eq!( + /// ['S', 'o', 'm', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g'].as_ref(), + /// &char_vec + /// ); + /// ```` + #[inline] + fn from(char: CharBuffer) -> Self { + // SAFETY: The structure stores a valid utf8 character + unsafe { char.chars().next().unwrap_unchecked() } + } +} + +impl From<&CharBuffer> for char { + /// Converts a `&CharBuffer` into a [`char`]. + /// + /// # Note + /// + /// In general, it is not recommended to perform `char -> CharBuffer -> char` + /// type conversions, as this may affect performance. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let char_buf = CharBuffer::from('a'); + /// let char: char = char::from(&char_buf); + /// assert_eq!('a', char); + /// + /// let string = "Some string"; + /// + /// // Such type conversions are not recommended, use `char` directly + /// let char_buf_vec = string.chars().map(CharBuffer::from).collect::>(); + /// let char_vec = char_buf_vec.iter().map(char::from).collect::>(); + /// + /// assert_eq!( + /// ['S', 'o', 'm', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g'].as_ref(), + /// &char_vec + /// ); + /// ```` + #[inline] + fn from(char: &CharBuffer) -> Self { + // SAFETY: The structure stores a valid utf8 character + unsafe { char.chars().next().unwrap_unchecked() } + } +} + +impl From<&CharBuffer> for CharBuffer { + /// Converts a `&CharBuffer` into a [`CharBuffer`]. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let char_buf1 = CharBuffer::from('a'); + /// let char_buf2: CharBuffer = CharBuffer::from(&char_buf1); + /// assert_eq!(char_buf1, char_buf2); + /// + /// let string = "Some string"; + /// let char_vec1 = string.chars().map(CharBuffer::from).collect::>(); + /// let char_vec2 = char_vec1.iter().map(CharBuffer::from).collect::>(); + /// + /// assert_eq!(char_vec1, char_vec2); + /// ```` + #[inline] + fn from(char: &CharBuffer) -> Self { + *char + } +} + +impl From for String { + /// Allocates an owned [`String`] from a single character. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let c: CharBuffer = CharBuffer::from('a'); + /// let s: String = String::from(c); + /// assert_eq!("a", &s[..]); + /// ``` + #[inline] + fn from(char: CharBuffer) -> Self { + char.as_ref().to_string() + } +} + +impl From<&CharBuffer> for String { + /// Allocates an owned [`String`] from a single character. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let c: CharBuffer = CharBuffer::from('a'); + /// let s: String = String::from(&c); + /// assert_eq!("a", &s[..]); + /// ``` + #[inline] + fn from(char: &CharBuffer) -> Self { + char.as_ref().to_string() + } +} + +impl<'a> From<&'a CharBuffer> for &'a str { + /// Converts a `&CharBuffer` into a [`prim@str`]. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let c: CharBuffer = CharBuffer::from('a'); + /// let s: &str = From::from(&c); + /// assert_eq!("a", &s[..]); + /// ``` + #[inline] + fn from(char: &'a CharBuffer) -> Self { + char + } +} + +impl<'a> From<&'a CharBuffer> for Cow<'a, str> { + /// Converts a `&'a CharBuffer` into a [`Cow<'a, str>`]. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// use std::borrow::Cow; + /// + /// let c: CharBuffer = CharBuffer::from('a'); + /// let s: Cow = From::from(&c); + /// assert_eq!("a", &s[..]); + /// ``` + /// [`Cow<'a, str>`]: https://doc.rust-lang.org/std/borrow/enum.Cow.html + #[inline] + fn from(s: &'a CharBuffer) -> Self { + Cow::Borrowed(&**s) + } +} + +impl From for Cow<'_, CharBuffer> { + /// Converts a `CharBuffer` into a [`Cow<'_, CharBuffer>`]. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// use std::borrow::Cow; + /// + /// let c: CharBuffer = CharBuffer::from('a'); + /// let s: Cow = From::from(c); + /// assert_eq!("a", &s[..]); + /// ``` + /// [`Cow<'_, CharBuffer>`]: https://doc.rust-lang.org/std/borrow/enum.Cow.html + #[inline] + fn from(s: CharBuffer) -> Self { + Cow::Owned(s) + } +} + +macro_rules! impl_from_to_ptr { + ( + $(#[$meta:meta])* + $ptr:ident + ) => { + $(#[$meta])* + impl From for $ptr { + #[inline] + fn from(s: CharBuffer) -> Self { + Self::from(&*s) + } + } + + $(#[$meta])* + impl From<&CharBuffer> for $ptr { + #[inline] + fn from(s: &CharBuffer) -> Self { + Self::from(&**s) + } + } + } +} + +impl_from_to_ptr! { + /// Converts a `CharBuffer` into a [`Arc`]. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// use std::sync::Arc; + /// + /// let c: CharBuffer = CharBuffer::from('a'); + /// let s1: Arc = From::from(&c); + /// assert_eq!("a", &s1[..]); + /// + /// let s2: Arc = From::from(c); + /// assert_eq!("a", &s2[..]); + /// ``` + /// [`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html + Arc +} + +impl_from_to_ptr! { + /// Converts a `CharBuffer` into a [`Box`]. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// + /// let c: CharBuffer = CharBuffer::from('a'); + /// let s1: Box = From::from(&c); + /// assert_eq!("a", &s1[..]); + /// + /// let s2: Box = From::from(c); + /// assert_eq!("a", &s2[..]); + /// ``` + /// [`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html + Box +} + +impl_from_to_ptr! { + /// Converts a `CharBuffer` into a [`Rc`]. + /// + /// # Example + /// + /// ``` + /// use floem_editor_core::char_buffer::CharBuffer; + /// use std::rc::Rc; + /// + /// let c: CharBuffer = CharBuffer::from('a'); + /// let s1: Rc = From::from(&c); + /// assert_eq!("a", &s1[..]); + /// + /// let s2: Rc = From::from(c); + /// assert_eq!("a", &s2[..]); + /// ``` + /// [`Rc`]: https://doc.rust-lang.org/std/rc/struct.Rc.html + Rc +} + +macro_rules! impl_try_from { + ($lhs:ty) => { + impl TryFrom<$lhs> for CharBuffer { + type Error = CharBufferTryFromError; + + fn try_from(str: $lhs) -> Result { + let mut chars = str.chars(); + match (chars.next(), chars.next()) { + (Some(char), None) => Ok(Self::new(char)), + _ => Err(CharBufferTryFromError(())), + } + } + } + }; +} + +impl_try_from!(&str); +impl_try_from!(&mut str); + +impl_try_from!(String); +impl_try_from!(&String); +impl_try_from!(&mut String); + +impl_try_from!(Box); +impl_try_from!(&Box); +impl_try_from!(&mut Box); + +impl_try_from!(Arc); +impl_try_from!(&Arc); +impl_try_from!(&mut Arc); + +impl_try_from!(Rc); +impl_try_from!(&Rc); +impl_try_from!(&mut Rc); + +impl Deref for CharBuffer { + type Target = str; + + #[inline] + fn deref(&self) -> &Self::Target { + // SAFETY: + // - This is the same buffer that we passed to `encode_utf8` during creating this structure, + // so valid utf8 is stored there; + // - The length was directly calculated from the `&str` returned by the `encode_utf8` function + unsafe { str::from_utf8_unchecked(self.buf.get_unchecked(..self.len)) } + } +} + +impl AsRef for CharBuffer { + #[inline] + fn as_ref(&self) -> &str { + self + } +} + +impl Borrow for CharBuffer { + #[inline] + fn borrow(&self) -> &str { + self + } +} + +#[allow(clippy::derived_hash_with_manual_eq)] +impl hash::Hash for CharBuffer { + #[inline] + fn hash(&self, hasher: &mut H) { + (**self).hash(hasher) + } +} + +impl fmt::Debug for CharBuffer { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&**self, f) + } +} + +impl fmt::Display for CharBuffer { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&**self, f) + } +} + +impl PartialOrd for CharBuffer { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for CharBuffer { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + (**self).cmp(&**other) + } +} + +macro_rules! impl_eq { + ($lhs:ty, $rhs: ty) => { + #[allow(unused_lifetimes)] + impl<'a, 'b> PartialEq<$rhs> for $lhs { + #[inline] + fn eq(&self, other: &$rhs) -> bool { + PartialEq::eq(&self[..], &other[..]) + } + } + + #[allow(unused_lifetimes)] + impl<'a, 'b> PartialEq<$lhs> for $rhs { + #[inline] + fn eq(&self, other: &$lhs) -> bool { + PartialEq::eq(&self[..], &other[..]) + } + } + }; +} + +impl_eq! { CharBuffer, str } +impl_eq! { CharBuffer, &'a str } +impl_eq! { CharBuffer, &'a mut str } + +impl_eq! { CharBuffer, String } +impl_eq! { CharBuffer, &'a String } +impl_eq! { CharBuffer, &'a mut String } + +impl_eq! { Cow<'a, str>, CharBuffer } +impl_eq! { Cow<'_, CharBuffer>, CharBuffer } + +#[allow(clippy::single_match)] +#[test] +fn test_char_buffer() { + #[cfg(miri)] + let mut string = String::from( + " + This is some text. Это некоторый текст. Αυτό είναι κάποιο κείμενο. 這是一些文字。", + ); + #[cfg(not(miri))] + let mut string = String::from( + " + https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html + + Original by Markus Kuhn, adapted for HTML by Martin Dürst. + + UTF-8 encoded sample plain-text file + ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + + Markus Kuhn [ˈmaʳkʊs kuːn] — 1999-08-20 + + + The ASCII compatible UTF-8 encoding of ISO 10646 and Unicode + plain-text files is defined in RFC 2279 and in ISO 10646-1 Annex R. + + + Using Unicode/UTF-8, you can write in emails and source code things such as + + Mathematics and Sciences: + + ∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β), + + ℕ ⊆ ℕ₀ ⊂ ℤ ⊂ ℚ ⊂ ℝ ⊂ ℂ, ⊥ < a ≠ b ≡ c ≤ d ≪ ⊤ ⇒ (A ⇔ B), + + 2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm + + Linguistics and dictionaries: + + ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn + Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ] + + APL: + + ((V⍳V)=⍳⍴V)/V←,V ⌷←⍳→⍴∆∇⊃‾⍎⍕⌈ + + Nicer typography in plain text files: + + ╔══════════════════════════════════════════╗ + ║ ║ + ║ • ‘single’ and “double” quotes ║ + ║ ║ + ║ • Curly apostrophes: “We’ve been here” ║ + ║ ║ + ║ • Latin-1 apostrophe and accents: '´` ║ + ║ ║ + ║ • ‚deutsche‘ „Anführungszeichen“ ║ + ║ ║ + ║ • †, ‡, ‰, •, 3–4, —, −5/+5, ™, … ║ + ║ ║ + ║ • ASCII safety test: 1lI|, 0OD, 8B ║ + ║ ╭─────────╮ ║ + ║ • the euro symbol: │ 14.95 € │ ║ + ║ ╰─────────╯ ║ + ╚══════════════════════════════════════════╝ + + Greek (in Polytonic): + + The Greek anthem: + + Σὲ γνωρίζω ἀπὸ τὴν κόψη + τοῦ σπαθιοῦ τὴν τρομερή, + σὲ γνωρίζω ἀπὸ τὴν ὄψη + ποὺ μὲ βία μετράει τὴ γῆ. + + ᾿Απ᾿ τὰ κόκκαλα βγαλμένη + τῶν ῾Ελλήνων τὰ ἱερά + καὶ σὰν πρῶτα ἀνδρειωμένη + χαῖρε, ὦ χαῖρε, ᾿Ελευθεριά! + + From a speech of Demosthenes in the 4th century BC: + + Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι, + ὅταν τ᾿ εἰς τὰ πράγματα ἀποβλέψω καὶ ὅταν πρὸς τοὺς + λόγους οὓς ἀκούω· τοὺς μὲν γὰρ λόγους περὶ τοῦ + τιμωρήσασθαι Φίλιππον ὁρῶ γιγνομένους, τὰ δὲ πράγματ᾿ + εἰς τοῦτο προήκοντα, ὥσθ᾿ ὅπως μὴ πεισόμεθ᾿ αὐτοὶ + πρότερον κακῶς σκέψασθαι δέον. οὐδέν οὖν ἄλλο μοι δοκοῦσιν + οἱ τὰ τοιαῦτα λέγοντες ἢ τὴν ὑπόθεσιν, περὶ ἧς βουλεύεσθαι, + οὐχὶ τὴν οὖσαν παριστάντες ὑμῖν ἁμαρτάνειν. ἐγὼ δέ, ὅτι μέν + ποτ᾿ ἐξῆν τῇ πόλει καὶ τὰ αὑτῆς ἔχειν ἀσφαλῶς καὶ Φίλιππον + τιμωρήσασθαι, καὶ μάλ᾿ ἀκριβῶς οἶδα· ἐπ᾿ ἐμοῦ γάρ, οὐ πάλαι + γέγονεν ταῦτ᾿ ἀμφότερα· νῦν μέντοι πέπεισμαι τοῦθ᾿ ἱκανὸν + προλαβεῖν ἡμῖν εἶναι τὴν πρώτην, ὅπως τοὺς συμμάχους + σώσομεν. ἐὰν γὰρ τοῦτο βεβαίως ὑπάρξῃ, τότε καὶ περὶ τοῦ + τίνα τιμωρήσεταί τις καὶ ὃν τρόπον ἐξέσται σκοπεῖν· πρὶν δὲ + τὴν ἀρχὴν ὀρθῶς ὑποθέσθαι, μάταιον ἡγοῦμαι περὶ τῆς + τελευτῆς ὁντινοῦν ποιεῖσθαι λόγον. + + Δημοσθένους, Γ´ ᾿Ολυνθιακὸς + + Georgian: + + From a Unicode conference invitation: + + გთხოვთ ახლავე გაიაროთ რეგისტრაცია Unicode-ის მეათე საერთაშორისო + კონფერენციაზე დასასწრებად, რომელიც გაიმართება 10-12 მარტს, + ქ. მაინცში, გერმანიაში. კონფერენცია შეჰკრებს ერთად მსოფლიოს + ექსპერტებს ისეთ დარგებში როგორიცაა ინტერნეტი და Unicode-ი, + ინტერნაციონალიზაცია და ლოკალიზაცია, Unicode-ის გამოყენება + ოპერაციულ სისტემებსა, და გამოყენებით პროგრამებში, შრიფტებში, + ტექსტების დამუშავებასა და მრავალენოვან კომპიუტერულ სისტემებში. + + Russian: + + From a Unicode conference invitation: + + Зарегистрируйтесь сейчас на Десятую Международную Конференцию по + Unicode, которая состоится 10-12 марта 1997 года в Майнце в Германии. + Конференция соберет широкий круг экспертов по вопросам глобального + Интернета и Unicode, локализации и интернационализации, воплощению и + применению Unicode в различных операционных системах и программных + приложениях, шрифтах, верстке и многоязычных компьютерных системах. + + Thai (UCS Level 2): + + Excerpt from a poetry on The Romance of The Three Kingdoms (a Chinese + classic 'San Gua'): + + [----------------------------|------------------------] + ๏ แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช พระปกเกศกองบู๊กู้ขึ้นใหม่ + สิบสองกษัตริย์ก่อนหน้าแลถัดไป สององค์ไซร้โง่เขลาเบาปัญญา + ทรงนับถือขันทีเป็นที่พึ่ง บ้านเมืองจึงวิปริตเป็นนักหนา + โฮจิ๋นเรียกทัพทั่วหัวเมืองมา หมายจะฆ่ามดชั่วตัวสำคัญ + เหมือนขับไสไล่เสือจากเคหา รับหมาป่าเข้ามาเลยอาสัญ + ฝ่ายอ้องอุ้นยุแยกให้แตกกัน ใช้สาวนั้นเป็นชนวนชื่นชวนใจ + พลันลิฉุยกุยกีกลับก่อเหตุ ช่างอาเพศจริงหนาฟ้าร้องไห้ + ต้องรบราฆ่าฟันจนบรรลัย ฤๅหาใครค้ำชูกู้บรรลังก์ ฯ + + (The above is a two-column text. If combining characters are handled + correctly, the lines of the second column should be aligned with the + | character above.) + + Ethiopian: + + Proverbs in the Amharic language: + + ሰማይ አይታረስ ንጉሥ አይከሰስ። + ብላ ካለኝ እንደአባቴ በቆመጠኝ። + ጌጥ ያለቤቱ ቁምጥና ነው። + ደሀ በሕልሙ ቅቤ ባይጠጣ ንጣት በገደለው። + የአፍ ወለምታ በቅቤ አይታሽም። + አይጥ በበላ ዳዋ ተመታ። + ሲተረጉሙ ይደረግሙ። + ቀስ በቀስ፥ ዕንቁላል በእግሩ ይሄዳል። + ድር ቢያብር አንበሳ ያስር። + ሰው እንደቤቱ እንጅ እንደ ጉረቤቱ አይተዳደርም። + እግዜር የከፈተውን ጉሮሮ ሳይዘጋው አይድርም። + የጎረቤት ሌባ፥ ቢያዩት ይስቅ ባያዩት ያጠልቅ። + ሥራ ከመፍታት ልጄን ላፋታት። + ዓባይ ማደሪያ የለው፥ ግንድ ይዞ ይዞራል። + የእስላም አገሩ መካ የአሞራ አገሩ ዋርካ። + ተንጋሎ ቢተፉ ተመልሶ ባፉ። + ወዳጅህ ማር ቢሆን ጨርስህ አትላሰው። + እግርህን በፍራሽህ ልክ ዘርጋ። + + Runes: + + ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ + + (Old English, which transcribed into Latin reads 'He cwaeth that he + bude thaem lande northweardum with tha Westsae.' and means 'He said + that he lived in the northern land near the Western Sea.') + + Braille: + + ⡌⠁⠧⠑ ⠼⠁⠒ ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌ + + ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠙⠑⠁⠙⠒ ⠞⠕ ⠃⠑⠛⠔ ⠺⠊⠹⠲ ⡹⠻⠑ ⠊⠎ ⠝⠕ ⠙⠳⠃⠞ + ⠱⠁⠞⠑⠧⠻ ⠁⠃⠳⠞ ⠹⠁⠞⠲ ⡹⠑ ⠗⠑⠛⠊⠌⠻ ⠕⠋ ⠙⠊⠎ ⠃⠥⠗⠊⠁⠇ ⠺⠁⠎ + ⠎⠊⠛⠝⠫ ⠃⠹ ⠹⠑ ⠊⠇⠻⠛⠹⠍⠁⠝⠂ ⠹⠑ ⠊⠇⠻⠅⠂ ⠹⠑ ⠥⠝⠙⠻⠞⠁⠅⠻⠂ + ⠁⠝⠙ ⠹⠑ ⠡⠊⠑⠋ ⠍⠳⠗⠝⠻⠲ ⡎⠊⠗⠕⠕⠛⠑ ⠎⠊⠛⠝⠫ ⠊⠞⠲ ⡁⠝⠙ + ⡎⠊⠗⠕⠕⠛⠑⠰⠎ ⠝⠁⠍⠑ ⠺⠁⠎ ⠛⠕⠕⠙ ⠥⠏⠕⠝ ⠰⡡⠁⠝⠛⠑⠂ ⠋⠕⠗ ⠁⠝⠹⠹⠔⠛ ⠙⠑ + ⠡⠕⠎⠑ ⠞⠕ ⠏⠥⠞ ⠙⠊⠎ ⠙⠁⠝⠙ ⠞⠕⠲ + + ⡕⠇⠙ ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ + + ⡍⠔⠙⠖ ⡊ ⠙⠕⠝⠰⠞ ⠍⠑⠁⠝ ⠞⠕ ⠎⠁⠹ ⠹⠁⠞ ⡊ ⠅⠝⠪⠂ ⠕⠋ ⠍⠹ + ⠪⠝ ⠅⠝⠪⠇⠫⠛⠑⠂ ⠱⠁⠞ ⠹⠻⠑ ⠊⠎ ⠏⠜⠞⠊⠊⠥⠇⠜⠇⠹ ⠙⠑⠁⠙ ⠁⠃⠳⠞ + ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ ⡊ ⠍⠊⠣⠞ ⠙⠁⠧⠑ ⠃⠑⠲ ⠔⠊⠇⠔⠫⠂ ⠍⠹⠎⠑⠇⠋⠂ ⠞⠕ + ⠗⠑⠛⠜⠙ ⠁ ⠊⠕⠋⠋⠔⠤⠝⠁⠊⠇ ⠁⠎ ⠹⠑ ⠙⠑⠁⠙⠑⠌ ⠏⠊⠑⠊⠑ ⠕⠋ ⠊⠗⠕⠝⠍⠕⠝⠛⠻⠹ + ⠔ ⠹⠑ ⠞⠗⠁⠙⠑⠲ ⡃⠥⠞ ⠹⠑ ⠺⠊⠎⠙⠕⠍ ⠕⠋ ⠳⠗ ⠁⠝⠊⠑⠌⠕⠗⠎ + ⠊⠎ ⠔ ⠹⠑ ⠎⠊⠍⠊⠇⠑⠆ ⠁⠝⠙ ⠍⠹ ⠥⠝⠙⠁⠇⠇⠪⠫ ⠙⠁⠝⠙⠎ + ⠩⠁⠇⠇ ⠝⠕⠞ ⠙⠊⠌⠥⠗⠃ ⠊⠞⠂ ⠕⠗ ⠹⠑ ⡊⠳⠝⠞⠗⠹⠰⠎ ⠙⠕⠝⠑ ⠋⠕⠗⠲ ⡹⠳ + ⠺⠊⠇⠇ ⠹⠻⠑⠋⠕⠗⠑ ⠏⠻⠍⠊⠞ ⠍⠑ ⠞⠕ ⠗⠑⠏⠑⠁⠞⠂ ⠑⠍⠏⠙⠁⠞⠊⠊⠁⠇⠇⠹⠂ ⠹⠁⠞ + ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ + + (The first couple of paragraphs of \"A Christmas Carol\" by Dickens) + + Compact font selection example text: + + ABCDEFGHIJKLMNOPQRSTUVWXYZ /0123456789 + abcdefghijklmnopqrstuvwxyz £©µÀÆÖÞßéöÿ + –—‘“”„†•…‰™œŠŸž€ ΑΒΓΔΩαβγδω АБВГДабвгд + ∀∂∈ℝ∧∪≡∞ ↑↗↨↻⇣ ┐┼╔╘░►☺♀ fi�⑀₂ἠḂӥẄɐː⍎אԱა + + Greetings in various languages: + + Hello world, Καλημέρα κόσμε, コンニチハ + + Box drawing alignment tests: █ + ▉ + ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳ + ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳ + ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳ + ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳ + ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎ + ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏ + ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█ + +", + ); + + match CharBuffer::try_from(string.as_str()) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + match CharBuffer::try_from(string.as_mut_str()) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + match CharBuffer::try_from(&string) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + match CharBuffer::try_from(&mut string) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + match CharBuffer::try_from(string.clone()) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + let mut some_box: Box = Box::from(string.clone()); + + match CharBuffer::try_from(&some_box) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + match CharBuffer::try_from(&mut some_box) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + match CharBuffer::try_from(some_box) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + let mut some_arc: Arc = Arc::from(string.clone()); + + match CharBuffer::try_from(&some_arc) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + match CharBuffer::try_from(&mut some_arc) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + match CharBuffer::try_from(some_arc) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + let mut some_rc: Rc = Rc::from(string.clone()); + + match CharBuffer::try_from(&some_rc) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + match CharBuffer::try_from(&mut some_rc) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + match CharBuffer::try_from(some_rc) { + Ok(_) => panic!("This should fail because of long string"), + Err(_) => {} + } + + let hash_builder = std::collections::hash_map::RandomState::default(); + + fn make_hash(hash_builder: &S, val: &Q) -> u64 + where + Q: std::hash::Hash + ?Sized, + S: core::hash::BuildHasher, + { + hash_builder.hash_one(val) + } + + for mut char in string.chars() { + let mut char_string = char.to_string(); + + assert_eq!(CharBuffer::new(char), char_string); + assert_eq!(CharBuffer::new(char).as_str(), char_string); + assert_eq!(CharBuffer::new(char).len(), char_string.len()); + assert_eq!(CharBuffer::from(char), char_string); + assert_eq!(CharBuffer::from(&char), char_string); + assert_eq!(CharBuffer::from(&mut char), char_string); + + let char_buf = CharBuffer::new(char); + + assert_eq!( + make_hash(&hash_builder, &char_buf), + make_hash(&hash_builder, &char_string) + ); + + assert_eq!(CharBuffer::new(char), char_buf); + assert_eq!(CharBuffer::new(char), CharBuffer::from(&char_buf)); + assert_eq!(&*char_buf, char_string.as_str()); + assert_eq!(char_buf.as_ref(), char_string.as_str()); + let str: &str = char_buf.borrow(); + assert_eq!(str, char_string.as_str()); + + assert_eq!(char::from(&char_buf), char); + assert_eq!(char::from(char_buf), char); + assert_eq!(String::from(char_buf), char_string); + assert_eq!(String::from(&char_buf), char_string); + + let str: &str = From::from(&char_buf); + assert_eq!(str, char_string); + + let str: Cow = From::from(&char_buf); + assert_eq!(str, char_string); + + let str: Cow = From::from(char_buf); + assert_eq!(str.as_str(), char_string); + + let str: Arc = From::from(char_buf); + assert_eq!(&str[..], char_string); + + let str: Arc = From::from(&char_buf); + assert_eq!(&str[..], char_string); + + let str: Box = From::from(char_buf); + assert_eq!(&str[..], char_string); + + let str: Box = From::from(&char_buf); + assert_eq!(&str[..], char_string); + + let str: Rc = From::from(char_buf); + assert_eq!(&str[..], char_string); + + let str: Rc = From::from(&char_buf); + assert_eq!(&str[..], char_string); + + match CharBuffer::try_from(char_string.as_str()) { + Ok(char_buf) => { + assert_eq!(char_buf, char_string.as_str()); + assert_eq!(char_string.as_str(), char_buf); + } + Err(_) => panic!("This should not fail because of single char"), + } + match CharBuffer::try_from(char_string.as_mut_str()) { + Ok(char_buf) => { + assert_eq!(char_buf, char_string.as_mut_str()); + assert_eq!(char_string.as_mut_str(), char_buf); + } + Err(_) => panic!("This should not fail because of single char"), + } + + match CharBuffer::try_from(&char_string) { + Ok(char_buf) => { + assert_eq!(char_buf, &char_string); + assert_eq!(&char_string, char_buf); + } + Err(_) => panic!("This should not fail because of single char"), + } + match CharBuffer::try_from(&mut char_string) { + Ok(char_buf) => { + assert_eq!(char_buf, &mut char_string); + assert_eq!(&mut char_string, char_buf); + } + Err(_) => panic!("This should not fail because of single char"), + } + match CharBuffer::try_from(char_string.clone()) { + Ok(char_buf) => { + assert_eq!(char_buf, char_string); + assert_eq!(char_string, char_buf); + } + Err(_) => panic!("This should not fail because of single char"), + } + + let mut some_box: Box = Box::from(char_string.clone()); + + match CharBuffer::try_from(&some_box) { + Ok(char_buf) => assert_eq!(char_buf, some_box.as_ref()), + Err(_) => panic!("This should not fail because of single char"), + } + match CharBuffer::try_from(&mut some_box) { + Ok(char_buf) => assert_eq!(char_buf, some_box.as_ref()), + Err(_) => panic!("This should not fail because of single char"), + } + match CharBuffer::try_from(some_box) { + Ok(char_buf) => assert_eq!(char_buf, char_string), + Err(_) => panic!("This should not fail because of single char"), + } + + let mut some_arc: Arc = Arc::from(char_string.clone()); + + match CharBuffer::try_from(&some_arc) { + Ok(char_buf) => assert_eq!(char_buf, some_arc.as_ref()), + Err(_) => panic!("This should not fail because of single char"), + } + match CharBuffer::try_from(&mut some_arc) { + Ok(char_buf) => assert_eq!(char_buf, some_arc.as_ref()), + Err(_) => panic!("This should not fail because of single char"), + } + match CharBuffer::try_from(some_arc) { + Ok(char_buf) => assert_eq!(char_buf, char_string), + Err(_) => panic!("This should not fail because of single char"), + } + + let mut some_rc: Rc = Rc::from(char_string.clone()); + + match CharBuffer::try_from(&some_rc) { + Ok(char_buf) => assert_eq!(char_buf, some_rc.as_ref()), + Err(_) => panic!("This should not fail because of single char"), + } + match CharBuffer::try_from(&mut some_rc) { + Ok(char_buf) => assert_eq!(char_buf, some_rc.as_ref()), + Err(_) => panic!("This should not fail because of single char"), + } + match CharBuffer::try_from(some_rc) { + Ok(char_buf) => assert_eq!(char_buf, char_string), + Err(_) => panic!("This should not fail because of single char"), + } + } +} diff --git a/editor-core/src/chars.rs b/editor-core/src/chars.rs new file mode 100644 index 00000000..34ebd135 --- /dev/null +++ b/editor-core/src/chars.rs @@ -0,0 +1,34 @@ +/// Determine whether a character is a line ending. +#[inline] +pub fn char_is_line_ending(ch: char) -> bool { + matches!(ch, '\u{000A}') +} + +/// Determine whether a character qualifies as (non-line-break) +/// whitespace. +#[inline] +pub fn char_is_whitespace(ch: char) -> bool { + // TODO: this is a naive binary categorization of whitespace + // characters. For display, word wrapping, etc. we'll need a better + // categorization based on e.g. breaking vs non-breaking spaces + // and whether they're zero-width or not. + match ch { + //'\u{1680}' | // Ogham Space Mark (here for completeness, but usually displayed as a dash, not as whitespace) + '\u{0009}' | // Character Tabulation + '\u{0020}' | // Space + '\u{00A0}' | // No-break Space + '\u{180E}' | // Mongolian Vowel Separator + '\u{202F}' | // Narrow No-break Space + '\u{205F}' | // Medium Mathematical Space + '\u{3000}' | // Ideographic Space + '\u{FEFF}' // Zero Width No-break Space + => true, + + // En Quad, Em Quad, En Space, Em Space, Three-per-em Space, + // Four-per-em Space, Six-per-em Space, Figure Space, + // Punctuation Space, Thin Space, Hair Space, Zero Width Space. + ch if ('\u{2000}' ..= '\u{200B}').contains(&ch) => true, + + _ => false, + } +} diff --git a/editor-core/src/command.rs b/editor-core/src/command.rs new file mode 100644 index 00000000..bf06dcc1 --- /dev/null +++ b/editor-core/src/command.rs @@ -0,0 +1,399 @@ +use strum_macros::{Display, EnumIter, EnumMessage, EnumString, IntoStaticStr}; + +use crate::movement::{LinePosition, Movement}; + +#[derive( + Display, EnumString, EnumIter, Clone, PartialEq, Eq, Debug, EnumMessage, IntoStaticStr, +)] +pub enum EditCommand { + #[strum(serialize = "move_line_up")] + MoveLineUp, + #[strum(serialize = "move_line_down")] + MoveLineDown, + #[strum(serialize = "insert_new_line")] + InsertNewLine, + #[strum(serialize = "insert_tab")] + InsertTab, + #[strum(serialize = "new_line_above")] + NewLineAbove, + #[strum(serialize = "new_line_below")] + NewLineBelow, + #[strum(serialize = "delete_backward")] + DeleteBackward, + #[strum(serialize = "delete_forward")] + DeleteForward, + #[strum(serialize = "delete_line")] + DeleteLine, + #[strum(serialize = "delete_forward_and_insert")] + DeleteForwardAndInsert, + #[strum(serialize = "delete_word_and_insert")] + DeleteWordAndInsert, + #[strum(serialize = "delete_line_and_insert")] + DeleteLineAndInsert, + #[strum(serialize = "delete_word_forward")] + DeleteWordForward, + #[strum(serialize = "delete_word_backward")] + DeleteWordBackward, + #[strum(serialize = "delete_to_beginning_of_line")] + DeleteToBeginningOfLine, + #[strum(serialize = "delete_to_end_of_line")] + DeleteToEndOfLine, + + #[strum(serialize = "delete_to_end_and_insert")] + DeleteToEndOfLineAndInsert, + #[strum(message = "Join Lines")] + #[strum(serialize = "join_lines")] + JoinLines, + #[strum(message = "Indent Line")] + #[strum(serialize = "indent_line")] + IndentLine, + #[strum(message = "Outdent Line")] + #[strum(serialize = "outdent_line")] + OutdentLine, + #[strum(message = "Toggle Line Comment")] + #[strum(serialize = "toggle_line_comment")] + ToggleLineComment, + #[strum(serialize = "undo")] + Undo, + #[strum(serialize = "redo")] + Redo, + #[strum(message = "Copy")] + #[strum(serialize = "clipboard_copy")] + ClipboardCopy, + #[strum(message = "Cut")] + #[strum(serialize = "clipboard_cut")] + ClipboardCut, + #[strum(message = "Paste")] + #[strum(serialize = "clipboard_paste")] + ClipboardPaste, + #[strum(serialize = "yank")] + Yank, + #[strum(serialize = "paste")] + Paste, + #[strum(serialize = "paste_before")] + PasteBefore, + + #[strum(serialize = "normal_mode")] + NormalMode, + #[strum(serialize = "insert_mode")] + InsertMode, + #[strum(serialize = "insert_first_non_blank")] + InsertFirstNonBlank, + #[strum(serialize = "append")] + Append, + #[strum(serialize = "append_end_of_line")] + AppendEndOfLine, + #[strum(serialize = "toggle_visual_mode")] + ToggleVisualMode, + #[strum(serialize = "toggle_linewise_visual_mode")] + ToggleLinewiseVisualMode, + #[strum(serialize = "toggle_blockwise_visual_mode")] + ToggleBlockwiseVisualMode, + #[strum(serialize = "duplicate_line_up")] + DuplicateLineUp, + #[strum(serialize = "duplicate_line_down")] + DuplicateLineDown, +} + +impl EditCommand { + pub fn not_changing_buffer(&self) -> bool { + matches!( + self, + &EditCommand::ClipboardCopy + | &EditCommand::Yank + | &EditCommand::NormalMode + | &EditCommand::InsertMode + | &EditCommand::InsertFirstNonBlank + | &EditCommand::Append + | &EditCommand::AppendEndOfLine + | &EditCommand::ToggleVisualMode + | &EditCommand::ToggleLinewiseVisualMode + | &EditCommand::ToggleBlockwiseVisualMode + ) + } +} + +#[derive( + Display, EnumString, EnumIter, Clone, PartialEq, Eq, Debug, EnumMessage, IntoStaticStr, +)] +pub enum MoveCommand { + #[strum(serialize = "down")] + Down, + #[strum(serialize = "up")] + Up, + #[strum(serialize = "left")] + Left, + #[strum(serialize = "right")] + Right, + #[strum(serialize = "word_backward")] + WordBackward, + #[strum(serialize = "word_forward")] + WordForward, + #[strum(serialize = "word_end_forward")] + WordEndForward, + #[strum(message = "Document Start")] + #[strum(serialize = "document_start")] + DocumentStart, + #[strum(message = "Document End")] + #[strum(serialize = "document_end")] + DocumentEnd, + #[strum(serialize = "line_end")] + LineEnd, + #[strum(serialize = "line_start")] + LineStart, + #[strum(serialize = "line_start_non_blank")] + LineStartNonBlank, + #[strum(serialize = "go_to_line_default_last")] + GotoLineDefaultLast, + #[strum(serialize = "go_to_line_default_first")] + GotoLineDefaultFirst, + #[strum(serialize = "match_pairs")] + MatchPairs, + #[strum(serialize = "next_unmatched_right_bracket")] + NextUnmatchedRightBracket, + #[strum(serialize = "previous_unmatched_left_bracket")] + PreviousUnmatchedLeftBracket, + #[strum(serialize = "next_unmatched_right_curly_bracket")] + NextUnmatchedRightCurlyBracket, + #[strum(serialize = "previous_unmatched_left_curly_bracket")] + PreviousUnmatchedLeftCurlyBracket, + #[strum(message = "Paragraph forward")] + #[strum(serialize = "paragraph_forward")] + ParagraphForward, + #[strum(message = "Paragraph backward")] + #[strum(serialize = "paragraph_backward")] + ParagraphBackward, +} + +impl MoveCommand { + pub fn to_movement(&self, count: Option) -> Movement { + use MoveCommand::*; + match self { + Left => Movement::Left, + Right => Movement::Right, + Up => Movement::Up, + Down => Movement::Down, + DocumentStart => Movement::DocumentStart, + DocumentEnd => Movement::DocumentEnd, + LineStart => Movement::StartOfLine, + LineStartNonBlank => Movement::FirstNonBlank, + LineEnd => Movement::EndOfLine, + GotoLineDefaultFirst => match count { + Some(n) => Movement::Line(LinePosition::Line(n)), + None => Movement::Line(LinePosition::First), + }, + GotoLineDefaultLast => match count { + Some(n) => Movement::Line(LinePosition::Line(n)), + None => Movement::Line(LinePosition::Last), + }, + WordBackward => Movement::WordBackward, + WordForward => Movement::WordForward, + WordEndForward => Movement::WordEndForward, + MatchPairs => Movement::MatchPairs, + NextUnmatchedRightBracket => Movement::NextUnmatched(')'), + PreviousUnmatchedLeftBracket => Movement::PreviousUnmatched('('), + NextUnmatchedRightCurlyBracket => Movement::NextUnmatched('}'), + PreviousUnmatchedLeftCurlyBracket => Movement::PreviousUnmatched('{'), + ParagraphForward => Movement::ParagraphForward, + ParagraphBackward => Movement::ParagraphBackward, + } + } +} + +#[derive( + Display, EnumString, EnumIter, Clone, PartialEq, Eq, Debug, EnumMessage, IntoStaticStr, +)] +pub enum ScrollCommand { + #[strum(serialize = "page_up")] + PageUp, + #[strum(serialize = "page_down")] + PageDown, + #[strum(serialize = "scroll_up")] + ScrollUp, + #[strum(serialize = "scroll_down")] + ScrollDown, + #[strum(serialize = "center_of_window")] + CenterOfWindow, + #[strum(serialize = "top_of_window")] + TopOfWindow, + #[strum(serialize = "bottom_of_window")] + BottomOfWindow, +} + +#[derive( + Display, EnumString, EnumIter, Clone, PartialEq, Eq, Debug, EnumMessage, IntoStaticStr, +)] +pub enum FocusCommand { + #[strum(serialize = "split_vertical")] + SplitVertical, + #[strum(serialize = "split_horizontal")] + SplitHorizontal, + #[strum(serialize = "split_exchange")] + SplitExchange, + #[strum(serialize = "split_close")] + SplitClose, + #[strum(serialize = "split_right")] + SplitRight, + #[strum(serialize = "split_left")] + SplitLeft, + #[strum(serialize = "split_up")] + SplitUp, + #[strum(serialize = "split_down")] + SplitDown, + #[strum(serialize = "search_whole_word_forward")] + SearchWholeWordForward, + #[strum(serialize = "search_forward")] + SearchForward, + #[strum(serialize = "search_backward")] + SearchBackward, + #[strum(serialize = "toggle_case_sensitive_search")] + ToggleCaseSensitive, + #[strum(serialize = "global_search_refresh")] + GlobalSearchRefresh, + #[strum(serialize = "clear_search")] + ClearSearch, + #[strum(serialize = "search_in_view")] + SearchInView, + #[strum(serialize = "list.select")] + ListSelect, + #[strum(serialize = "list.next")] + ListNext, + #[strum(serialize = "list.next_page")] + ListNextPage, + #[strum(serialize = "list.previous")] + ListPrevious, + #[strum(serialize = "list.previous_page")] + ListPreviousPage, + #[strum(serialize = "list.expand")] + ListExpand, + #[strum(serialize = "jump_to_next_snippet_placeholder")] + JumpToNextSnippetPlaceholder, + #[strum(serialize = "jump_to_prev_snippet_placeholder")] + JumpToPrevSnippetPlaceholder, + #[strum(serialize = "show_code_actions")] + ShowCodeActions, + #[strum(serialize = "get_completion")] + GetCompletion, + #[strum(serialize = "get_signature")] + GetSignature, + #[strum(serialize = "toggle_breakpoint")] + ToggleBreakpoint, + /// This will close a modal, such as the settings window or completion + #[strum(message = "Close Modal")] + #[strum(serialize = "modal.close")] + ModalClose, + #[strum(message = "Go to Definition")] + #[strum(serialize = "goto_definition")] + GotoDefinition, + #[strum(message = "Go to Type Definition")] + #[strum(serialize = "goto_type_definition")] + GotoTypeDefinition, + #[strum(message = "Show Hover")] + #[strum(serialize = "show_hover")] + ShowHover, + #[strum(message = "Go to Next Difference")] + #[strum(serialize = "next_diff")] + NextDiff, + #[strum(message = "Go to Previous Difference")] + #[strum(serialize = "previous_diff")] + PreviousDiff, + #[strum(message = "Toggle Code Lens")] + #[strum(serialize = "toggle_code_lens")] + ToggleCodeLens, + #[strum(message = "Toggle History")] + #[strum(serialize = "toggle_history")] + ToggleHistory, + #[strum(serialize = "format_document")] + #[strum(message = "Format Document")] + FormatDocument, + #[strum(serialize = "search")] + Search, + #[strum(serialize = "focus_replace_editor")] + FocusReplaceEditor, + #[strum(serialize = "focus_find_editor")] + FocusFindEditor, + #[strum(serialize = "inline_find_right")] + InlineFindRight, + #[strum(serialize = "inline_find_left")] + InlineFindLeft, + #[strum(serialize = "create_mark")] + CreateMark, + #[strum(serialize = "go_to_mark")] + GoToMark, + #[strum(serialize = "repeat_last_inline_find")] + RepeatLastInlineFind, + #[strum(message = "Save")] + #[strum(serialize = "save")] + Save, + #[strum(message = "Save Without Formatting")] + #[strum(serialize = "save_without_format")] + SaveWithoutFormatting, + #[strum(serialize = "save_and_exit")] + SaveAndExit, + #[strum(serialize = "force_exit")] + ForceExit, + #[strum(serialize = "rename_symbol")] + #[strum(message = "Rename Symbol")] + Rename, + #[strum(serialize = "confirm_rename")] + ConfirmRename, + #[strum(serialize = "select_next_syntax_item")] + SelectNextSyntaxItem, + #[strum(serialize = "select_previous_syntax_item")] + SelectPreviousSyntaxItem, + #[strum(serialize = "open_source_file")] + OpenSourceFile, + #[strum(serialize = "inline_completion.select")] + #[strum(message = "Inline Completion Select")] + InlineCompletionSelect, + #[strum(serialize = "inline_completion.next")] + #[strum(message = "Inline Completion Next")] + InlineCompletionNext, + #[strum(serialize = "inline_completion.previous")] + #[strum(message = "Inline Completion Previous")] + InlineCompletionPrevious, + #[strum(serialize = "inline_completion.cancel")] + #[strum(message = "Inline Completion Cancel")] + InlineCompletionCancel, + #[strum(serialize = "inline_completion.invoke")] + #[strum(message = "Inline Completion Invoke")] + InlineCompletionInvoke, +} + +#[derive( + Display, EnumString, EnumIter, Clone, PartialEq, Eq, Debug, EnumMessage, IntoStaticStr, +)] +pub enum MotionModeCommand { + #[strum(serialize = "motion_mode_delete")] + MotionModeDelete, + #[strum(serialize = "motion_mode_indent")] + MotionModeIndent, + #[strum(serialize = "motion_mode_outdent")] + MotionModeOutdent, + #[strum(serialize = "motion_mode_yank")] + MotionModeYank, +} + +#[derive( + Display, EnumString, EnumIter, Clone, PartialEq, Eq, Debug, EnumMessage, IntoStaticStr, +)] +pub enum MultiSelectionCommand { + #[strum(serialize = "select_undo")] + SelectUndo, + #[strum(serialize = "insert_cursor_above")] + InsertCursorAbove, + #[strum(serialize = "insert_cursor_below")] + InsertCursorBelow, + #[strum(serialize = "insert_cursor_end_of_line")] + InsertCursorEndOfLine, + #[strum(serialize = "select_current_line")] + SelectCurrentLine, + #[strum(serialize = "select_all_current")] + SelectAllCurrent, + #[strum(serialize = "select_next_current")] + SelectNextCurrent, + #[strum(serialize = "select_skip_current")] + SelectSkipCurrent, + #[strum(serialize = "select_all")] + SelectAll, +} diff --git a/editor-core/src/cursor.rs b/editor-core/src/cursor.rs new file mode 100644 index 00000000..1dfa14e7 --- /dev/null +++ b/editor-core/src/cursor.rs @@ -0,0 +1,616 @@ +use lapce_xi_rope::{RopeDelta, Transformer}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::{ + buffer::{rope_text::RopeText, Buffer}, + mode::{Mode, MotionMode, VisualMode}, + register::RegisterData, + selection::{InsertDrift, SelRegion, Selection}, +}; + +#[derive(Clone, Copy, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ColPosition { + FirstNonBlank, + Start, + End, + Col(f64), +} + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Cursor { + pub mode: CursorMode, + pub horiz: Option, + pub motion_mode: Option, + pub history_selections: Vec, + pub affinity: CursorAffinity, +} + +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CursorMode { + Normal(usize), + Visual { + start: usize, + end: usize, + mode: VisualMode, + }, + Insert(Selection), +} + +struct RegionsIter<'c> { + cursor_mode: &'c CursorMode, + idx: usize, +} + +impl<'c> Iterator for RegionsIter<'c> { + type Item = (usize, usize); + + fn next(&mut self) -> Option { + match self.cursor_mode { + &CursorMode::Normal(offset) => (self.idx == 0).then(|| { + self.idx = 1; + (offset, offset) + }), + &CursorMode::Visual { start, end, .. } => (self.idx == 0).then(|| { + self.idx = 1; + (start, end) + }), + CursorMode::Insert(selection) => { + let next = selection + .regions() + .get(self.idx) + .map(|&SelRegion { start, end, .. }| (start, end)); + + if next.is_some() { + self.idx += 1; + } + + next + } + } + } + + fn size_hint(&self) -> (usize, Option) { + let total_len = match self.cursor_mode { + CursorMode::Normal(_) | CursorMode::Visual { .. } => 1, + CursorMode::Insert(selection) => selection.len(), + }; + let len = total_len - self.idx; + + (len, Some(len)) + } +} + +impl<'c> ExactSizeIterator for RegionsIter<'c> {} + +impl CursorMode { + pub fn offset(&self) -> usize { + match &self { + CursorMode::Normal(offset) => *offset, + CursorMode::Visual { end, .. } => *end, + CursorMode::Insert(selection) => selection.get_cursor_offset(), + } + } + + pub fn start_offset(&self) -> usize { + match &self { + CursorMode::Normal(offset) => *offset, + CursorMode::Visual { start, .. } => *start, + CursorMode::Insert(selection) => selection.first().map(|s| s.start).unwrap_or(0), + } + } + + pub fn regions_iter(&self) -> impl ExactSizeIterator + '_ { + RegionsIter { + cursor_mode: self, + idx: 0, + } + } +} + +/// Decides how the cursor should be placed around special areas of text. +/// Ex: +/// ```rust,ignore +/// let j = // soft linewrap +/// 1 + 2 + 3; +/// ``` +/// where `let j = ` has the issue that there's two positions you might want your cursor to be: +/// `let j = |` or `|1 + 2 + 3;` +/// These are the same offset in the text, but it feels more natural to have it move in a certain +/// way. +/// If you're at `let j =| ` and you press the right-arrow key, then it uses your backwards +/// affinity to keep you on the line at `let j = |`. +/// If you're at `1| + 2 + 3;` and you press the left-arrow key, then it uses your forwards affinity +/// to keep you on the line at `|1 + 2 + 3;`. +/// +/// For other special text, like inlay hints, this can also apply. +/// ```rust,ignore +/// let j<: String> = ... +/// ``` +/// where `<: String>` is our inlay hint, then +/// `let |j<: String> =` and you press the right-arrow key, then it uses your backwards affinity to +/// keep you on the same side of the hint, `let j|<: String>`. +/// `let j<: String> |=` and you press the right-arrow key, then it uses your forwards affinity to +/// keep you on the same side of the hint, `let j<: String>| =`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CursorAffinity { + /// `<: String>|` + Forward, + /// `|<: String>` + Backward, +} +impl CursorAffinity { + pub fn invert(&self) -> Self { + match self { + CursorAffinity::Forward => CursorAffinity::Backward, + CursorAffinity::Backward => CursorAffinity::Forward, + } + } +} + +impl Cursor { + pub fn new( + mode: CursorMode, + horiz: Option, + motion_mode: Option, + ) -> Self { + Self { + mode, + horiz, + motion_mode, + history_selections: Vec::new(), + // It should appear before any inlay hints at the very first position + affinity: CursorAffinity::Backward, + } + } + + pub fn origin(modal: bool) -> Self { + Self::new( + if modal { + CursorMode::Normal(0) + } else { + CursorMode::Insert(Selection::caret(0)) + }, + None, + None, + ) + } + + pub fn offset(&self) -> usize { + self.mode.offset() + } + + pub fn start_offset(&self) -> usize { + self.mode.start_offset() + } + + pub fn regions_iter(&self) -> impl ExactSizeIterator + '_ { + self.mode.regions_iter() + } + + pub fn is_normal(&self) -> bool { + matches!(&self.mode, CursorMode::Normal(_)) + } + + pub fn is_insert(&self) -> bool { + matches!(&self.mode, CursorMode::Insert(_)) + } + + pub fn is_visual(&self) -> bool { + matches!(&self.mode, CursorMode::Visual { .. }) + } + + pub fn get_mode(&self) -> Mode { + match &self.mode { + CursorMode::Normal(_) => Mode::Normal, + CursorMode::Visual { mode, .. } => Mode::Visual(*mode), + CursorMode::Insert(_) => Mode::Insert, + } + } + + pub fn set_mode(&mut self, mode: CursorMode) { + if let CursorMode::Insert(selection) = &self.mode { + self.history_selections.push(selection.clone()); + } + self.mode = mode; + } + + pub fn set_insert(&mut self, selection: Selection) { + self.set_mode(CursorMode::Insert(selection)); + } + + pub fn update_selection(&mut self, buffer: &Buffer, selection: Selection) { + match self.mode { + CursorMode::Normal(_) | CursorMode::Visual { .. } => { + let offset = selection.min_offset(); + let offset = buffer.offset_line_end(offset, false).min(offset); + self.mode = CursorMode::Normal(offset); + } + CursorMode::Insert(_) => { + self.mode = CursorMode::Insert(selection); + } + } + } + + pub fn edit_selection(&self, text: &impl RopeText) -> Selection { + match &self.mode { + CursorMode::Insert(selection) => selection.clone(), + CursorMode::Normal(offset) => { + Selection::region(*offset, text.next_grapheme_offset(*offset, 1, text.len())) + } + CursorMode::Visual { start, end, mode } => match mode { + VisualMode::Normal => Selection::region( + *start.min(end), + text.next_grapheme_offset(*start.max(end), 1, text.len()), + ), + VisualMode::Linewise => { + let start_offset = text.offset_of_line(text.line_of_offset(*start.min(end))); + let end_offset = text.offset_of_line(text.line_of_offset(*start.max(end)) + 1); + Selection::region(start_offset, end_offset) + } + VisualMode::Blockwise => { + let mut selection = Selection::new(); + let (start_line, start_col) = text.offset_to_line_col(*start.min(end)); + let (end_line, end_col) = text.offset_to_line_col(*start.max(end)); + let left = start_col.min(end_col); + let right = start_col.max(end_col) + 1; + for line in start_line..end_line + 1 { + let max_col = text.line_end_col(line, true); + if left > max_col { + continue; + } + let right = match &self.horiz { + Some(ColPosition::End) => max_col, + _ => { + if right > max_col { + max_col + } else { + right + } + } + }; + let left = text.offset_of_line_col(line, left); + let right = text.offset_of_line_col(line, right); + selection.add_region(SelRegion::new(left, right, None)); + } + selection + } + }, + } + } + + pub fn apply_delta(&mut self, delta: &RopeDelta) { + match &self.mode { + CursorMode::Normal(offset) => { + let mut transformer = Transformer::new(delta); + let new_offset = transformer.transform(*offset, true); + self.mode = CursorMode::Normal(new_offset); + } + CursorMode::Visual { start, end, mode } => { + let mut transformer = Transformer::new(delta); + let start = transformer.transform(*start, false); + let end = transformer.transform(*end, true); + self.mode = CursorMode::Visual { + start, + end, + mode: *mode, + }; + } + CursorMode::Insert(selection) => { + let selection = selection.apply_delta(delta, true, InsertDrift::Default); + self.mode = CursorMode::Insert(selection); + } + } + self.horiz = None; + } + + pub fn yank(&self, text: &impl RopeText) -> RegisterData { + let (content, mode) = match &self.mode { + CursorMode::Insert(selection) => { + let mut mode = VisualMode::Normal; + let mut content = "".to_string(); + for region in selection.regions() { + let region_content = if region.is_caret() { + mode = VisualMode::Linewise; + let line = text.line_of_offset(region.start); + text.line_content(line) + } else { + text.slice_to_cow(region.min()..region.max()) + }; + if content.is_empty() { + content = region_content.to_string(); + } else if content.ends_with('\n') { + content += ®ion_content; + } else { + content += "\n"; + content += ®ion_content; + } + } + (content, mode) + } + CursorMode::Normal(offset) => { + let new_offset = text.next_grapheme_offset(*offset, 1, text.len()); + ( + text.slice_to_cow(*offset..new_offset).to_string(), + VisualMode::Normal, + ) + } + CursorMode::Visual { start, end, mode } => match mode { + VisualMode::Normal => ( + text.slice_to_cow( + *start.min(end)..text.next_grapheme_offset(*start.max(end), 1, text.len()), + ) + .to_string(), + VisualMode::Normal, + ), + VisualMode::Linewise => { + let start_offset = text.offset_of_line(text.line_of_offset(*start.min(end))); + let end_offset = text.offset_of_line(text.line_of_offset(*start.max(end)) + 1); + ( + text.slice_to_cow(start_offset..end_offset).to_string(), + VisualMode::Linewise, + ) + } + VisualMode::Blockwise => { + let mut lines = Vec::new(); + let (start_line, start_col) = text.offset_to_line_col(*start.min(end)); + let (end_line, end_col) = text.offset_to_line_col(*start.max(end)); + let left = start_col.min(end_col); + let right = start_col.max(end_col) + 1; + for line in start_line..end_line + 1 { + let max_col = text.line_end_col(line, true); + if left > max_col { + lines.push("".to_string()); + } else { + let right = match &self.horiz { + Some(ColPosition::End) => max_col, + _ => { + if right > max_col { + max_col + } else { + right + } + } + }; + let left = text.offset_of_line_col(line, left); + let right = text.offset_of_line_col(line, right); + lines.push(text.slice_to_cow(left..right).to_string()); + } + } + (lines.join("\n") + "\n", VisualMode::Blockwise) + } + }, + }; + RegisterData { content, mode } + } + + /// Return the current selection start and end position for a + /// Single cursor selection + pub fn get_selection(&self) -> Option<(usize, usize)> { + match &self.mode { + CursorMode::Visual { + start, + end, + mode: _, + } => Some((*start, *end)), + CursorMode::Insert(selection) => selection + .regions() + .first() + .map(|region| (region.start, region.end)), + _ => None, + } + } + + pub fn get_line_col_char(&self, buffer: &Buffer) -> Option<(usize, usize, usize)> { + match &self.mode { + CursorMode::Normal(offset) => { + let ln_col = buffer.offset_to_line_col(*offset); + Some((ln_col.0, ln_col.1, *offset)) + } + CursorMode::Visual { + start, + end, + mode: _, + } => { + let v = buffer.offset_to_line_col(*start.min(end)); + Some((v.0, v.1, *start)) + } + CursorMode::Insert(selection) => { + if selection.regions().len() > 1 { + return None; + } + + let x = selection.regions().first().unwrap(); + let v = buffer.offset_to_line_col(x.start); + + Some((v.0, v.1, x.start)) + } + } + } + + pub fn get_selection_count(&self) -> usize { + match &self.mode { + CursorMode::Insert(selection) => selection.regions().len(), + _ => 0, + } + } + + pub fn set_offset(&mut self, offset: usize, modify: bool, new_cursor: bool) { + match &self.mode { + CursorMode::Normal(old_offset) => { + if modify && *old_offset != offset { + self.mode = CursorMode::Visual { + start: *old_offset, + end: offset, + mode: VisualMode::Normal, + }; + } else { + self.mode = CursorMode::Normal(offset); + } + } + CursorMode::Visual { + start, + end: _, + mode: _, + } => { + if modify { + self.mode = CursorMode::Visual { + start: *start, + end: offset, + mode: VisualMode::Normal, + }; + } else { + self.mode = CursorMode::Normal(offset); + } + } + CursorMode::Insert(selection) => { + if new_cursor { + let mut new_selection = selection.clone(); + if modify { + if let Some(region) = new_selection.last_inserted_mut() { + region.end = offset; + } else { + new_selection.add_region(SelRegion::caret(offset)); + } + self.set_insert(new_selection); + } else { + let mut new_selection = selection.clone(); + new_selection.add_region(SelRegion::caret(offset)); + self.set_insert(new_selection); + } + } else if modify { + let mut new_selection = Selection::new(); + if let Some(region) = selection.first() { + let new_region = SelRegion::new(region.start, offset, None); + new_selection.add_region(new_region); + } else { + new_selection.add_region(SelRegion::new(offset, offset, None)); + } + self.set_insert(new_selection); + } else { + self.set_insert(Selection::caret(offset)); + } + } + } + } + + pub fn add_region(&mut self, start: usize, end: usize, modify: bool, new_cursor: bool) { + match &self.mode { + CursorMode::Normal(_offset) => { + self.mode = CursorMode::Visual { + start, + end: end - 1, + mode: VisualMode::Normal, + }; + } + CursorMode::Visual { + start: old_start, + end: old_end, + mode: _, + } => { + let forward = old_end >= old_start; + let new_start = (*old_start).min(*old_end).min(start).min(end - 1); + let new_end = (*old_start).max(*old_end).max(start).max(end - 1); + let (new_start, new_end) = if forward { + (new_start, new_end) + } else { + (new_end, new_start) + }; + self.mode = CursorMode::Visual { + start: new_start, + end: new_end, + mode: VisualMode::Normal, + }; + } + CursorMode::Insert(selection) => { + let new_selection = if new_cursor { + let mut new_selection = selection.clone(); + if modify { + let new_region = if let Some(last_inserted) = selection.last_inserted() { + last_inserted.merge_with(SelRegion::new(start, end, None)) + } else { + SelRegion::new(start, end, None) + }; + new_selection.replace_last_inserted_region(new_region); + } else { + new_selection.add_region(SelRegion::new(start, end, None)); + } + new_selection + } else if modify { + let mut new_selection = selection.clone(); + new_selection.add_region(SelRegion::new(start, end, None)); + new_selection + } else { + Selection::region(start, end) + }; + self.mode = CursorMode::Insert(new_selection); + } + } + } +} + +pub fn get_first_selection_after( + cursor: &Cursor, + buffer: &Buffer, + delta: &RopeDelta, +) -> Option { + let mut transformer = Transformer::new(delta); + + let offset = cursor.offset(); + let offset = transformer.transform(offset, false); + let (ins, del) = delta.clone().factor(); + let ins = ins.transform_shrink(&del); + for el in ins.els.iter() { + match el { + lapce_xi_rope::DeltaElement::Copy(b, e) => { + // if b == e, ins.inserted_subset() will panic + if b == e { + return None; + } + } + lapce_xi_rope::DeltaElement::Insert(_) => {} + } + } + + // TODO it's silly to store the whole thing in memory, we only need the first element. + let mut positions = ins + .inserted_subset() + .complement_iter() + .map(|s| s.1) + .collect::>(); + positions.append( + &mut del + .complement_iter() + .map(|s| transformer.transform(s.1, false)) + .collect::>(), + ); + positions.sort_by_key(|p| { + let p = *p as i32 - offset as i32; + if p > 0 { + p as usize + } else { + -p as usize + } + }); + + positions + .first() + .cloned() + .map(Selection::caret) + .map(|selection| { + let cursor_mode = match cursor.mode { + CursorMode::Normal(_) | CursorMode::Visual { .. } => { + let offset = selection.min_offset(); + let offset = buffer.offset_line_end(offset, false).min(offset); + CursorMode::Normal(offset) + } + CursorMode::Insert(_) => CursorMode::Insert(selection), + }; + + Cursor::new(cursor_mode, None, None) + }) +} diff --git a/editor-core/src/editor.rs b/editor-core/src/editor.rs new file mode 100644 index 00000000..5ca6f078 --- /dev/null +++ b/editor-core/src/editor.rs @@ -0,0 +1,1751 @@ +use std::{collections::HashSet, iter, ops::Range}; + +use itertools::Itertools; +use lapce_xi_rope::{Rope, RopeDelta}; + +use crate::{ + buffer::{rope_text::RopeText, Buffer, InvalLines}, + command::EditCommand, + cursor::{get_first_selection_after, Cursor, CursorMode}, + mode::{Mode, MotionMode, VisualMode}, + register::{Clipboard, Register, RegisterData, RegisterKind}, + selection::{InsertDrift, SelRegion, Selection}, + util::{ + has_unmatched_pair, matching_char, matching_pair_direction, str_is_pair_left, + str_matching_pair, + }, + word::{get_char_property, CharClassification}, +}; + +fn format_start_end( + buffer: &Buffer, + range: Range, + is_vertical: bool, + first_non_blank: bool, + count: usize, +) -> Range { + let start = range.start; + let end = range.end; + + if is_vertical { + let start_line = buffer.line_of_offset(start.min(end)); + let end_line = buffer.line_of_offset(end.max(start)); + let start = if first_non_blank { + buffer.first_non_blank_character_on_line(start_line) + } else { + buffer.offset_of_line(start_line) + }; + let end = buffer.offset_of_line(end_line + count); + start..end + } else { + let s = start.min(end); + let e = start.max(end); + s..e + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EditType { + InsertChars, + Delete, + DeleteSelection, + InsertNewline, + Cut, + Paste, + Indent, + Outdent, + ToggleComment, + MoveLine, + Completion, + DeleteWord, + DeleteToBeginningOfLine, + DeleteToEndOfLine, + DeleteToEndOfLineAndInsert, + MotionDelete, + Undo, + Redo, + Other, +} + +impl EditType { + /// Checks whether a new undo group should be created between two edits. + pub fn breaks_undo_group(self, previous: EditType) -> bool { + !((self == EditType::InsertChars || self == EditType::Delete) && self == previous) + } +} + +pub struct EditConf<'a> { + pub comment_token: &'a str, + pub modal: bool, + pub smart_tab: bool, + pub keep_indent: bool, + pub auto_indent: bool, +} + +pub struct Action {} + +impl Action { + pub fn insert( + cursor: &mut Cursor, + buffer: &mut Buffer, + s: &str, + prev_unmatched: &dyn Fn(&Buffer, char, usize) -> Option, + auto_closing_matching_pairs: bool, + auto_surround: bool, + ) -> Vec<(Rope, RopeDelta, InvalLines)> { + let mut deltas = Vec::new(); + if let CursorMode::Insert(selection) = &cursor.mode { + if s.chars().count() != 1 { + let (text, delta, inval_lines) = + buffer.edit([(selection, s)], EditType::InsertChars); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + deltas.push((text, delta, inval_lines)); + cursor.mode = CursorMode::Insert(selection); + } else { + let c = s.chars().next().unwrap(); + let matching_pair_type = matching_pair_direction(c); + + // The main edit operations + let mut edits = vec![]; + + // "Late edits" - characters to be inserted after particular regions + let mut edits_after = vec![]; + + let mut selection = selection.clone(); + for (idx, region) in selection.regions_mut().iter_mut().enumerate() { + let offset = region.end; + let cursor_char = buffer.char_at_offset(offset); + let prev_offset = buffer.move_left(offset, Mode::Normal, 1); + let prev_cursor_char = if prev_offset < offset { + buffer.char_at_offset(prev_offset) + } else { + None + }; + + // when text is selected, and [,{,(,'," is inserted + // wrap the text with that char and its corresponding closing pair + if region.start != region.end + && auto_surround + && (matching_pair_type == Some(true) || c == '"' || c == '\'') + { + edits.push((Selection::region(region.min(), region.min()), c.to_string())); + edits_after.push(( + idx, + match c { + '"' => '"', + '\'' => '\'', + _ => matching_char(c).unwrap(), + }, + )); + continue; + } + + if auto_closing_matching_pairs { + if (c == '"' || c == '\'') && cursor_char == Some(c) { + // Skip the closing character + let new_offset = buffer.next_grapheme_offset(offset, 1, buffer.len()); + + *region = SelRegion::caret(new_offset); + continue; + } + + if matching_pair_type == Some(false) { + if cursor_char == Some(c) { + // Skip the closing character + let new_offset = + buffer.next_grapheme_offset(offset, 1, buffer.len()); + + *region = SelRegion::caret(new_offset); + continue; + } + + let line = buffer.line_of_offset(offset); + let line_start = buffer.offset_of_line(line); + if buffer.slice_to_cow(line_start..offset).trim() == "" { + let opening_character = matching_char(c).unwrap(); + if let Some(previous_offset) = + prev_unmatched(buffer, opening_character, offset) + { + // Auto-indent closing character to the same level as the opening. + let previous_line = buffer.line_of_offset(previous_offset); + let line_indent = buffer.indent_on_line(previous_line); + + let current_selection = Selection::region(line_start, offset); + + edits.push((current_selection, format!("{line_indent}{c}"))); + continue; + } + } + } + + if matching_pair_type == Some(true) || c == '"' || c == '\'' { + // Create a late edit to insert the closing pair, if allowed. + let is_whitespace_or_punct = cursor_char + .map(|c| { + let prop = get_char_property(c); + prop == CharClassification::Lf + || prop == CharClassification::Space + || prop == CharClassification::Punctuation + }) + .unwrap_or(true); + + let should_insert_pair = match c { + '"' | '\'' => { + is_whitespace_or_punct + && prev_cursor_char + .map(|c| { + let prop = get_char_property(c); + prop == CharClassification::Lf + || prop == CharClassification::Space + || prop == CharClassification::Punctuation + }) + .unwrap_or(true) + } + _ => is_whitespace_or_punct, + }; + + if should_insert_pair { + let insert_after = match c { + '"' => '"', + '\'' => '\'', + _ => matching_char(c).unwrap(), + }; + edits_after.push((idx, insert_after)); + } + }; + } + + let current_selection = Selection::region(region.start, region.end); + + edits.push((current_selection, c.to_string())); + } + + // Apply edits to current selection + let edits = edits + .iter() + .map(|(selection, content)| (selection, content.as_str())) + .collect::>(); + + let (text, delta, inval_lines) = buffer.edit(&edits, EditType::InsertChars); + + buffer.set_cursor_before(CursorMode::Insert(selection.clone())); + + // Update selection + let mut selection = selection.apply_delta(&delta, true, InsertDrift::Default); + + buffer.set_cursor_after(CursorMode::Insert(selection.clone())); + + deltas.push((text, delta, inval_lines)); + // Apply late edits + let edits_after = edits_after + .iter() + .map(|(idx, content)| { + let region = &selection.regions()[*idx]; + ( + Selection::region(region.max(), region.max()), + content.to_string(), + ) + }) + .collect::>(); + + let edits_after = edits_after + .iter() + .map(|(selection, content)| (selection, content.as_str())) + .collect::>(); + + if !edits_after.is_empty() { + let (text, delta, inval_lines) = + buffer.edit(&edits_after, EditType::InsertChars); + deltas.push((text, delta, inval_lines)); + } + + // Adjust selection according to previous late edits + let mut adjustment = 0; + for region in selection + .regions_mut() + .iter_mut() + .sorted_by(|region_a, region_b| region_a.start.cmp(®ion_b.start)) + { + let new_region = + SelRegion::new(region.start + adjustment, region.end + adjustment, None); + + if let Some(inserted) = edits_after.iter().find_map(|(selection, str)| { + if selection.last_inserted().map(|r| r.start) == Some(region.start) { + Some(str) + } else { + None + } + }) { + adjustment += inserted.len(); + } + + *region = new_region; + } + + cursor.mode = CursorMode::Insert(selection); + } + } + deltas + } + + fn toggle_visual(cursor: &mut Cursor, visual_mode: VisualMode, modal: bool) { + if !modal { + return; + } + + match &cursor.mode { + CursorMode::Visual { start, end, mode } => { + if mode != &visual_mode { + cursor.mode = CursorMode::Visual { + start: *start, + end: *end, + mode: visual_mode, + }; + } else { + cursor.mode = CursorMode::Normal(*end); + }; + } + _ => { + let offset = cursor.offset(); + cursor.mode = CursorMode::Visual { + start: offset, + end: offset, + mode: visual_mode, + }; + } + } + } + + fn insert_new_line( + buffer: &mut Buffer, + cursor: &mut Cursor, + selection: Selection, + keep_indent: bool, + auto_indent: bool, + ) -> Vec<(Rope, RopeDelta, InvalLines)> { + let mut edits = Vec::with_capacity(selection.regions().len()); + let mut extra_edits = Vec::new(); + let mut shift = 0i32; + for region in selection.regions() { + let offset = region.max(); + let line = buffer.line_of_offset(offset); + let line_start = buffer.offset_of_line(line); + let line_end = buffer.line_end_offset(line, true); + let line_indent = buffer.indent_on_line(line); + let first_half = buffer.slice_to_cow(line_start..offset); + let second_half = buffer.slice_to_cow(offset..line_end); + let second_half = second_half.trim(); + + // TODO: this could be done with 1 string + let new_line_content = { + let indent_storage; + let indent = if auto_indent && has_unmatched_pair(&first_half) { + indent_storage = format!("{}{}", line_indent, buffer.indent_unit()); + &indent_storage + } else if keep_indent && second_half.is_empty() { + indent_storage = buffer.indent_on_line(line + 1); + if indent_storage.len() > line_indent.len() { + &indent_storage + } else { + &line_indent + } + } else if keep_indent { + &line_indent + } else { + indent_storage = String::new(); + &indent_storage + }; + format!("\n{indent}") + }; + + let selection = Selection::region(region.min(), region.max()); + + shift -= (region.max() - region.min()) as i32; + shift += new_line_content.len() as i32; + + edits.push((selection, new_line_content)); + + if let Some(c) = first_half.chars().rev().find(|&c| c != ' ') { + if let Some(true) = matching_pair_direction(c) { + if let Some(c) = matching_char(c) { + if second_half.starts_with(c) { + let selection = + Selection::caret((region.max() as i32 + shift) as usize); + let content = format!("\n{line_indent}"); + extra_edits.push((selection, content)); + } + } + } + } + } + + let edits = edits + .iter() + .map(|(selection, s)| (selection, s.as_str())) + .collect::>(); + let (text, delta, inval_lines) = buffer.edit(&edits, EditType::InsertNewline); + let mut selection = selection.apply_delta(&delta, true, InsertDrift::Default); + + let mut deltas = vec![(text, delta, inval_lines)]; + + if !extra_edits.is_empty() { + let edits = extra_edits + .iter() + .map(|(selection, s)| (selection, s.as_str())) + .collect::>(); + let (text, delta, inval_lines) = buffer.edit(&edits, EditType::InsertNewline); + selection = selection.apply_delta(&delta, false, InsertDrift::Default); + deltas.push((text, delta, inval_lines)); + } + + cursor.mode = CursorMode::Insert(selection); + + deltas + } + + pub fn execute_motion_mode( + cursor: &mut Cursor, + buffer: &mut Buffer, + motion_mode: MotionMode, + range: Range, + is_vertical: bool, + register: &mut Register, + ) -> Vec<(Rope, RopeDelta, InvalLines)> { + let mut deltas = Vec::new(); + match motion_mode { + MotionMode::Delete { .. } => { + let range = format_start_end(buffer, range, is_vertical, false, 1); + register.add( + RegisterKind::Delete, + RegisterData { + content: buffer.slice_to_cow(range.clone()).to_string(), + mode: if is_vertical { + VisualMode::Linewise + } else { + VisualMode::Normal + }, + }, + ); + let selection = Selection::region(range.start, range.end); + let (text, delta, inval_lines) = + buffer.edit([(&selection, "")], EditType::MotionDelete); + cursor.apply_delta(&delta); + deltas.push((text, delta, inval_lines)); + } + MotionMode::Yank { .. } => { + let range = format_start_end(buffer, range, is_vertical, false, 1); + register.add( + RegisterKind::Yank, + RegisterData { + content: buffer.slice_to_cow(range).to_string(), + mode: if is_vertical { + VisualMode::Linewise + } else { + VisualMode::Normal + }, + }, + ); + } + MotionMode::Indent => { + let selection = Selection::region(range.start, range.end); + let (text, delta, inval_lines) = Self::do_indent(buffer, selection); + deltas.push((text, delta, inval_lines)); + } + MotionMode::Outdent => { + let selection = Selection::region(range.start, range.end); + let (text, delta, inval_lines) = Self::do_outdent(buffer, selection); + deltas.push((text, delta, inval_lines)); + } + } + deltas + } + + /// Compute the result of pasting `content` into `selection`. + /// If the number of lines to be pasted is divisible by the number of [`SelRegion`]s in + /// `selection`, partition the content to be pasted into groups of equal numbers of lines and + /// paste one group at each [`SelRegion`]. + /// The way lines are counted and `content` is partitioned depends on `mode`. + fn compute_paste_edit( + buffer: &mut Buffer, + selection: &Selection, + content: &str, + mode: VisualMode, + ) -> (Rope, RopeDelta, InvalLines) { + if selection.len() > 1 { + let line_ends: Vec<_> = content.match_indices('\n').map(|(idx, _)| idx).collect(); + + match mode { + // Consider lines to be separated by the line terminator. + // The number of lines == number of line terminators + 1. + // The final line in each group does not include the line terminator. + VisualMode::Normal if (line_ends.len() + 1) % selection.len() == 0 => { + let lines_per_group = (line_ends.len() + 1) / selection.len(); + let mut start_idx = 0; + let last_line_start = line_ends + .len() + .checked_sub(lines_per_group) + .and_then(|line_idx| line_ends.get(line_idx)) + .map(|line_end| line_end + 1) + .unwrap_or(0); + + let groups = line_ends + .iter() + .skip(lines_per_group - 1) + .step_by(lines_per_group) + .map(|&end_idx| { + let group = &content[start_idx..end_idx]; + let group = group.strip_suffix('\r').unwrap_or(group); + start_idx = end_idx + 1; + + group + }) + .chain(iter::once(&content[last_line_start..])); + + let edits = selection + .regions() + .iter() + .copied() + .map(Selection::sel_region) + .zip(groups); + + buffer.edit(edits, EditType::Paste) + } + // Consider lines to be terminated by the line terminator. + // The number of lines == number of line terminators. + // The final line in each group includes the line terminator. + VisualMode::Linewise | VisualMode::Blockwise + if line_ends.len() % selection.len() == 0 => + { + let lines_per_group = line_ends.len() / selection.len(); + let mut start_idx = 0; + + let groups = line_ends + .iter() + .skip(lines_per_group - 1) + .step_by(lines_per_group) + .map(|&end_idx| { + let group = &content[start_idx..=end_idx]; + start_idx = end_idx + 1; + + group + }); + + let edits = selection + .regions() + .iter() + .copied() + .map(Selection::sel_region) + .zip(groups); + + buffer.edit(edits, EditType::Paste) + } + _ => buffer.edit([(&selection, content)], EditType::Paste), + } + } else { + buffer.edit([(&selection, content)], EditType::Paste) + } + } + + pub fn do_paste( + cursor: &mut Cursor, + buffer: &mut Buffer, + data: &RegisterData, + ) -> Vec<(Rope, RopeDelta, InvalLines)> { + let mut deltas = Vec::new(); + match data.mode { + VisualMode::Normal => { + let selection = match cursor.mode { + CursorMode::Normal(offset) => { + let line_end = buffer.offset_line_end(offset, true); + let offset = (offset + 1).min(line_end); + Selection::caret(offset) + } + CursorMode::Insert { .. } | CursorMode::Visual { .. } => { + cursor.edit_selection(buffer) + } + }; + let after = cursor.is_insert() || !data.content.contains('\n'); + let (text, delta, inval_lines) = + Self::compute_paste_edit(buffer, &selection, &data.content, data.mode); + let selection = selection.apply_delta(&delta, after, InsertDrift::Default); + deltas.push((text, delta, inval_lines)); + if !after { + cursor.update_selection(buffer, selection); + } else { + match cursor.mode { + CursorMode::Normal(_) | CursorMode::Visual { .. } => { + let offset = buffer.prev_grapheme_offset(selection.min_offset(), 1, 0); + cursor.mode = CursorMode::Normal(offset); + } + CursorMode::Insert { .. } => { + cursor.mode = CursorMode::Insert(selection); + } + } + } + } + VisualMode::Linewise | VisualMode::Blockwise => { + let (selection, content) = match &cursor.mode { + CursorMode::Normal(offset) => { + let line = buffer.line_of_offset(*offset); + let offset = buffer.offset_of_line(line + 1); + (Selection::caret(offset), data.content.clone()) + } + CursorMode::Insert(selection) => { + let mut selection = selection.clone(); + for region in selection.regions_mut() { + if region.is_caret() { + let line = buffer.line_of_offset(region.start); + let start = buffer.offset_of_line(line); + region.start = start; + region.end = start; + } + } + (selection, data.content.clone()) + } + CursorMode::Visual { mode, .. } => { + let selection = cursor.edit_selection(buffer); + let data = match mode { + VisualMode::Linewise => data.content.clone(), + _ => "\n".to_string() + &data.content, + }; + (selection, data) + } + }; + let (text, delta, inval_lines) = + Self::compute_paste_edit(buffer, &selection, &content, data.mode); + let selection = + selection.apply_delta(&delta, cursor.is_insert(), InsertDrift::Default); + deltas.push((text, delta, inval_lines)); + match cursor.mode { + CursorMode::Normal(_) | CursorMode::Visual { .. } => { + let offset = selection.min_offset(); + let offset = if cursor.is_visual() { + offset + 1 + } else { + offset + }; + let line = buffer.line_of_offset(offset); + let offset = buffer.first_non_blank_character_on_line(line); + cursor.mode = CursorMode::Normal(offset); + } + CursorMode::Insert(_) => { + cursor.mode = CursorMode::Insert(selection); + } + } + } + } + deltas + } + + fn do_indent(buffer: &mut Buffer, selection: Selection) -> (Rope, RopeDelta, InvalLines) { + let indent = buffer.indent_unit(); + let mut edits = Vec::new(); + + let mut lines = HashSet::new(); + for region in selection.regions() { + let start_line = buffer.line_of_offset(region.min()); + let mut end_line = buffer.line_of_offset(region.max()); + if end_line > start_line { + let end_line_start = buffer.offset_of_line(end_line); + if end_line_start == region.max() { + end_line -= 1; + } + } + for line in start_line..=end_line { + if lines.insert(line) { + let line_content = buffer.line_content(line); + if line_content == "\n" || line_content == "\r\n" { + continue; + } + let nonblank = buffer.first_non_blank_character_on_line(line); + let edit = crate::indent::create_edit(buffer, nonblank, indent); + edits.push(edit); + } + } + } + + buffer.edit(&edits, EditType::Indent) + } + + fn do_outdent(buffer: &mut Buffer, selection: Selection) -> (Rope, RopeDelta, InvalLines) { + let indent = buffer.indent_unit(); + let mut edits = Vec::new(); + + let mut lines = HashSet::new(); + for region in selection.regions() { + let start_line = buffer.line_of_offset(region.min()); + let mut end_line = buffer.line_of_offset(region.max()); + if end_line > start_line { + let end_line_start = buffer.offset_of_line(end_line); + if end_line_start == region.max() { + end_line -= 1; + } + } + for line in start_line..=end_line { + if lines.insert(line) { + let line_content = buffer.line_content(line); + if line_content == "\n" || line_content == "\r\n" { + continue; + } + let nonblank = buffer.first_non_blank_character_on_line(line); + if let Some(edit) = crate::indent::create_outdent(buffer, nonblank, indent) { + edits.push(edit); + } + } + } + } + + buffer.edit(&edits, EditType::Outdent) + } + + fn duplicate_line( + cursor: &mut Cursor, + buffer: &mut Buffer, + direction: DuplicateDirection, + ) -> Vec<(Rope, RopeDelta, InvalLines)> { + // TODO other modes + let selection = match cursor.mode { + CursorMode::Insert(ref mut sel) => sel, + _ => return vec![], + }; + + let mut line_ranges = HashSet::new(); + for region in selection.regions_mut() { + let start_line = buffer.line_of_offset(region.start); + let end_line = buffer.line_of_offset(region.end) + 1; + + line_ranges.insert(start_line..end_line); + } + + let mut edits = vec![]; + for range in line_ranges { + let start = buffer.offset_of_line(range.start); + let end = buffer.offset_of_line(range.end); + + let content = buffer.slice_to_cow(start..end).into_owned(); + edits.push(( + match direction { + DuplicateDirection::Up => Selection::caret(end), + DuplicateDirection::Down => Selection::caret(start), + }, + content, + )); + } + + let edits = edits + .iter() + .map(|(sel, content)| (sel, content.as_str())) + .collect::>(); + + let (text, delta, inval_lines) = buffer.edit(&edits, EditType::InsertChars); + + *selection = selection.apply_delta(&delta, true, InsertDrift::Default); + + vec![(text, delta, inval_lines)] + } + + #[allow(clippy::too_many_arguments)] + pub fn do_edit( + cursor: &mut Cursor, + buffer: &mut Buffer, + cmd: &EditCommand, + clipboard: &mut T, + register: &mut Register, + EditConf { + comment_token, + modal, + smart_tab, + keep_indent, + auto_indent, + }: EditConf, + ) -> Vec<(Rope, RopeDelta, InvalLines)> { + use crate::command::EditCommand::*; + match cmd { + MoveLineUp => { + let mut deltas = Vec::new(); + if let CursorMode::Insert(mut selection) = cursor.mode.clone() { + for region in selection.regions_mut() { + let start_line = buffer.line_of_offset(region.min()); + if start_line > 0 { + let previous_line_len = buffer.line_content(start_line - 1).len(); + + let end_line = buffer.line_of_offset(region.max()); + let start = buffer.offset_of_line(start_line); + let end = buffer.offset_of_line(end_line + 1); + let content = buffer.slice_to_cow(start..end).to_string(); + let (text, delta, inval_lines) = buffer.edit( + [ + (&Selection::region(start, end), ""), + ( + &Selection::caret(buffer.offset_of_line(start_line - 1)), + &content, + ), + ], + EditType::MoveLine, + ); + deltas.push((text, delta, inval_lines)); + region.start -= previous_line_len; + region.end -= previous_line_len; + } + } + cursor.mode = CursorMode::Insert(selection); + } + deltas + } + MoveLineDown => { + let mut deltas = Vec::new(); + if let CursorMode::Insert(mut selection) = cursor.mode.clone() { + for region in selection.regions_mut().iter_mut().rev() { + let last_line = buffer.last_line(); + let start_line = buffer.line_of_offset(region.min()); + let end_line = buffer.line_of_offset(region.max()); + if end_line < last_line { + let next_line_len = buffer.line_content(end_line + 1).len(); + + let start = buffer.offset_of_line(start_line); + let end = buffer.offset_of_line(end_line + 1); + let content = buffer.slice_to_cow(start..end).to_string(); + let (text, delta, inval_lines) = buffer.edit( + [ + ( + &Selection::caret(buffer.offset_of_line(end_line + 2)), + content.as_str(), + ), + (&Selection::region(start, end), ""), + ], + EditType::MoveLine, + ); + deltas.push((text, delta, inval_lines)); + region.start += next_line_len; + region.end += next_line_len; + } + } + cursor.mode = CursorMode::Insert(selection); + } + deltas + } + InsertNewLine => match cursor.mode.clone() { + CursorMode::Normal(offset) => Self::insert_new_line( + buffer, + cursor, + Selection::caret(offset), + keep_indent, + auto_indent, + ), + CursorMode::Insert(selection) => { + Self::insert_new_line(buffer, cursor, selection, keep_indent, auto_indent) + } + CursorMode::Visual { + start: _, + end: _, + mode: _, + } => { + vec![] + } + }, + InsertTab => { + let mut deltas = Vec::new(); + if let CursorMode::Insert(selection) = &cursor.mode { + if smart_tab { + let indent = buffer.indent_unit(); + let mut edits = Vec::new(); + + for region in selection.regions() { + if region.is_caret() { + edits.push(crate::indent::create_edit(buffer, region.start, indent)) + } else { + let start_line = buffer.line_of_offset(region.min()); + let end_line = buffer.line_of_offset(region.max()); + for line in start_line..=end_line { + let offset = buffer.first_non_blank_character_on_line(line); + edits.push(crate::indent::create_edit(buffer, offset, indent)) + } + } + } + + let (text, delta, inval_lines) = buffer.edit(&edits, EditType::InsertChars); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + deltas.push((text, delta, inval_lines)); + cursor.mode = CursorMode::Insert(selection); + } else { + let (text, delta, inval_lines) = + buffer.edit([(&selection, "\t")], EditType::InsertChars); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + deltas.push((text, delta, inval_lines)); + cursor.mode = CursorMode::Insert(selection); + } + } + deltas + } + IndentLine => { + let selection = cursor.edit_selection(buffer); + let (text, delta, inval_lines) = Self::do_indent(buffer, selection); + cursor.apply_delta(&delta); + vec![(text, delta, inval_lines)] + } + JoinLines => { + let offset = cursor.offset(); + let (line, _col) = buffer.offset_to_line_col(offset); + if line < buffer.last_line() { + let start = buffer.line_end_offset(line, true); + let end = buffer.first_non_blank_character_on_line(line + 1); + vec![buffer.edit([(&Selection::region(start, end), " ")], EditType::Other)] + } else { + vec![] + } + } + OutdentLine => { + let selection = cursor.edit_selection(buffer); + let (text, delta, inval_lines) = Self::do_outdent(buffer, selection); + cursor.apply_delta(&delta); + vec![(text, delta, inval_lines)] + } + ToggleLineComment => { + let mut lines = HashSet::new(); + let selection = cursor.edit_selection(buffer); + let mut had_comment = true; + let mut smallest_indent = usize::MAX; + for region in selection.regions() { + let mut line = buffer.line_of_offset(region.min()); + let end_line = buffer.line_of_offset(region.max()); + let end_line_offset = buffer.offset_of_line(end_line); + let end = if end_line > line && region.max() == end_line_offset { + end_line_offset + } else { + buffer.offset_of_line(end_line + 1) + }; + let start = buffer.offset_of_line(line); + for content in buffer.text().lines(start..end) { + let trimmed_content = content.trim_start(); + if trimmed_content.is_empty() { + line += 1; + continue; + } + let indent = content.len() - trimmed_content.len(); + if indent < smallest_indent { + smallest_indent = indent; + } + if !trimmed_content.starts_with(comment_token) { + had_comment = false; + lines.insert((line, indent, 0)); + } else { + let had_space_after_comment = + trimmed_content.chars().nth(comment_token.len()) == Some(' '); + lines.insert(( + line, + indent, + comment_token.len() + usize::from(had_space_after_comment), + )); + } + line += 1; + } + } + + let (text, delta, inval_lines) = if had_comment { + let mut selection = Selection::new(); + for (line, indent, len) in lines.iter() { + let start = buffer.offset_of_line(*line) + indent; + selection.add_region(SelRegion::new(start, start + len, None)) + } + buffer.edit([(&selection, "")], EditType::ToggleComment) + } else { + let mut selection = Selection::new(); + for (line, _, _) in lines.iter() { + let start = buffer.offset_of_line(*line) + smallest_indent; + selection.add_region(SelRegion::new(start, start, None)) + } + buffer.edit( + [(&selection, format!("{comment_token} ").as_str())], + EditType::ToggleComment, + ) + }; + cursor.apply_delta(&delta); + vec![(text, delta, inval_lines)] + } + Undo => { + if let Some((text, delta, inval_lines, cursor_mode)) = buffer.do_undo() { + if let Some(cursor_mode) = cursor_mode { + cursor.mode = if modal { + CursorMode::Normal(cursor_mode.offset()) + } else if cursor.is_insert() { + cursor_mode + } else { + CursorMode::Insert(Selection::caret(cursor_mode.offset())) + }; + } else if let Some(new_cursor) = + get_first_selection_after(cursor, buffer, &delta) + { + *cursor = new_cursor + } else { + cursor.apply_delta(&delta); + } + vec![(text, delta, inval_lines)] + } else { + vec![] + } + } + Redo => { + if let Some((text, delta, inval_lines, cursor_mode)) = buffer.do_redo() { + if let Some(cursor_mode) = cursor_mode { + cursor.mode = if modal { + CursorMode::Normal(cursor_mode.offset()) + } else if cursor.is_insert() { + cursor_mode + } else { + CursorMode::Insert(Selection::caret(cursor_mode.offset())) + }; + } else if let Some(new_cursor) = + get_first_selection_after(cursor, buffer, &delta) + { + *cursor = new_cursor + } else { + cursor.apply_delta(&delta); + } + vec![(text, delta, inval_lines)] + } else { + vec![] + } + } + ClipboardCopy => { + let data = cursor.yank(buffer); + clipboard.put_string(data.content); + + match &cursor.mode { + CursorMode::Visual { + start, + end, + mode: _, + } => { + let offset = *start.min(end); + let offset = buffer.offset_line_end(offset, false).min(offset); + cursor.mode = CursorMode::Normal(offset); + } + CursorMode::Normal(_) | CursorMode::Insert(_) => {} + } + vec![] + } + ClipboardCut => { + let data = cursor.yank(buffer); + clipboard.put_string(data.content); + + let selection = if let CursorMode::Insert(mut selection) = cursor.mode.clone() { + for region in selection.regions_mut() { + if region.is_caret() { + let line = buffer.line_of_offset(region.start); + let start = buffer.offset_of_line(line); + let end = buffer.offset_of_line(line + 1); + region.start = start; + region.end = end; + } + } + selection + } else { + cursor.edit_selection(buffer) + }; + + let (text, delta, inval_lines) = buffer.edit([(&selection, "")], EditType::Cut); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + cursor.update_selection(buffer, selection); + vec![(text, delta, inval_lines)] + } + ClipboardPaste => { + if let Some(s) = clipboard.get_string() { + let mode = if s.ends_with('\n') { + VisualMode::Linewise + } else { + VisualMode::Normal + }; + let data = RegisterData { content: s, mode }; + Self::do_paste(cursor, buffer, &data) + } else { + vec![] + } + } + Yank => { + match &cursor.mode { + CursorMode::Visual { start, end, .. } => { + let data = cursor.yank(buffer); + register.add_yank(data); + + let offset = *start.min(end); + let offset = buffer.offset_line_end(offset, false).min(offset); + cursor.mode = CursorMode::Normal(offset); + } + CursorMode::Normal(_) => {} + CursorMode::Insert(_) => {} + } + vec![] + } + Paste => { + let data = register.unnamed.clone(); + Self::do_paste(cursor, buffer, &data) + } + PasteBefore => { + let offset = cursor.offset(); + let data = register.unnamed.clone(); + let mut local_cursor = + Cursor::new(CursorMode::Insert(Selection::new()), None, None); + local_cursor.set_offset(offset, false, false); + Self::do_paste(&mut local_cursor, buffer, &data) + } + NewLineAbove => { + let offset = cursor.offset(); + let line = buffer.line_of_offset(offset); + let offset = if line > 0 { + buffer.line_end_offset(line - 1, true) + } else { + buffer.first_non_blank_character_on_line(line) + }; + let delta = Self::insert_new_line( + buffer, + cursor, + Selection::caret(offset), + keep_indent, + auto_indent, + ); + if line == 0 { + cursor.mode = CursorMode::Insert(Selection::caret(offset)); + } + delta + } + NewLineBelow => { + let offset = cursor.offset(); + let offset = buffer.offset_line_end(offset, true); + Self::insert_new_line( + buffer, + cursor, + Selection::caret(offset), + keep_indent, + auto_indent, + ) + } + DeleteBackward => { + let (selection, edit_type) = match cursor.mode { + CursorMode::Normal(_) => (cursor.edit_selection(buffer), EditType::Delete), + CursorMode::Visual { .. } => { + (cursor.edit_selection(buffer), EditType::DeleteSelection) + } + CursorMode::Insert(_) => { + let selection = cursor.edit_selection(buffer); + let edit_type = if selection.is_caret() { + EditType::Delete + } else { + EditType::DeleteSelection + }; + let indent = buffer.indent_unit(); + let mut new_selection = Selection::new(); + for region in selection.regions() { + let new_region = if region.is_caret() { + if indent.starts_with('\t') { + let new_end = buffer.move_left(region.end, Mode::Insert, 1); + SelRegion::new(region.start, new_end, None) + } else { + let line = buffer.line_of_offset(region.start); + let nonblank = buffer.first_non_blank_character_on_line(line); + let (_, col) = buffer.offset_to_line_col(region.start); + let count = if region.start <= nonblank && col > 0 { + let r = col % indent.len(); + if r == 0 { + indent.len() + } else { + r + } + } else { + 1 + }; + let new_end = buffer.move_left(region.end, Mode::Insert, count); + SelRegion::new(region.start, new_end, None) + } + } else { + *region + }; + new_selection.add_region(new_region); + } + + let mut selection = new_selection; + if selection.regions().len() == 1 { + let delete_str = buffer + .slice_to_cow(selection.min_offset()..selection.max_offset()) + .to_string(); + if str_is_pair_left(&delete_str) + || delete_str == "\"" + || delete_str == "'" + { + let matching_char = match delete_str.as_str() { + "\"" => Some('"'), + "'" => Some('\''), + _ => str_matching_pair(&delete_str), + }; + if let Some(c) = matching_char { + let offset = selection.max_offset(); + let line = buffer.line_of_offset(offset); + let line_end = buffer.line_end_offset(line, true); + let content = buffer.slice_to_cow(offset..line_end).to_string(); + if content.trim().starts_with(&c.to_string()) { + let index = content.match_indices(c).next().unwrap().0; + selection = Selection::region( + selection.min_offset(), + offset + index + 1, + ); + } + } + } + } + (selection, edit_type) + } + }; + let (text, delta, inval_lines) = buffer.edit([(&selection, "")], edit_type); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + cursor.update_selection(buffer, selection); + vec![(text, delta, inval_lines)] + } + DeleteForward => { + let (selection, edit_type) = match cursor.mode { + CursorMode::Normal(_) => (cursor.edit_selection(buffer), EditType::Delete), + CursorMode::Visual { .. } => { + (cursor.edit_selection(buffer), EditType::DeleteSelection) + } + CursorMode::Insert(_) => { + let selection = cursor.edit_selection(buffer); + let edit_type = if selection.is_caret() { + EditType::Delete + } else { + EditType::DeleteSelection + }; + let mut new_selection = Selection::new(); + for region in selection.regions() { + let new_region = if region.is_caret() { + let new_end = buffer.move_right(region.end, Mode::Insert, 1); + SelRegion::new(region.start, new_end, None) + } else { + *region + }; + new_selection.add_region(new_region); + } + (new_selection, edit_type) + } + }; + let (text, delta, inval_lines) = buffer.edit([(&selection, "")], edit_type); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + cursor.update_selection(buffer, selection); + vec![(text, delta, inval_lines)] + } + DeleteLine => { + let selection = cursor.edit_selection(buffer); + let range = format_start_end( + buffer, + selection.min_offset()..selection.max_offset(), + true, + false, + 1, + ); + let selection = Selection::region(range.start, range.end); + let (text, delta, inval_lines) = buffer.edit([(&selection, "")], EditType::Delete); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + cursor.mode = CursorMode::Insert(selection); + vec![(text, delta, inval_lines)] + } + DeleteWordForward => { + let selection = match cursor.mode { + CursorMode::Normal(_) | CursorMode::Visual { .. } => { + cursor.edit_selection(buffer) + } + CursorMode::Insert(_) => { + let mut new_selection = Selection::new(); + let selection = cursor.edit_selection(buffer); + + for region in selection.regions() { + let end = buffer.move_word_forward(region.end); + let new_region = SelRegion::new(region.start, end, None); + new_selection.add_region(new_region); + } + + new_selection + } + }; + let (text, delta, inval_lines) = + buffer.edit([(&selection, "")], EditType::DeleteWord); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + cursor.update_selection(buffer, selection); + vec![(text, delta, inval_lines)] + } + DeleteWordBackward => { + let selection = match cursor.mode { + CursorMode::Normal(_) | CursorMode::Visual { .. } => { + cursor.edit_selection(buffer) + } + CursorMode::Insert(_) => { + let mut new_selection = Selection::new(); + let selection = cursor.edit_selection(buffer); + + for region in selection.regions() { + let end = buffer.move_word_backward_deletion(region.end); + let new_region = SelRegion::new(region.start, end, None); + new_selection.add_region(new_region); + } + + new_selection + } + }; + let (text, delta, inval_lines) = + buffer.edit([(&selection, "")], EditType::DeleteWord); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + cursor.update_selection(buffer, selection); + vec![(text, delta, inval_lines)] + } + DeleteToBeginningOfLine => { + let selection = match cursor.mode { + CursorMode::Normal(_) | CursorMode::Visual { .. } => { + cursor.edit_selection(buffer) + } + CursorMode::Insert(_) => { + let selection = cursor.edit_selection(buffer); + + let mut new_selection = Selection::new(); + for region in selection.regions() { + let line = buffer.line_of_offset(region.end); + let end = buffer.offset_of_line(line); + let new_region = SelRegion::new(region.start, end, None); + new_selection.add_region(new_region); + } + + new_selection + } + }; + let (text, delta, inval_lines) = + buffer.edit([(&selection, "")], EditType::DeleteToBeginningOfLine); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + cursor.update_selection(buffer, selection); + vec![(text, delta, inval_lines)] + } + DeleteToEndOfLine => { + let selection = match cursor.mode { + CursorMode::Normal(_) | CursorMode::Visual { .. } => { + cursor.edit_selection(buffer) + } + CursorMode::Insert(_) => { + let mut selection = cursor.edit_selection(buffer); + + let cursor_offset = cursor.offset(); + let line = buffer.line_of_offset(cursor_offset); + let end_of_line_offset = buffer.line_end_offset(line, true); + let new_region = SelRegion::new(cursor_offset, end_of_line_offset, None); + selection.add_region(new_region); + + selection + } + }; + let (text, delta, inval_lines) = + buffer.edit([(&selection, "")], EditType::DeleteToEndOfLine); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + cursor.update_selection(buffer, selection); + vec![(text, delta, inval_lines)] + } + DeleteForwardAndInsert => { + let selection = cursor.edit_selection(buffer); + let (text, delta, inval_lines) = buffer.edit([(&selection, "")], EditType::Delete); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + cursor.mode = CursorMode::Insert(selection); + vec![(text, delta, inval_lines)] + } + DeleteWordAndInsert => { + let selection = { + let mut new_selection = Selection::new(); + let selection = cursor.edit_selection(buffer); + + for region in selection.regions() { + let end = buffer.move_word_forward(region.end); + let new_region = SelRegion::new(region.start, end, None); + new_selection.add_region(new_region); + } + + new_selection + }; + let (text, delta, inval_lines) = + buffer.edit([(&selection, "")], EditType::DeleteWord); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + cursor.mode = CursorMode::Insert(selection); + vec![(text, delta, inval_lines)] + } + DeleteLineAndInsert => { + let selection = cursor.edit_selection(buffer); + let range = format_start_end( + buffer, + selection.min_offset()..selection.max_offset(), + true, + true, + 1, + ); + let selection = Selection::region(range.start, range.end - 1); // -1 because we want to keep the line itself + let (text, delta, inval_lines) = buffer.edit([(&selection, "")], EditType::Delete); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + cursor.mode = CursorMode::Insert(selection); + vec![(text, delta, inval_lines)] + } + DeleteToEndOfLineAndInsert => { + let mut selection = cursor.edit_selection(buffer); + + let cursor_offset = cursor.offset(); + let line = buffer.line_of_offset(cursor_offset); + let end_of_line_offset = buffer.line_end_offset(line, true); + + let new_region = SelRegion::new(cursor_offset, end_of_line_offset, None); + selection.add_region(new_region); + + let (text, delta, inval_lines) = buffer.edit([(&selection, "")], EditType::Delete); + let selection = selection.apply_delta(&delta, true, InsertDrift::Default); + cursor.mode = CursorMode::Insert(selection); + vec![(text, delta, inval_lines)] + } + NormalMode => { + if !modal { + if let CursorMode::Insert(selection) = &cursor.mode { + match selection.regions().len() { + i if i > 1 => { + if let Some(region) = selection.last_inserted() { + let new_selection = Selection::region(region.start, region.end); + cursor.mode = CursorMode::Insert(new_selection); + return vec![]; + } + } + 1 => { + let region = selection.regions()[0]; + if !region.is_caret() { + let new_selection = Selection::caret(region.end); + cursor.mode = CursorMode::Insert(new_selection); + return vec![]; + } + } + _ => (), + } + } + + return vec![]; + } + + let offset = match &cursor.mode { + CursorMode::Insert(selection) => { + let offset = selection.min_offset(); + buffer.prev_grapheme_offset( + offset, + 1, + buffer.offset_of_line(buffer.line_of_offset(offset)), + ) + } + CursorMode::Visual { end, .. } => buffer.offset_line_end(*end, false).min(*end), + CursorMode::Normal(offset) => *offset, + }; + + buffer.reset_edit_type(); + cursor.mode = CursorMode::Normal(offset); + cursor.horiz = None; + vec![] + } + InsertMode => { + cursor.mode = CursorMode::Insert(Selection::caret(cursor.offset())); + vec![] + } + InsertFirstNonBlank => { + match &cursor.mode { + CursorMode::Normal(offset) => { + let line = buffer.line_of_offset(*offset); + let offset = buffer.first_non_blank_character_on_line(line); + cursor.mode = CursorMode::Insert(Selection::caret(offset)); + } + CursorMode::Visual { .. } => { + let mut selection = Selection::new(); + for region in cursor.edit_selection(buffer).regions() { + selection.add_region(SelRegion::caret(region.min())); + } + cursor.mode = CursorMode::Insert(selection); + } + CursorMode::Insert(_) => {} + }; + vec![] + } + Append => { + let offset = cursor.offset(); + let line = buffer.line_of_offset(offset); + let line_len = buffer.line_len(line); + let count = (line_len > 1 || (buffer.last_line() == line && line_len > 0)) as usize; + let offset = buffer.move_right(cursor.offset(), Mode::Insert, count); + cursor.mode = CursorMode::Insert(Selection::caret(offset)); + vec![] + } + AppendEndOfLine => { + let offset = cursor.offset(); + let line = buffer.line_of_offset(offset); + let offset = buffer.line_end_offset(line, true); + cursor.mode = CursorMode::Insert(Selection::caret(offset)); + vec![] + } + ToggleVisualMode => { + Self::toggle_visual(cursor, VisualMode::Normal, modal); + vec![] + } + ToggleLinewiseVisualMode => { + Self::toggle_visual(cursor, VisualMode::Linewise, modal); + vec![] + } + ToggleBlockwiseVisualMode => { + Self::toggle_visual(cursor, VisualMode::Blockwise, modal); + vec![] + } + DuplicateLineUp => Self::duplicate_line(cursor, buffer, DuplicateDirection::Up), + DuplicateLineDown => Self::duplicate_line(cursor, buffer, DuplicateDirection::Down), + } + } +} + +enum DuplicateDirection { + Up, + Down, +} + +#[cfg(test)] +mod test { + use crate::{ + buffer::{rope_text::RopeText, Buffer}, + cursor::{Cursor, CursorMode}, + editor::{Action, DuplicateDirection}, + selection::{SelRegion, Selection}, + word::WordCursor, + }; + + fn prev_unmatched(buffer: &Buffer, c: char, offset: usize) -> Option { + WordCursor::new(buffer.text(), offset).previous_unmatched(c) + } + + #[test] + fn test_insert_simple() { + let mut buffer = Buffer::new("abc"); + let mut cursor = Cursor::new(CursorMode::Insert(Selection::caret(1)), None, None); + + Action::insert(&mut cursor, &mut buffer, "e", &prev_unmatched, true, true); + assert_eq!("aebc", buffer.slice_to_cow(0..buffer.len())); + } + + #[test] + fn test_insert_multiple_cursor() { + let mut buffer = Buffer::new("abc\nefg\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(1)); + selection.add_region(SelRegion::caret(5)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::insert(&mut cursor, &mut buffer, "i", &prev_unmatched, true, true); + assert_eq!("aibc\neifg\n", buffer.slice_to_cow(0..buffer.len())); + } + + #[test] + fn test_insert_complex() { + let mut buffer = Buffer::new("abc\nefg\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(1)); + selection.add_region(SelRegion::caret(5)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::insert(&mut cursor, &mut buffer, "i", &prev_unmatched, true, true); + assert_eq!("aibc\neifg\n", buffer.slice_to_cow(0..buffer.len())); + Action::insert(&mut cursor, &mut buffer, "j", &prev_unmatched, true, true); + assert_eq!("aijbc\neijfg\n", buffer.slice_to_cow(0..buffer.len())); + Action::insert(&mut cursor, &mut buffer, "{", &prev_unmatched, true, true); + assert_eq!("aij{bc\neij{fg\n", buffer.slice_to_cow(0..buffer.len())); + Action::insert(&mut cursor, &mut buffer, " ", &prev_unmatched, true, true); + assert_eq!("aij{ bc\neij{ fg\n", buffer.slice_to_cow(0..buffer.len())); + } + + #[test] + fn test_insert_pair() { + let mut buffer = Buffer::new("a bc\ne fg\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(1)); + selection.add_region(SelRegion::caret(6)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::insert(&mut cursor, &mut buffer, "{", &prev_unmatched, true, true); + assert_eq!("a{} bc\ne{} fg\n", buffer.slice_to_cow(0..buffer.len())); + Action::insert(&mut cursor, &mut buffer, "}", &prev_unmatched, true, true); + assert_eq!("a{} bc\ne{} fg\n", buffer.slice_to_cow(0..buffer.len())); + } + + #[test] + fn test_insert_pair_with_selection() { + let mut buffer = Buffer::new("a bc\ne fg\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::new(0, 4, None)); + selection.add_region(SelRegion::new(5, 9, None)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + Action::insert(&mut cursor, &mut buffer, "{", &prev_unmatched, true, true); + assert_eq!("{a bc}\n{e fg}\n", buffer.slice_to_cow(0..buffer.len())); + } + + #[test] + fn test_insert_pair_without_auto_closing() { + let mut buffer = Buffer::new("a bc\ne fg\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(1)); + selection.add_region(SelRegion::caret(6)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::insert(&mut cursor, &mut buffer, "{", &prev_unmatched, false, false); + assert_eq!("a{ bc\ne{ fg\n", buffer.slice_to_cow(0..buffer.len())); + Action::insert(&mut cursor, &mut buffer, "}", &prev_unmatched, false, false); + assert_eq!("a{} bc\ne{} fg\n", buffer.slice_to_cow(0..buffer.len())); + } + + #[test] + fn duplicate_down_simple() { + let mut buffer = Buffer::new("first line\nsecond line\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(0)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::duplicate_line(&mut cursor, &mut buffer, DuplicateDirection::Down); + + assert_ne!(cursor.offset(), 0); + assert_eq!( + "first line\nfirst line\nsecond line\n", + buffer.slice_to_cow(0..buffer.len()) + ); + } + + #[test] + fn duplicate_up_simple() { + let mut buffer = Buffer::new("first line\nsecond line\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(0)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::duplicate_line(&mut cursor, &mut buffer, DuplicateDirection::Up); + + assert_eq!(cursor.offset(), 0); + assert_eq!( + "first line\nfirst line\nsecond line\n", + buffer.slice_to_cow(0..buffer.len()) + ); + } + + #[test] + fn duplicate_down_multiple_cursors_in_same_line() { + let mut buffer = Buffer::new("first line\nsecond line\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(0)); + selection.add_region(SelRegion::caret(1)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::duplicate_line(&mut cursor, &mut buffer, DuplicateDirection::Down); + + assert_eq!( + "first line\nfirst line\nsecond line\n", + buffer.slice_to_cow(0..buffer.len()) + ); + } + + #[test] + fn duplicate_up_multiple_cursors_in_same_line() { + let mut buffer = Buffer::new("first line\nsecond line\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(0)); + selection.add_region(SelRegion::caret(1)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::duplicate_line(&mut cursor, &mut buffer, DuplicateDirection::Up); + + assert_eq!( + "first line\nfirst line\nsecond line\n", + buffer.slice_to_cow(0..buffer.len()) + ); + } + + #[test] + fn duplicate_down_multiple() { + let mut buffer = Buffer::new("first line\nsecond line\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(0)); + selection.add_region(SelRegion::caret(15)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::duplicate_line(&mut cursor, &mut buffer, DuplicateDirection::Down); + + assert_eq!( + "first line\nfirst line\nsecond line\nsecond line\n", + buffer.slice_to_cow(0..buffer.len()) + ); + } + + #[test] + fn duplicate_up_multiple() { + let mut buffer = Buffer::new("first line\nsecond line\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(0)); + selection.add_region(SelRegion::caret(15)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::duplicate_line(&mut cursor, &mut buffer, DuplicateDirection::Up); + + assert_eq!( + "first line\nfirst line\nsecond line\nsecond line\n", + buffer.slice_to_cow(0..buffer.len()) + ); + } + + #[test] + fn duplicate_down_multiple_with_swapped_cursor_order() { + let mut buffer = Buffer::new("first line\nsecond line\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(15)); + selection.add_region(SelRegion::caret(0)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::duplicate_line(&mut cursor, &mut buffer, DuplicateDirection::Down); + + assert_eq!( + "first line\nfirst line\nsecond line\nsecond line\n", + buffer.slice_to_cow(0..buffer.len()) + ); + } + + #[test] + fn duplicate_up_multiple_with_swapped_cursor_order() { + let mut buffer = Buffer::new("first line\nsecond line\n"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(15)); + selection.add_region(SelRegion::caret(0)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::duplicate_line(&mut cursor, &mut buffer, DuplicateDirection::Up); + + assert_eq!( + "first line\nfirst line\nsecond line\nsecond line\n", + buffer.slice_to_cow(0..buffer.len()) + ); + } + + #[test] + fn check_multiple_cursor_match_insertion() { + let mut buffer = Buffer::new(" 123 567 9ab def"); + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(0)); + selection.add_region(SelRegion::caret(4)); + selection.add_region(SelRegion::caret(8)); + selection.add_region(SelRegion::caret(12)); + let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); + + Action::insert(&mut cursor, &mut buffer, "(", &prev_unmatched, true, true); + + assert_eq!( + "() 123() 567() 9ab() def", + buffer.slice_to_cow(0..buffer.len()) + ); + + let mut end_selection = Selection::new(); + end_selection.add_region(SelRegion::caret(1)); + end_selection.add_region(SelRegion::caret(7)); + end_selection.add_region(SelRegion::caret(13)); + end_selection.add_region(SelRegion::caret(19)); + assert_eq!(cursor.mode, CursorMode::Insert(end_selection)); + } + + // TODO(dbuga): add tests duplicating selections (multiple line blocks) +} diff --git a/editor-core/src/indent.rs b/editor-core/src/indent.rs new file mode 100644 index 00000000..bb71137c --- /dev/null +++ b/editor-core/src/indent.rs @@ -0,0 +1,201 @@ +use lapce_xi_rope::Rope; + +use crate::{ + buffer::{rope_text::RopeText, Buffer}, + chars::{char_is_line_ending, char_is_whitespace}, + selection::Selection, +}; + +/// Enum representing indentation style. +/// +/// Only values 1-8 are valid for the `Spaces` variant. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum IndentStyle { + Tabs, + Spaces(u8), +} + +impl IndentStyle { + pub const LONGEST_INDENT: &'static str = " "; // 8 spaces + pub const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4); + + /// Creates an `IndentStyle` from an indentation string. + /// + /// For example, passing `" "` (four spaces) will create `IndentStyle::Spaces(4)`. + #[allow(clippy::should_implement_trait)] + #[inline] + pub fn from_str(indent: &str) -> Self { + debug_assert!(!indent.is_empty() && indent.len() <= Self::LONGEST_INDENT.len()); + if indent.starts_with(' ') { + IndentStyle::Spaces(indent.len() as u8) + } else { + IndentStyle::Tabs + } + } + + #[inline] + pub fn as_str(&self) -> &'static str { + match *self { + IndentStyle::Tabs => "\t", + IndentStyle::Spaces(x) if x <= Self::LONGEST_INDENT.len() as u8 => { + Self::LONGEST_INDENT.split_at(x.into()).0 + } + // Unsupported indentation style. This should never happen, + // but just in case fall back to the default of 4 spaces + IndentStyle::Spaces(n) => { + debug_assert!(n > 0 && n <= Self::LONGEST_INDENT.len() as u8); + " " + } + } + } +} + +pub fn create_edit<'s>(buffer: &Buffer, offset: usize, indent: &'s str) -> (Selection, &'s str) { + let indent = if indent.starts_with('\t') { + indent + } else { + let (_, col) = buffer.offset_to_line_col(offset); + indent.split_at(indent.len() - col % indent.len()).0 + }; + (Selection::caret(offset), indent) +} + +pub fn create_outdent<'s>( + buffer: &Buffer, + offset: usize, + indent: &'s str, +) -> Option<(Selection, &'s str)> { + let (_, col) = buffer.offset_to_line_col(offset); + if col == 0 { + return None; + } + + let start = if indent.starts_with('\t') { + offset - 1 + } else { + let r = col % indent.len(); + let r = if r == 0 { indent.len() } else { r }; + offset - r + }; + + Some((Selection::region(start, offset), "")) +} + +/// Attempts to detect the indentation style used in a document. +/// +/// Returns the indentation style if the auto-detect confidence is +/// reasonably high, otherwise returns `None`. +pub fn auto_detect_indent_style(document_text: &Rope) -> Option { + // Build a histogram of the indentation *increases* between + // subsequent lines, ignoring lines that are all whitespace. + // + // Index 0 is for tabs, the rest are 1-8 spaces. + let histogram: [usize; 9] = { + let mut histogram = [0; 9]; + let mut prev_line_is_tabs = false; + let mut prev_line_leading_count = 0usize; + + // Loop through the lines, checking for and recording indentation + // increases as we go. + let offset = document_text + .offset_of_line(document_text.line_of_offset(document_text.len()).min(1000)); + 'outer: for line in document_text.lines(..offset) { + let mut c_iter = line.chars(); + + // Is first character a tab or space? + let is_tabs = match c_iter.next() { + Some('\t') => true, + Some(' ') => false, + + // Ignore blank lines. + Some(c) if char_is_line_ending(c) => continue, + + _ => { + prev_line_is_tabs = false; + prev_line_leading_count = 0; + continue; + } + }; + + // Count the line's total leading tab/space characters. + let mut leading_count = 1; + let mut count_is_done = false; + for c in c_iter { + match c { + '\t' if is_tabs && !count_is_done => leading_count += 1, + ' ' if !is_tabs && !count_is_done => leading_count += 1, + + // We stop counting if we hit whitespace that doesn't + // qualify as indent or doesn't match the leading + // whitespace, but we don't exit the loop yet because + // we still want to determine if the line is blank. + c if char_is_whitespace(c) => count_is_done = true, + + // Ignore blank lines. + c if char_is_line_ending(c) => continue 'outer, + + _ => break, + } + + // Bound the worst-case execution time for weird text files. + if leading_count > 256 { + continue 'outer; + } + } + + // If there was an increase in indentation over the previous + // line, update the histogram with that increase. + if (prev_line_is_tabs == is_tabs || prev_line_leading_count == 0) + && prev_line_leading_count < leading_count + { + if is_tabs { + histogram[0] += 1; + } else { + let amount = leading_count - prev_line_leading_count; + if amount <= 8 { + histogram[amount] += 1; + } + } + } + + // Store this line's leading whitespace info for use with + // the next line. + prev_line_is_tabs = is_tabs; + prev_line_leading_count = leading_count; + } + + // Give more weight to tabs, because their presence is a very + // strong indicator. + histogram[0] *= 2; + + histogram + }; + + // Find the most frequent indent, its frequency, and the frequency of + // the next-most frequent indent. + let indent = histogram + .iter() + .enumerate() + .max_by_key(|kv| kv.1) + .unwrap() + .0; + let indent_freq = histogram[indent]; + let indent_freq_2 = *histogram + .iter() + .enumerate() + .filter(|kv| kv.0 != indent) + .map(|kv| kv.1) + .max() + .unwrap(); + + // Return the the auto-detected result if we're confident enough in its + // accuracy, based on some heuristics. + if indent_freq >= 1 && (indent_freq_2 as f64 / indent_freq as f64) < 0.66 { + Some(match indent { + 0 => IndentStyle::Tabs, + _ => IndentStyle::Spaces(indent as u8), + }) + } else { + None + } +} diff --git a/editor-core/src/lib.rs b/editor-core/src/lib.rs new file mode 100644 index 00000000..dadb5599 --- /dev/null +++ b/editor-core/src/lib.rs @@ -0,0 +1,15 @@ +pub mod buffer; +pub mod char_buffer; +pub mod chars; +pub mod command; +pub mod cursor; +pub mod editor; +pub mod indent; +pub mod mode; +pub mod movement; +pub mod paragraph; +pub mod register; +pub mod selection; +pub mod soft_tab; +pub mod util; +pub mod word; diff --git a/editor-core/src/mode.rs b/editor-core/src/mode.rs new file mode 100644 index 00000000..7eb364ff --- /dev/null +++ b/editor-core/src/mode.rs @@ -0,0 +1,98 @@ +use std::fmt::Write; + +use bitflags::bitflags; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum MotionMode { + Delete { count: usize }, + Yank { count: usize }, + Indent, + Outdent, +} + +impl MotionMode { + pub fn count(&self) -> usize { + match self { + MotionMode::Delete { count } => *count, + MotionMode::Yank { count } => *count, + MotionMode::Indent => 1, + MotionMode::Outdent => 1, + } + } +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug, Copy, Default, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum VisualMode { + #[default] + Normal, + Linewise, + Blockwise, +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug, Copy, PartialOrd, Ord)] +pub enum Mode { + Normal, + Insert, + Visual(VisualMode), + Terminal, +} + +bitflags! { + pub struct Modes: u32 { + const NORMAL = 0x1; + const INSERT = 0x2; + const VISUAL = 0x4; + const TERMINAL = 0x8; + } +} + +impl From for Modes { + fn from(mode: Mode) -> Self { + match mode { + Mode::Normal => Self::NORMAL, + Mode::Insert => Self::INSERT, + Mode::Visual(_) => Self::VISUAL, + Mode::Terminal => Self::TERMINAL, + } + } +} + +impl Modes { + pub fn parse(modes_str: &str) -> Self { + let mut this = Self::empty(); + + for c in modes_str.chars() { + match c { + 'i' | 'I' => this.set(Self::INSERT, true), + 'n' | 'N' => this.set(Self::NORMAL, true), + 'v' | 'V' => this.set(Self::VISUAL, true), + 't' | 'T' => this.set(Self::TERMINAL, true), + _ => {} + } + } + + this + } +} + +impl std::fmt::Display for Modes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let bits = [ + (Self::INSERT, 'i'), + (Self::NORMAL, 'n'), + (Self::VISUAL, 'v'), + (Self::TERMINAL, 't'), + ]; + for (bit, chr) in bits { + if self.contains(bit) { + f.write_char(chr)?; + } + } + + Ok(()) + } +} diff --git a/editor-core/src/movement.rs b/editor-core/src/movement.rs new file mode 100644 index 00000000..75824e24 --- /dev/null +++ b/editor-core/src/movement.rs @@ -0,0 +1,140 @@ +#[derive(Clone, Debug)] +pub enum LinePosition { + First, + Last, + Line(usize), +} + +#[derive(Clone, Debug)] +pub enum Movement { + Left, + Right, + Up, + Down, + DocumentStart, + DocumentEnd, + FirstNonBlank, + StartOfLine, + EndOfLine, + Line(LinePosition), + Offset(usize), + WordEndForward, + WordForward, + WordBackward, + NextUnmatched(char), + PreviousUnmatched(char), + MatchPairs, + ParagraphForward, + ParagraphBackward, +} + +impl PartialEq for Movement { + fn eq(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } +} + +impl Movement { + pub fn is_vertical(&self) -> bool { + matches!( + self, + Movement::Up + | Movement::Down + | Movement::Line(_) + | Movement::DocumentStart + | Movement::DocumentEnd + | Movement::ParagraphForward + | Movement::ParagraphBackward + ) + } + + pub fn is_inclusive(&self) -> bool { + matches!(self, Movement::WordEndForward) + } + + pub fn is_jump(&self) -> bool { + matches!( + self, + Movement::Line(_) + | Movement::Offset(_) + | Movement::DocumentStart + | Movement::DocumentEnd + | Movement::ParagraphForward + | Movement::ParagraphBackward + ) + } + + pub fn update_index(&self, index: usize, len: usize, count: usize, wrapping: bool) -> usize { + if len == 0 { + return 0; + } + let last = len - 1; + match self { + // Select the next entry/line + Movement::Down if wrapping => (index + count) % len, + Movement::Down => (index + count).min(last), + + // Selects the previous entry/line + Movement::Up if wrapping => (index + (len.saturating_sub(count))) % len, + Movement::Up => index.saturating_sub(count), + + Movement::Line(position) => match position { + // Selects the nth line + LinePosition::Line(n) => (*n).min(last), + LinePosition::First => 0, + LinePosition::Last => last, + }, + + Movement::ParagraphForward => 0, + Movement::ParagraphBackward => 0, + _ => index, + } + } +} + +#[cfg(test)] +mod test { + use crate::movement::Movement; + + #[test] + fn test_wrapping() { + // Move by 1 position + // List length of 1 + assert_eq!(0, Movement::Up.update_index(0, 1, 1, true)); + assert_eq!(0, Movement::Down.update_index(0, 1, 1, true)); + + // List length of 5 + assert_eq!(4, Movement::Up.update_index(0, 5, 1, true)); + assert_eq!(1, Movement::Down.update_index(0, 5, 1, true)); + + // Move by 2 positions + // List length of 1 + assert_eq!(0, Movement::Up.update_index(0, 1, 2, true)); + assert_eq!(0, Movement::Down.update_index(0, 1, 2, true)); + + // List length of 5 + assert_eq!(3, Movement::Up.update_index(0, 5, 2, true)); + assert_eq!(2, Movement::Down.update_index(0, 5, 2, true)); + } + + #[test] + fn test_non_wrapping() { + // Move by 1 position + // List length of 1 + assert_eq!(0, Movement::Up.update_index(0, 1, 1, false)); + assert_eq!(0, Movement::Down.update_index(0, 1, 1, false)); + + // List length of 5 + assert_eq!(0, Movement::Up.update_index(0, 5, 1, false)); + assert_eq!(1, Movement::Down.update_index(0, 5, 1, false)); + + // Move by 2 positions + // List length of 1 + assert_eq!(0, Movement::Up.update_index(0, 1, 2, false)); + assert_eq!(0, Movement::Down.update_index(0, 1, 2, false)); + + // List length of 5 + assert_eq!(0, Movement::Up.update_index(0, 5, 2, false)); + assert_eq!(2, Movement::Down.update_index(0, 5, 2, false)); + } +} diff --git a/editor-core/src/paragraph.rs b/editor-core/src/paragraph.rs new file mode 100644 index 00000000..9b57a1d0 --- /dev/null +++ b/editor-core/src/paragraph.rs @@ -0,0 +1,135 @@ +use lapce_xi_rope::{Cursor, Rope, RopeInfo}; + +/// Describe char classifications used to compose word boundaries +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum CharClassification { + /// Carriage Return (`r`) + Cr, + /// Line feed (`\n`) + Lf, + /// Includes letters and all of non-ascii unicode + Other, +} + +/// A word boundary can be the start of a word, its end or both for punctuation +#[derive(PartialEq, Eq)] +enum ParagraphBoundary { + /// Denote that this is not a boundary + Interior, + /// A boundary indicating the end of a new-line sequence + Start, + /// A boundary indicating the start of a new-line sequence + End, + /// Both start and end boundaries (when we have only one empty + /// line) + Both, +} + +impl ParagraphBoundary { + fn is_start(&self) -> bool { + *self == ParagraphBoundary::Start || *self == ParagraphBoundary::Both + } + + fn is_end(&self) -> bool { + *self == ParagraphBoundary::End || *self == ParagraphBoundary::Both + } + + #[allow(unused)] + fn is_boundary(&self) -> bool { + *self != ParagraphBoundary::Interior + } +} + +/// A cursor providing utility function to navigate the rope +/// by parahraphs boundaries. +/// Boundaries can be the start of a word, its end, punctuation etc. +pub struct ParagraphCursor<'a> { + pub(crate) inner: Cursor<'a, RopeInfo>, +} + +impl<'a> ParagraphCursor<'a> { + pub fn new(text: &'a Rope, pos: usize) -> ParagraphCursor<'a> { + let inner = Cursor::new(text, pos); + ParagraphCursor { inner } + } + + pub fn prev_boundary(&mut self) -> Option { + if let (Some(ch1), Some(ch2), Some(ch3)) = ( + self.inner.prev_codepoint(), + self.inner.prev_codepoint(), + self.inner.prev_codepoint(), + ) { + let mut prop1 = get_char_property(ch1); + let mut prop2 = get_char_property(ch2); + let mut prop3 = get_char_property(ch3); + let mut candidate = self.inner.pos(); + + while let Some(prev) = self.inner.prev_codepoint() { + let prop_prev = get_char_property(prev); + if classify_boundary(prop_prev, prop3, prop2, prop1).is_start() { + break; + } + (prop3, prop2, prop1) = (prop_prev, prop3, prop2); + candidate = self.inner.pos(); + } + + self.inner.set(candidate + 1); + return Some(candidate + 1); + } + + None + } + + pub fn next_boundary(&mut self) -> Option { + if let (Some(ch1), Some(ch2), Some(ch3)) = ( + self.inner.next_codepoint(), + self.inner.next_codepoint(), + self.inner.next_codepoint(), + ) { + let mut prop1 = get_char_property(ch1); + let mut prop2 = get_char_property(ch2); + let mut prop3 = get_char_property(ch3); + let mut candidate = self.inner.pos(); + + while let Some(next) = self.inner.next_codepoint() { + let prop_next = get_char_property(next); + if classify_boundary(prop1, prop2, prop3, prop_next).is_end() { + break; + } + + (prop1, prop2, prop3) = (prop2, prop3, prop_next); + candidate = self.inner.pos(); + } + self.inner.set(candidate - 1); + return Some(candidate - 1); + } + None + } +} + +/// Return the [`CharClassification`] of the input character +pub fn get_char_property(codepoint: char) -> CharClassification { + match codepoint { + '\r' => CharClassification::Cr, + '\n' => CharClassification::Lf, + _ => CharClassification::Other, + } +} + +fn classify_boundary( + before_prev: CharClassification, + prev: CharClassification, + next: CharClassification, + after_next: CharClassification, +) -> ParagraphBoundary { + use self::{CharClassification::*, ParagraphBoundary::*}; + + match (before_prev, prev, next, after_next) { + (Other, Lf, Lf, Other) => Both, + (_, Lf, Lf, Other) => Start, + (Lf, Cr, Lf, Other) => Start, + (Other, Lf, Lf, _) => End, + (Other, Cr, Lf, Cr) => End, + _ => Interior, + } +} diff --git a/editor-core/src/register.rs b/editor-core/src/register.rs new file mode 100644 index 00000000..cc8bc55c --- /dev/null +++ b/editor-core/src/register.rs @@ -0,0 +1,41 @@ +use crate::mode::VisualMode; + +pub trait Clipboard { + fn get_string(&mut self) -> Option; + fn put_string(&mut self, s: impl AsRef); +} + +#[derive(Clone, Default)] +pub struct RegisterData { + pub content: String, + pub mode: VisualMode, +} + +#[derive(Clone, Default)] +pub struct Register { + pub unnamed: RegisterData, + last_yank: RegisterData, +} + +pub enum RegisterKind { + Delete, + Yank, +} + +impl Register { + pub fn add(&mut self, kind: RegisterKind, data: RegisterData) { + match kind { + RegisterKind::Delete => self.add_delete(data), + RegisterKind::Yank => self.add_yank(data), + } + } + + pub fn add_delete(&mut self, data: RegisterData) { + self.unnamed = data; + } + + pub fn add_yank(&mut self, data: RegisterData) { + self.unnamed = data.clone(); + self.last_yank = data; + } +} diff --git a/editor-core/src/selection.rs b/editor-core/src/selection.rs new file mode 100644 index 00000000..26f72f10 --- /dev/null +++ b/editor-core/src/selection.rs @@ -0,0 +1,764 @@ +use std::cmp::{max, min, Ordering}; + +use lapce_xi_rope::{RopeDelta, Transformer}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::cursor::ColPosition; + +/// Indicate whether a delta should be applied inside, outside non-caret selection or +/// after a caret selection (see [`Selection::apply_delta`]. +#[derive(Copy, Clone)] +pub enum InsertDrift { + /// Indicates this edit should happen within any (non-caret) selections if possible. + Inside, + /// Indicates this edit should happen outside any selections if possible. + Outside, + /// Indicates to do whatever the `after` bool says to do + Default, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SelRegion { + /// Region start offset + pub start: usize, + /// Region end offset + pub end: usize, + /// Horizontal rules for multiple selection + pub horiz: Option, +} + +/// A selection holding one or more [`SelRegion`]. +/// Regions are kept in order from the leftmost selection to the rightmost selection. +#[derive(Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Selection { + regions: Vec, + last_inserted: usize, +} + +impl AsRef for Selection { + fn as_ref(&self) -> &Selection { + self + } +} + +impl SelRegion { + /// Creates new [`SelRegion`] from `start` and `end` offset. + pub fn new(start: usize, end: usize, horiz: Option) -> SelRegion { + SelRegion { start, end, horiz } + } + + /// Creates a caret [`SelRegion`], + /// i.e. `start` and `end` position are both set to `offset` value. + pub fn caret(offset: usize) -> SelRegion { + SelRegion { + start: offset, + end: offset, + horiz: None, + } + } + + /// Return the minimum value between region's start and end position + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::selection::SelRegion; + /// let region = SelRegion::new(1, 10, None); + /// assert_eq!(region.min(), region.start); + /// let region = SelRegion::new(42, 1, None); + /// assert_eq!(region.min(), region.end); + /// ``` + pub fn min(self) -> usize { + min(self.start, self.end) + } + + /// Return the maximum value between region's start and end position. + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::selection::SelRegion; + /// let region = SelRegion::new(1, 10, None); + /// assert_eq!(region.max(), region.end); + /// let region = SelRegion::new(42, 1, None); + /// assert_eq!(region.max(), region.start); + /// ``` + pub fn max(self) -> usize { + max(self.start, self.end) + } + + /// A [`SelRegion`] is considered to be a caret when its start and end position are equal. + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::selection::SelRegion; + /// let region = SelRegion::new(1, 1, None); + /// assert!(region.is_caret()); + /// ``` + pub fn is_caret(self) -> bool { + self.start == self.end + } + + /// Merge two [`SelRegion`] into a single one. + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::selection::SelRegion; + /// let region = SelRegion::new(1, 2, None); + /// let other = SelRegion::new(3, 4, None); + /// assert_eq!(region.merge_with(other), SelRegion::new(1, 4, None)); + /// ``` + pub fn merge_with(self, other: SelRegion) -> SelRegion { + let is_forward = self.end >= self.start; + let new_min = min(self.min(), other.min()); + let new_max = max(self.max(), other.max()); + let (start, end) = if is_forward { + (new_min, new_max) + } else { + (new_max, new_min) + }; + SelRegion::new(start, end, None) + } + + fn should_merge(self, other: SelRegion) -> bool { + other.min() < self.max() + || ((self.is_caret() || other.is_caret()) && other.min() == self.max()) + } + + fn contains(&self, offset: usize) -> bool { + self.min() <= offset && offset <= self.max() + } +} + +impl Selection { + /// Creates a new empty [`Selection`] + pub fn new() -> Selection { + Selection { + regions: Vec::new(), + last_inserted: 0, + } + } + + /// Creates a caret [`Selection`], i.e. a selection with a single caret [`SelRegion`] + pub fn caret(offset: usize) -> Selection { + Selection { + regions: vec![SelRegion::caret(offset)], + last_inserted: 0, + } + } + + /// Creates a region [`Selection`], i.e. a selection with a single [`SelRegion`] + /// from `start` to `end` position + pub fn region(start: usize, end: usize) -> Self { + Self::sel_region(SelRegion { + start, + end, + horiz: None, + }) + } + + /// Creates a [`Selection`], with a single [`SelRegion`] equal to `region`. + pub fn sel_region(region: SelRegion) -> Self { + Self { + regions: vec![region], + last_inserted: 0, + } + } + + /// Returns whether this [`Selection`], contains the given `offset` position or not. + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::selection::Selection; + /// let selection = Selection::region(0, 2); + /// assert!(selection.contains(0)); + /// assert!(selection.contains(1)); + /// assert!(selection.contains(2)); + /// assert!(!selection.contains(3)); + /// ``` + pub fn contains(&self, offset: usize) -> bool { + for region in self.regions.iter() { + if region.contains(offset) { + return true; + } + } + false + } + + /// Returns this selection regions + pub fn regions(&self) -> &[SelRegion] { + &self.regions + } + + /// Returns a mutable reference to this selection regions + pub fn regions_mut(&mut self) -> &mut [SelRegion] { + &mut self.regions + } + + /// Returns a copy of [`self`] with all regions converted to caret region at their respective + /// [`SelRegion::min`] offset. + /// + /// **Examples:** + /// + /// ```rust + /// # use floem_editor_core::selection::{Selection, SelRegion}; + /// let mut selection = Selection::new(); + /// selection.add_region(SelRegion::new(1, 3, None)); + /// selection.add_region(SelRegion::new(6, 12, None)); + /// selection.add_region(SelRegion::new(24, 48, None)); + /// + /// assert_eq!(selection.min().regions(), vec![ + /// SelRegion::caret(1), + /// SelRegion::caret(6), + /// SelRegion::caret(24) + /// ]); + pub fn min(&self) -> Selection { + let mut selection = Self::new(); + for region in &self.regions { + let new_region = SelRegion::new(region.min(), region.min(), None); + selection.add_region(new_region); + } + selection + } + + /// Get the leftmost [`SelRegion`] in this selection if present. + pub fn first(&self) -> Option<&SelRegion> { + self.regions.first() + } + + /// Get the rightmost [`SelRegion`] in this selection if present. + pub fn last(&self) -> Option<&SelRegion> { + self.regions.get(self.len() - 1) + } + + /// Get the last inserted [`SelRegion`] in this selection if present. + pub fn last_inserted(&self) -> Option<&SelRegion> { + self.regions.get(self.last_inserted) + } + + /// Get a mutable reference to the last inserted [`SelRegion`] in this selection if present. + pub fn last_inserted_mut(&mut self) -> Option<&mut SelRegion> { + self.regions.get_mut(self.last_inserted) + } + + /// The number of [`SelRegion`] in this selection. + pub fn len(&self) -> usize { + self.regions.len() + } + + /// A [`Selection`] is considered to be a caret if it contains + /// only caret [`SelRegion`] (see [`SelRegion::is_caret`]) + pub fn is_caret(&self) -> bool { + self.regions.iter().all(|region| region.is_caret()) + } + + /// Returns `true` if `self` has zero [`SelRegion`] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the minimal offset across all region of this selection. + /// + /// This function panics if the selection is empty. + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::selection::{Selection, SelRegion}; + /// let mut selection = Selection::new(); + /// selection.add_region(SelRegion::caret(4)); + /// selection.add_region(SelRegion::new(0, 12, None)); + /// selection.add_region(SelRegion::new(24, 48, None)); + /// assert_eq!(selection.min_offset(), 0); + /// ``` + pub fn min_offset(&self) -> usize { + let mut offset = self.regions()[0].min(); + for region in &self.regions { + offset = offset.min(region.min()); + } + offset + } + + /// Returns the maximal offset across all region of this selection. + /// + /// This function panics if the selection is empty. + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::selection::{Selection, SelRegion}; + /// let mut selection = Selection::new(); + /// selection.add_region(SelRegion::caret(4)); + /// selection.add_region(SelRegion::new(0, 12, None)); + /// selection.add_region(SelRegion::new(24, 48, None)); + /// assert_eq!(selection.max_offset(), 48); + /// ``` + pub fn max_offset(&self) -> usize { + let mut offset = self.regions()[0].max(); + for region in &self.regions { + offset = offset.max(region.max()); + } + offset + } + + /// Returns regions in [`self`] overlapping or fully enclosed in the provided + /// `start` to `end` range. + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::selection::{Selection, SelRegion}; + /// let mut selection = Selection::new(); + /// selection.add_region(SelRegion::new(0, 3, None)); + /// selection.add_region(SelRegion::new(3, 6, None)); + /// selection.add_region(SelRegion::new(7, 8, None)); + /// selection.add_region(SelRegion::new(9, 11, None)); + /// let regions = selection.regions_in_range(5, 10); + /// assert_eq!(regions, vec![ + /// SelRegion::new(3, 6, None), + /// SelRegion::new(7, 8, None), + /// SelRegion::new(9, 11, None) + /// ]); + /// ``` + pub fn regions_in_range(&self, start: usize, end: usize) -> &[SelRegion] { + let first = self.search(start); + let mut last = self.search(end); + if last < self.regions.len() && self.regions[last].min() <= end { + last += 1; + } + &self.regions[first..last] + } + + /// Returns regions in [`self`] starting between `start` to `end` range. + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::selection::{Selection, SelRegion}; + /// let mut selection = Selection::new(); + /// selection.add_region(SelRegion::new(0, 3, None)); + /// selection.add_region(SelRegion::new(3, 6, None)); + /// selection.add_region(SelRegion::new(7, 8, None)); + /// selection.add_region(SelRegion::new(9, 11, None)); + /// let regions = selection.full_regions_in_range(5, 10); + /// assert_eq!(regions, vec![ + /// SelRegion::new(7, 8, None), + /// SelRegion::new(9, 11, None) + /// ]); + /// ``` + pub fn full_regions_in_range(&self, start: usize, end: usize) -> &[SelRegion] { + let first = self.search_min(start); + let mut last = self.search_min(end); + if last < self.regions.len() && self.regions[last].min() <= end { + last += 1; + } + &self.regions[first..last] + } + + /// Deletes regions in [`self`] overlapping or enclosing in `start` to `end` range. + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::selection::{Selection, SelRegion}; + /// let mut selection = Selection::new(); + /// selection.add_region(SelRegion::new(0, 3, None)); + /// selection.add_region(SelRegion::new(3, 6, None)); + /// selection.add_region(SelRegion::new(7, 8, None)); + /// selection.add_region(SelRegion::new(9, 11, None)); + /// selection.delete_range(5, 10); + /// assert_eq!(selection.regions(), vec![SelRegion::new(0, 3, None)]); + /// ``` + pub fn delete_range(&mut self, start: usize, end: usize) { + let mut first = self.search(start); + let mut last = self.search(end); + if first >= self.regions.len() { + return; + } + if self.regions[first].max() == start { + first += 1; + } + if last < self.regions.len() && self.regions[last].min() < end { + last += 1; + } + remove_n_at(&mut self.regions, first, last - first); + } + + /// Add a regions to [`self`]. Note that if provided region overlap + /// on of the selection regions they will be merged in a single region. + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::selection::{Selection, SelRegion}; + /// let mut selection = Selection::new(); + /// // Overlapping + /// selection.add_region(SelRegion::new(0, 4, None)); + /// selection.add_region(SelRegion::new(3, 6, None)); + /// assert_eq!(selection.regions(), vec![SelRegion::new(0, 6, None)]); + /// // Non-overlapping + /// let mut selection = Selection::new(); + /// selection.add_region(SelRegion::new(0, 3, None)); + /// selection.add_region(SelRegion::new(3, 6, None)); + /// assert_eq!(selection.regions(), vec![ + /// SelRegion::new(0, 3, None), + /// SelRegion::new(3, 6, None) + /// ]); + /// ``` + pub fn add_region(&mut self, region: SelRegion) { + let mut ix = self.search(region.min()); + if ix == self.regions.len() { + self.regions.push(region); + self.last_inserted = self.regions.len() - 1; + return; + } + let mut region = region; + let mut end_ix = ix; + if self.regions[ix].min() <= region.min() { + if self.regions[ix].should_merge(region) { + region = self.regions[ix].merge_with(region); + } else { + ix += 1; + } + end_ix += 1; + } + while end_ix < self.regions.len() && region.should_merge(self.regions[end_ix]) { + region = self.regions[end_ix].merge_with(region); + end_ix += 1; + } + if ix == end_ix { + self.regions.insert(ix, region); + self.last_inserted = ix; + } else { + self.regions[ix] = region; + remove_n_at(&mut self.regions, ix + 1, end_ix - ix - 1); + } + } + + /// Add a region to the selection. This method does not merge regions and does not allow + /// ambiguous regions (regions that overlap). + /// + /// On ambiguous regions, the region with the lower start position wins. That is, in such a + /// case, the new region is either not added at all, because there is an ambiguous region with + /// a lower start position, or existing regions that intersect with the new region but do + /// not start before the new region, are deleted. + pub fn add_range_distinct(&mut self, region: SelRegion) -> (usize, usize) { + let mut ix = self.search(region.min()); + + if ix < self.regions.len() && self.regions[ix].max() == region.min() { + ix += 1; + } + + if ix < self.regions.len() { + // in case of ambiguous regions the region closer to the left wins + let occ = &self.regions[ix]; + let is_eq = occ.min() == region.min() && occ.max() == region.max(); + let is_intersect_before = region.min() >= occ.min() && occ.max() > region.min(); + if is_eq || is_intersect_before { + return (occ.min(), occ.max()); + } + } + + // delete ambiguous regions to the right + let mut last = self.search(region.max()); + if last < self.regions.len() && self.regions[last].min() < region.max() { + last += 1; + } + remove_n_at(&mut self.regions, ix, last - ix); + + if ix == self.regions.len() { + self.regions.push(region); + } else { + self.regions.insert(ix, region); + } + + (self.regions[ix].min(), self.regions[ix].max()) + } + + /// Apply [`xi_rope::RopeDelta`] to this selection. + /// Typically used to apply an edit to a buffer and update its selections + /// **Parameters*:* + /// - `delta`[`xi_rope::RopeDelta`] + /// - `after` parameter indicate if the delta should be applied before or after the selection + /// - `drift` see [`InsertDrift`] + pub fn apply_delta(&self, delta: &RopeDelta, after: bool, drift: InsertDrift) -> Selection { + let mut result = Selection::new(); + let mut transformer = Transformer::new(delta); + for region in self.regions() { + let is_region_forward = region.start < region.end; + + let (start_after, end_after) = match (drift, region.is_caret()) { + (InsertDrift::Inside, false) => (!is_region_forward, is_region_forward), + (InsertDrift::Outside, false) => (is_region_forward, !is_region_forward), + _ => (after, after), + }; + + let new_region = SelRegion::new( + transformer.transform(region.start, start_after), + transformer.transform(region.end, end_after), + None, + ); + result.add_region(new_region); + } + result + } + + /// Returns cursor position, which corresponds to last inserted region `end` offset, + pub fn get_cursor_offset(&self) -> usize { + if self.is_empty() { + return 0; + } + self.regions[self.last_inserted].end + } + + /// Replaces last inserted [`SelRegion`] of this selection with the provided one. + pub fn replace_last_inserted_region(&mut self, region: SelRegion) { + if self.is_empty() { + self.add_region(region); + return; + } + + self.regions.remove(self.last_inserted); + self.add_region(region); + } + + fn search(&self, offset: usize) -> usize { + if self.regions.is_empty() || offset > self.regions.last().unwrap().max() { + return self.regions.len(); + } + match self.regions.binary_search_by(|r| r.max().cmp(&offset)) { + Ok(ix) => ix, + Err(ix) => ix, + } + } + + fn search_min(&self, offset: usize) -> usize { + if self.regions.is_empty() || offset > self.regions.last().unwrap().max() { + return self.regions.len(); + } + match self + .regions + .binary_search_by(|r| r.min().cmp(&(offset + 1))) + { + Ok(ix) => ix, + Err(ix) => ix, + } + } +} + +impl Default for Selection { + fn default() -> Self { + Self::new() + } +} + +fn remove_n_at(v: &mut Vec, index: usize, n: usize) { + match n.cmp(&1) { + Ordering::Equal => { + v.remove(index); + } + Ordering::Greater => { + v.drain(index..index + n); + } + _ => (), + }; +} + +#[cfg(test)] +mod test { + use crate::{ + buffer::Buffer, + editor::EditType, + selection::{InsertDrift, SelRegion, Selection}, + }; + + #[test] + fn should_return_selection_region_min() { + let region = SelRegion::new(1, 10, None); + assert_eq!(region.min(), region.start); + + let region = SelRegion::new(42, 1, None); + assert_eq!(region.min(), region.end); + } + + #[test] + fn should_return_selection_region_max() { + let region = SelRegion::new(1, 10, None); + assert_eq!(region.max(), region.end); + + let region = SelRegion::new(42, 1, None); + assert_eq!(region.max(), region.start); + } + + #[test] + fn is_caret_should_return_true() { + let region = SelRegion::new(1, 10, None); + assert!(!region.is_caret()); + } + + #[test] + fn is_caret_should_return_false() { + let region = SelRegion::new(1, 1, None); + assert!(region.is_caret()); + } + + #[test] + fn should_merge_regions() { + let region = SelRegion::new(1, 2, None); + let other = SelRegion::new(3, 4, None); + assert_eq!(region.merge_with(other), SelRegion::new(1, 4, None)); + + let region = SelRegion::new(2, 1, None); + let other = SelRegion::new(4, 3, None); + assert_eq!(region.merge_with(other), SelRegion::new(4, 1, None)); + + let region = SelRegion::new(1, 1, None); + let other = SelRegion::new(6, 6, None); + assert_eq!(region.merge_with(other), SelRegion::new(1, 6, None)); + } + + #[test] + fn selection_should_be_caret() { + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(1)); + selection.add_region(SelRegion::caret(6)); + assert!(selection.is_caret()); + } + + #[test] + fn selection_should_not_be_caret() { + let mut selection = Selection::new(); + selection.add_region(SelRegion::caret(1)); + selection.add_region(SelRegion::new(4, 6, None)); + assert!(!selection.is_caret()); + } + + #[test] + fn should_return_min_selection() { + let mut selection = Selection::new(); + selection.add_region(SelRegion::new(1, 3, None)); + selection.add_region(SelRegion::new(4, 6, None)); + assert_eq!( + selection.min().regions, + vec![SelRegion::caret(1), SelRegion::caret(4)] + ); + } + + #[test] + fn selection_should_contains_region() { + let selection = Selection::region(0, 2); + assert!(selection.contains(0)); + assert!(selection.contains(1)); + assert!(selection.contains(2)); + assert!(!selection.contains(3)); + } + + #[test] + fn should_return_last_inserted_region() { + let mut selection = Selection::region(5, 6); + selection.add_region(SelRegion::caret(1)); + assert_eq!(selection.last_inserted(), Some(&SelRegion::caret(1))); + } + + #[test] + fn should_return_last_region() { + let mut selection = Selection::region(5, 6); + selection.add_region(SelRegion::caret(1)); + assert_eq!(selection.last(), Some(&SelRegion::new(5, 6, None))); + } + + #[test] + fn should_return_first_region() { + let mut selection = Selection::region(5, 6); + selection.add_region(SelRegion::caret(1)); + assert_eq!(selection.first(), Some(&SelRegion::caret(1))); + } + + #[test] + fn should_return_regions_in_range() { + let mut selection = Selection::new(); + selection.add_region(SelRegion::new(0, 3, None)); + selection.add_region(SelRegion::new(3, 6, None)); + selection.add_region(SelRegion::new(7, 8, None)); + selection.add_region(SelRegion::new(9, 11, None)); + + let regions = selection.regions_in_range(5, 10); + + assert_eq!( + regions, + vec![ + SelRegion::new(3, 6, None), + SelRegion::new(7, 8, None), + SelRegion::new(9, 11, None), + ] + ); + } + + #[test] + fn should_return_regions_in_full_range() { + let mut selection = Selection::new(); + selection.add_region(SelRegion::new(0, 3, None)); + selection.add_region(SelRegion::new(3, 6, None)); + selection.add_region(SelRegion::new(7, 8, None)); + selection.add_region(SelRegion::new(9, 11, None)); + + let regions = selection.full_regions_in_range(5, 10); + + assert_eq!( + regions, + vec![SelRegion::new(7, 8, None), SelRegion::new(9, 11, None),] + ); + } + + #[test] + fn should_delete_regions() { + let mut selection = Selection::new(); + selection.add_region(SelRegion::new(0, 3, None)); + selection.add_region(SelRegion::new(3, 6, None)); + selection.add_region(SelRegion::new(7, 8, None)); + selection.add_region(SelRegion::new(9, 11, None)); + selection.delete_range(5, 10); + assert_eq!(selection.regions(), vec![SelRegion::new(0, 3, None)]); + } + + #[test] + fn should_add_regions() { + let mut selection = Selection::new(); + selection.add_region(SelRegion::new(0, 3, None)); + selection.add_region(SelRegion::new(3, 6, None)); + assert_eq!( + selection.regions(), + vec![SelRegion::new(0, 3, None), SelRegion::new(3, 6, None),] + ); + } + + #[test] + fn should_add_and_merge_regions() { + let mut selection = Selection::new(); + + selection.add_region(SelRegion::new(0, 4, None)); + selection.add_region(SelRegion::new(3, 6, None)); + assert_eq!(selection.regions(), vec![SelRegion::new(0, 6, None)]); + } + + #[test] + fn should_apply_delta_after_insertion() { + let selection = Selection::caret(0); + + let (_, mock_delta, _) = { + let mut buffer = Buffer::new(""); + buffer.edit(&[(selection.clone(), "Hello")], EditType::InsertChars) + }; + + assert_eq!( + selection.apply_delta(&mock_delta, true, InsertDrift::Inside), + Selection::caret(5) + ); + } +} diff --git a/editor-core/src/soft_tab.rs b/editor-core/src/soft_tab.rs new file mode 100644 index 00000000..7fcd4f9d --- /dev/null +++ b/editor-core/src/soft_tab.rs @@ -0,0 +1,237 @@ +use lapce_xi_rope::Rope; + +/// The direction to snap. Left is used when moving left, Right when moving right. +/// Nearest is used for mouse selection. +pub enum SnapDirection { + Left, + Right, + Nearest, +} + +/// If the cursor is inside a soft tab at the start of the line, snap it to the +/// nearest, left or right edge. This version takes an offset and returns an offset. +pub fn snap_to_soft_tab( + text: &Rope, + offset: usize, + direction: SnapDirection, + tab_width: usize, +) -> usize { + // Fine which line we're on. + let line = text.line_of_offset(offset); + // Get the offset to the start of the line. + let start_line_offset = text.offset_of_line(line); + // And the offset within the lint. + let offset_within_line = offset - start_line_offset; + + start_line_offset + + snap_to_soft_tab_logic( + text, + offset_within_line, + start_line_offset, + direction, + tab_width, + ) +} + +/// If the cursor is inside a soft tab at the start of the line, snap it to the +/// nearest, left or right edge. This version takes a line/column and returns a column. +pub fn snap_to_soft_tab_line_col( + text: &Rope, + line: usize, + col: usize, + direction: SnapDirection, + tab_width: usize, +) -> usize { + // Get the offset to the start of the line. + let start_line_offset = text.offset_of_line(line); + + snap_to_soft_tab_logic(text, col, start_line_offset, direction, tab_width) +} + +/// Internal shared logic that performs the actual snapping. It can be passed +/// either an column or offset within the line since it is only modified when it makes no +/// difference which is used (since they're equal for spaces). +/// It returns the column or offset within the line (depending on what you passed in). +fn snap_to_soft_tab_logic( + text: &Rope, + offset_or_col: usize, + start_line_offset: usize, + direction: SnapDirection, + tab_width: usize, +) -> usize { + assert!(tab_width >= 1); + + // Number of spaces, ignoring incomplete soft tabs. + let space_count = (count_spaces_from(text, start_line_offset) / tab_width) * tab_width; + + // If we're past the soft tabs, we don't need to snap. + if offset_or_col >= space_count { + return offset_or_col; + } + + let bias = match direction { + SnapDirection::Left => 0, + SnapDirection::Right => tab_width - 1, + SnapDirection::Nearest => tab_width / 2, + }; + + ((offset_or_col + bias) / tab_width) * tab_width +} + +/// Count the number of spaces found after a certain offset. +fn count_spaces_from(text: &Rope, from_offset: usize) -> usize { + let mut cursor = lapce_xi_rope::Cursor::new(text, from_offset); + let mut space_count = 0usize; + while let Some(next) = cursor.next_codepoint() { + if next != ' ' { + break; + } + space_count += 1; + } + space_count +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_count_spaces_from() { + let text = Rope::from(" abc\n def\nghi\n"); + assert_eq!(count_spaces_from(&text, 0), 5); + assert_eq!(count_spaces_from(&text, 1), 4); + assert_eq!(count_spaces_from(&text, 5), 0); + assert_eq!(count_spaces_from(&text, 6), 0); + + assert_eq!(count_spaces_from(&text, 8), 0); + assert_eq!(count_spaces_from(&text, 9), 3); + assert_eq!(count_spaces_from(&text, 10), 2); + + assert_eq!(count_spaces_from(&text, 16), 0); + assert_eq!(count_spaces_from(&text, 17), 0); + } + + #[test] + fn test_snap_to_soft_tab() { + let text = Rope::from(" abc\n def\n ghi\nklm\n opq"); + + let tab_width = 4; + + // Input offset, and output offset for Left, Nearest and Right respectively. + let test_cases = [ + (0, 0, 0, 0), + (1, 0, 0, 4), + (2, 0, 4, 4), + (3, 0, 4, 4), + (4, 4, 4, 4), + (5, 4, 4, 8), + (6, 4, 8, 8), + (7, 4, 8, 8), + (8, 8, 8, 8), + (9, 9, 9, 9), + (10, 10, 10, 10), + (11, 11, 11, 11), + (12, 12, 12, 12), + (13, 13, 13, 13), + (14, 14, 14, 14), + (15, 14, 14, 18), + (16, 14, 18, 18), + (17, 14, 18, 18), + (18, 18, 18, 18), + (19, 19, 19, 19), + (20, 20, 20, 20), + (21, 21, 21, 21), + ]; + + for test_case in test_cases { + assert_eq!( + snap_to_soft_tab(&text, test_case.0, SnapDirection::Left, tab_width), + test_case.1 + ); + assert_eq!( + snap_to_soft_tab(&text, test_case.0, SnapDirection::Nearest, tab_width), + test_case.2 + ); + assert_eq!( + snap_to_soft_tab(&text, test_case.0, SnapDirection::Right, tab_width), + test_case.3 + ); + } + } + + #[test] + fn test_snap_to_soft_tab_line_col() { + let text = Rope::from(" abc\n def\n ghi\nklm\n opq"); + + let tab_width = 4; + + // Input line, column, and output column for Left, Nearest and Right respectively. + let test_cases = [ + (0, 0, 0, 0, 0), + (0, 1, 0, 0, 4), + (0, 2, 0, 4, 4), + (0, 3, 0, 4, 4), + (0, 4, 4, 4, 4), + (0, 5, 4, 4, 8), + (0, 6, 4, 8, 8), + (0, 7, 4, 8, 8), + (0, 8, 8, 8, 8), + (0, 9, 9, 9, 9), + (0, 10, 10, 10, 10), + (0, 11, 11, 11, 11), + (0, 12, 12, 12, 12), + (0, 13, 13, 13, 13), + (1, 0, 0, 0, 0), + (1, 1, 0, 0, 4), + (1, 2, 0, 4, 4), + (1, 3, 0, 4, 4), + (1, 4, 4, 4, 4), + (1, 5, 5, 5, 5), + (1, 6, 6, 6, 6), + (1, 7, 7, 7, 7), + (4, 0, 0, 0, 0), + (4, 1, 0, 0, 4), + (4, 2, 0, 4, 4), + (4, 3, 0, 4, 4), + (4, 4, 4, 4, 4), + (4, 5, 4, 4, 8), + (4, 6, 4, 8, 8), + (4, 7, 4, 8, 8), + (4, 8, 8, 8, 8), + (4, 9, 9, 9, 9), + ]; + + for test_case in test_cases { + assert_eq!( + snap_to_soft_tab_line_col( + &text, + test_case.0, + test_case.1, + SnapDirection::Left, + tab_width + ), + test_case.2 + ); + assert_eq!( + snap_to_soft_tab_line_col( + &text, + test_case.0, + test_case.1, + SnapDirection::Nearest, + tab_width + ), + test_case.3 + ); + assert_eq!( + snap_to_soft_tab_line_col( + &text, + test_case.0, + test_case.1, + SnapDirection::Right, + tab_width + ), + test_case.4 + ); + } + } +} diff --git a/editor-core/src/util.rs b/editor-core/src/util.rs new file mode 100644 index 00000000..a6002b4f --- /dev/null +++ b/editor-core/src/util.rs @@ -0,0 +1,129 @@ +use core::str::FromStr; +use std::collections::HashMap; + +/// If the character is an opening bracket return Some(true), if closing, return Some(false) +pub fn matching_pair_direction(c: char) -> Option { + Some(match c { + '{' => true, + '}' => false, + '(' => true, + ')' => false, + '[' => true, + ']' => false, + _ => return None, + }) +} + +pub fn matching_char(c: char) -> Option { + Some(match c { + '{' => '}', + '}' => '{', + '(' => ')', + ')' => '(', + '[' => ']', + ']' => '[', + _ => return None, + }) +} + +/// If the given character is a parenthesis, returns its matching bracket +pub fn matching_bracket_general(char: char) -> Option +where + &'static str: ToStaticTextType, +{ + let pair = match char { + '{' => "}", + '}' => "{", + '(' => ")", + ')' => "(", + '[' => "]", + ']' => "[", + _ => return None, + }; + Some(pair.to_static()) +} + +pub trait ToStaticTextType: 'static { + fn to_static(self) -> R; +} + +impl ToStaticTextType for &'static str { + #[inline] + fn to_static(self) -> &'static str { + self + } +} + +impl ToStaticTextType for &'static str { + #[inline] + fn to_static(self) -> char { + char::from_str(self).unwrap() + } +} + +impl ToStaticTextType for &'static str { + #[inline] + fn to_static(self) -> String { + self.to_string() + } +} + +impl ToStaticTextType for char { + #[inline] + fn to_static(self) -> char { + self + } +} + +impl ToStaticTextType for String { + #[inline] + fn to_static(self) -> String { + self + } +} + +pub fn has_unmatched_pair(line: &str) -> bool { + let mut count = HashMap::new(); + let mut pair_first = HashMap::new(); + for c in line.chars().rev() { + if let Some(left) = matching_pair_direction(c) { + let key = if left { c } else { matching_char(c).unwrap() }; + let pair_count = *count.get(&key).unwrap_or(&0i32); + pair_first.entry(key).or_insert(left); + if left { + count.insert(key, pair_count - 1); + } else { + count.insert(key, pair_count + 1); + } + } + } + for (_, pair_count) in count.iter() { + if *pair_count < 0 { + return true; + } + } + for (_, left) in pair_first.iter() { + if *left { + return true; + } + } + false +} + +pub fn str_is_pair_left(c: &str) -> bool { + if c.chars().count() == 1 { + let c = c.chars().next().unwrap(); + if matching_pair_direction(c).unwrap_or(false) { + return true; + } + } + false +} + +pub fn str_matching_pair(c: &str) -> Option { + if c.chars().count() == 1 { + let c = c.chars().next().unwrap(); + return matching_char(c); + } + None +} diff --git a/editor-core/src/word.rs b/editor-core/src/word.rs new file mode 100644 index 00000000..b8bf6f9e --- /dev/null +++ b/editor-core/src/word.rs @@ -0,0 +1,701 @@ +use lapce_xi_rope::{Cursor, Rope, RopeInfo}; + +use crate::{ + mode::Mode, + util::{matching_char, matching_pair_direction}, +}; + +/// Describe char classifications used to compose word boundaries +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum CharClassification { + /// Carriage Return (`r`) + Cr, + /// Line feed (`\n`) + Lf, + /// Whitespace character + Space, + /// Any punctuation character + Punctuation, + /// Includes letters and all of non-ascii unicode + Other, +} + +/// A word boundary can be the start of a word, its end or both for punctuation +#[derive(PartialEq, Eq)] +enum WordBoundary { + /// Denote that this is not a boundary + Interior, + /// A boundary indicating the end of a word + Start, + /// A boundary indicating the start of a word + End, + /// Both start and end boundaries (ex: punctuation characters) + Both, +} + +impl WordBoundary { + fn is_start(&self) -> bool { + *self == WordBoundary::Start || *self == WordBoundary::Both + } + + fn is_end(&self) -> bool { + *self == WordBoundary::End || *self == WordBoundary::Both + } + + #[allow(unused)] + fn is_boundary(&self) -> bool { + *self != WordBoundary::Interior + } +} + +/// A cursor providing utility function to navigate the rope +/// by word boundaries. +/// Boundaries can be the start of a word, its end, punctuation etc. +pub struct WordCursor<'a> { + pub(crate) inner: Cursor<'a, RopeInfo>, +} + +impl<'a> WordCursor<'a> { + pub fn new(text: &'a Rope, pos: usize) -> WordCursor<'a> { + let inner = Cursor::new(text, pos); + WordCursor { inner } + } + + /// Get the previous start boundary of a word, and set the cursor position to the boundary found. + /// The behaviour diffs a bit on new line character with modal and non modal, + /// while on modal, it will ignore the new line character and on non-modal, + /// it will stop at the new line character + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::word::WordCursor; + /// # use floem_editor_core::mode::Mode; + /// # use lapce_xi_rope::Rope; + /// let rope = Rope::from("Hello world"); + /// let mut cursor = WordCursor::new(&rope, 4); + /// let boundary = cursor.prev_boundary(Mode::Insert); + /// assert_eq!(boundary, Some(0)); + ///``` + pub fn prev_boundary(&mut self, mode: Mode) -> Option { + if let Some(ch) = self.inner.prev_codepoint() { + let mut prop = get_char_property(ch); + let mut candidate = self.inner.pos(); + while let Some(prev) = self.inner.prev_codepoint() { + let prop_prev = get_char_property(prev); + if classify_boundary(prop_prev, prop).is_start() { + break; + } + + // Stop if line beginning reached, without any non-whitespace characters + if mode == Mode::Insert + && prop_prev == CharClassification::Lf + && prop == CharClassification::Space + { + break; + } + + prop = prop_prev; + candidate = self.inner.pos(); + } + self.inner.set(candidate); + return Some(candidate); + } + None + } + + /// Computes where the cursor position should be after backward deletion. + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::word::WordCursor; + /// # use lapce_xi_rope::Rope; + /// let text = "violet are blue"; + /// let rope = Rope::from(text); + /// let mut cursor = WordCursor::new(&rope, 9); + /// let position = cursor.prev_deletion_boundary(); + /// let position = position; + /// + /// assert_eq!(position, Some(7)); + /// assert_eq!(&text[..position.unwrap()], "violet "); + ///``` + pub fn prev_deletion_boundary(&mut self) -> Option { + if let Some(ch) = self.inner.prev_codepoint() { + let mut prop = get_char_property(ch); + let mut candidate = self.inner.pos(); + + // Flag, determines if the word should be deleted or not + // If not, erase only whitespace characters. + let mut keep_word = false; + while let Some(prev) = self.inner.prev_codepoint() { + let prop_prev = get_char_property(prev); + + // Stop if line beginning reached, without any non-whitespace characters + if prop_prev == CharClassification::Lf && prop == CharClassification::Space { + break; + } + + // More than a single whitespace: keep word, remove only whitespaces + if prop == CharClassification::Space && prop_prev == CharClassification::Space { + keep_word = true; + } + + // Line break found: keep words, delete line break & trailing whitespaces + if prop == CharClassification::Lf || prop == CharClassification::Cr { + keep_word = true; + } + + // Skip word deletion if above conditions were met + if keep_word + && (prop_prev == CharClassification::Punctuation + || prop_prev == CharClassification::Other) + { + break; + } + + // Default deletion + if classify_boundary(prop_prev, prop).is_start() { + break; + } + prop = prop_prev; + candidate = self.inner.pos(); + } + self.inner.set(candidate); + return Some(candidate); + } + None + } + + /// Get the position of the next non blank character in the rope + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::word::WordCursor; + /// # use lapce_xi_rope::Rope; + /// let rope = Rope::from(" world"); + /// let mut cursor = WordCursor::new(&rope, 0); + /// let char_position = cursor.next_non_blank_char(); + /// assert_eq!(char_position, 4); + ///``` + pub fn next_non_blank_char(&mut self) -> usize { + let mut candidate = self.inner.pos(); + while let Some(next) = self.inner.next_codepoint() { + let prop = get_char_property(next); + if prop != CharClassification::Space { + break; + } + candidate = self.inner.pos(); + } + self.inner.set(candidate); + candidate + } + + /// Get the next start boundary of a word, and set the cursor position to the boundary found. + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::word::WordCursor; + /// # use lapce_xi_rope::Rope; + /// let rope = Rope::from("Hello world"); + /// let mut cursor = WordCursor::new(&rope, 0); + /// let boundary = cursor.next_boundary(); + /// assert_eq!(boundary, Some(6)); + ///``` + pub fn next_boundary(&mut self) -> Option { + if let Some(ch) = self.inner.next_codepoint() { + let mut prop = get_char_property(ch); + let mut candidate = self.inner.pos(); + while let Some(next) = self.inner.next_codepoint() { + let prop_next = get_char_property(next); + if classify_boundary(prop, prop_next).is_start() { + break; + } + prop = prop_next; + candidate = self.inner.pos(); + } + self.inner.set(candidate); + return Some(candidate); + } + None + } + + /// Get the next end boundary, and set the cursor position to the boundary found. + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::word::WordCursor; + /// # use lapce_xi_rope::Rope; + /// let rope = Rope::from("Hello world"); + /// let mut cursor = WordCursor::new(&rope, 3); + /// let end_boundary = cursor.end_boundary(); + /// assert_eq!(end_boundary, Some(5)); + ///``` + pub fn end_boundary(&mut self) -> Option { + self.inner.next_codepoint(); + if let Some(ch) = self.inner.next_codepoint() { + let mut prop = get_char_property(ch); + let mut candidate = self.inner.pos(); + while let Some(next) = self.inner.next_codepoint() { + let prop_next = get_char_property(next); + if classify_boundary(prop, prop_next).is_end() { + break; + } + prop = prop_next; + candidate = self.inner.pos(); + } + self.inner.set(candidate); + return Some(candidate); + } + None + } + + /// Get the first matching [`CharClassification::Other`] backward and set the cursor position to this location . + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::word::WordCursor; + /// # use lapce_xi_rope::Rope; + /// let text = "violet, are\n blue"; + /// let rope = Rope::from(text); + /// let mut cursor = WordCursor::new(&rope, 11); + /// let position = cursor.prev_code_boundary(); + /// assert_eq!(&text[position..], "are\n blue"); + ///``` + pub fn prev_code_boundary(&mut self) -> usize { + let mut candidate = self.inner.pos(); + while let Some(prev) = self.inner.prev_codepoint() { + let prop_prev = get_char_property(prev); + if prop_prev != CharClassification::Other { + break; + } + candidate = self.inner.pos(); + } + candidate + } + + /// Get the first matching [`CharClassification::Other`] forward and set the cursor position to this location . + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::word::WordCursor; + /// # use lapce_xi_rope::Rope; + /// let text = "violet, are\n blue"; + /// let rope = Rope::from(text); + /// let mut cursor = WordCursor::new(&rope, 11); + /// let position = cursor.next_code_boundary(); + /// assert_eq!(&text[position..], "\n blue"); + ///``` + pub fn next_code_boundary(&mut self) -> usize { + let mut candidate = self.inner.pos(); + while let Some(prev) = self.inner.next_codepoint() { + let prop_prev = get_char_property(prev); + if prop_prev != CharClassification::Other { + break; + } + candidate = self.inner.pos(); + } + candidate + } + + /// Looks for a matching pair character, either forward for opening chars (ex: `(`) or + /// backward for closing char (ex: `}`), and return the matched character position if found. + /// Will return `None` if the character under cursor is not matchable (see [`crate::syntax::util::matching_char`]). + /// + /// **Example:** + /// + /// ```rust + /// # use floem_editor_core::word::WordCursor; + /// # use lapce_xi_rope::Rope; + /// let text = "{ }"; + /// let rope = Rope::from(text); + /// let mut cursor = WordCursor::new(&rope, 2); + /// let position = cursor.match_pairs(); + /// assert_eq!(position, Some(0)); + ///``` + pub fn match_pairs(&mut self) -> Option { + let c = self.inner.peek_next_codepoint()?; + let other = matching_char(c)?; + let left = matching_pair_direction(other)?; + if left { + self.previous_unmatched(other) + } else { + self.inner.next_codepoint(); + let offset = self.next_unmatched(other)?; + Some(offset - 1) + } + } + + /// Take a matchable character and look cforward for the first unmatched one + /// ignoring the encountered matched pairs. + /// + /// **Example**: + /// ```rust + /// # use lapce_xi_rope::Rope; + /// # use floem_editor_core::word::WordCursor; + /// let rope = Rope::from("outer {inner}} world"); + /// let mut cursor = WordCursor::new(&rope, 0); + /// let position = cursor.next_unmatched('}'); + /// assert_eq!(position, Some(14)); + /// ``` + pub fn next_unmatched(&mut self, c: char) -> Option { + let other = matching_char(c)?; + let mut n = 0; + while let Some(current) = self.inner.next_codepoint() { + if current == c && n == 0 { + return Some(self.inner.pos()); + } + if current == other { + n += 1; + } else if current == c { + n -= 1; + } + } + None + } + + /// Take a matchable character and look backward for the first unmatched one + /// ignoring the encountered matched pairs. + /// + /// **Example**: + /// + /// ```rust + /// # use lapce_xi_rope::Rope; + /// # use floem_editor_core::word::WordCursor; + /// let rope = Rope::from("outer {{inner} world"); + /// let mut cursor = WordCursor::new(&rope, 15); + /// let position = cursor.previous_unmatched('{'); + /// assert_eq!(position, Some(6)); + /// ``` + pub fn previous_unmatched(&mut self, c: char) -> Option { + let other = matching_char(c)?; + let mut n = 0; + while let Some(current) = self.inner.prev_codepoint() { + if current == c && n == 0 { + return Some(self.inner.pos()); + } + if current == other { + n += 1; + } else if current == c { + n -= 1; + } + } + None + } + + /// Return the previous and end boundaries of the word under cursor. + /// + /// **Example**: + /// + ///```rust + /// # use floem_editor_core::word::WordCursor; + /// # use lapce_xi_rope::Rope; + /// let text = "violet are blue"; + /// let rope = Rope::from(text); + /// let mut cursor = WordCursor::new(&rope, 9); + /// let (start, end) = cursor.select_word(); + /// assert_eq!(&text[start..end], "are"); + ///``` + pub fn select_word(&mut self) -> (usize, usize) { + let initial = self.inner.pos(); + let end = self.next_code_boundary(); + self.inner.set(initial); + let start = self.prev_code_boundary(); + (start, end) + } + + /// Return the enclosing brackets of the current position + /// + /// **Example**: + /// + ///```rust + /// # use floem_editor_core::word::WordCursor; + /// # use lapce_xi_rope::Rope; + /// let text = "outer {{inner} world"; + /// let rope = Rope::from(text); + /// let mut cursor = WordCursor::new(&rope, 10); + /// let (start, end) = cursor.find_enclosing_pair().unwrap(); + /// assert_eq!(start, 7); + /// assert_eq!(end, 13) + ///``` + pub fn find_enclosing_pair(&mut self) -> Option<(usize, usize)> { + let old_offset = self.inner.pos(); + while let Some(c) = self.inner.prev_codepoint() { + if matching_pair_direction(c) == Some(true) { + let opening_bracket_offset = self.inner.pos(); + if let Some(closing_bracket_offset) = self.match_pairs() { + if (opening_bracket_offset..=closing_bracket_offset).contains(&old_offset) { + return Some((opening_bracket_offset, closing_bracket_offset)); + } else { + self.inner.set(opening_bracket_offset); + } + } + } + } + None + } +} + +/// Return the [`CharClassification`] of the input character +pub fn get_char_property(codepoint: char) -> CharClassification { + if codepoint <= ' ' { + if codepoint == '\r' { + return CharClassification::Cr; + } + if codepoint == '\n' { + return CharClassification::Lf; + } + return CharClassification::Space; + } else if codepoint <= '\u{3f}' { + if (0xfc00fffe00000000u64 >> (codepoint as u32)) & 1 != 0 { + return CharClassification::Punctuation; + } + } else if codepoint <= '\u{7f}' { + // Hardcoded: @[\]^`{|}~ + if (0x7800000178000001u64 >> ((codepoint as u32) & 0x3f)) & 1 != 0 { + return CharClassification::Punctuation; + } + } + CharClassification::Other +} + +fn classify_boundary(prev: CharClassification, next: CharClassification) -> WordBoundary { + use self::{CharClassification::*, WordBoundary::*}; + match (prev, next) { + (Lf, Lf) => Start, + (Lf, Space) => Interior, + (Cr, Lf) => Interior, + (Space, Lf) => Interior, + (Space, Cr) => Interior, + (Space, Space) => Interior, + (_, Space) => End, + (Space, _) => Start, + (Lf, _) => Start, + (_, Cr) => End, + (_, Lf) => End, + (Punctuation, Other) => Both, + (Other, Punctuation) => Both, + _ => Interior, + } +} + +#[cfg(test)] +mod test { + use lapce_xi_rope::Rope; + + use super::WordCursor; + use crate::mode::Mode; + + #[test] + fn prev_boundary_should_be_none_at_position_zero() { + let rope = Rope::from("Hello world"); + let mut cursor = WordCursor::new(&rope, 0); + let boudary = cursor.prev_boundary(Mode::Insert); + assert!(boudary.is_none()) + } + + #[test] + fn prev_boundary_should_be_zero_when_cursor_on_first_word() { + let rope = Rope::from("Hello world"); + let mut cursor = WordCursor::new(&rope, 4); + let boundary = cursor.prev_boundary(Mode::Insert); + assert_eq!(boundary, Some(0)); + } + + #[test] + fn prev_boundary_should_be_at_word_start() { + let rope = Rope::from("Hello world"); + let mut cursor = WordCursor::new(&rope, 9); + let boundary = cursor.prev_boundary(Mode::Insert); + assert_eq!(boundary, Some(6)); + } + + #[test] + fn on_whitespace_prev_boundary_should_be_at_line_start_for_non_modal() { + let rope = Rope::from("Hello\n world"); + let mut cursor = WordCursor::new(&rope, 10); + let boundary = cursor.prev_boundary(Mode::Insert); + assert_eq!(boundary, Some(6)); + } + + #[test] + fn on_whitespace_prev_boundary_should_cross_line_for_modal() { + let rope = Rope::from("Hello\n world"); + let mut cursor = WordCursor::new(&rope, 10); + let boundary = cursor.prev_boundary(Mode::Normal); + assert_eq!(boundary, Some(0)); + } + + #[test] + fn should_get_next_word_boundary() { + let rope = Rope::from("Hello world"); + let mut cursor = WordCursor::new(&rope, 0); + let boundary = cursor.next_boundary(); + assert_eq!(boundary, Some(6)); + } + + #[test] + fn next_word_boundary_should_be_none_at_last_position() { + let rope = Rope::from("Hello world"); + let mut cursor = WordCursor::new(&rope, 11); + let boundary = cursor.next_boundary(); + assert_eq!(boundary, None); + } + + #[test] + fn should_get_previous_code_boundary() { + let text = "violet, are\n blue"; + let rope = Rope::from(text); + let mut cursor = WordCursor::new(&rope, 11); + let position = cursor.prev_code_boundary(); + assert_eq!(&text[position..], "are\n blue"); + } + + #[test] + fn should_get_next_code_boundary() { + let text = "violet, are\n blue"; + let rope = Rope::from(text); + let mut cursor = WordCursor::new(&rope, 11); + let position = cursor.next_code_boundary(); + assert_eq!(&text[position..], "\n blue"); + } + + #[test] + fn get_next_non_blank_char_should_skip_whitespace() { + let rope = Rope::from("Hello world"); + let mut cursor = WordCursor::new(&rope, 5); + let char_position = cursor.next_non_blank_char(); + assert_eq!(char_position, 6); + } + + #[test] + fn get_next_non_blank_char_should_return_current_position_on_non_blank_char() { + let rope = Rope::from("Hello world"); + let mut cursor = WordCursor::new(&rope, 3); + let char_position = cursor.next_non_blank_char(); + assert_eq!(char_position, 3); + } + + #[test] + fn should_get_end_boundary() { + let rope = Rope::from("Hello world"); + let mut cursor = WordCursor::new(&rope, 3); + let end_boundary = cursor.end_boundary(); + assert_eq!(end_boundary, Some(5)); + } + + #[test] + fn should_get_next_unmatched_char() { + let rope = Rope::from("hello { world"); + let mut cursor = WordCursor::new(&rope, 0); + let position = cursor.next_unmatched('{'); + assert_eq!(position, Some(7)); + } + + #[test] + fn should_get_next_unmatched_char_witch_matched_chars() { + let rope = Rope::from("hello {} world }"); + let mut cursor = WordCursor::new(&rope, 0); + let position = cursor.next_unmatched('}'); + assert_eq!(position, Some(16)); + } + + #[test] + fn should_get_previous_unmatched_char() { + let rope = Rope::from("hello { world"); + let mut cursor = WordCursor::new(&rope, 12); + let position = cursor.previous_unmatched('{'); + assert_eq!(position, Some(6)); + } + + #[test] + fn should_get_previous_unmatched_char_with_inner_matched_chars() { + let rope = Rope::from("{hello {} world"); + let mut cursor = WordCursor::new(&rope, 10); + let position = cursor.previous_unmatched('{'); + assert_eq!(position, Some(0)); + } + + #[test] + fn should_match_pair_forward() { + let text = "{ }"; + let rope = Rope::from(text); + let mut cursor = WordCursor::new(&rope, 0); + let position = cursor.match_pairs(); + assert_eq!(position, Some(2)); + } + + #[test] + fn should_match_pair_backward() { + let text = "{ }"; + let rope = Rope::from(text); + let mut cursor = WordCursor::new(&rope, 2); + let position = cursor.match_pairs(); + assert_eq!(position, Some(0)); + } + + #[test] + fn match_pair_should_be_none() { + let text = "{ }"; + let rope = Rope::from(text); + let mut cursor = WordCursor::new(&rope, 1); + let position = cursor.match_pairs(); + assert_eq!(position, None); + } + + #[test] + fn select_word_should_return_word_boundaries() { + let text = "violet are blue"; + let rope = Rope::from(text); + let mut cursor = WordCursor::new(&rope, 9); + let (start, end) = cursor.select_word(); + assert_eq!(&text[start..end], "are"); + } + + #[test] + fn should_get_deletion_boundary_backward() { + let text = "violet are blue"; + let rope = Rope::from(text); + let mut cursor = WordCursor::new(&rope, 9); + let position = cursor.prev_deletion_boundary(); + + assert_eq!(position, Some(7)); + assert_eq!(&text[..position.unwrap()], "violet "); + } + + #[test] + fn find_pair_should_return_positions() { + let text = "violet (are) blue"; + let rope = Rope::from(text); + let mut cursor = WordCursor::new(&rope, 9); + let positions = cursor.find_enclosing_pair(); + assert_eq!(positions, Some((7, 11))); + } + + #[test] + fn find_pair_should_return_next_pair() { + let text = "violets {are (blue) }"; + let rope = Rope::from(text); + + let mut cursor = WordCursor::new(&rope, 11); + let positions = cursor.find_enclosing_pair(); + assert_eq!(positions, Some((8, 23))); + + let mut cursor = WordCursor::new(&rope, 20); + let positions = cursor.find_enclosing_pair(); + assert_eq!(positions, Some((8, 23))); + + let mut cursor = WordCursor::new(&rope, 18); + let positions = cursor.find_enclosing_pair(); + assert_eq!(positions, Some((13, 18))); + } + + #[test] + fn find_pair_should_return_none() { + let text = "violet (are) blue"; + let rope = Rope::from(text); + let mut cursor = WordCursor::new(&rope, 1); + let positions = cursor.find_enclosing_pair(); + assert_eq!(positions, None); + } +} diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml new file mode 100644 index 00000000..6f842dc1 --- /dev/null +++ b/examples/editor/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "editor" +version = "0.1.0" +edition = "2021" + +[dependencies] +im = "15.1.0" +floem = { path = "../..", features = ["editor"] } diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs new file mode 100644 index 00000000..73eba8e8 --- /dev/null +++ b/examples/editor/src/main.rs @@ -0,0 +1,57 @@ +use floem::{ + keyboard::{Key, ModifiersState, NamedKey}, + view::View, + views::{ + editor::{ + command::{Command, CommandExecuted}, + core::{command::EditCommand, editor::EditType, selection::Selection}, + text::SimpleStyling, + }, + stack, text_editor, Decorators, + }, + widgets::button, +}; + +fn app_view() -> impl View { + let editor_a = text_editor("Hello World!").styling(SimpleStyling::dark()); + let editor_b = editor_a + .shared_editor() + .pre_command(|ev| { + if matches!(ev.cmd, Command::Edit(EditCommand::Undo)) { + println!("Undo command executed on editor B, ignoring!"); + return CommandExecuted::Yes; + } + CommandExecuted::No + }) + .update(|_| { + // This hooks up to both editors! + println!("Editor changed"); + }); + let doc = editor_a.doc(); + + let view = stack(( + editor_a, + editor_b, + button(|| "Clear") + .on_click_stop(move |_| { + doc.edit_single( + Selection::region(0, doc.text().len()), + "", + EditType::DeleteSelection, + ); + }) + .style(|s| s.width_full()), + )) + .style(|s| s.size_full().flex_col().items_center().justify_center()); + + let id = view.id(); + view.on_key_up( + Key::Named(NamedKey::F11), + ModifiersState::empty(), + move |_| id.inspect(), + ) +} + +fn main() { + floem::launch(app_view) +} diff --git a/src/views/editor/actions.rs b/src/views/editor/actions.rs new file mode 100644 index 00000000..3ec6e9a0 --- /dev/null +++ b/src/views/editor/actions.rs @@ -0,0 +1,189 @@ +use std::ops::Range; + +use crate::keyboard::ModifiersState; +use floem_editor_core::{ + command::{EditCommand, MotionModeCommand, MultiSelectionCommand, ScrollCommand}, + cursor::Cursor, + mode::MotionMode, + movement::Movement, + register::Register, +}; + +use super::{ + command::{Command, CommandExecuted}, + movement, Editor, +}; + +pub fn handle_command_default( + ed: &Editor, + action: &dyn CommonAction, + cmd: &Command, + count: Option, + modifiers: ModifiersState, +) -> CommandExecuted { + match cmd { + Command::Edit(cmd) => handle_edit_command_default(ed, action, cmd), + Command::Move(cmd) => { + let movement = cmd.to_movement(count); + handle_move_command_default(ed, action, movement, count, modifiers) + } + Command::Scroll(cmd) => handle_scroll_command_default(ed, cmd, count, modifiers), + Command::MotionMode(cmd) => handle_motion_mode_command_default(ed, action, cmd, count), + Command::MultiSelection(cmd) => handle_multi_selection_command_default(ed, cmd), + } +} +fn handle_edit_command_default( + ed: &Editor, + action: &dyn CommonAction, + cmd: &EditCommand, +) -> CommandExecuted { + let modal = ed.modal.get_untracked(); + let smart_tab = ed.smart_tab.get_untracked(); + let mut cursor = ed.cursor.get_untracked(); + let mut register = ed.register.get_untracked(); + + let text = ed.rope_text(); + + let yank_data = if let floem_editor_core::cursor::CursorMode::Visual { .. } = &cursor.mode { + Some(cursor.yank(&text)) + } else { + None + }; + + // TODO: Should we instead pass the editor so that it can grab + // modal + smart-tab (etc) if it wants? + // That would end up with some duplication of logic, but it would + // be more flexible. + let had_edits = action.do_edit(ed, &mut cursor, cmd, modal, &mut register, smart_tab); + + if had_edits { + if let Some(data) = yank_data { + register.add_delete(data); + } + } + + ed.cursor.set(cursor); + ed.register.set(register); + + CommandExecuted::Yes +} +fn handle_move_command_default( + ed: &Editor, + action: &dyn CommonAction, + movement: Movement, + count: Option, + modifiers: ModifiersState, +) -> CommandExecuted { + // TODO: should we track jump locations? + + ed.last_movement.set(movement.clone()); + + let mut cursor = ed.cursor.get_untracked(); + let modify = modifiers.shift_key(); + ed.register.update(|register| { + movement::move_cursor( + ed, + action, + &mut cursor, + &movement, + count.unwrap_or(1), + modify, + register, + ) + }); + + ed.cursor.set(cursor); + + CommandExecuted::Yes +} + +fn handle_scroll_command_default( + ed: &Editor, + cmd: &ScrollCommand, + count: Option, + mods: ModifiersState, +) -> CommandExecuted { + match cmd { + ScrollCommand::PageUp => { + ed.page_move(false, mods); + } + ScrollCommand::PageDown => { + ed.page_move(true, mods); + } + ScrollCommand::ScrollUp => ed.scroll(0.0, false, count.unwrap_or(1), mods), + ScrollCommand::ScrollDown => { + ed.scroll(0.0, true, count.unwrap_or(1), mods); + } + // TODO: + ScrollCommand::CenterOfWindow => {} + ScrollCommand::TopOfWindow => {} + ScrollCommand::BottomOfWindow => {} + } + + CommandExecuted::Yes +} + +fn handle_motion_mode_command_default( + ed: &Editor, + action: &dyn CommonAction, + cmd: &MotionModeCommand, + count: Option, +) -> CommandExecuted { + let count = count.unwrap_or(1); + let motion_mode = match cmd { + MotionModeCommand::MotionModeDelete => MotionMode::Delete { count }, + MotionModeCommand::MotionModeIndent => MotionMode::Indent, + MotionModeCommand::MotionModeOutdent => MotionMode::Outdent, + MotionModeCommand::MotionModeYank => MotionMode::Yank { count }, + }; + let mut cursor = ed.cursor.get_untracked(); + let mut register = ed.register.get_untracked(); + + movement::do_motion_mode(ed, action, &mut cursor, motion_mode, &mut register); + + ed.cursor.set(cursor); + ed.register.set(register); + + CommandExecuted::Yes +} + +fn handle_multi_selection_command_default( + ed: &Editor, + cmd: &MultiSelectionCommand, +) -> CommandExecuted { + let mut cursor = ed.cursor.get_untracked(); + movement::do_multi_selection(ed, &mut cursor, cmd); + ed.cursor.set(cursor); + + CommandExecuted::Yes +} + +/// Trait for common actions needed for the default implementation of the +/// operations. +pub trait CommonAction { + // TODO: should this use Rope's Interval instead of Range? + fn exec_motion_mode( + &self, + ed: &Editor, + cursor: &mut Cursor, + motion_mode: MotionMode, + range: Range, + is_vertical: bool, + register: &mut Register, + ); + + // TODO: should we have a more general cursor state structure? + // since modal is about cursor, and register is sortof about cursor + // but also there might be other state it wants. Should we just pass Editor to it? + /// Perform an edit. + /// Returns `true` if there was any change. + fn do_edit( + &self, + ed: &Editor, + cursor: &mut Cursor, + cmd: &EditCommand, + modal: bool, + register: &mut Register, + smart_tab: bool, + ) -> bool; +} diff --git a/src/views/editor/color.rs b/src/views/editor/color.rs new file mode 100644 index 00000000..aad0f039 --- /dev/null +++ b/src/views/editor/color.rs @@ -0,0 +1,33 @@ +use strum_macros::{Display, EnumIter, EnumString, IntoStaticStr}; + +#[derive(Display, EnumString, EnumIter, IntoStaticStr, Debug, Clone, Copy, PartialEq, Eq)] +pub enum EditorColor { + #[strum(serialize = "editor.background")] + Background, + #[strum(serialize = "editor.scroll_bar")] + Scrollbar, + #[strum(serialize = "editor.dropdown_shadow")] + DropdownShadow, + #[strum(serialize = "editor.foreground")] + Foreground, + #[strum(serialize = "editor.dim")] + Dim, + #[strum(serialize = "editor.focus")] + Focus, + #[strum(serialize = "editor.caret")] + Caret, + #[strum(serialize = "editor.selection")] + Selection, + #[strum(serialize = "editor.current_line")] + CurrentLine, + #[strum(serialize = "editor.link")] + Link, + #[strum(serialize = "editor.visible_whitespace")] + VisibleWhitespace, + #[strum(serialize = "editor.indent_guide")] + IndentGuide, + #[strum(serialize = "editor.sticky_header_background")] + StickyHeaderBackground, + #[strum(serialize = "editor.preedit.underline")] + PreeditUnderline, +} diff --git a/src/views/editor/command.rs b/src/views/editor/command.rs new file mode 100644 index 00000000..978a47c3 --- /dev/null +++ b/src/views/editor/command.rs @@ -0,0 +1,41 @@ +use floem_editor_core::command::{ + EditCommand, MotionModeCommand, MoveCommand, MultiSelectionCommand, ScrollCommand, +}; +use strum::EnumMessage; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Command { + Edit(EditCommand), + Move(MoveCommand), + Scroll(ScrollCommand), + MotionMode(MotionModeCommand), + MultiSelection(MultiSelectionCommand), +} + +impl Command { + pub fn desc(&self) -> Option<&'static str> { + match &self { + Command::Edit(cmd) => cmd.get_message(), + Command::Move(cmd) => cmd.get_message(), + Command::Scroll(cmd) => cmd.get_message(), + Command::MotionMode(cmd) => cmd.get_message(), + Command::MultiSelection(cmd) => cmd.get_message(), + } + } + + pub fn str(&self) -> &'static str { + match &self { + Command::Edit(cmd) => cmd.into(), + Command::Move(cmd) => cmd.into(), + Command::Scroll(cmd) => cmd.into(), + Command::MotionMode(cmd) => cmd.into(), + Command::MultiSelection(cmd) => cmd.into(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CommandExecuted { + Yes, + No, +} diff --git a/src/views/editor/gutter.rs b/src/views/editor/gutter.rs new file mode 100644 index 00000000..f719a2b5 --- /dev/null +++ b/src/views/editor/gutter.rs @@ -0,0 +1,123 @@ +use crate::{ + context::PaintCx, + cosmic_text::{Attrs, AttrsList, TextLayout}, + id::Id, + peniko::kurbo::Point, + view::{AnyWidget, View, ViewData, Widget}, + Renderer, +}; +use floem_editor_core::mode::Mode; +use floem_reactive::RwSignal; +use kurbo::Rect; + +use super::{color::EditorColor, Editor}; + +pub struct EditorGutterView { + data: ViewData, + editor: RwSignal, + width: f64, +} + +pub fn editor_gutter_view(editor: RwSignal) -> EditorGutterView { + let id = Id::next(); + + EditorGutterView { + data: ViewData::new(id), + editor, + width: 0.0, + } +} + +impl View for EditorGutterView { + fn view_data(&self) -> &ViewData { + &self.data + } + + fn view_data_mut(&mut self) -> &mut ViewData { + &mut self.data + } + + fn build(self) -> AnyWidget { + Box::new(self) + } +} +impl Widget for EditorGutterView { + fn view_data(&self) -> &ViewData { + &self.data + } + + fn view_data_mut(&mut self) -> &mut ViewData { + &mut self.data + } + + fn compute_layout(&mut self, cx: &mut crate::context::ComputeLayoutCx) -> Option { + if let Some(width) = cx.get_layout(self.data.id()).map(|l| l.size.width as f64) { + self.width = width; + } + None + } + + fn paint(&mut self, cx: &mut PaintCx) { + let editor = self.editor.get_untracked(); + + let viewport = editor.viewport.get_untracked(); + let cursor = editor.cursor; + let style = editor.style.get_untracked(); + + let (offset, mode) = cursor.with_untracked(|c| (c.offset(), c.get_mode())); + let last_line = editor.last_line(); + let current_line = editor.line_of_offset(offset); + + // TODO: don't assume font family is constant for each line + let family = style.font_family(0); + let attrs = Attrs::new() + .family(&family) + .color(style.color(EditorColor::Dim)) + .font_size(style.font_size(0) as f32); + let attrs_list = AttrsList::new(attrs); + let current_line_attrs_list = + AttrsList::new(attrs.color(style.color(EditorColor::Foreground))); + let show_relative = editor.modal.get_untracked() + && editor.modal_relative_line_numbers.get_untracked() + && mode != Mode::Insert; + + editor.screen_lines.with_untracked(|screen_lines| { + for (line, y) in screen_lines.iter_lines_y() { + // If it ends up outside the bounds of the file, stop trying to display line numbers + if line > last_line { + break; + } + + let line_height = f64::from(style.line_height(line)); + + let text = if show_relative { + if line == current_line { + line + 1 + } else { + line.abs_diff(current_line) + } + } else { + line + 1 + } + .to_string(); + + let mut text_layout = TextLayout::new(); + if line == current_line { + text_layout.set_text(&text, current_line_attrs_list.clone()); + } else { + text_layout.set_text(&text, attrs_list.clone()); + } + let size = text_layout.size(); + let height = size.height; + + cx.draw_text( + &text_layout, + Point::new( + (self.width - (size.width)).max(0.0), + y + (line_height - height) / 2.0 - viewport.y0, + ), + ); + } + }); + } +} diff --git a/src/views/editor/id.rs b/src/views/editor/id.rs new file mode 100644 index 00000000..66235cd3 --- /dev/null +++ b/src/views/editor/id.rs @@ -0,0 +1,18 @@ +use std::sync::atomic::AtomicU64; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Id(u64); + +impl Id { + /// Allocate a new, unique `Id`. + pub fn next() -> Id { + static TIMER_COUNTER: AtomicU64 = AtomicU64::new(0); + Id(TIMER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) + } + + pub fn to_raw(self) -> u64 { + self.0 + } +} + +pub type EditorId = Id; diff --git a/src/views/editor/keypress/key.rs b/src/views/editor/keypress/key.rs new file mode 100644 index 00000000..b6e94106 --- /dev/null +++ b/src/views/editor/keypress/key.rs @@ -0,0 +1,816 @@ +use std::{ + fmt::Display, + hash::{Hash, Hasher}, + str::FromStr, +}; + +use crate::keyboard::{Key, KeyCode, NativeKey, PhysicalKey}; + +#[derive(Clone, Debug, Eq)] +pub enum KeyInput { + Keyboard(crate::keyboard::Key, crate::keyboard::PhysicalKey), + Pointer(crate::pointer::PointerButton), +} + +impl KeyInput { + fn keyboard_from_str(s: &str) -> Option<(Key, PhysicalKey)> { + // Checks if it is a character key + fn is_key_string(s: &str) -> bool { + s.chars().all(|c| !c.is_control()) && s.chars().skip(1).all(|c| !c.is_ascii()) + } + + fn char_to_keycode(char: &str) -> PhysicalKey { + match char { + "a" => PhysicalKey::Code(KeyCode::KeyA), + "b" => PhysicalKey::Code(KeyCode::KeyB), + "c" => PhysicalKey::Code(KeyCode::KeyC), + "d" => PhysicalKey::Code(KeyCode::KeyD), + "e" => PhysicalKey::Code(KeyCode::KeyE), + "f" => PhysicalKey::Code(KeyCode::KeyF), + "g" => PhysicalKey::Code(KeyCode::KeyG), + "h" => PhysicalKey::Code(KeyCode::KeyH), + "i" => PhysicalKey::Code(KeyCode::KeyI), + "j" => PhysicalKey::Code(KeyCode::KeyJ), + "k" => PhysicalKey::Code(KeyCode::KeyK), + "l" => PhysicalKey::Code(KeyCode::KeyL), + "m" => PhysicalKey::Code(KeyCode::KeyM), + "n" => PhysicalKey::Code(KeyCode::KeyN), + "o" => PhysicalKey::Code(KeyCode::KeyO), + "p" => PhysicalKey::Code(KeyCode::KeyP), + "q" => PhysicalKey::Code(KeyCode::KeyQ), + "r" => PhysicalKey::Code(KeyCode::KeyR), + "s" => PhysicalKey::Code(KeyCode::KeyS), + "t" => PhysicalKey::Code(KeyCode::KeyT), + "u" => PhysicalKey::Code(KeyCode::KeyU), + "v" => PhysicalKey::Code(KeyCode::KeyV), + "w" => PhysicalKey::Code(KeyCode::KeyW), + "x" => PhysicalKey::Code(KeyCode::KeyX), + "y" => PhysicalKey::Code(KeyCode::KeyY), + "z" => PhysicalKey::Code(KeyCode::KeyZ), + "=" => PhysicalKey::Code(KeyCode::Equal), + "-" => PhysicalKey::Code(KeyCode::Minus), + "0" => PhysicalKey::Code(KeyCode::Digit0), + "1" => PhysicalKey::Code(KeyCode::Digit1), + "2" => PhysicalKey::Code(KeyCode::Digit2), + "3" => PhysicalKey::Code(KeyCode::Digit3), + "4" => PhysicalKey::Code(KeyCode::Digit4), + "5" => PhysicalKey::Code(KeyCode::Digit5), + "6" => PhysicalKey::Code(KeyCode::Digit6), + "7" => PhysicalKey::Code(KeyCode::Digit7), + "8" => PhysicalKey::Code(KeyCode::Digit8), + "9" => PhysicalKey::Code(KeyCode::Digit9), + "`" => PhysicalKey::Code(KeyCode::Backquote), + "/" => PhysicalKey::Code(KeyCode::Slash), + "\\" => PhysicalKey::Code(KeyCode::Backslash), + "," => PhysicalKey::Code(KeyCode::Comma), + "." => PhysicalKey::Code(KeyCode::Period), + "*" => PhysicalKey::Code(KeyCode::NumpadMultiply), + "+" => PhysicalKey::Code(KeyCode::NumpadAdd), + ";" => PhysicalKey::Code(KeyCode::Semicolon), + "'" => PhysicalKey::Code(KeyCode::Quote), + "[" => PhysicalKey::Code(KeyCode::BracketLeft), + "]" => PhysicalKey::Code(KeyCode::BracketRight), + "<" => PhysicalKey::Code(KeyCode::IntlBackslash), + _ => PhysicalKey::Code(KeyCode::Fn), + } + } + + // Import into scope to reduce noise + use crate::keyboard::NamedKey::*; + Some(match s { + s if is_key_string(s) => { + let char = Key::Character(s.into()); + (char.clone(), char_to_keycode(char.to_text().unwrap())) + } + "unidentified" => ( + Key::Unidentified(NativeKey::Unidentified), + PhysicalKey::Code(KeyCode::Fn), + ), + "alt" => (Key::Named(Alt), PhysicalKey::Code(KeyCode::AltLeft)), + "altgraph" => (Key::Named(AltGraph), PhysicalKey::Code(KeyCode::AltRight)), + "capslock" => (Key::Named(CapsLock), PhysicalKey::Code(KeyCode::CapsLock)), + "control" => (Key::Named(Control), PhysicalKey::Code(KeyCode::ControlLeft)), + "fn" => (Key::Named(Fn), PhysicalKey::Code(KeyCode::Fn)), + "fnlock" => (Key::Named(FnLock), PhysicalKey::Code(KeyCode::FnLock)), + "meta" => (Key::Named(Meta), PhysicalKey::Code(KeyCode::Meta)), + "numlock" => (Key::Named(NumLock), PhysicalKey::Code(KeyCode::NumLock)), + "scrolllock" => ( + Key::Named(ScrollLock), + PhysicalKey::Code(KeyCode::ScrollLock), + ), + "shift" => (Key::Named(Shift), PhysicalKey::Code(KeyCode::ShiftLeft)), + "symbol" => (Key::Named(Symbol), PhysicalKey::Code(KeyCode::Fn)), + "symbollock" => (Key::Named(SymbolLock), PhysicalKey::Code(KeyCode::Fn)), + "hyper" => (Key::Named(Hyper), PhysicalKey::Code(KeyCode::Hyper)), + "super" => (Key::Named(Super), PhysicalKey::Code(KeyCode::Meta)), + "enter" => (Key::Named(Enter), PhysicalKey::Code(KeyCode::Enter)), + "tab" => (Key::Named(Tab), PhysicalKey::Code(KeyCode::Tab)), + "arrowdown" => (Key::Named(ArrowDown), PhysicalKey::Code(KeyCode::ArrowDown)), + "arrowleft" => (Key::Named(ArrowLeft), PhysicalKey::Code(KeyCode::ArrowLeft)), + "arrowright" => ( + Key::Named(ArrowRight), + PhysicalKey::Code(KeyCode::ArrowRight), + ), + "arrowup" => (Key::Named(ArrowUp), PhysicalKey::Code(KeyCode::ArrowUp)), + "end" => (Key::Named(End), PhysicalKey::Code(KeyCode::End)), + "home" => (Key::Named(Home), PhysicalKey::Code(KeyCode::Home)), + "pagedown" => (Key::Named(PageDown), PhysicalKey::Code(KeyCode::PageDown)), + "pageup" => (Key::Named(PageUp), PhysicalKey::Code(KeyCode::PageUp)), + "backspace" => (Key::Named(Backspace), PhysicalKey::Code(KeyCode::Backspace)), + "clear" => (Key::Named(Clear), PhysicalKey::Code(KeyCode::Fn)), + "copy" => (Key::Named(Copy), PhysicalKey::Code(KeyCode::Copy)), + "crsel" => (Key::Named(CrSel), PhysicalKey::Code(KeyCode::Fn)), + "cut" => (Key::Named(Cut), PhysicalKey::Code(KeyCode::Cut)), + "delete" => (Key::Named(Delete), PhysicalKey::Code(KeyCode::Delete)), + "eraseeof" => (Key::Named(EraseEof), PhysicalKey::Code(KeyCode::Fn)), + "exsel" => (Key::Named(ExSel), PhysicalKey::Code(KeyCode::Fn)), + "insert" => (Key::Named(Insert), PhysicalKey::Code(KeyCode::Insert)), + "paste" => (Key::Named(Paste), PhysicalKey::Code(KeyCode::Paste)), + "redo" => (Key::Named(Redo), PhysicalKey::Code(KeyCode::Fn)), + "undo" => (Key::Named(Undo), PhysicalKey::Code(KeyCode::Undo)), + "accept" => (Key::Named(Accept), PhysicalKey::Code(KeyCode::Fn)), + "again" => (Key::Named(Again), PhysicalKey::Code(KeyCode::Again)), + "attn" => (Key::Named(Attn), PhysicalKey::Code(KeyCode::Fn)), + "cancel" => (Key::Named(Cancel), PhysicalKey::Code(KeyCode::Fn)), + "contextmenu" => ( + Key::Named(ContextMenu), + PhysicalKey::Code(KeyCode::ContextMenu), + ), + "escape" => (Key::Named(Escape), PhysicalKey::Code(KeyCode::Escape)), + "execute" => (Key::Named(Execute), PhysicalKey::Code(KeyCode::Fn)), + "find" => (Key::Named(Find), PhysicalKey::Code(KeyCode::Find)), + "help" => (Key::Named(Help), PhysicalKey::Code(KeyCode::Help)), + "pause" => (Key::Named(Pause), PhysicalKey::Code(KeyCode::Pause)), + "play" => (Key::Named(Play), PhysicalKey::Code(KeyCode::MediaPlayPause)), + "props" => (Key::Named(Props), PhysicalKey::Code(KeyCode::Props)), + "select" => (Key::Named(Select), PhysicalKey::Code(KeyCode::Select)), + "zoomin" => (Key::Named(ZoomIn), PhysicalKey::Code(KeyCode::Fn)), + "zoomout" => (Key::Named(ZoomOut), PhysicalKey::Code(KeyCode::Fn)), + "brightnessdown" => (Key::Named(BrightnessDown), PhysicalKey::Code(KeyCode::Fn)), + "brightnessup" => (Key::Named(BrightnessUp), PhysicalKey::Code(KeyCode::Fn)), + "eject" => (Key::Named(Eject), PhysicalKey::Code(KeyCode::Eject)), + "logoff" => (Key::Named(LogOff), PhysicalKey::Code(KeyCode::Fn)), + "power" => (Key::Named(Power), PhysicalKey::Code(KeyCode::Power)), + "poweroff" => (Key::Named(PowerOff), PhysicalKey::Code(KeyCode::Fn)), + "printscreen" => ( + Key::Named(PrintScreen), + PhysicalKey::Code(KeyCode::PrintScreen), + ), + "hibernate" => (Key::Named(Hibernate), PhysicalKey::Code(KeyCode::Fn)), + "standby" => (Key::Named(Standby), PhysicalKey::Code(KeyCode::Fn)), + "wakeup" => (Key::Named(WakeUp), PhysicalKey::Code(KeyCode::WakeUp)), + "allcandidates" => (Key::Named(AllCandidates), PhysicalKey::Code(KeyCode::Fn)), + "alphanumeric" => (Key::Named(Alphanumeric), PhysicalKey::Code(KeyCode::Fn)), + "codeinput" => (Key::Named(CodeInput), PhysicalKey::Code(KeyCode::Fn)), + "compose" => (Key::Named(Compose), PhysicalKey::Code(KeyCode::Fn)), + "convert" => (Key::Named(Convert), PhysicalKey::Code(KeyCode::Convert)), + "dead" => (Key::Dead(None), PhysicalKey::Code(KeyCode::Fn)), + "finalmode" => (Key::Named(FinalMode), PhysicalKey::Code(KeyCode::Fn)), + "groupfirst" => (Key::Named(GroupFirst), PhysicalKey::Code(KeyCode::Fn)), + "grouplast" => (Key::Named(GroupLast), PhysicalKey::Code(KeyCode::Fn)), + "groupnext" => (Key::Named(GroupNext), PhysicalKey::Code(KeyCode::Fn)), + "groupprevious" => (Key::Named(GroupPrevious), PhysicalKey::Code(KeyCode::Fn)), + "modechange" => (Key::Named(ModeChange), PhysicalKey::Code(KeyCode::Fn)), + "nextcandidate" => (Key::Named(NextCandidate), PhysicalKey::Code(KeyCode::Fn)), + "nonconvert" => ( + Key::Named(NonConvert), + PhysicalKey::Code(KeyCode::NonConvert), + ), + "previouscandidate" => ( + Key::Named(PreviousCandidate), + PhysicalKey::Code(KeyCode::Fn), + ), + "process" => (Key::Named(Process), PhysicalKey::Code(KeyCode::Fn)), + "singlecandidate" => (Key::Named(SingleCandidate), PhysicalKey::Code(KeyCode::Fn)), + "hangulmode" => (Key::Named(HangulMode), PhysicalKey::Code(KeyCode::Fn)), + "hanjamode" => (Key::Named(HanjaMode), PhysicalKey::Code(KeyCode::Fn)), + "junjamode" => (Key::Named(JunjaMode), PhysicalKey::Code(KeyCode::Fn)), + "eisu" => (Key::Named(Eisu), PhysicalKey::Code(KeyCode::Fn)), + "hankaku" => (Key::Named(Hankaku), PhysicalKey::Code(KeyCode::Fn)), + "hiragana" => (Key::Named(Hiragana), PhysicalKey::Code(KeyCode::Hiragana)), + "hiraganakatakana" => (Key::Named(HiraganaKatakana), PhysicalKey::Code(KeyCode::Fn)), + "kanamode" => (Key::Named(KanaMode), PhysicalKey::Code(KeyCode::KanaMode)), + "kanjimode" => (Key::Named(KanjiMode), PhysicalKey::Code(KeyCode::Fn)), + "katakana" => (Key::Named(Katakana), PhysicalKey::Code(KeyCode::Katakana)), + "romaji" => (Key::Named(Romaji), PhysicalKey::Code(KeyCode::Fn)), + "zenkaku" => (Key::Named(Zenkaku), PhysicalKey::Code(KeyCode::Fn)), + "zenkakuhankaku" => (Key::Named(ZenkakuHankaku), PhysicalKey::Code(KeyCode::Fn)), + "f1" => (Key::Named(F1), PhysicalKey::Code(KeyCode::F1)), + "f2" => (Key::Named(F2), PhysicalKey::Code(KeyCode::F2)), + "f3" => (Key::Named(F3), PhysicalKey::Code(KeyCode::F3)), + "f4" => (Key::Named(F4), PhysicalKey::Code(KeyCode::F4)), + "f5" => (Key::Named(F5), PhysicalKey::Code(KeyCode::F5)), + "f6" => (Key::Named(F6), PhysicalKey::Code(KeyCode::F6)), + "f7" => (Key::Named(F7), PhysicalKey::Code(KeyCode::F7)), + "f8" => (Key::Named(F8), PhysicalKey::Code(KeyCode::F8)), + "f9" => (Key::Named(F9), PhysicalKey::Code(KeyCode::F9)), + "f10" => (Key::Named(F10), PhysicalKey::Code(KeyCode::F10)), + "f11" => (Key::Named(F11), PhysicalKey::Code(KeyCode::F11)), + "f12" => (Key::Named(F12), PhysicalKey::Code(KeyCode::F12)), + "soft1" => (Key::Named(Soft1), PhysicalKey::Code(KeyCode::Fn)), + "soft2" => (Key::Named(Soft2), PhysicalKey::Code(KeyCode::Fn)), + "soft3" => (Key::Named(Soft3), PhysicalKey::Code(KeyCode::Fn)), + "soft4" => (Key::Named(Soft4), PhysicalKey::Code(KeyCode::Fn)), + "channeldown" => (Key::Named(ChannelDown), PhysicalKey::Code(KeyCode::Fn)), + "channelup" => (Key::Named(ChannelUp), PhysicalKey::Code(KeyCode::Fn)), + "close" => (Key::Named(Close), PhysicalKey::Code(KeyCode::Fn)), + "mailforward" => (Key::Named(MailForward), PhysicalKey::Code(KeyCode::Fn)), + "mailreply" => (Key::Named(MailReply), PhysicalKey::Code(KeyCode::Fn)), + "mailsend" => (Key::Named(MailSend), PhysicalKey::Code(KeyCode::Fn)), + "mediaclose" => (Key::Named(MediaClose), PhysicalKey::Code(KeyCode::Fn)), + "mediafastforward" => (Key::Named(MediaFastForward), PhysicalKey::Code(KeyCode::Fn)), + "mediapause" => (Key::Named(MediaPause), PhysicalKey::Code(KeyCode::Fn)), + "mediaplay" => (Key::Named(MediaPlay), PhysicalKey::Code(KeyCode::Fn)), + "mediaplaypause" => ( + Key::Named(MediaPlayPause), + PhysicalKey::Code(KeyCode::MediaPlayPause), + ), + "mediarecord" => (Key::Named(MediaRecord), PhysicalKey::Code(KeyCode::Fn)), + "mediarewind" => (Key::Named(MediaRewind), PhysicalKey::Code(KeyCode::Fn)), + "mediastop" => (Key::Named(MediaStop), PhysicalKey::Code(KeyCode::MediaStop)), + "mediatracknext" => ( + Key::Named(MediaTrackNext), + PhysicalKey::Code(KeyCode::MediaTrackNext), + ), + "mediatrackprevious" => ( + Key::Named(MediaTrackPrevious), + PhysicalKey::Code(KeyCode::MediaTrackPrevious), + ), + "new" => (Key::Named(New), PhysicalKey::Code(KeyCode::Fn)), + "open" => (Key::Named(Open), PhysicalKey::Code(KeyCode::Open)), + "print" => (Key::Named(Print), PhysicalKey::Code(KeyCode::Fn)), + "save" => (Key::Named(Save), PhysicalKey::Code(KeyCode::Fn)), + "spellcheck" => (Key::Named(SpellCheck), PhysicalKey::Code(KeyCode::Fn)), + "key11" => (Key::Named(Key11), PhysicalKey::Code(KeyCode::Fn)), + "key12" => (Key::Named(Key12), PhysicalKey::Code(KeyCode::Fn)), + "audiobalanceleft" => (Key::Named(AudioBalanceLeft), PhysicalKey::Code(KeyCode::Fn)), + "audiobalanceright" => ( + Key::Named(AudioBalanceRight), + PhysicalKey::Code(KeyCode::Fn), + ), + "audiobassboostdown" => ( + Key::Named(AudioBassBoostDown), + PhysicalKey::Code(KeyCode::Fn), + ), + "audiobassboosttoggle" => ( + Key::Named(AudioBassBoostToggle), + PhysicalKey::Code(KeyCode::Fn), + ), + "audiobassboostup" => (Key::Named(AudioBassBoostUp), PhysicalKey::Code(KeyCode::Fn)), + "audiofaderfront" => (Key::Named(AudioFaderFront), PhysicalKey::Code(KeyCode::Fn)), + "audiofaderrear" => (Key::Named(AudioFaderRear), PhysicalKey::Code(KeyCode::Fn)), + "audiosurroundmodenext" => ( + Key::Named(AudioSurroundModeNext), + PhysicalKey::Code(KeyCode::Fn), + ), + "audiotrebledown" => (Key::Named(AudioTrebleDown), PhysicalKey::Code(KeyCode::Fn)), + "audiotrebleup" => (Key::Named(AudioTrebleUp), PhysicalKey::Code(KeyCode::Fn)), + "audiovolumedown" => ( + Key::Named(AudioVolumeDown), + PhysicalKey::Code(KeyCode::AudioVolumeDown), + ), + "audiovolumeup" => ( + Key::Named(AudioVolumeUp), + PhysicalKey::Code(KeyCode::AudioVolumeUp), + ), + "audiovolumemute" => ( + Key::Named(AudioVolumeMute), + PhysicalKey::Code(KeyCode::AudioVolumeMute), + ), + "microphonetoggle" => (Key::Named(MicrophoneToggle), PhysicalKey::Code(KeyCode::Fn)), + "microphonevolumedown" => ( + Key::Named(MicrophoneVolumeDown), + PhysicalKey::Code(KeyCode::Fn), + ), + "microphonevolumeup" => ( + Key::Named(MicrophoneVolumeUp), + PhysicalKey::Code(KeyCode::Fn), + ), + "microphonevolumemute" => ( + Key::Named(MicrophoneVolumeMute), + PhysicalKey::Code(KeyCode::Fn), + ), + "speechcorrectionlist" => ( + Key::Named(SpeechCorrectionList), + PhysicalKey::Code(KeyCode::Fn), + ), + "speechinputtoggle" => ( + Key::Named(SpeechInputToggle), + PhysicalKey::Code(KeyCode::Fn), + ), + "launchapplication1" => ( + Key::Named(LaunchApplication1), + PhysicalKey::Code(KeyCode::Fn), + ), + "launchapplication2" => ( + Key::Named(LaunchApplication2), + PhysicalKey::Code(KeyCode::Fn), + ), + "launchcalendar" => (Key::Named(LaunchCalendar), PhysicalKey::Code(KeyCode::Fn)), + "launchcontacts" => (Key::Named(LaunchContacts), PhysicalKey::Code(KeyCode::Fn)), + "launchmail" => ( + Key::Named(LaunchMail), + PhysicalKey::Code(KeyCode::LaunchMail), + ), + "launchmediaplayer" => ( + Key::Named(LaunchMediaPlayer), + PhysicalKey::Code(KeyCode::Fn), + ), + "launchmusicplayer" => ( + Key::Named(LaunchMusicPlayer), + PhysicalKey::Code(KeyCode::Fn), + ), + "launchphone" => (Key::Named(LaunchPhone), PhysicalKey::Code(KeyCode::Fn)), + "launchscreensaver" => ( + Key::Named(LaunchScreenSaver), + PhysicalKey::Code(KeyCode::Fn), + ), + "launchspreadsheet" => ( + Key::Named(LaunchSpreadsheet), + PhysicalKey::Code(KeyCode::Fn), + ), + "launchwebbrowser" => (Key::Named(LaunchWebBrowser), PhysicalKey::Code(KeyCode::Fn)), + "launchwebcam" => (Key::Named(LaunchWebCam), PhysicalKey::Code(KeyCode::Fn)), + "launchwordprocessor" => ( + Key::Named(LaunchWordProcessor), + PhysicalKey::Code(KeyCode::Fn), + ), + "browserback" => ( + Key::Named(BrowserBack), + PhysicalKey::Code(KeyCode::BrowserBack), + ), + "browserfavorites" => ( + Key::Named(BrowserFavorites), + PhysicalKey::Code(KeyCode::BrowserFavorites), + ), + "browserforward" => ( + Key::Named(BrowserForward), + PhysicalKey::Code(KeyCode::BrowserForward), + ), + "browserhome" => ( + Key::Named(BrowserHome), + PhysicalKey::Code(KeyCode::BrowserHome), + ), + "browserrefresh" => ( + Key::Named(BrowserRefresh), + PhysicalKey::Code(KeyCode::BrowserRefresh), + ), + "browsersearch" => ( + Key::Named(BrowserSearch), + PhysicalKey::Code(KeyCode::BrowserSearch), + ), + "browserstop" => ( + Key::Named(BrowserStop), + PhysicalKey::Code(KeyCode::BrowserStop), + ), + "appswitch" => (Key::Named(AppSwitch), PhysicalKey::Code(KeyCode::Fn)), + "call" => (Key::Named(Call), PhysicalKey::Code(KeyCode::Fn)), + "camera" => (Key::Named(Camera), PhysicalKey::Code(KeyCode::Fn)), + "camerafocus" => (Key::Named(CameraFocus), PhysicalKey::Code(KeyCode::Fn)), + "endcall" => (Key::Named(EndCall), PhysicalKey::Code(KeyCode::Fn)), + "goback" => (Key::Named(GoBack), PhysicalKey::Code(KeyCode::Fn)), + "gohome" => (Key::Named(GoHome), PhysicalKey::Code(KeyCode::Fn)), + "headsethook" => (Key::Named(HeadsetHook), PhysicalKey::Code(KeyCode::Fn)), + "lastnumberredial" => (Key::Named(LastNumberRedial), PhysicalKey::Code(KeyCode::Fn)), + "notification" => (Key::Named(Notification), PhysicalKey::Code(KeyCode::Fn)), + "mannermode" => (Key::Named(MannerMode), PhysicalKey::Code(KeyCode::Fn)), + "voicedial" => (Key::Named(VoiceDial), PhysicalKey::Code(KeyCode::Fn)), + "tv" => (Key::Named(TV), PhysicalKey::Code(KeyCode::Fn)), + "tv3dmode" => (Key::Named(TV3DMode), PhysicalKey::Code(KeyCode::Fn)), + "tvantennacable" => (Key::Named(TVAntennaCable), PhysicalKey::Code(KeyCode::Fn)), + "tvaudiodescription" => ( + Key::Named(TVAudioDescription), + PhysicalKey::Code(KeyCode::Fn), + ), + "tvaudiodescriptionmixdown" => ( + Key::Named(TVAudioDescriptionMixDown), + PhysicalKey::Code(KeyCode::Fn), + ), + "tvaudiodescriptionmixup" => ( + Key::Named(TVAudioDescriptionMixUp), + PhysicalKey::Code(KeyCode::Fn), + ), + "tvcontentsmenu" => (Key::Named(TVContentsMenu), PhysicalKey::Code(KeyCode::Fn)), + "tvdataservice" => (Key::Named(TVDataService), PhysicalKey::Code(KeyCode::Fn)), + "tvinput" => (Key::Named(TVInput), PhysicalKey::Code(KeyCode::Fn)), + "tvinputcomponent1" => ( + Key::Named(TVInputComponent1), + PhysicalKey::Code(KeyCode::Fn), + ), + "tvinputcomponent2" => ( + Key::Named(TVInputComponent2), + PhysicalKey::Code(KeyCode::Fn), + ), + "tvinputcomposite1" => ( + Key::Named(TVInputComposite1), + PhysicalKey::Code(KeyCode::Fn), + ), + "tvinputcomposite2" => ( + Key::Named(TVInputComposite2), + PhysicalKey::Code(KeyCode::Fn), + ), + "tvinputhdmi1" => (Key::Named(TVInputHDMI1), PhysicalKey::Code(KeyCode::Fn)), + "tvinputhdmi2" => (Key::Named(TVInputHDMI2), PhysicalKey::Code(KeyCode::Fn)), + "tvinputhdmi3" => (Key::Named(TVInputHDMI3), PhysicalKey::Code(KeyCode::Fn)), + "tvinputhdmi4" => (Key::Named(TVInputHDMI4), PhysicalKey::Code(KeyCode::Fn)), + "tvinputvga1" => (Key::Named(TVInputVGA1), PhysicalKey::Code(KeyCode::Fn)), + "tvmediacontext" => (Key::Named(TVMediaContext), PhysicalKey::Code(KeyCode::Fn)), + "tvnetwork" => (Key::Named(TVNetwork), PhysicalKey::Code(KeyCode::Fn)), + "tvnumberentry" => (Key::Named(TVNumberEntry), PhysicalKey::Code(KeyCode::Fn)), + "tvpower" => (Key::Named(TVPower), PhysicalKey::Code(KeyCode::Fn)), + "tvradioservice" => (Key::Named(TVRadioService), PhysicalKey::Code(KeyCode::Fn)), + "tvsatellite" => (Key::Named(TVSatellite), PhysicalKey::Code(KeyCode::Fn)), + "tvsatellitebs" => (Key::Named(TVSatelliteBS), PhysicalKey::Code(KeyCode::Fn)), + "tvsatellitecs" => (Key::Named(TVSatelliteCS), PhysicalKey::Code(KeyCode::Fn)), + "tvsatellitetoggle" => ( + Key::Named(TVSatelliteToggle), + PhysicalKey::Code(KeyCode::Fn), + ), + "tvterrestrialanalog" => ( + Key::Named(TVTerrestrialAnalog), + PhysicalKey::Code(KeyCode::Fn), + ), + "tvterrestrialdigital" => ( + Key::Named(TVTerrestrialDigital), + PhysicalKey::Code(KeyCode::Fn), + ), + "tvtimer" => (Key::Named(TVTimer), PhysicalKey::Code(KeyCode::Fn)), + "avrinput" => (Key::Named(AVRInput), PhysicalKey::Code(KeyCode::Fn)), + "avrpower" => (Key::Named(AVRPower), PhysicalKey::Code(KeyCode::Fn)), + "colorf0red" => (Key::Named(ColorF0Red), PhysicalKey::Code(KeyCode::Fn)), + "colorf1green" => (Key::Named(ColorF1Green), PhysicalKey::Code(KeyCode::Fn)), + "colorf2yellow" => (Key::Named(ColorF2Yellow), PhysicalKey::Code(KeyCode::Fn)), + "colorf3blue" => (Key::Named(ColorF3Blue), PhysicalKey::Code(KeyCode::Fn)), + "colorf4grey" => (Key::Named(ColorF4Grey), PhysicalKey::Code(KeyCode::Fn)), + "colorf5brown" => (Key::Named(ColorF5Brown), PhysicalKey::Code(KeyCode::Fn)), + "closedcaptiontoggle" => ( + Key::Named(ClosedCaptionToggle), + PhysicalKey::Code(KeyCode::Fn), + ), + "dimmer" => (Key::Named(Dimmer), PhysicalKey::Code(KeyCode::Fn)), + "displayswap" => (Key::Named(DisplaySwap), PhysicalKey::Code(KeyCode::Fn)), + "dvr" => (Key::Named(DVR), PhysicalKey::Code(KeyCode::Fn)), + "exit" => (Key::Named(Exit), PhysicalKey::Code(KeyCode::Fn)), + "favoriteclear0" => (Key::Named(FavoriteClear0), PhysicalKey::Code(KeyCode::Fn)), + "favoriteclear1" => (Key::Named(FavoriteClear1), PhysicalKey::Code(KeyCode::Fn)), + "favoriteclear2" => (Key::Named(FavoriteClear2), PhysicalKey::Code(KeyCode::Fn)), + "favoriteclear3" => (Key::Named(FavoriteClear3), PhysicalKey::Code(KeyCode::Fn)), + "favoriterecall0" => (Key::Named(FavoriteRecall0), PhysicalKey::Code(KeyCode::Fn)), + "favoriterecall1" => (Key::Named(FavoriteRecall1), PhysicalKey::Code(KeyCode::Fn)), + "favoriterecall2" => (Key::Named(FavoriteRecall2), PhysicalKey::Code(KeyCode::Fn)), + "favoriterecall3" => (Key::Named(FavoriteRecall3), PhysicalKey::Code(KeyCode::Fn)), + "favoritestore0" => (Key::Named(FavoriteStore0), PhysicalKey::Code(KeyCode::Fn)), + "favoritestore1" => (Key::Named(FavoriteStore1), PhysicalKey::Code(KeyCode::Fn)), + "favoritestore2" => (Key::Named(FavoriteStore2), PhysicalKey::Code(KeyCode::Fn)), + "favoritestore3" => (Key::Named(FavoriteStore3), PhysicalKey::Code(KeyCode::Fn)), + "guide" => (Key::Named(Guide), PhysicalKey::Code(KeyCode::Fn)), + "guidenextday" => (Key::Named(GuideNextDay), PhysicalKey::Code(KeyCode::Fn)), + "guidepreviousday" => (Key::Named(GuidePreviousDay), PhysicalKey::Code(KeyCode::Fn)), + "info" => (Key::Named(Info), PhysicalKey::Code(KeyCode::Fn)), + "instantreplay" => (Key::Named(InstantReplay), PhysicalKey::Code(KeyCode::Fn)), + "link" => (Key::Named(Link), PhysicalKey::Code(KeyCode::Fn)), + "listprogram" => (Key::Named(ListProgram), PhysicalKey::Code(KeyCode::Fn)), + "livecontent" => (Key::Named(LiveContent), PhysicalKey::Code(KeyCode::Fn)), + "lock" => (Key::Named(Lock), PhysicalKey::Code(KeyCode::Fn)), + "mediaapps" => (Key::Named(MediaApps), PhysicalKey::Code(KeyCode::Fn)), + "mediaaudiotrack" => (Key::Named(MediaAudioTrack), PhysicalKey::Code(KeyCode::Fn)), + "medialast" => (Key::Named(MediaLast), PhysicalKey::Code(KeyCode::Fn)), + "mediaskipbackward" => ( + Key::Named(MediaSkipBackward), + PhysicalKey::Code(KeyCode::Fn), + ), + "mediaskipforward" => (Key::Named(MediaSkipForward), PhysicalKey::Code(KeyCode::Fn)), + "mediastepbackward" => ( + Key::Named(MediaStepBackward), + PhysicalKey::Code(KeyCode::Fn), + ), + "mediastepforward" => (Key::Named(MediaStepForward), PhysicalKey::Code(KeyCode::Fn)), + "mediatopmenu" => (Key::Named(MediaTopMenu), PhysicalKey::Code(KeyCode::Fn)), + "navigatein" => (Key::Named(NavigateIn), PhysicalKey::Code(KeyCode::Fn)), + "navigatenext" => (Key::Named(NavigateNext), PhysicalKey::Code(KeyCode::Fn)), + "navigateout" => (Key::Named(NavigateOut), PhysicalKey::Code(KeyCode::Fn)), + "navigateprevious" => (Key::Named(NavigatePrevious), PhysicalKey::Code(KeyCode::Fn)), + "nextfavoritechannel" => ( + Key::Named(NextFavoriteChannel), + PhysicalKey::Code(KeyCode::Fn), + ), + "nextuserprofile" => (Key::Named(NextUserProfile), PhysicalKey::Code(KeyCode::Fn)), + "ondemand" => (Key::Named(OnDemand), PhysicalKey::Code(KeyCode::Fn)), + "pairing" => (Key::Named(Pairing), PhysicalKey::Code(KeyCode::Fn)), + "pinpdown" => (Key::Named(PinPDown), PhysicalKey::Code(KeyCode::Fn)), + "pinpmove" => (Key::Named(PinPMove), PhysicalKey::Code(KeyCode::Fn)), + "pinptoggle" => (Key::Named(PinPToggle), PhysicalKey::Code(KeyCode::Fn)), + "pinpup" => (Key::Named(PinPUp), PhysicalKey::Code(KeyCode::Fn)), + "playspeeddown" => (Key::Named(PlaySpeedDown), PhysicalKey::Code(KeyCode::Fn)), + "playspeedreset" => (Key::Named(PlaySpeedReset), PhysicalKey::Code(KeyCode::Fn)), + "playspeedup" => (Key::Named(PlaySpeedUp), PhysicalKey::Code(KeyCode::Fn)), + "randomtoggle" => (Key::Named(RandomToggle), PhysicalKey::Code(KeyCode::Fn)), + "rclowbattery" => (Key::Named(RcLowBattery), PhysicalKey::Code(KeyCode::Fn)), + "recordspeednext" => (Key::Named(RecordSpeedNext), PhysicalKey::Code(KeyCode::Fn)), + "rfbypass" => (Key::Named(RfBypass), PhysicalKey::Code(KeyCode::Fn)), + "scanchannelstoggle" => ( + Key::Named(ScanChannelsToggle), + PhysicalKey::Code(KeyCode::Fn), + ), + "screenmodenext" => (Key::Named(ScreenModeNext), PhysicalKey::Code(KeyCode::Fn)), + "settings" => (Key::Named(Settings), PhysicalKey::Code(KeyCode::Fn)), + "splitscreentoggle" => ( + Key::Named(SplitScreenToggle), + PhysicalKey::Code(KeyCode::Fn), + ), + "stbinput" => (Key::Named(STBInput), PhysicalKey::Code(KeyCode::Fn)), + "stbpower" => (Key::Named(STBPower), PhysicalKey::Code(KeyCode::Fn)), + "subtitle" => (Key::Named(Subtitle), PhysicalKey::Code(KeyCode::Fn)), + "teletext" => (Key::Named(Teletext), PhysicalKey::Code(KeyCode::Fn)), + "videomodenext" => (Key::Named(VideoModeNext), PhysicalKey::Code(KeyCode::Fn)), + "wink" => (Key::Named(Wink), PhysicalKey::Code(KeyCode::Fn)), + "zoomtoggle" => (Key::Named(ZoomToggle), PhysicalKey::Code(KeyCode::Fn)), + + // Custom key name mappings + "esc" => (Key::Named(Escape), PhysicalKey::Code(KeyCode::Escape)), + "space" => (Key::Named(Space), PhysicalKey::Code(KeyCode::Space)), + "bs" => (Key::Named(Backspace), PhysicalKey::Code(KeyCode::Backspace)), + "up" => (Key::Named(ArrowUp), PhysicalKey::Code(KeyCode::ArrowUp)), + "down" => (Key::Named(ArrowDown), PhysicalKey::Code(KeyCode::ArrowDown)), + "right" => ( + Key::Named(ArrowRight), + PhysicalKey::Code(KeyCode::ArrowRight), + ), + "left" => (Key::Named(ArrowLeft), PhysicalKey::Code(KeyCode::ArrowLeft)), + "del" => (Key::Named(Delete), PhysicalKey::Code(KeyCode::Delete)), + + _ => return None, + }) + } + + fn mouse_from_str(s: &str) -> Option { + use crate::pointer::PointerButton as B; + + Some(match s { + "mousemiddle" => B::Auxiliary, + "mouseforward" => B::X2, + "mousebackward" => B::X1, + _ => return None, + }) + } +} + +impl Display for KeyInput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use crate::pointer::PointerButton as B; + + match self { + Self::Keyboard(_key, key_code) => match key_code { + PhysicalKey::Unidentified(_) => f.write_str("Unidentified"), + PhysicalKey::Code(KeyCode::Backquote) => f.write_str("`"), + PhysicalKey::Code(KeyCode::Backslash) => f.write_str("\\"), + PhysicalKey::Code(KeyCode::BracketLeft) => f.write_str("["), + PhysicalKey::Code(KeyCode::BracketRight) => f.write_str("]"), + PhysicalKey::Code(KeyCode::Comma) => f.write_str(","), + PhysicalKey::Code(KeyCode::Digit0) => f.write_str("0"), + PhysicalKey::Code(KeyCode::Digit1) => f.write_str("1"), + PhysicalKey::Code(KeyCode::Digit2) => f.write_str("2"), + PhysicalKey::Code(KeyCode::Digit3) => f.write_str("3"), + PhysicalKey::Code(KeyCode::Digit4) => f.write_str("4"), + PhysicalKey::Code(KeyCode::Digit5) => f.write_str("5"), + PhysicalKey::Code(KeyCode::Digit6) => f.write_str("6"), + PhysicalKey::Code(KeyCode::Digit7) => f.write_str("7"), + PhysicalKey::Code(KeyCode::Digit8) => f.write_str("8"), + PhysicalKey::Code(KeyCode::Digit9) => f.write_str("9"), + PhysicalKey::Code(KeyCode::Equal) => f.write_str("="), + PhysicalKey::Code(KeyCode::IntlBackslash) => f.write_str("<"), + PhysicalKey::Code(KeyCode::IntlRo) => f.write_str("IntlRo"), + PhysicalKey::Code(KeyCode::IntlYen) => f.write_str("IntlYen"), + PhysicalKey::Code(KeyCode::KeyA) => f.write_str("A"), + PhysicalKey::Code(KeyCode::KeyB) => f.write_str("B"), + PhysicalKey::Code(KeyCode::KeyC) => f.write_str("C"), + PhysicalKey::Code(KeyCode::KeyD) => f.write_str("D"), + PhysicalKey::Code(KeyCode::KeyE) => f.write_str("E"), + PhysicalKey::Code(KeyCode::KeyF) => f.write_str("F"), + PhysicalKey::Code(KeyCode::KeyG) => f.write_str("G"), + PhysicalKey::Code(KeyCode::KeyH) => f.write_str("H"), + PhysicalKey::Code(KeyCode::KeyI) => f.write_str("I"), + PhysicalKey::Code(KeyCode::KeyJ) => f.write_str("J"), + PhysicalKey::Code(KeyCode::KeyK) => f.write_str("K"), + PhysicalKey::Code(KeyCode::KeyL) => f.write_str("L"), + PhysicalKey::Code(KeyCode::KeyM) => f.write_str("M"), + PhysicalKey::Code(KeyCode::KeyN) => f.write_str("N"), + PhysicalKey::Code(KeyCode::KeyO) => f.write_str("O"), + PhysicalKey::Code(KeyCode::KeyP) => f.write_str("P"), + PhysicalKey::Code(KeyCode::KeyQ) => f.write_str("Q"), + PhysicalKey::Code(KeyCode::KeyR) => f.write_str("R"), + PhysicalKey::Code(KeyCode::KeyS) => f.write_str("S"), + PhysicalKey::Code(KeyCode::KeyT) => f.write_str("T"), + PhysicalKey::Code(KeyCode::KeyU) => f.write_str("U"), + PhysicalKey::Code(KeyCode::KeyV) => f.write_str("V"), + PhysicalKey::Code(KeyCode::KeyW) => f.write_str("W"), + PhysicalKey::Code(KeyCode::KeyX) => f.write_str("X"), + PhysicalKey::Code(KeyCode::KeyY) => f.write_str("Y"), + PhysicalKey::Code(KeyCode::KeyZ) => f.write_str("Z"), + PhysicalKey::Code(KeyCode::Minus) => f.write_str("-"), + PhysicalKey::Code(KeyCode::Period) => f.write_str("."), + PhysicalKey::Code(KeyCode::Quote) => f.write_str("'"), + PhysicalKey::Code(KeyCode::Semicolon) => f.write_str(";"), + PhysicalKey::Code(KeyCode::Slash) => f.write_str("/"), + PhysicalKey::Code(KeyCode::AltLeft) => f.write_str("Alt"), + PhysicalKey::Code(KeyCode::AltRight) => f.write_str("Alt"), + PhysicalKey::Code(KeyCode::Backspace) => f.write_str("backspace"), + PhysicalKey::Code(KeyCode::CapsLock) => f.write_str("CapsLock"), + PhysicalKey::Code(KeyCode::ContextMenu) => f.write_str("ContextMenu"), + PhysicalKey::Code(KeyCode::ControlLeft) => f.write_str("Ctrl"), + PhysicalKey::Code(KeyCode::ControlRight) => f.write_str("Ctrl"), + PhysicalKey::Code(KeyCode::Enter) => f.write_str("Enter"), + PhysicalKey::Code(KeyCode::SuperLeft) => match std::env::consts::OS { + "macos" => f.write_str("Cmd"), + "windows" => f.write_str("Win"), + _ => f.write_str("Meta"), + }, + PhysicalKey::Code(KeyCode::SuperRight) => match std::env::consts::OS { + "macos" => f.write_str("Cmd"), + "windows" => f.write_str("Win"), + _ => f.write_str("Meta"), + }, + PhysicalKey::Code(KeyCode::ShiftLeft) => f.write_str("Shift"), + PhysicalKey::Code(KeyCode::ShiftRight) => f.write_str("Shift"), + PhysicalKey::Code(KeyCode::Space) => f.write_str("Space"), + PhysicalKey::Code(KeyCode::Tab) => f.write_str("Tab"), + PhysicalKey::Code(KeyCode::Convert) => f.write_str("Convert"), + PhysicalKey::Code(KeyCode::KanaMode) => f.write_str("KanaMode"), + PhysicalKey::Code(KeyCode::Lang1) => f.write_str("Lang1"), + PhysicalKey::Code(KeyCode::Lang2) => f.write_str("Lang2"), + PhysicalKey::Code(KeyCode::Lang3) => f.write_str("Lang3"), + PhysicalKey::Code(KeyCode::Lang4) => f.write_str("Lang4"), + PhysicalKey::Code(KeyCode::Lang5) => f.write_str("Lang5"), + PhysicalKey::Code(KeyCode::NonConvert) => f.write_str("NonConvert"), + PhysicalKey::Code(KeyCode::Delete) => f.write_str("Delete"), + PhysicalKey::Code(KeyCode::End) => f.write_str("End"), + PhysicalKey::Code(KeyCode::Help) => f.write_str("Help"), + PhysicalKey::Code(KeyCode::Home) => f.write_str("Home"), + PhysicalKey::Code(KeyCode::Insert) => f.write_str("Insert"), + PhysicalKey::Code(KeyCode::PageDown) => f.write_str("PageDown"), + PhysicalKey::Code(KeyCode::PageUp) => f.write_str("PageUp"), + PhysicalKey::Code(KeyCode::ArrowDown) => f.write_str("Down"), + PhysicalKey::Code(KeyCode::ArrowLeft) => f.write_str("Left"), + PhysicalKey::Code(KeyCode::ArrowRight) => f.write_str("Right"), + PhysicalKey::Code(KeyCode::ArrowUp) => f.write_str("Up"), + PhysicalKey::Code(KeyCode::NumLock) => f.write_str("NumLock"), + PhysicalKey::Code(KeyCode::Numpad0) => f.write_str("Numpad0"), + PhysicalKey::Code(KeyCode::Numpad1) => f.write_str("Numpad1"), + PhysicalKey::Code(KeyCode::Numpad2) => f.write_str("Numpad2"), + PhysicalKey::Code(KeyCode::Numpad3) => f.write_str("Numpad3"), + PhysicalKey::Code(KeyCode::Numpad4) => f.write_str("Numpad4"), + PhysicalKey::Code(KeyCode::Numpad5) => f.write_str("Numpad5"), + PhysicalKey::Code(KeyCode::Numpad6) => f.write_str("Numpad6"), + PhysicalKey::Code(KeyCode::Numpad7) => f.write_str("Numpad7"), + PhysicalKey::Code(KeyCode::Numpad8) => f.write_str("Numpad8"), + PhysicalKey::Code(KeyCode::Numpad9) => f.write_str("Numpad9"), + PhysicalKey::Code(KeyCode::NumpadAdd) => f.write_str("NumpadAdd"), + PhysicalKey::Code(KeyCode::NumpadBackspace) => f.write_str("NumpadBackspace"), + PhysicalKey::Code(KeyCode::NumpadClear) => f.write_str("NumpadClear"), + PhysicalKey::Code(KeyCode::NumpadClearEntry) => f.write_str("NumpadClearEntry"), + PhysicalKey::Code(KeyCode::NumpadComma) => f.write_str("NumpadComma"), + PhysicalKey::Code(KeyCode::NumpadDecimal) => f.write_str("NumpadDecimal"), + PhysicalKey::Code(KeyCode::NumpadDivide) => f.write_str("NumpadDivide"), + PhysicalKey::Code(KeyCode::NumpadEnter) => f.write_str("NumpadEnter"), + PhysicalKey::Code(KeyCode::NumpadEqual) => f.write_str("NumpadEqual"), + PhysicalKey::Code(KeyCode::NumpadHash) => f.write_str("NumpadHash"), + PhysicalKey::Code(KeyCode::NumpadMemoryAdd) => f.write_str("NumpadMemoryAdd"), + PhysicalKey::Code(KeyCode::NumpadMemoryClear) => f.write_str("NumpadMemoryClear"), + PhysicalKey::Code(KeyCode::NumpadMemoryRecall) => f.write_str("NumpadMemoryRecall"), + PhysicalKey::Code(KeyCode::NumpadMemoryStore) => f.write_str("NumpadMemoryStore"), + PhysicalKey::Code(KeyCode::NumpadMemorySubtract) => { + f.write_str("NumpadMemorySubtract") + } + PhysicalKey::Code(KeyCode::NumpadMultiply) => f.write_str("NumpadMultiply"), + PhysicalKey::Code(KeyCode::NumpadParenLeft) => f.write_str("NumpadParenLeft"), + PhysicalKey::Code(KeyCode::NumpadParenRight) => f.write_str("NumpadParenRight"), + PhysicalKey::Code(KeyCode::NumpadStar) => f.write_str("NumpadStar"), + PhysicalKey::Code(KeyCode::NumpadSubtract) => f.write_str("NumpadSubtract"), + PhysicalKey::Code(KeyCode::Escape) => f.write_str("Escape"), + PhysicalKey::Code(KeyCode::Fn) => f.write_str("Fn"), + PhysicalKey::Code(KeyCode::FnLock) => f.write_str("FnLock"), + PhysicalKey::Code(KeyCode::PrintScreen) => f.write_str("PrintScreen"), + PhysicalKey::Code(KeyCode::ScrollLock) => f.write_str("ScrollLock"), + PhysicalKey::Code(KeyCode::Pause) => f.write_str("Pause"), + PhysicalKey::Code(KeyCode::BrowserBack) => f.write_str("BrowserBack"), + PhysicalKey::Code(KeyCode::BrowserFavorites) => f.write_str("BrowserFavorites"), + PhysicalKey::Code(KeyCode::BrowserForward) => f.write_str("BrowserForward"), + PhysicalKey::Code(KeyCode::BrowserHome) => f.write_str("BrowserHome"), + PhysicalKey::Code(KeyCode::BrowserRefresh) => f.write_str("BrowserRefresh"), + PhysicalKey::Code(KeyCode::BrowserSearch) => f.write_str("BrowserSearch"), + PhysicalKey::Code(KeyCode::BrowserStop) => f.write_str("BrowserStop"), + PhysicalKey::Code(KeyCode::Eject) => f.write_str("Eject"), + PhysicalKey::Code(KeyCode::LaunchApp1) => f.write_str("LaunchApp1"), + PhysicalKey::Code(KeyCode::LaunchApp2) => f.write_str("LaunchApp2"), + PhysicalKey::Code(KeyCode::LaunchMail) => f.write_str("LaunchMail"), + PhysicalKey::Code(KeyCode::MediaPlayPause) => f.write_str("MediaPlayPause"), + PhysicalKey::Code(KeyCode::MediaSelect) => f.write_str("MediaSelect"), + PhysicalKey::Code(KeyCode::MediaStop) => f.write_str("MediaStop"), + PhysicalKey::Code(KeyCode::MediaTrackNext) => f.write_str("MediaTrackNext"), + PhysicalKey::Code(KeyCode::MediaTrackPrevious) => f.write_str("MediaTrackPrevious"), + PhysicalKey::Code(KeyCode::Power) => f.write_str("Power"), + PhysicalKey::Code(KeyCode::Sleep) => f.write_str("Sleep"), + PhysicalKey::Code(KeyCode::AudioVolumeDown) => f.write_str("AudioVolumeDown"), + PhysicalKey::Code(KeyCode::AudioVolumeMute) => f.write_str("AudioVolumeMute"), + PhysicalKey::Code(KeyCode::AudioVolumeUp) => f.write_str("AudioVolumeUp"), + PhysicalKey::Code(KeyCode::WakeUp) => f.write_str("WakeUp"), + PhysicalKey::Code(KeyCode::Meta) => match std::env::consts::OS { + "macos" => f.write_str("Cmd"), + "windows" => f.write_str("Win"), + _ => f.write_str("Meta"), + }, + PhysicalKey::Code(KeyCode::Hyper) => f.write_str("Hyper"), + PhysicalKey::Code(KeyCode::Turbo) => f.write_str("Turbo"), + PhysicalKey::Code(KeyCode::Abort) => f.write_str("Abort"), + PhysicalKey::Code(KeyCode::Resume) => f.write_str("Resume"), + PhysicalKey::Code(KeyCode::Suspend) => f.write_str("Suspend"), + PhysicalKey::Code(KeyCode::Again) => f.write_str("Again"), + PhysicalKey::Code(KeyCode::Copy) => f.write_str("Copy"), + PhysicalKey::Code(KeyCode::Cut) => f.write_str("Cut"), + PhysicalKey::Code(KeyCode::Find) => f.write_str("Find"), + PhysicalKey::Code(KeyCode::Open) => f.write_str("Open"), + PhysicalKey::Code(KeyCode::Paste) => f.write_str("Paste"), + PhysicalKey::Code(KeyCode::Props) => f.write_str("Props"), + PhysicalKey::Code(KeyCode::Select) => f.write_str("Select"), + PhysicalKey::Code(KeyCode::Undo) => f.write_str("Undo"), + PhysicalKey::Code(KeyCode::Hiragana) => f.write_str("Hiragana"), + PhysicalKey::Code(KeyCode::Katakana) => f.write_str("Katakana"), + PhysicalKey::Code(KeyCode::F1) => f.write_str("F1"), + PhysicalKey::Code(KeyCode::F2) => f.write_str("F2"), + PhysicalKey::Code(KeyCode::F3) => f.write_str("F3"), + PhysicalKey::Code(KeyCode::F4) => f.write_str("F4"), + PhysicalKey::Code(KeyCode::F5) => f.write_str("F5"), + PhysicalKey::Code(KeyCode::F6) => f.write_str("F6"), + PhysicalKey::Code(KeyCode::F7) => f.write_str("F7"), + PhysicalKey::Code(KeyCode::F8) => f.write_str("F8"), + PhysicalKey::Code(KeyCode::F9) => f.write_str("F9"), + PhysicalKey::Code(KeyCode::F10) => f.write_str("F10"), + PhysicalKey::Code(KeyCode::F11) => f.write_str("F11"), + PhysicalKey::Code(KeyCode::F12) => f.write_str("F12"), + PhysicalKey::Code(KeyCode::F13) => f.write_str("F13"), + PhysicalKey::Code(KeyCode::F14) => f.write_str("F14"), + PhysicalKey::Code(KeyCode::F15) => f.write_str("F15"), + PhysicalKey::Code(KeyCode::F16) => f.write_str("F16"), + PhysicalKey::Code(KeyCode::F17) => f.write_str("F17"), + PhysicalKey::Code(KeyCode::F18) => f.write_str("F18"), + PhysicalKey::Code(KeyCode::F19) => f.write_str("F19"), + PhysicalKey::Code(KeyCode::F20) => f.write_str("F20"), + PhysicalKey::Code(KeyCode::F21) => f.write_str("F21"), + PhysicalKey::Code(KeyCode::F22) => f.write_str("F22"), + PhysicalKey::Code(KeyCode::F23) => f.write_str("F23"), + PhysicalKey::Code(KeyCode::F24) => f.write_str("F24"), + PhysicalKey::Code(KeyCode::F25) => f.write_str("F25"), + PhysicalKey::Code(KeyCode::F26) => f.write_str("F26"), + PhysicalKey::Code(KeyCode::F27) => f.write_str("F27"), + PhysicalKey::Code(KeyCode::F28) => f.write_str("F28"), + PhysicalKey::Code(KeyCode::F29) => f.write_str("F29"), + PhysicalKey::Code(KeyCode::F30) => f.write_str("F30"), + PhysicalKey::Code(KeyCode::F31) => f.write_str("F31"), + PhysicalKey::Code(KeyCode::F32) => f.write_str("F32"), + PhysicalKey::Code(KeyCode::F33) => f.write_str("F33"), + PhysicalKey::Code(KeyCode::F34) => f.write_str("F34"), + PhysicalKey::Code(KeyCode::F35) => f.write_str("F35"), + _ => f.write_str("Unidentified"), + }, + Self::Pointer(B::Auxiliary) => f.write_str("MouseMiddle"), + Self::Pointer(B::X2) => f.write_str("MouseForward"), + Self::Pointer(B::X1) => f.write_str("MouseBackward"), + Self::Pointer(_) => f.write_str("MouseUnimplemented"), + } + } +} + +impl FromStr for KeyInput { + type Err = (); + + fn from_str(s: &str) -> Result { + let s = s.to_lowercase(); + + KeyInput::keyboard_from_str(&s) + .map(|key| KeyInput::Keyboard(key.0, key.1)) + .or_else(|| KeyInput::mouse_from_str(&s).map(KeyInput::Pointer)) + .ok_or(()) + } +} + +impl Hash for KeyInput { + fn hash(&self, state: &mut H) { + match self { + Self::Keyboard(_key, key_code) => key_code.hash(state), + // TODO: Implement `Hash` for `druid::MouseButton` + Self::Pointer(btn) => (*btn as u8).hash(state), + } + } +} + +impl PartialEq for KeyInput { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (KeyInput::Keyboard(_key_a, key_code_a), KeyInput::Keyboard(_key_b, key_code_b)) => { + key_code_a.eq(key_code_b) + } + (KeyInput::Pointer(a), KeyInput::Pointer(b)) => a.eq(b), + _ => false, + } + } +} diff --git a/src/views/editor/keypress/mod.rs b/src/views/editor/keypress/mod.rs new file mode 100644 index 00000000..19a2652b --- /dev/null +++ b/src/views/editor/keypress/mod.rs @@ -0,0 +1,408 @@ +pub mod key; +pub mod press; + +use std::{collections::HashMap, str::FromStr}; + +use crate::{keyboard::ModifiersState, reactive::RwSignal}; +use floem_editor_core::{ + command::{EditCommand, MoveCommand, MultiSelectionCommand, ScrollCommand}, + mode::Mode, +}; + +use super::{ + command::{Command, CommandExecuted}, + Editor, +}; + +use self::{key::KeyInput, press::KeyPress}; + +/// The default keymap handler does not have modal-mode specific +/// keybindings. +#[derive(Clone)] +pub struct KeypressMap { + pub keymaps: HashMap, +} +impl KeypressMap { + pub fn default_windows() -> Self { + let mut keymaps = HashMap::new(); + add_default_common(&mut keymaps); + add_default_windows(&mut keymaps); + Self { keymaps } + } + + pub fn default_macos() -> Self { + let mut keymaps = HashMap::new(); + add_default_common(&mut keymaps); + add_default_macos(&mut keymaps); + Self { keymaps } + } + + pub fn default_linux() -> Self { + let mut keymaps = HashMap::new(); + add_default_common(&mut keymaps); + add_default_linux(&mut keymaps); + Self { keymaps } + } +} +impl Default for KeypressMap { + fn default() -> Self { + match std::env::consts::OS { + "macos" => Self::default_macos(), + "windows" => Self::default_windows(), + _ => Self::default_linux(), + } + } +} + +fn key(s: &str, m: ModifiersState) -> KeyPress { + KeyPress::new(KeyInput::from_str(s).unwrap(), m) +} + +fn key_d(s: &str) -> KeyPress { + key(s, ModifiersState::default()) +} + +fn add_default_common(c: &mut HashMap) { + // Note: this should typically be kept in sync with Lapce's + // `defaults/keymaps-common.toml` + + // --- Basic editing --- + + c.insert( + key("up", ModifiersState::ALT), + Command::Edit(EditCommand::MoveLineUp), + ); + c.insert( + key("down", ModifiersState::ALT), + Command::Edit(EditCommand::MoveLineDown), + ); + + c.insert(key_d("delete"), Command::Edit(EditCommand::DeleteForward)); + c.insert( + key_d("backspace"), + Command::Edit(EditCommand::DeleteBackward), + ); + c.insert( + key("backspace", ModifiersState::SHIFT), + Command::Edit(EditCommand::DeleteForward), + ); + + c.insert(key_d("home"), Command::Move(MoveCommand::LineStartNonBlank)); + c.insert(key_d("end"), Command::Move(MoveCommand::LineEnd)); + + c.insert(key_d("pageup"), Command::Scroll(ScrollCommand::PageUp)); + c.insert(key_d("pagedown"), Command::Scroll(ScrollCommand::PageDown)); + c.insert( + key("pageup", ModifiersState::CONTROL), + Command::Scroll(ScrollCommand::ScrollUp), + ); + c.insert( + key("pagedown", ModifiersState::CONTROL), + Command::Scroll(ScrollCommand::ScrollDown), + ); + + // --- Multi cursor --- + + c.insert( + key("i", ModifiersState::ALT | ModifiersState::SHIFT), + Command::MultiSelection(MultiSelectionCommand::InsertCursorEndOfLine), + ); + + // TODO: should we have jump location backward/forward? + + // TODO: jump to snippet positions? + + // --- ---- --- + c.insert(key_d("right"), Command::Move(MoveCommand::Right)); + c.insert(key_d("left"), Command::Move(MoveCommand::Left)); + c.insert(key_d("up"), Command::Move(MoveCommand::Up)); + c.insert(key_d("down"), Command::Move(MoveCommand::Down)); + + c.insert(key_d("enter"), Command::Edit(EditCommand::InsertNewLine)); + + c.insert(key_d("tab"), Command::Edit(EditCommand::InsertTab)); + + c.insert( + key("up", ModifiersState::ALT | ModifiersState::SHIFT), + Command::Edit(EditCommand::DuplicateLineUp), + ); + c.insert( + key("down", ModifiersState::ALT | ModifiersState::SHIFT), + Command::Edit(EditCommand::DuplicateLineDown), + ); +} + +fn add_default_windows(c: &mut HashMap) { + add_default_nonmacos(c); +} + +fn add_default_macos(c: &mut HashMap) { + // Note: this should typically be kept in sync with Lapce's + // `defaults/keymaps-macos.toml` + + // --- Basic editing --- + c.insert( + key("z", ModifiersState::SUPER), + Command::Edit(EditCommand::Undo), + ); + c.insert( + key("z", ModifiersState::SUPER | ModifiersState::SHIFT), + Command::Edit(EditCommand::Redo), + ); + c.insert( + key("y", ModifiersState::SUPER), + Command::Edit(EditCommand::Redo), + ); + c.insert( + key("x", ModifiersState::SUPER), + Command::Edit(EditCommand::ClipboardCut), + ); + c.insert( + key("c", ModifiersState::SUPER), + Command::Edit(EditCommand::ClipboardCopy), + ); + c.insert( + key("v", ModifiersState::SUPER), + Command::Edit(EditCommand::ClipboardPaste), + ); + + c.insert( + key("right", ModifiersState::ALT), + Command::Move(MoveCommand::WordEndForward), + ); + c.insert( + key("left", ModifiersState::ALT), + Command::Move(MoveCommand::WordBackward), + ); + c.insert( + key("left", ModifiersState::SUPER), + Command::Move(MoveCommand::LineStartNonBlank), + ); + c.insert( + key("right", ModifiersState::SUPER), + Command::Move(MoveCommand::LineEnd), + ); + + c.insert( + key("a", ModifiersState::CONTROL), + Command::Move(MoveCommand::LineStartNonBlank), + ); + c.insert( + key("e", ModifiersState::CONTROL), + Command::Move(MoveCommand::LineEnd), + ); + + c.insert( + key("k", ModifiersState::SUPER | ModifiersState::SHIFT), + Command::Edit(EditCommand::DeleteLine), + ); + + c.insert( + key("backspace", ModifiersState::ALT), + Command::Edit(EditCommand::DeleteWordBackward), + ); + c.insert( + key("backspace", ModifiersState::SUPER), + Command::Edit(EditCommand::DeleteToBeginningOfLine), + ); + c.insert( + key("k", ModifiersState::CONTROL), + Command::Edit(EditCommand::DeleteToEndOfLine), + ); + c.insert( + key("delete", ModifiersState::ALT), + Command::Edit(EditCommand::DeleteWordForward), + ); + + // TODO: match pairs? + // TODO: indent/outdent line? + + c.insert( + key("a", ModifiersState::SUPER), + Command::MultiSelection(MultiSelectionCommand::SelectAll), + ); + + c.insert( + key("enter", ModifiersState::SUPER), + Command::Edit(EditCommand::NewLineBelow), + ); + c.insert( + key("enter", ModifiersState::SUPER | ModifiersState::SHIFT), + Command::Edit(EditCommand::NewLineAbove), + ); + + // --- Multi cursor --- + c.insert( + key("up", ModifiersState::ALT | ModifiersState::SUPER), + Command::MultiSelection(MultiSelectionCommand::InsertCursorAbove), + ); + c.insert( + key("down", ModifiersState::ALT | ModifiersState::SUPER), + Command::MultiSelection(MultiSelectionCommand::InsertCursorBelow), + ); + + c.insert( + key("l", ModifiersState::SUPER), + Command::MultiSelection(MultiSelectionCommand::SelectCurrentLine), + ); + c.insert( + key("l", ModifiersState::SUPER | ModifiersState::SHIFT), + Command::MultiSelection(MultiSelectionCommand::SelectAllCurrent), + ); + + c.insert( + key("u", ModifiersState::SUPER), + Command::MultiSelection(MultiSelectionCommand::SelectUndo), + ); + + // --- ---- --- + c.insert( + key("up", ModifiersState::SUPER), + Command::Move(MoveCommand::DocumentStart), + ); + c.insert( + key("down", ModifiersState::SUPER), + Command::Move(MoveCommand::DocumentEnd), + ); +} + +fn add_default_linux(c: &mut HashMap) { + add_default_nonmacos(c); +} + +fn add_default_nonmacos(c: &mut HashMap) { + // Note: this should typically be kept in sync with Lapce's + // `defaults/keymaps-nonmacos.toml` + + // --- Basic editing --- + c.insert( + key("z", ModifiersState::CONTROL), + Command::Edit(EditCommand::Undo), + ); + c.insert( + key("z", ModifiersState::CONTROL | ModifiersState::SHIFT), + Command::Edit(EditCommand::Redo), + ); + c.insert( + key("y", ModifiersState::CONTROL), + Command::Edit(EditCommand::Redo), + ); + c.insert( + key("x", ModifiersState::CONTROL), + Command::Edit(EditCommand::ClipboardCut), + ); + c.insert( + key("delete", ModifiersState::SHIFT), + Command::Edit(EditCommand::ClipboardCut), + ); + c.insert( + key("c", ModifiersState::CONTROL), + Command::Edit(EditCommand::ClipboardCopy), + ); + c.insert( + key("insert", ModifiersState::CONTROL), + Command::Edit(EditCommand::ClipboardCopy), + ); + c.insert( + key("v", ModifiersState::CONTROL), + Command::Edit(EditCommand::ClipboardPaste), + ); + c.insert( + key("insert", ModifiersState::SHIFT), + Command::Edit(EditCommand::ClipboardPaste), + ); + + c.insert( + key("right", ModifiersState::CONTROL), + Command::Move(MoveCommand::WordEndForward), + ); + c.insert( + key("left", ModifiersState::CONTROL), + Command::Move(MoveCommand::WordBackward), + ); + + c.insert( + key("backspace", ModifiersState::CONTROL), + Command::Edit(EditCommand::DeleteWordBackward), + ); + c.insert( + key("delete", ModifiersState::CONTROL), + Command::Edit(EditCommand::DeleteWordForward), + ); + + // TODO: match pairs? + + // TODO: indent/outdent line? + + c.insert( + key("a", ModifiersState::CONTROL), + Command::MultiSelection(MultiSelectionCommand::SelectAll), + ); + + c.insert( + key("enter", ModifiersState::CONTROL), + Command::Edit(EditCommand::NewLineAbove), + ); + + // --- Multi cursor --- + c.insert( + key("up", ModifiersState::CONTROL | ModifiersState::ALT), + Command::MultiSelection(MultiSelectionCommand::InsertCursorAbove), + ); + c.insert( + key("down", ModifiersState::CONTROL | ModifiersState::ALT), + Command::MultiSelection(MultiSelectionCommand::InsertCursorBelow), + ); + + c.insert( + key("l", ModifiersState::CONTROL), + Command::MultiSelection(MultiSelectionCommand::SelectCurrentLine), + ); + c.insert( + key("l", ModifiersState::CONTROL | ModifiersState::SHIFT), + Command::MultiSelection(MultiSelectionCommand::SelectAllCurrent), + ); + + c.insert( + key("u", ModifiersState::CONTROL), + Command::MultiSelection(MultiSelectionCommand::SelectUndo), + ); + + // --- Navigation --- + c.insert( + key("home", ModifiersState::CONTROL), + Command::Move(MoveCommand::DocumentStart), + ); + c.insert( + key("end", ModifiersState::CONTROL), + Command::Move(MoveCommand::DocumentEnd), + ); +} + +pub fn default_key_handler( + editor: RwSignal, +) -> impl Fn(&KeyPress, ModifiersState) -> CommandExecuted + 'static { + let keypress_map = KeypressMap::default(); + move |keypress, modifiers| { + let command = keypress_map.keymaps.get(keypress).or_else(|| { + let mode = editor.get_untracked().cursor.get_untracked().get_mode(); + if mode == Mode::Insert { + let mut keypress = keypress.clone(); + keypress.mods.set(ModifiersState::SHIFT, false); + keypress_map.keymaps.get(&keypress) + } else { + None + } + }); + + let Some(command) = command else { + return CommandExecuted::No; + }; + + editor.with_untracked(|editor| { + editor + .doc() + .run_command(editor, command, Some(1), modifiers) + }) + } +} diff --git a/src/views/editor/keypress/press.rs b/src/views/editor/keypress/press.rs new file mode 100644 index 00000000..715a3eaf --- /dev/null +++ b/src/views/editor/keypress/press.rs @@ -0,0 +1,160 @@ +use std::fmt::Display; + +use crate::keyboard::{Key, KeyCode, KeyEvent, ModifiersState, NamedKey, PhysicalKey}; + +use super::key::KeyInput; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct KeyPress { + pub key: KeyInput, + pub mods: ModifiersState, +} + +impl KeyPress { + pub fn new(key: KeyInput, mods: ModifiersState) -> Self { + Self { key, mods } + } + + pub fn to_lowercase(&self) -> Self { + let key = match &self.key { + KeyInput::Keyboard(Key::Character(c), key_code) => { + KeyInput::Keyboard(Key::Character(c.to_lowercase().into()), *key_code) + } + _ => self.key.clone(), + }; + Self { + key, + mods: self.mods, + } + } + + pub fn is_char(&self) -> bool { + let mut mods = self.mods; + mods.set(ModifiersState::SHIFT, false); + if mods.is_empty() { + if let KeyInput::Keyboard(Key::Character(_c), _) = &self.key { + return true; + } + } + false + } + + pub fn is_modifiers(&self) -> bool { + if let KeyInput::Keyboard(_, scancode) = &self.key { + matches!( + scancode, + PhysicalKey::Code(KeyCode::Meta) + | PhysicalKey::Code(KeyCode::SuperLeft) + | PhysicalKey::Code(KeyCode::SuperRight) + | PhysicalKey::Code(KeyCode::ShiftLeft) + | PhysicalKey::Code(KeyCode::ShiftRight) + | PhysicalKey::Code(KeyCode::ControlLeft) + | PhysicalKey::Code(KeyCode::ControlRight) + | PhysicalKey::Code(KeyCode::AltLeft) + | PhysicalKey::Code(KeyCode::AltRight) + ) + } else { + false + } + } + + pub fn label(&self) -> String { + let mut keys = String::from(""); + if self.mods.control_key() { + keys.push_str("Ctrl+"); + } + if self.mods.alt_key() { + keys.push_str("Alt+"); + } + if self.mods.super_key() { + let keyname = match std::env::consts::OS { + "macos" => "Cmd+", + "windows" => "Win+", + _ => "Meta+", + }; + keys.push_str(keyname); + } + if self.mods.shift_key() { + keys.push_str("Shift+"); + } + keys.push_str(&self.key.to_string()); + keys.trim().to_string() + } + + pub fn parse(key: &str) -> Vec { + key.split(' ') + .filter_map(|k| { + let (modifiers, key) = match k.rsplit_once('+') { + Some(pair) => pair, + None => ("", k), + }; + + let key = match key.parse().ok() { + Some(key) => key, + None => { + // Skip past unrecognized key definitions + // warn!("Unrecognized key: {key}"); + return None; + } + }; + + let mut mods = ModifiersState::empty(); + for part in modifiers.to_lowercase().split('+') { + match part { + "ctrl" => mods.set(ModifiersState::CONTROL, true), + "meta" => mods.set(ModifiersState::SUPER, true), + "shift" => mods.set(ModifiersState::SHIFT, true), + "alt" => mods.set(ModifiersState::ALT, true), + "" => (), + // other => warn!("Invalid key modifier: {}", other), + _ => {} + } + } + + Some(KeyPress { key, mods }) + }) + .collect() + } +} + +impl Display for KeyPress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.mods.contains(ModifiersState::CONTROL) { + let _ = f.write_str("Ctrl+"); + } + if self.mods.contains(ModifiersState::ALT) { + let _ = f.write_str("Alt+"); + } + if self.mods.contains(ModifiersState::SUPER) { + let _ = f.write_str("Meta+"); + } + if self.mods.contains(ModifiersState::SHIFT) { + let _ = f.write_str("Shift+"); + } + f.write_str(&self.key.to_string()) + } +} +impl TryFrom<&KeyEvent> for KeyPress { + type Error = (); + + fn try_from(ev: &KeyEvent) -> Result { + Ok(KeyPress { + key: KeyInput::Keyboard(ev.key.logical_key.clone(), ev.key.physical_key), + mods: get_key_modifiers(ev), + }) + } +} + +pub fn get_key_modifiers(key_event: &KeyEvent) -> ModifiersState { + let mut mods = key_event.modifiers; + + match &key_event.key.logical_key { + Key::Named(NamedKey::Shift) => mods.set(ModifiersState::SHIFT, false), + Key::Named(NamedKey::Alt) => mods.set(ModifiersState::ALT, false), + Key::Named(NamedKey::Meta) => mods.set(ModifiersState::SUPER, false), + Key::Named(NamedKey::Control) => mods.set(ModifiersState::CONTROL, false), + _ => (), + } + + mods +} diff --git a/src/views/editor/layout.rs b/src/views/editor/layout.rs new file mode 100644 index 00000000..108cb599 --- /dev/null +++ b/src/views/editor/layout.rs @@ -0,0 +1,149 @@ +use crate::{ + cosmic_text::{LayoutLine, TextLayout}, + peniko::Color, +}; +use floem_editor_core::buffer::rope_text::RopeText; + +use super::visual_line::TextLayoutProvider; + +#[derive(Clone, Debug)] +pub struct LineExtraStyle { + pub x: f64, + pub y: f64, + pub width: Option, + pub height: f64, + pub bg_color: Option, + pub under_line: Option, + pub wave_line: Option, +} + +#[derive(Clone)] +pub struct TextLayoutLine { + /// Extra styling that should be applied to the text + /// (x0, x1 or line display end, style) + pub extra_style: Vec, + pub text: TextLayout, + pub whitespaces: Option>, + pub indent: f64, +} +impl TextLayoutLine { + /// The number of line breaks in the text layout. Always at least `1`. + pub fn line_count(&self) -> usize { + self.relevant_layouts().count().max(1) + } + + /// Iterate over all the layouts that are nonempty. + /// Note that this may be empty if the line is completely empty, like the last line + pub fn relevant_layouts(&self) -> impl Iterator + '_ { + // Even though we only have one hard line (and thus only one `lines` entry) typically, for + // normal buffer lines, we can have more than one due to multiline phantom text. So we have + // to sum over all of the entries line counts. + self.text + .lines + .iter() + .flat_map(|l| l.layout_opt().as_deref()) + .flat_map(|ls| ls.iter()) + .filter(|l| !l.glyphs.is_empty()) + } + + /// Iterator over the (start, end) columns of the relevant layouts. + pub fn layout_cols<'a>( + &'a self, + text_prov: impl TextLayoutProvider + 'a, + line: usize, + ) -> impl Iterator + 'a { + let mut prefix = None; + // Include an entry if there is nothing + if self.text.lines.len() == 1 { + let line_start = self.text.lines[0].start_index(); + if let Some(layouts) = self.text.lines[0].layout_opt().as_deref() { + // Do we need to require !layouts.is_empty()? + if !layouts.is_empty() && layouts.iter().all(|l| l.glyphs.is_empty()) { + // We assume the implicit glyph start is zero + prefix = Some((line_start, line_start)); + } + } + } + + let line_v = line; + let iter = self + .text + .lines + .iter() + .filter_map(|line| line.layout_opt().as_deref().map(|ls| (line, ls))) + .flat_map(|(line, ls)| ls.iter().map(move |l| (line, l))) + .filter(|(_, l)| !l.glyphs.is_empty()) + .map(move |(tl_line, l)| { + let line_start = tl_line.start_index(); + + let start = line_start + l.glyphs[0].start; + let end = line_start + l.glyphs.last().unwrap().end; + + let text = text_prov.rope_text(); + // We can't just use the original end, because the *true* last glyph on the line + // may be a space, but it isn't included in the layout! Though this only happens + // for single spaces, for some reason. + let pre_end = text_prov.before_phantom_col(line_v, end); + let line_offset = text.offset_of_line(line); + + // TODO(minor): We don't really need the entire line, just the two characters after + let line_end = text.line_end_col(line, true); + + let end = if pre_end <= line_end { + let after = text.slice_to_cow(line_offset + pre_end..line_offset + line_end); + if after.starts_with(' ') && !after.starts_with(" ") { + end + 1 + } else { + end + } + } else { + end + }; + + (start, end) + }); + + prefix.into_iter().chain(iter) + } + + /// Iterator over the start columns of the relevant layouts + pub fn start_layout_cols<'a>( + &'a self, + text_prov: impl TextLayoutProvider + 'a, + line: usize, + ) -> impl Iterator + 'a { + self.layout_cols(text_prov, line).map(|(start, _)| start) + } + + /// Get the top y position of the given line index + pub fn get_layout_y(&self, nth: usize) -> Option { + if nth == 0 { + return Some(0.0); + } + + let mut line_y = 0.0; + for (i, layout) in self.relevant_layouts().enumerate() { + // This logic matches how layout run iter computes the line_y + let line_height = layout.line_ascent + layout.line_descent; + if i == nth { + let offset = (line_height - (layout.glyph_ascent + layout.glyph_descent)) / 2.0; + + return Some((line_y - offset - layout.glyph_descent) as f64); + } + + line_y += line_height; + } + + None + } + + /// Get the (start x, end x) positions of the given line index + pub fn get_layout_x(&self, nth: usize) -> Option<(f32, f32)> { + let layout = self.relevant_layouts().nth(nth)?; + + let start = layout.glyphs.first().map(|g| g.x).unwrap_or(0.0); + let end = layout.glyphs.last().map(|g| g.x + g.w).unwrap_or(0.0); + + Some((start, end)) + } +} diff --git a/src/views/editor/listener.rs b/src/views/editor/listener.rs new file mode 100644 index 00000000..21e87668 --- /dev/null +++ b/src/views/editor/listener.rs @@ -0,0 +1,65 @@ +use floem_reactive::{RwSignal, Scope}; + +/// A signal listener that receives 'events' from the outside and runs the callback. +/// This is implemented using effects and normal rw signals. This should be used when it doesn't +/// make sense to think of it as 'storing' a value, like an `RwSignal` would typically be used for. +/// +/// Copied/Cloned listeners refer to the same listener. +#[derive(Debug)] +pub struct Listener { + cx: Scope, + val: RwSignal>, +} + +impl Listener { + pub fn new(cx: Scope, on_val: impl Fn(T) + 'static) -> Listener { + let val = cx.create_rw_signal(None); + + let listener = Listener { val, cx }; + listener.listen(on_val); + + listener + } + + /// Construct a listener when you can't yet give it a callback. + /// Call `listen` to set a callback. + pub fn new_empty(cx: Scope) -> Listener { + let val = cx.create_rw_signal(None); + Listener { val, cx } + } + + pub fn scope(&self) -> Scope { + self.cx + } + + /// Listen for values sent to this listener. + pub fn listen(self, on_val: impl Fn(T) + 'static) { + self.listen_with(self.cx, on_val) + } + + /// Listen for values sent to this listener. + /// Allows creating the effect with a custom scope, letting it be disposed of. + pub fn listen_with(self, cx: Scope, on_val: impl Fn(T) + 'static) { + let val = self.val; + + cx.create_effect(move |_| { + // TODO(minor): Signals could have a `take` method to avoid cloning. + if let Some(cmd) = val.get() { + on_val(cmd); + } + }); + } + + /// Send a value to the listener. + pub fn send(&self, v: T) { + self.val.set(Some(v)); + } +} + +impl Copy for Listener {} + +impl Clone for Listener { + fn clone(&self) -> Self { + *self + } +} diff --git a/src/views/editor/mod.rs b/src/views/editor/mod.rs new file mode 100644 index 00000000..3d63df1a --- /dev/null +++ b/src/views/editor/mod.rs @@ -0,0 +1,1437 @@ +use std::{ + cell::{Cell, RefCell}, + cmp::Ordering, + collections::{hash_map::DefaultHasher, HashMap}, + hash::{Hash, Hasher}, + rc::Rc, + sync::Arc, + time::Duration, +}; + +use crate::{ + action::{exec_after, TimerToken}, + cosmic_text::{Attrs, AttrsList, LineHeightValue, TextLayout, Wrap}, + keyboard::ModifiersState, + kurbo::{Point, Rect, Vec2}, + peniko::Color, + pointer::{PointerButton, PointerInputEvent, PointerMoveEvent}, + reactive::{batch, untrack, ReadSignal, RwSignal, Scope}, +}; +use floem_editor_core::{ + buffer::rope_text::{RopeText, RopeTextVal}, + command::MoveCommand, + cursor::{ColPosition, Cursor, CursorAffinity, CursorMode}, + mode::Mode, + movement::Movement, + register::Register, + selection::Selection, + soft_tab::{snap_to_soft_tab_line_col, SnapDirection}, +}; +use lapce_xi_rope::Rope; + +pub mod actions; +pub mod color; +pub mod command; +pub mod gutter; +pub mod id; +pub mod keypress; +pub mod layout; +pub mod listener; +pub mod movement; +pub mod phantom_text; +pub mod text; +pub mod text_document; +pub mod view; +pub mod visual_line; + +pub use floem_editor_core as core; + +use self::{ + color::EditorColor, + command::Command, + id::EditorId, + layout::TextLayoutLine, + phantom_text::PhantomTextLine, + text::{Document, Preedit, PreeditData, RenderWhitespace, Styling, WrapMethod}, + view::{LineInfo, ScreenLines, ScreenLinesBase}, + visual_line::{ + hit_position_aff, FontSizeCacheId, LayoutEvent, LineFontSizeProvider, Lines, RVLine, + ResolvedWrap, TextLayoutProvider, VLine, VLineInfo, + }, +}; + +pub(crate) const CHAR_WIDTH: f64 = 7.5; + +/// The main structure for the editor view itself. +/// This can be considered to be the data part of the `View`. +/// It holds an `Rc` within as the document it is a view into. +#[derive(Clone)] +pub struct Editor { + pub cx: Cell, + effects_cx: Cell, + + id: EditorId, + + pub active: RwSignal, + + /// Whether you can edit within this editor. + pub read_only: RwSignal, + /// Whether you can scroll beyond the last line of the document. + pub scroll_beyond_last_line: RwSignal, + pub cursor_surrounding_lines: RwSignal, + + pub show_indent_guide: RwSignal, + + /// Whether modal mode is enabled + pub modal: RwSignal, + /// Whether line numbers are relative in modal mode + pub modal_relative_line_numbers: RwSignal, + + /// Whether to insert the indent that is detected for the file when a tab character + /// is inputted. + pub smart_tab: RwSignal, + + pub(crate) doc: RwSignal>, + pub(crate) style: RwSignal>, + + pub cursor: RwSignal, + + pub window_origin: RwSignal, + pub viewport: RwSignal, + + /// The current scroll position. + pub scroll_delta: RwSignal, + pub scroll_to: RwSignal>, + + /// Holds the cache of the lines and provides many utility functions for them. + lines: Rc, + pub screen_lines: RwSignal, + + /// Modal mode register + pub register: RwSignal, + /// Cursor rendering information, such as the cursor blinking state. + pub cursor_info: CursorInfo, + + pub last_movement: RwSignal, + + /// Whether ime input is allowed. + /// Should not be set manually outside of the specific handling for ime. + pub ime_allowed: RwSignal, + // TODO: this could have the Lapce snippet support built-in +} +impl Editor { + /// Create a new editor into the given document, using the styling. + /// `doc`: The backing [`Document`], such as [`TextDocument`] + /// `style`: How the editor should be styled, such as [`SimpleStyling`] + pub fn new(cx: Scope, doc: Rc, style: Rc) -> Editor { + let id = EditorId::next(); + Editor::new_id(cx, id, doc, style) + } + + /// Create a new editor into the given document, using the styling. + /// `id` should typically be constructed by [`EditorId::next`] + /// `doc`: The backing [`Document`], such as [`TextDocument`] + /// `style`: How the editor should be styled, such as [`SimpleStyling`] + pub fn new_id( + cx: Scope, + id: EditorId, + doc: Rc, + style: Rc, + ) -> Editor { + let editor = Editor::new_direct(cx, id, doc, style); + editor.recreate_view_effects(); + + editor + } + + // TODO: shouldn't this accept an `RwSignal>` so that it can listen for + // changes in other editors? + // TODO: should we really allow callers to arbitrarily specify the Id? That could open up + // confusing behavior. + + /// Create a new editor into the given document, using the styling. + /// `id` should typically be constructed by [`EditorId::next`] + /// `doc`: The backing [`Document`], such as [`TextDocument`] + /// `style`: How the editor should be styled, such as [`SimpleStyling`] + /// This does *not* create the view effects. Use this if you're creating an editor and then + /// replacing signals. Invoke [`Editor::recreate_view_effects`] when you are done. + /// ```rust,ignore + /// let shared_scroll_beyond_last_line = /* ... */; + /// let editor = Editor::new_direct(cx, id, doc, style); + /// editor.scroll_beyond_last_line.set(shared_scroll_beyond_last_line); + /// ``` + pub fn new_direct( + cx: Scope, + id: EditorId, + doc: Rc, + style: Rc, + ) -> Editor { + let cx = cx.create_child(); + + let viewport = cx.create_rw_signal(Rect::ZERO); + let modal = false; + let cursor_mode = if modal { + CursorMode::Normal(0) + } else { + CursorMode::Insert(Selection::caret(0)) + }; + let cursor = Cursor::new(cursor_mode, None, None); + let cursor = cx.create_rw_signal(cursor); + + let doc = cx.create_rw_signal(doc); + let style = cx.create_rw_signal(style); + + let font_sizes = RefCell::new(Rc::new(EditorFontSizes { + style: style.read_only(), + doc: doc.read_only(), + })); + let lines = Rc::new(Lines::new(cx, font_sizes)); + let screen_lines = cx.create_rw_signal(ScreenLines::new(cx, viewport.get_untracked())); + + let ed = Editor { + cx: Cell::new(cx), + effects_cx: Cell::new(cx.create_child()), + id, + active: cx.create_rw_signal(false), + read_only: cx.create_rw_signal(false), + scroll_beyond_last_line: cx.create_rw_signal(false), + cursor_surrounding_lines: cx.create_rw_signal(1), + show_indent_guide: cx.create_rw_signal(false), + modal: cx.create_rw_signal(modal), + modal_relative_line_numbers: cx.create_rw_signal(true), + smart_tab: cx.create_rw_signal(true), + doc, + style, + cursor, + window_origin: cx.create_rw_signal(Point::ZERO), + viewport, + scroll_delta: cx.create_rw_signal(Vec2::ZERO), + scroll_to: cx.create_rw_signal(None), + lines, + screen_lines, + register: cx.create_rw_signal(Register::default()), + cursor_info: CursorInfo::new(cx), + last_movement: cx.create_rw_signal(Movement::Left), + ime_allowed: cx.create_rw_signal(false), + }; + + create_view_effects(ed.effects_cx.get(), &ed); + + ed + } + + pub fn id(&self) -> EditorId { + self.id + } + + /// Get the document untracked + pub fn doc(&self) -> Rc { + self.doc.get_untracked() + } + + pub fn doc_track(&self) -> Rc { + self.doc.get() + } + + // TODO: should this be `ReadSignal`? but read signal doesn't have .track + pub fn doc_signal(&self) -> RwSignal> { + self.doc + } + + pub fn recreate_view_effects(&self) { + batch(|| { + self.effects_cx.get().dispose(); + self.effects_cx.set(self.cx.get().create_child()); + create_view_effects(self.effects_cx.get(), self); + }); + } + + /// Swap the underlying document out + pub fn update_doc(&self, doc: Rc, styling: Option>) { + batch(|| { + // Get rid of all the effects + self.effects_cx.get().dispose(); + + *self.lines.font_sizes.borrow_mut() = Rc::new(EditorFontSizes { + style: self.style.read_only(), + doc: self.doc.read_only(), + }); + self.lines.clear(0, None); + self.doc.set(doc); + if let Some(styling) = styling { + self.style.set(styling); + } + self.screen_lines.update(|screen_lines| { + screen_lines.clear(self.viewport.get_untracked()); + }); + + // Recreate the effects + self.effects_cx.set(self.cx.get().create_child()); + create_view_effects(self.effects_cx.get(), self); + }); + } + + pub fn update_styling(&self, styling: Rc) { + batch(|| { + // Get rid of all the effects + self.effects_cx.get().dispose(); + + *self.lines.font_sizes.borrow_mut() = Rc::new(EditorFontSizes { + style: self.style.read_only(), + doc: self.doc.read_only(), + }); + self.lines.clear(0, None); + + self.style.set(styling); + + self.screen_lines.update(|screen_lines| { + screen_lines.clear(self.viewport.get_untracked()); + }); + + // Recreate the effects + self.effects_cx.set(self.cx.get().create_child()); + create_view_effects(self.effects_cx.get(), self); + }); + } + + pub fn duplicate(&self, editor_id: Option) -> Editor { + let doc = self.doc(); + let style = self.style(); + let mut editor = Editor::new_direct( + self.cx.get(), + editor_id.unwrap_or_else(EditorId::next), + doc, + style, + ); + + batch(|| { + editor.read_only.set(self.read_only.get_untracked()); + editor + .scroll_beyond_last_line + .set(self.scroll_beyond_last_line.get_untracked()); + editor + .cursor_surrounding_lines + .set(self.cursor_surrounding_lines.get_untracked()); + editor + .show_indent_guide + .set(self.show_indent_guide.get_untracked()); + editor.modal.set(self.modal.get_untracked()); + editor + .modal_relative_line_numbers + .set(self.modal_relative_line_numbers.get_untracked()); + editor.smart_tab.set(self.smart_tab.get_untracked()); + editor.cursor.set(self.cursor.get_untracked()); + editor.scroll_delta.set(self.scroll_delta.get_untracked()); + editor.scroll_to.set(self.scroll_to.get_untracked()); + editor.window_origin.set(self.window_origin.get_untracked()); + editor.viewport.set(self.viewport.get_untracked()); + editor.register.set(self.register.get_untracked()); + editor.cursor_info = self.cursor_info.clone(); + editor.last_movement.set(self.last_movement.get_untracked()); + // ? + // editor.ime_allowed.set(self.ime_allowed.get_untracked()); + }); + + editor.recreate_view_effects(); + + editor + } + + /// Get the styling untracked + pub fn style(&self) -> Rc { + self.style.get_untracked() + } + + /// Get the text of the document + /// You should typically prefer [`Self::rope_text`] + pub fn text(&self) -> Rope { + self.doc().text() + } + + /// Get the [`RopeTextVal`] from `doc` untracked + pub fn rope_text(&self) -> RopeTextVal { + self.doc().rope_text() + } + + pub fn lines(&self) -> &Lines { + &self.lines + } + + // Get the text layout for a document line, creating it if needed. + pub fn text_layout(&self, line: usize) -> Arc { + self.text_layout_trigger(line, true) + } + + pub fn text_layout_trigger(&self, line: usize, trigger: bool) -> Arc { + let id = self.style().id(); + let text_prov = self.text_prov(); + self.lines + .get_init_text_layout(id, &text_prov, line, trigger) + } + + pub fn text_prov(&self) -> EditorTextProv { + let doc = self.doc.get_untracked(); + EditorTextProv { + text: doc.text(), + doc, + lines: self.lines.clone(), + style: self.style.get_untracked(), + viewport: self.viewport.get_untracked(), + } + } + + fn preedit(&self) -> PreeditData { + self.doc.with_untracked(|doc| doc.preedit()) + } + + pub fn set_preedit(&self, text: String, cursor: Option<(usize, usize)>, offset: usize) { + batch(|| { + self.preedit().preedit.set(Some(Preedit { + text, + cursor, + offset, + })); + + self.doc().cache_rev().update(|cache_rev| { + *cache_rev += 1; + }); + }); + } + + pub fn clear_preedit(&self) { + let preedit = self.preedit(); + if preedit.preedit.with_untracked(|preedit| preedit.is_none()) { + return; + } + + batch(|| { + preedit.preedit.set(None); + self.doc().cache_rev().update(|cache_rev| { + *cache_rev += 1; + }); + }); + } + + pub fn receive_char(&self, c: &str) { + self.doc().receive_char(self, c) + } + + fn compute_screen_lines(&self, base: RwSignal) -> ScreenLines { + // This function *cannot* access `ScreenLines` with how it is currently implemented. + // This is being called from within an update to screen lines. + + self.doc().compute_screen_lines(self, base) + } + + /// Default handler for `PointerDown` event + pub fn pointer_down(&self, pointer_event: &PointerInputEvent) { + match pointer_event.button { + PointerButton::Primary => { + self.active.set(true); + self.left_click(pointer_event); + } + PointerButton::Secondary => { + self.right_click(pointer_event); + } + _ => {} + } + } + + pub fn left_click(&self, pointer_event: &PointerInputEvent) { + match pointer_event.count { + 1 => { + self.single_click(pointer_event); + } + 2 => { + self.double_click(pointer_event); + } + 3 => { + self.triple_click(pointer_event); + } + _ => {} + } + } + + pub fn single_click(&self, pointer_event: &PointerInputEvent) { + let mode = self.cursor.with_untracked(|c| c.get_mode()); + let (new_offset, _) = self.offset_of_point(mode, pointer_event.pos); + self.cursor.update(|cursor| { + cursor.set_offset( + new_offset, + pointer_event.modifiers.shift_key(), + pointer_event.modifiers.alt_key(), + ) + }); + } + + pub fn double_click(&self, pointer_event: &PointerInputEvent) { + let mode = self.cursor.with_untracked(|c| c.get_mode()); + let (mouse_offset, _) = self.offset_of_point(mode, pointer_event.pos); + let (start, end) = self.select_word(mouse_offset); + + self.cursor.update(|cursor| { + cursor.add_region( + start, + end, + pointer_event.modifiers.shift_key(), + pointer_event.modifiers.alt_key(), + ) + }); + } + + pub fn triple_click(&self, pointer_event: &PointerInputEvent) { + let mode = self.cursor.with_untracked(|c| c.get_mode()); + let (mouse_offset, _) = self.offset_of_point(mode, pointer_event.pos); + let line = self.line_of_offset(mouse_offset); + let start = self.offset_of_line(line); + let end = self.offset_of_line(line + 1); + + self.cursor.update(|cursor| { + cursor.add_region( + start, + end, + pointer_event.modifiers.shift_key(), + pointer_event.modifiers.alt_key(), + ) + }); + } + + pub fn pointer_move(&self, pointer_event: &PointerMoveEvent) { + let mode = self.cursor.with_untracked(|c| c.get_mode()); + let (offset, _is_inside) = self.offset_of_point(mode, pointer_event.pos); + if self.active.get_untracked() && self.cursor.with_untracked(|c| c.offset()) != offset { + self.cursor.update(|cursor| { + cursor.set_offset(offset, true, pointer_event.modifiers.alt_key()) + }); + } + } + + pub fn pointer_up(&self, _pointer_event: &PointerInputEvent) { + self.active.set(false); + } + + fn right_click(&self, pointer_event: &PointerInputEvent) { + let mode = self.cursor.with_untracked(|c| c.get_mode()); + let (offset, _) = self.offset_of_point(mode, pointer_event.pos); + let doc = self.doc(); + let pointer_inside_selection = self + .cursor + .with_untracked(|c| c.edit_selection(&doc.rope_text()).contains(offset)); + if !pointer_inside_selection { + // move cursor to pointer position if outside current selection + self.single_click(pointer_event); + } + } + + // TODO: should this have modifiers state in its api + pub fn page_move(&self, down: bool, mods: ModifiersState) { + let viewport = self.viewport.get_untracked(); + // TODO: don't assume line height is constant + let line_height = f64::from(self.line_height(0)); + let lines = (viewport.height() / line_height / 2.0).round() as usize; + let distance = (lines as f64) * line_height; + self.scroll_delta + .set(Vec2::new(0.0, if down { distance } else { -distance })); + let cmd = if down { + MoveCommand::Down + } else { + MoveCommand::Up + }; + let cmd = Command::Move(cmd); + self.doc().run_command(self, &cmd, Some(lines), mods); + } + + pub fn scroll(&self, top_shift: f64, down: bool, count: usize, mods: ModifiersState) { + let viewport = self.viewport.get_untracked(); + // TODO: don't assume line height is constant + let line_height = f64::from(self.line_height(0)); + let diff = line_height * count as f64; + let diff = if down { diff } else { -diff }; + + let offset = self.cursor.with_untracked(|cursor| cursor.offset()); + let (line, _col) = self.offset_to_line_col(offset); + let top = viewport.y0 + diff + top_shift; + let bottom = viewport.y0 + diff + viewport.height(); + + let new_line = if (line + 1) as f64 * line_height + line_height > bottom { + let line = (bottom / line_height).floor() as usize; + if line > 2 { + line - 2 + } else { + 0 + } + } else if line as f64 * line_height - line_height < top { + let line = (top / line_height).ceil() as usize; + line + 1 + } else { + line + }; + + self.scroll_delta.set(Vec2::new(0.0, diff)); + + let res = match new_line.cmp(&line) { + Ordering::Greater => Some((MoveCommand::Down, new_line - line)), + Ordering::Less => Some((MoveCommand::Up, line - new_line)), + _ => None, + }; + + if let Some((cmd, count)) = res { + let cmd = Command::Move(cmd); + self.doc().run_command(self, &cmd, Some(count), mods); + } + } + + // === Information === + + pub fn phantom_text(&self, line: usize) -> PhantomTextLine { + self.doc().phantom_text(line) + } + + pub fn line_height(&self, line: usize) -> f32 { + self.style().line_height(line) + } + + pub fn color(&self, color: EditorColor) -> Color { + self.style().color(color) + } + + // === Line Information === + + /// Iterate over the visual lines in the view, starting at the given line. + pub fn iter_vlines(&self, backwards: bool, start: VLine) -> impl Iterator { + self.lines.iter_vlines(self.text_prov(), backwards, start) + } + + /// Iterate over the visual lines in the view, starting at the given line and ending at the + /// given line. `start_line..end_line` + pub fn iter_vlines_over( + &self, + backwards: bool, + start: VLine, + end: VLine, + ) -> impl Iterator { + self.lines + .iter_vlines_over(self.text_prov(), backwards, start, end) + } + + /// Iterator over *relative* [`VLineInfo`]s, starting at the buffer line, `start_line`. + /// The `visual_line`s provided by this will start at 0 from your `start_line`. + /// This is preferable over `iter_lines` if you do not need to absolute visual line value. + pub fn iter_rvlines( + &self, + backwards: bool, + start: RVLine, + ) -> impl Iterator> { + self.lines.iter_rvlines(self.text_prov(), backwards, start) + } + + /// Iterator over *relative* [`VLineInfo`]s, starting at the buffer line, `start_line` and + /// ending at `end_line`. + /// `start_line..end_line` + /// This is preferable over `iter_lines` if you do not need to absolute visual line value. + pub fn iter_rvlines_over( + &self, + backwards: bool, + start: RVLine, + end_line: usize, + ) -> impl Iterator> { + self.lines + .iter_rvlines_over(self.text_prov(), backwards, start, end_line) + } + + // ==== Position Information ==== + + pub fn first_rvline_info(&self) -> VLineInfo<()> { + self.rvline_info(RVLine::default()) + } + + /// The number of lines in the document. + pub fn num_lines(&self) -> usize { + self.rope_text().num_lines() + } + + /// The last allowed buffer line in the document. + pub fn last_line(&self) -> usize { + self.rope_text().last_line() + } + + pub fn last_vline(&self) -> VLine { + self.lines.last_vline(self.text_prov()) + } + + pub fn last_rvline(&self) -> RVLine { + self.lines.last_rvline(self.text_prov()) + } + + pub fn last_rvline_info(&self) -> VLineInfo<()> { + self.rvline_info(self.last_rvline()) + } + + // ==== Line/Column Positioning ==== + + /// Convert an offset into the buffer into a line and idx. + pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) { + self.rope_text().offset_to_line_col(offset) + } + + pub fn offset_of_line(&self, offset: usize) -> usize { + self.rope_text().offset_of_line(offset) + } + + pub fn offset_of_line_col(&self, line: usize, col: usize) -> usize { + self.rope_text().offset_of_line_col(line, col) + } + + /// Get the buffer line of an offset + pub fn line_of_offset(&self, offset: usize) -> usize { + self.rope_text().line_of_offset(offset) + } + + /// Returns the offset into the buffer of the first non blank character on the given line. + pub fn first_non_blank_character_on_line(&self, line: usize) -> usize { + self.rope_text().first_non_blank_character_on_line(line) + } + + pub fn line_end_col(&self, line: usize, caret: bool) -> usize { + self.rope_text().line_end_col(line, caret) + } + + pub fn select_word(&self, offset: usize) -> (usize, usize) { + self.rope_text().select_word(offset) + } + + /// `affinity` decides whether an offset at a soft line break is considered to be on the + /// previous line or the next line. + /// If `affinity` is `CursorAffinity::Forward` and is at the very end of the wrapped line, then + /// the offset is considered to be on the next line. + pub fn vline_of_offset(&self, offset: usize, affinity: CursorAffinity) -> VLine { + self.lines + .vline_of_offset(&self.text_prov(), offset, affinity) + } + + pub fn vline_of_line(&self, line: usize) -> VLine { + self.lines.vline_of_line(&self.text_prov(), line) + } + + pub fn rvline_of_line(&self, line: usize) -> RVLine { + self.lines.rvline_of_line(&self.text_prov(), line) + } + + pub fn vline_of_rvline(&self, rvline: RVLine) -> VLine { + self.lines.vline_of_rvline(&self.text_prov(), rvline) + } + + /// Get the nearest offset to the start of the visual line. + pub fn offset_of_vline(&self, vline: VLine) -> usize { + self.lines.offset_of_vline(&self.text_prov(), vline) + } + + /// Get the visual line and column of the given offset. + /// The column is before phantom text is applied. + pub fn vline_col_of_offset(&self, offset: usize, affinity: CursorAffinity) -> (VLine, usize) { + self.lines + .vline_col_of_offset(&self.text_prov(), offset, affinity) + } + + pub fn rvline_of_offset(&self, offset: usize, affinity: CursorAffinity) -> RVLine { + self.lines + .rvline_of_offset(&self.text_prov(), offset, affinity) + } + + pub fn rvline_col_of_offset(&self, offset: usize, affinity: CursorAffinity) -> (RVLine, usize) { + self.lines + .rvline_col_of_offset(&self.text_prov(), offset, affinity) + } + + pub fn offset_of_rvline(&self, rvline: RVLine) -> usize { + self.lines.offset_of_rvline(&self.text_prov(), rvline) + } + + pub fn vline_info(&self, vline: VLine) -> VLineInfo { + let vline = vline.min(self.last_vline()); + self.iter_vlines(false, vline).next().unwrap() + } + + pub fn screen_rvline_info_of_offset( + &self, + offset: usize, + affinity: CursorAffinity, + ) -> Option> { + let rvline = self.rvline_of_offset(offset, affinity); + self.screen_lines.with_untracked(|screen_lines| { + screen_lines + .iter_vline_info() + .find(|vline_info| vline_info.rvline == rvline) + }) + } + + pub fn rvline_info(&self, rvline: RVLine) -> VLineInfo<()> { + let rvline = rvline.min(self.last_rvline()); + self.iter_rvlines(false, rvline).next().unwrap() + } + + pub fn rvline_info_of_offset(&self, offset: usize, affinity: CursorAffinity) -> VLineInfo<()> { + let rvline = self.rvline_of_offset(offset, affinity); + self.rvline_info(rvline) + } + + /// Get the first column of the overall line of the visual line + pub fn first_col(&self, info: VLineInfo) -> usize { + info.first_col(&self.text_prov()) + } + + /// Get the last column in the overall line of the visual line + pub fn last_col(&self, info: VLineInfo, caret: bool) -> usize { + info.last_col(&self.text_prov(), caret) + } + + // ==== Points of locations ==== + + pub fn max_line_width(&self) -> f64 { + self.lines.max_width() + } + + /// Returns the point into the text layout of the line at the given offset. + /// `x` being the leading edge of the character, and `y` being the baseline. + pub fn line_point_of_offset(&self, offset: usize, affinity: CursorAffinity) -> Point { + let (line, col) = self.offset_to_line_col(offset); + self.line_point_of_line_col(line, col, affinity) + } + + /// Returns the point into the text layout of the line at the given line and col. + /// `x` being the leading edge of the character, and `y` being the baseline. + pub fn line_point_of_line_col( + &self, + line: usize, + col: usize, + affinity: CursorAffinity, + ) -> Point { + let text_layout = self.text_layout(line); + hit_position_aff(&text_layout.text, col, affinity == CursorAffinity::Backward).point + } + + /// Get the (point above, point below) of a particular offset within the editor. + pub fn points_of_offset(&self, offset: usize, affinity: CursorAffinity) -> (Point, Point) { + let line = self.line_of_offset(offset); + let line_height = f64::from(self.style().line_height(line)); + + let info = self.screen_lines.with_untracked(|sl| { + sl.iter_line_info() + .find(|info| info.vline_info.interval.contains(offset)) + }); + let Some(info) = info else { + // TODO: We could do a smarter method where we get the approximate y position + // because, for example, this spot could be folded away, and so it would be better to + // supply the *nearest* position on the screen. + return (Point::new(0.0, 0.0), Point::new(0.0, 0.0)); + }; + + let y = info.vline_y; + + let x = self.line_point_of_offset(offset, affinity).x; + + (Point::new(x, y), Point::new(x, y + line_height)) + } + + /// Get the offset of a particular point within the editor. + /// The boolean indicates whether the point is inside the text or not + /// Points outside of vertical bounds will return the last line. + /// Points outside of horizontal bounds will return the last column on the line. + pub fn offset_of_point(&self, mode: Mode, point: Point) -> (usize, bool) { + let ((line, col), is_inside) = self.line_col_of_point(mode, point); + (self.offset_of_line_col(line, col), is_inside) + } + + /// Get the (line, col) of a particular point within the editor. + /// The boolean indicates whether the point is within the text bounds. + /// Points outside of vertical bounds will return the last line. + /// Points outside of horizontal bounds will return the last column on the line. + pub fn line_col_of_point(&self, mode: Mode, point: Point) -> ((usize, usize), bool) { + // TODO: this assumes that line height is constant! + let line_height = f64::from(self.style().line_height(0)); + let info = if point.y <= 0.0 { + Some(self.first_rvline_info()) + } else { + self.screen_lines + .with_untracked(|sl| { + sl.iter_line_info().find(|info| { + info.vline_y <= point.y && info.vline_y + line_height >= point.y + }) + }) + .map(|info| info.vline_info) + }; + let info = info.unwrap_or_else(|| { + for (y_idx, info) in self.iter_rvlines(false, RVLine::default()).enumerate() { + let vline_y = y_idx as f64 * line_height; + if vline_y <= point.y && vline_y + line_height >= point.y { + return info; + } + } + + self.last_rvline_info() + }); + + let rvline = info.rvline; + let line = rvline.line; + let text_layout = self.text_layout(line); + + let y = text_layout.get_layout_y(rvline.line_index).unwrap_or(0.0); + + let hit_point = text_layout.text.hit_point(Point::new(point.x, y)); + // We have to unapply the phantom text shifting in order to get back to the column in + // the actual buffer + let phantom_text = self.doc().phantom_text(line); + let col = phantom_text.before_col(hit_point.index); + // Ensure that the column doesn't end up out of bounds, so things like clicking on the far + // right end will just go to the end of the line. + let max_col = self.line_end_col(line, mode != Mode::Normal); + let mut col = col.min(max_col); + + // TODO: we need to handle affinity. Clicking at end of a wrapped line should give it a + // backwards affinity, while being at the start of the next line should be a forwards aff + + // TODO: this is a hack to get around text layouts not including spaces at the end of + // wrapped lines, but we want to be able to click on them + if !hit_point.is_inside { + // TODO(minor): this is probably wrong in some manners + col = info.last_col(&self.text_prov(), true); + } + + let tab_width = self.style().tab_width(line); + if self.style().atomic_soft_tabs(line) && tab_width > 1 { + col = snap_to_soft_tab_line_col( + &self.text(), + line, + col, + SnapDirection::Nearest, + tab_width, + ); + } + + ((line, col), hit_point.is_inside) + } + + // TODO: colposition probably has issues with wrapping? + pub fn line_horiz_col(&self, line: usize, horiz: &ColPosition, caret: bool) -> usize { + match *horiz { + ColPosition::Col(x) => { + // TODO: won't this be incorrect with phantom text? Shouldn't this just use + // line_col_of_point and get the col from that? + let text_layout = self.text_layout(line); + let hit_point = text_layout.text.hit_point(Point::new(x, 0.0)); + let n = hit_point.index; + + n.min(self.line_end_col(line, caret)) + } + ColPosition::End => self.line_end_col(line, caret), + ColPosition::Start => 0, + ColPosition::FirstNonBlank => self.first_non_blank_character_on_line(line), + } + } + + /// Advance to the right in the manner of the given mode. + /// Get the column from a horizontal at a specific line index (in a text layout) + pub fn rvline_horiz_col( + &self, + RVLine { line, line_index }: RVLine, + horiz: &ColPosition, + caret: bool, + ) -> usize { + match *horiz { + ColPosition::Col(x) => { + let text_layout = self.text_layout(line); + // TODO: It would be better to have an alternate hit point function that takes a + // line index.. + let y_pos = text_layout + .relevant_layouts() + .take(line_index) + .map(|l| (l.line_ascent + l.line_descent) as f64) + .sum(); + let hit_point = text_layout.text.hit_point(Point::new(x, y_pos)); + let n = hit_point.index; + + n.min(self.line_end_col(line, caret)) + } + // Otherwise it is the same as the other function + _ => self.line_horiz_col(line, horiz, caret), + } + } + + /// Advance to the right in the manner of the given mode. + /// This is not the same as the [`Movement::Right`] command. + pub fn move_right(&self, offset: usize, mode: Mode, count: usize) -> usize { + self.rope_text().move_right(offset, mode, count) + } + + /// Advance to the left in the manner of the given mode. + /// This is not the same as the [`Movement::Left`] command. + pub fn move_left(&self, offset: usize, mode: Mode, count: usize) -> usize { + self.rope_text().move_left(offset, mode, count) + } +} + +impl std::fmt::Debug for Editor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Editor").field(&self.id).finish() + } +} + +#[derive(Clone)] +pub struct EditorTextProv { + text: Rope, + doc: Rc, + style: Rc, + lines: Rc, + + viewport: Rect, +} +impl EditorTextProv { + // Get the text layout for a document line, creating it if needed. + pub fn text_layout(&self, line: usize) -> Arc { + self.text_layout_trigger(line, true) + } + + pub fn text_layout_trigger(&self, line: usize, trigger: bool) -> Arc { + let id = self.style.id(); + self.lines.get_init_text_layout(id, self, line, trigger) + } + + /// Create rendable whitespace layout by creating a new text layout + /// with invisible spaces and special utf8 characters that display + /// the different white space characters. + fn new_whitespace_layout( + line_content: &str, + text_layout: &TextLayout, + phantom: &PhantomTextLine, + render_whitespace: RenderWhitespace, + ) -> Option> { + let mut render_leading = false; + let mut render_boundary = false; + let mut render_between = false; + + // TODO: render whitespaces only on highlighted text + match render_whitespace { + RenderWhitespace::All => { + render_leading = true; + render_boundary = true; + render_between = true; + } + RenderWhitespace::Boundary => { + render_leading = true; + render_boundary = true; + } + RenderWhitespace::Trailing => {} // All configs include rendering trailing whitespace + RenderWhitespace::None => return None, + } + + let mut whitespace_buffer = Vec::new(); + let mut rendered_whitespaces: Vec<(char, (f64, f64))> = Vec::new(); + let mut char_found = false; + let mut col = 0; + for c in line_content.chars() { + match c { + '\t' => { + let col_left = phantom.col_after(col, true); + let col_right = phantom.col_after(col + 1, false); + let x0 = text_layout.hit_position(col_left).point.x; + let x1 = text_layout.hit_position(col_right).point.x; + whitespace_buffer.push(('\t', (x0, x1))); + } + ' ' => { + let col_left = phantom.col_after(col, true); + let col_right = phantom.col_after(col + 1, false); + let x0 = text_layout.hit_position(col_left).point.x; + let x1 = text_layout.hit_position(col_right).point.x; + whitespace_buffer.push((' ', (x0, x1))); + } + _ => { + if (char_found && render_between) + || (char_found && render_boundary && whitespace_buffer.len() > 1) + || (!char_found && render_leading) + { + rendered_whitespaces.extend(whitespace_buffer.iter()); + } + + char_found = true; + whitespace_buffer.clear(); + } + } + col += c.len_utf8(); + } + rendered_whitespaces.extend(whitespace_buffer.iter()); + + Some(rendered_whitespaces) + } +} +impl TextLayoutProvider for EditorTextProv { + // TODO: should this just return a `Rope`? + fn text(&self) -> &Rope { + &self.text + } + + fn new_text_layout( + &self, + line: usize, + _font_size: usize, + _wrap: ResolvedWrap, + ) -> Arc { + // TODO: we could share text layouts between different editor views given some knowledge of + // their wrapping + let text = self.rope_text(); + + let line_content_original = text.line_content(line); + + let font_size = self.style.font_size(self.style.font_size(line)); + + // Get the line content with newline characters replaced with spaces + // and the content without the newline characters + // TODO: cache or add some way that text layout is created to auto insert the spaces instead + // though we immediately combine with phantom text so that's a thing. + let line_content = if let Some(s) = line_content_original.strip_suffix("\r\n") { + format!("{s} ") + } else if let Some(s) = line_content_original.strip_suffix('\n') { + format!("{s} ",) + } else { + line_content_original.to_string() + }; + // Combine the phantom text with the line content + let phantom_text = self.doc.phantom_text(line); + let line_content = phantom_text.combine_with_text(&line_content); + + let family = self.style.font_family(line); + let attrs = Attrs::new() + .color(self.style.color(EditorColor::Foreground)) + .family(&family) + .font_size(font_size as f32) + .line_height(LineHeightValue::Px(self.style.line_height(line))); + let mut attrs_list = AttrsList::new(attrs); + + self.style.apply_attr_styles(line, attrs, &mut attrs_list); + + // Apply phantom text specific styling + for (offset, size, col, phantom) in phantom_text.offset_size_iter() { + let start = col + offset; + let end = start + size; + + let mut attrs = attrs; + if let Some(fg) = phantom.fg { + attrs = attrs.color(fg); + } + if let Some(phantom_font_size) = phantom.font_size { + attrs = attrs.font_size(phantom_font_size.min(font_size) as f32); + } + attrs_list.add_span(start..end, attrs); + // if let Some(font_family) = phantom.font_family.clone() { + // layout_builder = layout_builder.range_attribute( + // start..end, + // TextAttribute::FontFamily(font_family), + // ); + // } + } + + let mut text_layout = TextLayout::new(); + // TODO: we could move tab width setting to be done by the document + text_layout.set_tab_width(self.style.tab_width(line)); + text_layout.set_text(&line_content, attrs_list); + + match self.style.wrap() { + WrapMethod::None => {} + WrapMethod::EditorWidth => { + text_layout.set_wrap(Wrap::Word); + text_layout.set_size(self.viewport.width() as f32, f32::MAX); + } + WrapMethod::WrapWidth { width } => { + text_layout.set_wrap(Wrap::Word); + text_layout.set_size(width, f32::MAX); + } + // TODO: + WrapMethod::WrapColumn { .. } => {} + } + + let whitespaces = Self::new_whitespace_layout( + &line_content_original, + &text_layout, + &phantom_text, + self.style.render_whitespace(), + ); + + let indent_line = self.style.indent_line(line, &line_content_original); + + let indent = if indent_line != line { + self.text_layout(indent_line).indent + 1.0 + } else { + let offset = text.first_non_blank_character_on_line(indent_line); + let (_, col) = text.offset_to_line_col(offset); + text_layout.hit_position(col).point.x + }; + + let mut layout_line = TextLayoutLine { + text: text_layout, + extra_style: Vec::new(), + whitespaces, + indent, + }; + self.style.apply_layout_styles(line, &mut layout_line); + + Arc::new(layout_line) + } + + fn before_phantom_col(&self, line: usize, col: usize) -> usize { + self.doc.before_phantom_col(line, col) + } + + fn has_multiline_phantom(&self) -> bool { + self.doc.has_multiline_phantom() + } +} + +struct EditorFontSizes { + style: ReadSignal>, + doc: ReadSignal>, +} +impl LineFontSizeProvider for EditorFontSizes { + fn font_size(&self, line: usize) -> usize { + self.style.with_untracked(|style| style.font_size(line)) + } + + fn cache_id(&self) -> FontSizeCacheId { + let mut hasher = DefaultHasher::new(); + + // TODO: is this actually good enough for comparing cache state? + // We could just have it return an arbitrary type that impl's Eq? + self.style + .with_untracked(|style| style.id().hash(&mut hasher)); + self.doc + .with_untracked(|doc| doc.cache_rev().get_untracked().hash(&mut hasher)); + + hasher.finish() + } +} + +/// Minimum width that we'll allow the view to be wrapped at. +const MIN_WRAPPED_WIDTH: f32 = 100.0; + +/// Create various reactive effects to update the screen lines whenever relevant parts of the view, +/// doc, text layouts, viewport, etc. change. +/// This tries to be smart to a degree. +fn create_view_effects(cx: Scope, ed: &Editor) { + // Cloning is fun. + let ed2 = ed.clone(); + let ed3 = ed.clone(); + let ed4 = ed.clone(); + + // Reset cursor blinking whenever the cursor changes + { + let cursor_info = ed.cursor_info.clone(); + let cursor = ed.cursor; + cx.create_effect(move |_| { + cursor.track(); + cursor_info.reset(); + }); + } + + let update_screen_lines = |ed: &Editor| { + // This function should not depend on the viewport signal directly. + + // This is wrapped in an update to make any updates-while-updating very obvious + // which they wouldn't be if we computed and then `set`. + ed.screen_lines.update(|screen_lines| { + let new_screen_lines = ed.compute_screen_lines(screen_lines.base); + + *screen_lines = new_screen_lines; + }); + }; + + // Listen for layout events, currently only when a layout is created, and update screen + // lines based on that + ed3.lines.layout_event.listen_with(cx, move |val| { + let ed = &ed2; + // TODO: Move this logic onto screen lines somehow, perhaps just an auxilary + // function, to avoid getting confused about what is relevant where. + + match val { + LayoutEvent::CreatedLayout { line, .. } => { + let sl = ed.screen_lines.get_untracked(); + + // Intelligently update screen lines, avoiding recalculation if possible + let should_update = sl.on_created_layout(ed, line); + + if should_update { + untrack(|| { + update_screen_lines(ed); + }); + + // Ensure that it is created even after the base/viewport signals have been + // updated. + // But we have to trigger an event since it could alter the screenlines + // TODO: this has some risk for infinite looping if we're unlucky. + ed2.text_layout_trigger(line, true); + } + } + } + }); + + // TODO: should we have some debouncing for editor width? Ideally we'll be fast enough to not + // even need it, though we might not want to use a bunch of cpu whilst resizing anyway. + + let viewport_changed_trigger = cx.create_trigger(); + + // Watch for changes to the viewport so that we can alter the wrapping + // As well as updating the screen lines base + cx.create_effect(move |_| { + let ed = &ed3; + + let viewport = ed.viewport.get(); + + let wrap = match ed.style.get().wrap() { + WrapMethod::None => ResolvedWrap::None, + WrapMethod::EditorWidth => { + ResolvedWrap::Width((viewport.width() as f32).max(MIN_WRAPPED_WIDTH)) + } + WrapMethod::WrapColumn { .. } => todo!(), + WrapMethod::WrapWidth { width } => ResolvedWrap::Width(width), + }; + + ed.lines.set_wrap(wrap); + + // Update the base + let base = ed.screen_lines.with_untracked(|sl| sl.base); + + // TODO: should this be a with or with_untracked? + if viewport != base.with_untracked(|base| base.active_viewport) { + batch(|| { + base.update(|base| { + base.active_viewport = viewport; + }); + // TODO: Can I get rid of this and just call update screen lines with an + // untrack around it? + viewport_changed_trigger.notify(); + }); + } + }); + // Watch for when the viewport as changed in a relevant manner + // and for anything that `update_screen_lines` tracks. + cx.create_effect(move |_| { + viewport_changed_trigger.track(); + + update_screen_lines(&ed4); + }); +} + +pub fn normal_compute_screen_lines( + editor: &Editor, + base: RwSignal, +) -> ScreenLines { + let lines = &editor.lines; + let style = editor.style.get(); + // TODO: don't assume universal line height! + let line_height = style.line_height(0); + + let (y0, y1) = base.with_untracked(|base| (base.active_viewport.y0, base.active_viewport.y1)); + // Get the start and end (visual) lines that are visible in the viewport + let min_vline = VLine((y0 / line_height as f64).floor() as usize); + let max_vline = VLine((y1 / line_height as f64).ceil() as usize); + + let cache_rev = editor.doc.get().cache_rev().get(); + editor.lines.check_cache_rev(cache_rev); + + let min_info = editor.iter_vlines(false, min_vline).next(); + + let mut rvlines = Vec::new(); + let mut info = HashMap::new(); + + let Some(min_info) = min_info else { + return ScreenLines { + lines: Rc::new(rvlines), + info: Rc::new(info), + diff_sections: None, + base, + }; + }; + + // TODO: the original was min_line..max_line + 1, are we iterating too little now? + // the iterator is from min_vline..max_vline + let count = max_vline.get() - min_vline.get(); + let iter = lines + .iter_rvlines_init(editor.text_prov(), style.id(), min_info.rvline, false) + .take(count); + + for (i, vline_info) in iter.enumerate() { + rvlines.push(vline_info.rvline); + + let line_height = f64::from(style.line_height(vline_info.rvline.line)); + + let y_idx = min_vline.get() + i; + let vline_y = y_idx as f64 * line_height; + let line_y = vline_y - vline_info.rvline.line_index as f64 * line_height; + + // Add the information to make it cheap to get in the future. + // This y positions are shifted by the baseline y0 + info.insert( + vline_info.rvline, + LineInfo { + y: line_y - y0, + vline_y: vline_y - y0, + vline_info, + }, + ); + } + + ScreenLines { + lines: Rc::new(rvlines), + info: Rc::new(info), + diff_sections: None, + base, + } +} + +// TODO: should we put `cursor` on this structure? +/// Cursor rendering information +#[derive(Clone)] +pub struct CursorInfo { + pub hidden: RwSignal, + + pub blink_timer: RwSignal, + // TODO: should these just be rwsignals? + pub should_blink: Rc bool + 'static>, + pub blink_interval: Rc u64 + 'static>, +} +impl CursorInfo { + pub fn new(cx: Scope) -> CursorInfo { + CursorInfo { + hidden: cx.create_rw_signal(false), + + blink_timer: cx.create_rw_signal(TimerToken::INVALID), + should_blink: Rc::new(|| true), + blink_interval: Rc::new(|| 500), + } + } + + pub fn blink(&self) { + let info = self.clone(); + let blink_interval = (info.blink_interval)(); + if blink_interval > 0 && (info.should_blink)() { + let blink_timer = info.blink_timer; + let timer_token = + exec_after(Duration::from_millis(blink_interval), move |timer_token| { + if info.blink_timer.try_get_untracked() == Some(timer_token) { + info.hidden.update(|hide| { + *hide = !*hide; + }); + info.blink(); + } + }); + blink_timer.set(timer_token); + } + } + + pub fn reset(&self) { + if self.hidden.get_untracked() { + self.hidden.set(false); + } + + self.blink_timer.set(TimerToken::INVALID); + + self.blink(); + } +} diff --git a/src/views/editor/movement.rs b/src/views/editor/movement.rs new file mode 100644 index 00000000..ccd62683 --- /dev/null +++ b/src/views/editor/movement.rs @@ -0,0 +1,761 @@ +//! Movement logic for the editor. + +use floem_editor_core::{ + buffer::rope_text::RopeText, + command::MultiSelectionCommand, + cursor::{ColPosition, Cursor, CursorAffinity, CursorMode}, + mode::{Mode, MotionMode, VisualMode}, + movement::{LinePosition, Movement}, + register::Register, + selection::{SelRegion, Selection}, + soft_tab::{snap_to_soft_tab, SnapDirection}, +}; + +use super::{ + actions::CommonAction, + visual_line::{RVLine, VLineInfo}, + Editor, +}; + +/// Move a selection region by a given movement. +/// Much of the time, this will just be a matter of moving the cursor, but +/// some movements may depend on the current selection. +fn move_region( + view: &Editor, + region: &SelRegion, + affinity: &mut CursorAffinity, + count: usize, + modify: bool, + movement: &Movement, + mode: Mode, +) -> SelRegion { + let (count, region) = if count >= 1 && !modify && !region.is_caret() { + // If we're not a caret, and we are moving left/up or right/down, we want to move + // the cursor to the left or right side of the selection. + // Ex: `|abc|` -> left/up arrow key -> `|abc` + // Ex: `|abc|` -> right/down arrow key -> `abc|` + // and it doesn't matter which direction the selection is going, so we use min/max + match movement { + Movement::Left | Movement::Up => { + let leftmost = region.min(); + (count - 1, SelRegion::new(leftmost, leftmost, region.horiz)) + } + Movement::Right | Movement::Down => { + let rightmost = region.max(); + ( + count - 1, + SelRegion::new(rightmost, rightmost, region.horiz), + ) + } + _ => (count, *region), + } + } else { + (count, *region) + }; + + let (end, horiz) = move_offset( + view, + region.end, + region.horiz.as_ref(), + affinity, + count, + movement, + mode, + ); + let start = match modify { + true => region.start, + false => end, + }; + SelRegion::new(start, end, horiz) +} + +pub fn move_selection( + view: &Editor, + selection: &Selection, + affinity: &mut CursorAffinity, + count: usize, + modify: bool, + movement: &Movement, + mode: Mode, +) -> Selection { + let mut new_selection = Selection::new(); + for region in selection.regions() { + new_selection.add_region(move_region( + view, region, affinity, count, modify, movement, mode, + )); + } + new_selection +} + +// TODO: It would probably fit the overall logic better if affinity was immutable and it just returned the new affinity! +pub fn move_offset( + view: &Editor, + offset: usize, + horiz: Option<&ColPosition>, + affinity: &mut CursorAffinity, + count: usize, + movement: &Movement, + mode: Mode, +) -> (usize, Option) { + match movement { + Movement::Left => { + let new_offset = move_left(view, offset, affinity, mode, count); + + (new_offset, None) + } + Movement::Right => { + let new_offset = move_right(view, offset, affinity, mode, count); + + (new_offset, None) + } + Movement::Up => { + let (new_offset, horiz) = move_up(view, offset, affinity, horiz.cloned(), mode, count); + + (new_offset, Some(horiz)) + } + Movement::Down => { + let (new_offset, horiz) = + move_down(view, offset, affinity, horiz.cloned(), mode, count); + + (new_offset, Some(horiz)) + } + Movement::DocumentStart => { + // Put it before any inlay hints at the very start + *affinity = CursorAffinity::Backward; + (0, Some(ColPosition::Start)) + } + Movement::DocumentEnd => { + let (new_offset, horiz) = document_end(view.rope_text(), affinity, mode); + + (new_offset, Some(horiz)) + } + Movement::FirstNonBlank => { + let (new_offset, horiz) = first_non_blank(view, affinity, offset); + + (new_offset, Some(horiz)) + } + Movement::StartOfLine => { + let (new_offset, horiz) = start_of_line(view, affinity, offset); + + (new_offset, Some(horiz)) + } + Movement::EndOfLine => { + let (new_offset, horiz) = end_of_line(view, affinity, offset, mode); + + (new_offset, Some(horiz)) + } + Movement::Line(position) => { + let (new_offset, horiz) = to_line(view, offset, horiz.cloned(), mode, position); + + (new_offset, Some(horiz)) + } + Movement::Offset(offset) => { + let new_offset = view.text().prev_grapheme_offset(*offset + 1).unwrap(); + (new_offset, None) + } + Movement::WordEndForward => { + let new_offset = + view.rope_text() + .move_n_wordends_forward(offset, count, mode == Mode::Insert); + (new_offset, None) + } + Movement::WordForward => { + let new_offset = view.rope_text().move_n_words_forward(offset, count); + (new_offset, None) + } + Movement::WordBackward => { + let new_offset = view.rope_text().move_n_words_backward(offset, count, mode); + (new_offset, None) + } + Movement::NextUnmatched(char) => { + let new_offset = view.doc().find_unmatched(offset, false, *char); + + (new_offset, None) + } + Movement::PreviousUnmatched(char) => { + let new_offset = view.doc().find_unmatched(offset, true, *char); + + (new_offset, None) + } + Movement::MatchPairs => { + let new_offset = view.doc().find_matching_pair(offset); + + (new_offset, None) + } + Movement::ParagraphForward => { + let new_offset = view.rope_text().move_n_paragraphs_forward(offset, count); + + (new_offset, None) + } + Movement::ParagraphBackward => { + let new_offset = view.rope_text().move_n_paragraphs_backward(offset, count); + + (new_offset, None) + } + } +} + +fn atomic_soft_tab_width_for_offset(ed: &Editor, offset: usize) -> Option { + let line = ed.line_of_offset(offset); + let style = ed.style(); + if style.atomic_soft_tabs(line) { + Some(style.tab_width(line)) + } else { + None + } +} + +/// Move the offset to the left by `count` amount. +/// If `soft_tab_width` is `Some` (and greater than 1) then the offset will snap to the soft tab. +fn move_left( + ed: &Editor, + offset: usize, + affinity: &mut CursorAffinity, + mode: Mode, + count: usize, +) -> usize { + let rope_text = ed.rope_text(); + let mut new_offset = rope_text.move_left(offset, mode, count); + + if let Some(soft_tab_width) = atomic_soft_tab_width_for_offset(ed, offset) { + if soft_tab_width > 1 { + new_offset = snap_to_soft_tab( + rope_text.text(), + new_offset, + SnapDirection::Left, + soft_tab_width, + ); + } + } + + *affinity = CursorAffinity::Forward; + + new_offset +} + +/// Move the offset to the right by `count` amount. +/// If `soft_tab_width` is `Some` (and greater than 1) then the offset will snap to the soft tab. +fn move_right( + view: &Editor, + offset: usize, + affinity: &mut CursorAffinity, + mode: Mode, + count: usize, +) -> usize { + let rope_text = view.rope_text(); + let mut new_offset = rope_text.move_right(offset, mode, count); + + if let Some(soft_tab_width) = atomic_soft_tab_width_for_offset(view, offset) { + if soft_tab_width > 1 { + new_offset = snap_to_soft_tab( + rope_text.text(), + new_offset, + SnapDirection::Right, + soft_tab_width, + ); + } + } + + let (rvline, col) = view.rvline_col_of_offset(offset, *affinity); + let info = view.rvline_info(rvline); + + *affinity = if col == info.last_col(&view.text_prov(), false) { + CursorAffinity::Backward + } else { + CursorAffinity::Forward + }; + + new_offset +} + +fn find_prev_rvline(view: &Editor, start: RVLine, count: usize) -> Option { + if count == 0 { + return Some(start); + } + + // We can't just directly subtract count because of multi-line phantom text. + // As just subtracting count wouldn't properly skip over the phantom lines. + // So we have to search backwards for the previous line that has real content. + let mut info = None; + let mut found_count = 0; + for prev_info in view.iter_rvlines(true, start).skip(1) { + if prev_info.is_empty() { + // We skip any phantom text lines in our consideration + continue; + } + + // Otherwise we found a real line. + found_count += 1; + + if found_count == count { + // If we've completed all the count instances then we're done + info = Some(prev_info); + break; + } + // Otherwise we continue on to find the previous line with content before that. + } + + info.map(|info| info.rvline) +} + +/// Move the offset up by `count` amount. +/// `count` may be zero, because moving up in a selection just jumps to the start of the selection. +fn move_up( + view: &Editor, + offset: usize, + affinity: &mut CursorAffinity, + horiz: Option, + mode: Mode, + count: usize, +) -> (usize, ColPosition) { + let rvline = view.rvline_of_offset(offset, *affinity); + if rvline.line == 0 && rvline.line_index == 0 { + // Zeroth line + let horiz = horiz + .unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x)); + + *affinity = CursorAffinity::Backward; + + return (0, horiz); + } + + let Some(rvline) = find_prev_rvline(view, rvline, count) else { + // Zeroth line + let horiz = horiz + .unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x)); + + *affinity = CursorAffinity::Backward; + + return (0, horiz); + }; + + let horiz = + horiz.unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x)); + let col = view.rvline_horiz_col(rvline, &horiz, mode != Mode::Normal); + + // TODO: this should maybe be doing `new_offset == info.interval.start`? + *affinity = if col == 0 { + CursorAffinity::Forward + } else { + CursorAffinity::Backward + }; + + let new_offset = view.offset_of_line_col(rvline.line, col); + + (new_offset, horiz) +} + +/// Move down for when the cursor is on the last visual line. +fn move_down_last_rvline( + view: &Editor, + offset: usize, + affinity: &mut CursorAffinity, + horiz: Option, + mode: Mode, +) -> (usize, ColPosition) { + let rope_text = view.rope_text(); + + let last_line = rope_text.last_line(); + let new_offset = rope_text.line_end_offset(last_line, mode != Mode::Normal); + + // We should appear after any phantom text at the very end of the line. + *affinity = CursorAffinity::Forward; + + let horiz = + horiz.unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x)); + + (new_offset, horiz) +} + +fn find_next_rvline_info( + view: &Editor, + offset: usize, + start: RVLine, + count: usize, +) -> Option> { + // We can't just directly add count because of multi-line phantom text. + // These lines are 'not there' and also don't have any position that can be moved into + // (unlike phantom text that is mixed with real text) + // So we have to search forward for the next line that has real content. + // The typical iteration count for this is 1, and even after that it is usually only a handful. + let mut found_count = 0; + for next_info in view.iter_rvlines(false, start) { + if count == 0 { + return Some(next_info); + } + + if next_info.is_empty() { + // We skip any phantom text lines in our consideration + // TODO: Would this skip over an empty line? + continue; + } + + if next_info.interval.start <= offset { + // If we're on or before our current visual line then we skip it + continue; + } + + // Otherwise we found a real line. + found_count += 1; + + if found_count == count { + // If we've completed all the count instances then we're done + return Some(next_info); + } + // Otherwise we continue on to find the next line with content after that. + } + + None +} + +/// Move the offset down by `count` amount. +/// `count` may be zero, because moving down in a selection just jumps to the end of the selection. +fn move_down( + view: &Editor, + offset: usize, + affinity: &mut CursorAffinity, + horiz: Option, + mode: Mode, + count: usize, +) -> (usize, ColPosition) { + let rvline = view.rvline_of_offset(offset, *affinity); + + let Some(info) = find_next_rvline_info(view, offset, rvline, count) else { + // There was no next entry, this typically means that we would go past the end if we went + // further + return move_down_last_rvline(view, offset, affinity, horiz, mode); + }; + + // TODO(minor): is this the right affinity? + let horiz = + horiz.unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x)); + + let col = view.rvline_horiz_col(info.rvline, &horiz, mode != Mode::Normal); + + let new_offset = view.offset_of_line_col(info.rvline.line, col); + + *affinity = if new_offset == info.interval.start { + // The column was zero so we shift it to be at the line itself. + // This lets us move down to an empty - for example - next line and appear at the + // start of that line without coinciding with the offset at the end of the previous line. + CursorAffinity::Forward + } else { + CursorAffinity::Backward + }; + + (new_offset, horiz) +} + +fn document_end( + rope_text: impl RopeText, + affinity: &mut CursorAffinity, + mode: Mode, +) -> (usize, ColPosition) { + let last_offset = rope_text.offset_line_end(rope_text.len(), mode != Mode::Normal); + + // Put it past any inlay hints directly at the end + *affinity = CursorAffinity::Forward; + + (last_offset, ColPosition::End) +} + +fn first_non_blank( + view: &Editor, + affinity: &mut CursorAffinity, + offset: usize, +) -> (usize, ColPosition) { + let info = view.rvline_info_of_offset(offset, *affinity); + let non_blank_offset = info.first_non_blank_character(&view.text_prov()); + let start_line_offset = info.interval.start; + // TODO: is this always the correct affinity? It might be desirable for the very first character on a wrapped line? + *affinity = CursorAffinity::Forward; + + if offset > non_blank_offset { + // Jump to the first non-whitespace character if we're strictly after it + (non_blank_offset, ColPosition::FirstNonBlank) + } else { + // If we're at the start of the line, also jump to the first not blank + if start_line_offset == offset { + (non_blank_offset, ColPosition::FirstNonBlank) + } else { + // Otherwise, jump to the start of the line + (start_line_offset, ColPosition::Start) + } + } +} + +fn start_of_line( + view: &Editor, + affinity: &mut CursorAffinity, + offset: usize, +) -> (usize, ColPosition) { + let rvline = view.rvline_of_offset(offset, *affinity); + let new_offset = view.offset_of_rvline(rvline); + // TODO(minor): if the line has zero characters, it should probably be forward affinity but + // other cases might be better as backwards? + *affinity = CursorAffinity::Forward; + + (new_offset, ColPosition::Start) +} + +fn end_of_line( + view: &Editor, + affinity: &mut CursorAffinity, + offset: usize, + mode: Mode, +) -> (usize, ColPosition) { + let info = view.rvline_info_of_offset(offset, *affinity); + let new_col = info.last_col(&view.text_prov(), mode != Mode::Normal); + *affinity = if new_col == 0 { + CursorAffinity::Forward + } else { + CursorAffinity::Backward + }; + + let new_offset = view.offset_of_line_col(info.rvline.line, new_col); + + (new_offset, ColPosition::End) +} + +fn to_line( + view: &Editor, + offset: usize, + horiz: Option, + mode: Mode, + position: &LinePosition, +) -> (usize, ColPosition) { + let rope_text = view.rope_text(); + + // TODO(minor): Should this use rvline? + let line = match position { + LinePosition::Line(line) => (line - 1).min(rope_text.last_line()), + LinePosition::First => 0, + LinePosition::Last => rope_text.last_line(), + }; + // TODO(minor): is this the best affinity? + let horiz = horiz.unwrap_or_else(|| { + ColPosition::Col( + view.line_point_of_offset(offset, CursorAffinity::Backward) + .x, + ) + }); + let col = view.line_horiz_col(line, &horiz, mode != Mode::Normal); + let new_offset = rope_text.offset_of_line_col(line, col); + + (new_offset, horiz) +} + +/// Move the current cursor. +/// This will signal-update the document for some motion modes. +pub fn move_cursor( + ed: &Editor, + action: &dyn CommonAction, + cursor: &mut Cursor, + movement: &Movement, + count: usize, + modify: bool, + register: &mut Register, +) { + match cursor.mode { + CursorMode::Normal(offset) => { + let count = if let Some(motion_mode) = cursor.motion_mode.as_ref() { + count.max(motion_mode.count()) + } else { + count + }; + let (new_offset, horiz) = move_offset( + ed, + offset, + cursor.horiz.as_ref(), + &mut cursor.affinity, + count, + movement, + Mode::Normal, + ); + if let Some(motion_mode) = cursor.motion_mode.clone() { + let (moved_new_offset, _) = move_offset( + ed, + new_offset, + None, + &mut cursor.affinity, + 1, + &Movement::Right, + Mode::Insert, + ); + let range = match movement { + Movement::EndOfLine | Movement::WordEndForward => offset..moved_new_offset, + Movement::MatchPairs => { + if new_offset > offset { + offset..moved_new_offset + } else { + moved_new_offset..new_offset + } + } + _ => offset..new_offset, + }; + action.exec_motion_mode( + ed, + cursor, + motion_mode, + range, + movement.is_vertical(), + register, + ); + cursor.motion_mode = None; + } else { + cursor.mode = CursorMode::Normal(new_offset); + cursor.horiz = horiz; + } + } + CursorMode::Visual { start, end, mode } => { + let (new_offset, horiz) = move_offset( + ed, + end, + cursor.horiz.as_ref(), + &mut cursor.affinity, + count, + movement, + Mode::Visual(VisualMode::Normal), + ); + cursor.mode = CursorMode::Visual { + start, + end: new_offset, + mode, + }; + cursor.horiz = horiz; + } + CursorMode::Insert(ref selection) => { + let selection = move_selection( + ed, + selection, + &mut cursor.affinity, + count, + modify, + movement, + Mode::Insert, + ); + cursor.set_insert(selection); + } + } +} + +pub fn do_multi_selection(view: &Editor, cursor: &mut Cursor, cmd: &MultiSelectionCommand) { + use MultiSelectionCommand::*; + let rope_text = view.rope_text(); + + match cmd { + SelectUndo => { + if let CursorMode::Insert(_) = cursor.mode.clone() { + if let Some(selection) = cursor.history_selections.last().cloned() { + cursor.mode = CursorMode::Insert(selection); + } + cursor.history_selections.pop(); + } + } + InsertCursorAbove => { + if let CursorMode::Insert(mut selection) = cursor.mode.clone() { + let offset = selection.first().map(|s| s.end).unwrap_or(0); + let (new_offset, _) = move_offset( + view, + offset, + cursor.horiz.as_ref(), + &mut cursor.affinity, + 1, + &Movement::Up, + Mode::Insert, + ); + if new_offset != offset { + selection.add_region(SelRegion::new(new_offset, new_offset, None)); + } + cursor.set_insert(selection); + } + } + InsertCursorBelow => { + if let CursorMode::Insert(mut selection) = cursor.mode.clone() { + let offset = selection.last().map(|s| s.end).unwrap_or(0); + let (new_offset, _) = move_offset( + view, + offset, + cursor.horiz.as_ref(), + &mut cursor.affinity, + 1, + &Movement::Down, + Mode::Insert, + ); + if new_offset != offset { + selection.add_region(SelRegion::new(new_offset, new_offset, None)); + } + cursor.set_insert(selection); + } + } + InsertCursorEndOfLine => { + if let CursorMode::Insert(selection) = cursor.mode.clone() { + let mut new_selection = Selection::new(); + for region in selection.regions() { + let (start_line, _) = rope_text.offset_to_line_col(region.min()); + let (end_line, end_col) = rope_text.offset_to_line_col(region.max()); + for line in start_line..end_line + 1 { + let offset = if line == end_line { + rope_text.offset_of_line_col(line, end_col) + } else { + rope_text.line_end_offset(line, true) + }; + new_selection.add_region(SelRegion::new(offset, offset, None)); + } + } + cursor.set_insert(new_selection); + } + } + SelectCurrentLine => { + if let CursorMode::Insert(selection) = cursor.mode.clone() { + let mut new_selection = Selection::new(); + for region in selection.regions() { + let start_line = rope_text.line_of_offset(region.min()); + let start = rope_text.offset_of_line(start_line); + let end_line = rope_text.line_of_offset(region.max()); + let end = rope_text.offset_of_line(end_line + 1); + new_selection.add_region(SelRegion::new(start, end, None)); + } + cursor.set_insert(new_selection); + } + } + SelectAllCurrent | SelectNextCurrent | SelectSkipCurrent => { + // TODO: How should we handle these? + // The specific common editor behavior is to use the editor's find + // to do these finds and use it for the selections. + // However, we haven't included a `find` in floem-editor + } + SelectAll => { + let new_selection = Selection::region(0, rope_text.len()); + cursor.set_insert(new_selection); + } + } +} + +pub fn do_motion_mode( + ed: &Editor, + action: &dyn CommonAction, + cursor: &mut Cursor, + motion_mode: MotionMode, + register: &mut Register, +) { + if let Some(cached_motion_mode) = cursor.motion_mode.take() { + // If it's the same MotionMode discriminant, continue, count is cached in the old motion_mode. + if core::mem::discriminant(&cached_motion_mode) == core::mem::discriminant(&motion_mode) { + let offset = cursor.offset(); + action.exec_motion_mode( + ed, + cursor, + cached_motion_mode, + offset..offset, + true, + register, + ); + } + } else { + cursor.motion_mode = Some(motion_mode); + } +} + +// TODO: Write tests for the various functions. We'll need a more easily swappable API than +// `Editor` for that. diff --git a/src/views/editor/phantom_text.rs b/src/views/editor/phantom_text.rs new file mode 100644 index 00000000..d6ffae74 --- /dev/null +++ b/src/views/editor/phantom_text.rs @@ -0,0 +1,174 @@ +use std::borrow::Cow; + +use crate::{ + cosmic_text::{Attrs, AttrsList}, + peniko::Color, +}; +use smallvec::SmallVec; + +/// `PhantomText` is for text that is not in the actual document, but should be rendered with it. +/// Ex: Inlay hints, IME text, error lens' diagnostics, etc +#[derive(Debug, Clone)] +pub struct PhantomText { + /// The kind is currently used for sorting the phantom text on a line + pub kind: PhantomTextKind, + /// Column on the line that the phantom text should be displayed at + pub col: usize, + pub text: String, + pub font_size: Option, + // font_family: Option, + pub fg: Option, + pub bg: Option, + pub under_line: Option, +} + +#[derive(Debug, Clone, Copy, Ord, Eq, PartialEq, PartialOrd)] +pub enum PhantomTextKind { + /// Input methods + Ime, + /// Completion lens / Inline completion + Completion, + /// Inlay hints supplied by an LSP/PSP (like type annotations) + InlayHint, + /// Error lens + Diagnostic, +} + +/// Information about the phantom text on a specific line. +/// This has various utility functions for transforming a coordinate (typically a column) into the +/// resulting coordinate after the phantom text is combined with the line's real content. +#[derive(Debug, Default, Clone)] +pub struct PhantomTextLine { + /// This uses a smallvec because most lines rarely have more than a couple phantom texts + pub text: SmallVec<[PhantomText; 6]>, +} + +impl PhantomTextLine { + /// Translate a column position into the text into what it would be after combining + pub fn col_at(&self, pre_col: usize) -> usize { + let mut last = pre_col; + for (col_shift, size, col, _) in self.offset_size_iter() { + if pre_col >= col { + last = pre_col + col_shift + size; + } + } + + last + } + + /// Translate a column position into the text into what it would be after combining + /// If `before_cursor` is false and the cursor is right at the start then it will stay there + /// (Think 'is the phantom text before the cursor') + pub fn col_after(&self, pre_col: usize, before_cursor: bool) -> usize { + let mut last = pre_col; + for (col_shift, size, col, _) in self.offset_size_iter() { + if pre_col > col || (pre_col == col && before_cursor) { + last = pre_col + col_shift + size; + } + } + + last + } + + /// Translate a column position into the text into what it would be after combining + /// If `before_cursor` is false and the cursor is right at the start then it will stay there + /// (Think 'is the phantom text before the cursor') + /// This accepts a `PhantomTextKind` to ignore. Primarily for IME due to it needing to put the + /// cursor in the middle. + pub fn col_after_ignore( + &self, + pre_col: usize, + before_cursor: bool, + skip: impl Fn(&PhantomText) -> bool, + ) -> usize { + let mut last = pre_col; + for (col_shift, size, col, phantom) in self.offset_size_iter() { + if skip(phantom) { + continue; + } + + if pre_col > col || (pre_col == col && before_cursor) { + last = pre_col + col_shift + size; + } + } + + last + } + + /// Translate a column position into the position it would be before combining + pub fn before_col(&self, col: usize) -> usize { + let mut last = col; + for (col_shift, size, hint_col, _) in self.offset_size_iter() { + let shifted_start = hint_col + col_shift; + let shifted_end = shifted_start + size; + if col >= shifted_start { + if col >= shifted_end { + last = col - col_shift - size; + } else { + last = hint_col; + } + } + } + last + } + + /// Insert the hints at their positions in the text + pub fn combine_with_text<'a>(&self, text: &'a str) -> Cow<'a, str> { + let mut text = Cow::Borrowed(text); + let mut col_shift = 0; + + for phantom in self.text.iter() { + let location = phantom.col + col_shift; + + // Stop iterating if the location is bad + if text.get(location..).is_none() { + return text; + } + + let mut text_o = text.into_owned(); + text_o.insert_str(location, &phantom.text); + text = Cow::Owned(text_o); + + col_shift += phantom.text.len(); + } + + text + } + + /// Iterator over (col_shift, size, hint, pre_column) + /// Note that this only iterates over the ordered text, since those depend on the text for where + /// they'll be positioned + pub fn offset_size_iter( + &self, + ) -> impl Iterator + '_ { + let mut col_shift = 0; + + self.text.iter().map(move |phantom| { + let pre_col_shift = col_shift; + col_shift += phantom.text.len(); + ( + pre_col_shift, + col_shift - pre_col_shift, + phantom.col, + phantom, + ) + }) + } + + pub fn apply_attr_styles(&self, default: Attrs, attrs_list: &mut AttrsList) { + for (offset, size, col, phantom) in self.offset_size_iter() { + let start = col + offset; + let end = start + size; + + let mut attrs = default; + if let Some(fg) = phantom.fg { + attrs = attrs.color(fg); + } + if let Some(phantom_font_size) = phantom.font_size { + attrs = attrs.font_size((phantom_font_size as f32).min(attrs.font_size)); + } + + attrs_list.add_span(start..end, attrs); + } + } +} diff --git a/src/views/editor/text.rs b/src/views/editor/text.rs new file mode 100644 index 00000000..0040f456 --- /dev/null +++ b/src/views/editor/text.rs @@ -0,0 +1,814 @@ +use std::{borrow::Cow, fmt::Debug, ops::Range, rc::Rc}; + +use crate::{ + cosmic_text::{Attrs, AttrsList, FamilyOwned, Stretch, Weight}, + keyboard::ModifiersState, + peniko::Color, + reactive::{RwSignal, Scope}, +}; +use downcast_rs::{impl_downcast, Downcast}; +use floem_editor_core::{ + buffer::rope_text::{RopeText, RopeTextVal}, + command::EditCommand, + cursor::Cursor, + editor::EditType, + indent::IndentStyle, + mode::MotionMode, + register::{Clipboard, Register}, + selection::Selection, + word::WordCursor, +}; +use lapce_xi_rope::Rope; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use super::{ + actions::CommonAction, + command::{Command, CommandExecuted}, + layout::TextLayoutLine, + normal_compute_screen_lines, + phantom_text::{PhantomText, PhantomTextKind, PhantomTextLine}, + view::{ScreenLines, ScreenLinesBase}, + Editor, +}; + +use super::color::EditorColor; + +// TODO(minor): Should we get rid of this now that this is in floem? +pub struct SystemClipboard; + +impl Default for SystemClipboard { + fn default() -> Self { + Self::new() + } +} + +impl SystemClipboard { + pub fn new() -> Self { + Self + } +} + +impl Clipboard for SystemClipboard { + fn get_string(&mut self) -> Option { + crate::Clipboard::get_contents().ok() + } + + fn put_string(&mut self, s: impl AsRef) { + let _ = crate::Clipboard::set_contents(s.as_ref().to_string()); + } +} + +#[derive(Clone)] +pub struct Preedit { + pub text: String, + pub cursor: Option<(usize, usize)>, + pub offset: usize, +} + +/// IME Preedit +/// This is used for IME input, and must be owned by the `Document`. +#[derive(Debug, Clone)] +pub struct PreeditData { + pub preedit: RwSignal>, +} +impl PreeditData { + pub fn new(cx: Scope) -> PreeditData { + PreeditData { + preedit: cx.create_rw_signal(None), + } + } +} + +/// A document. This holds text. +pub trait Document: DocumentPhantom + Downcast { + /// Get the text of the document + /// Note: typically you should call [`Document::rope_text`] as that provides more checks and + /// utility functions. + fn text(&self) -> Rope; + + fn rope_text(&self) -> RopeTextVal { + RopeTextVal::new(self.text()) + } + + fn cache_rev(&self) -> RwSignal; + + /// Find the next/previous offset of the match of the given character. + /// This is intended for use by the [`Movement::NextUnmatched`] and + /// [`Movement::PreviousUnmatched`] commands. + fn find_unmatched(&self, offset: usize, previous: bool, ch: char) -> usize { + let text = self.text(); + let mut cursor = WordCursor::new(&text, offset); + let new_offset = if previous { + cursor.previous_unmatched(ch) + } else { + cursor.next_unmatched(ch) + }; + + new_offset.unwrap_or(offset) + } + + /// Find the offset of the matching pair character. + /// This is intended for use by the [`Movement::MatchPairs`] command. + fn find_matching_pair(&self, offset: usize) -> usize { + WordCursor::new(&self.text(), offset) + .match_pairs() + .unwrap_or(offset) + } + + fn preedit(&self) -> PreeditData; + + // TODO: I don't like passing `under_line` as a parameter but `Document` doesn't have styling + // should we just move preedit + phantom text into `Styling`? + fn preedit_phantom(&self, under_line: Option, line: usize) -> Option { + let preedit = self.preedit().preedit.get_untracked()?; + + let rope_text = self.rope_text(); + + let (ime_line, col) = rope_text.offset_to_line_col(preedit.offset); + + if line != ime_line { + return None; + } + + Some(PhantomText { + kind: PhantomTextKind::Ime, + text: preedit.text, + col, + font_size: None, + fg: None, + bg: None, + under_line, + }) + } + + /// Compute the visible screen lines. + /// Note: you should typically *not* need to implement this, unless you have some custom + /// behavior. Unfortunately this needs an `&self` to be a trait object. So don't call `.update` + /// on `Self` + fn compute_screen_lines( + &self, + editor: &Editor, + base: RwSignal, + ) -> ScreenLines { + normal_compute_screen_lines(editor, base) + } + + /// Run a command on the document. + /// The `ed` will contain this document (at some level, if it was wrapped then it may not be + /// directly `Rc`) + fn run_command( + &self, + ed: &Editor, + cmd: &Command, + count: Option, + modifiers: ModifiersState, + ) -> CommandExecuted; + + fn receive_char(&self, ed: &Editor, c: &str); + + /// Perform a single edit. + fn edit_single(&self, selection: Selection, content: &str, edit_type: EditType) { + let mut iter = std::iter::once((selection, content)); + self.edit(&mut iter, edit_type); + } + + /// Perform the edit(s) on this document. + /// This intentionally does not require an `Editor` as this is primarily intended for use by + /// code that wants to modify the document from 'outside' the usual keybinding/command logic. + /// ```rust,ignore + /// let editor: TextEditor = text_editor(); + /// let doc: Rc = editor.doc(); + /// + /// stack(( + /// editor, + /// button(|| "Append 'Hello'").on_click_stop(move |_| { + /// let text = doc.text(); + /// doc.edit_single(Selection::caret(text.len()), "Hello", EditType::InsertChars); + /// }) + /// )) + /// ``` + fn edit(&self, iter: &mut dyn Iterator, edit_type: EditType); +} + +impl_downcast!(Document); + +pub trait DocumentPhantom { + fn phantom_text(&self, line: usize) -> PhantomTextLine; + + /// Translate a column position into the position it would be before combining with + /// the phantom text. + fn before_phantom_col(&self, line: usize, col: usize) -> usize { + let phantom = self.phantom_text(line); + phantom.before_col(col) + } + + fn has_multiline_phantom(&self) -> bool { + true + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum WrapMethod { + None, + #[default] + EditorWidth, + WrapColumn { + col: usize, + }, + WrapWidth { + width: f32, + }, +} +impl WrapMethod { + pub fn is_none(&self) -> bool { + matches!(self, WrapMethod::None) + } + + pub fn is_constant(&self) -> bool { + matches!( + self, + WrapMethod::None | WrapMethod::WrapColumn { .. } | WrapMethod::WrapWidth { .. } + ) + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))] +pub enum RenderWhitespace { + #[default] + None, + All, + Boundary, + Trailing, +} + +/// There's currently three stages of styling text: +/// - `Attrs`: This sets the default values for the text +/// - Default font size, font family, etc. +/// - `AttrsList`: This lets you set spans of text to have different styling +/// - Syntax highlighting, bolding specific words, etc. +/// Then once the text layout for the line is created from that, we have: +/// - `Layout Styles`: Where it may depend on the position of text in the line (after wrapping) +/// - Outline boxes +/// +/// TODO: We could unify the first two steps if we expose a `.defaults_mut()` on `AttrsList`, and +/// then `Styling` mostly just applies whatever attributes it wants and defaults at the same time? +/// but that would complicate pieces of code that need the font size or line height independently. +pub trait Styling { + // TODO: use a more granular system for invalidating styling, because it may simply be that + // one line gets different styling. + /// The id for caching the styling. + fn id(&self) -> u64; + + fn font_size(&self, _line: usize) -> usize { + 16 + } + + fn line_height(&self, line: usize) -> f32 { + let font_size = self.font_size(line) as f32; + (1.5 * font_size).round().max(font_size) + } + + fn font_family(&self, _line: usize) -> Cow<[FamilyOwned]> { + Cow::Borrowed(&[FamilyOwned::SansSerif]) + } + + fn weight(&self, _line: usize) -> Weight { + Weight::NORMAL + } + + // TODO(minor): better name? + fn italic_style(&self, _line: usize) -> crate::cosmic_text::Style { + crate::cosmic_text::Style::Normal + } + + fn stretch(&self, _line: usize) -> Stretch { + Stretch::Normal + } + + fn indent_style(&self) -> IndentStyle { + IndentStyle::Spaces(4) + } + + /// Which line the indentation line should be based off of + /// This is used for lining it up under a scope. + fn indent_line(&self, line: usize, _line_content: &str) -> usize { + line + } + + fn tab_width(&self, _line: usize) -> usize { + 4 + } + + /// Whether the cursor should treat leading soft tabs as if they are hard tabs + fn atomic_soft_tabs(&self, _line: usize) -> bool { + false + } + + // TODO: get other style information based on EditorColor enum? + // TODO: line_style equivalent? + + /// Apply custom attribute styles to the line + fn apply_attr_styles(&self, _line: usize, _default: Attrs, _attrs: &mut AttrsList) {} + + // TODO: we could have line-specific wrapping, but that would need some extra functions for + // questions that visual lines' [`Lines`] uses + fn wrap(&self) -> WrapMethod { + WrapMethod::EditorWidth + } + + fn render_whitespace(&self) -> RenderWhitespace { + RenderWhitespace::None + } + + fn apply_layout_styles(&self, _line: usize, _layout_line: &mut TextLayoutLine) {} + + // TODO: should we replace `foreground` with using `editor.foreground` here? + fn color(&self, color: EditorColor) -> Color { + default_light_color(color) + } + + /// Whether it should draw the cursor caret on the given line. + /// Note that these are extra conditions on top of the typical hide cursor & + /// the editor being active conditions + /// This is called whenever we paint the line. + fn paint_caret(&self, _editor: &Editor, _line: usize) -> bool { + true + } +} + +pub fn default_light_color(color: EditorColor) -> Color { + let fg = Color::rgb8(0x38, 0x3A, 0x42); + let bg = Color::rgb8(0xFA, 0xFA, 0xFA); + let blue = Color::rgb8(0x40, 0x78, 0xF2); + let grey = Color::rgb8(0xE5, 0xE5, 0xE6); + match color { + EditorColor::Background => bg, + EditorColor::Scrollbar => Color::rgba8(0xB4, 0xB4, 0xB4, 0xBB), + EditorColor::DropdownShadow => Color::rgb8(0xB4, 0xB4, 0xB4), + EditorColor::Foreground => fg, + EditorColor::Dim => Color::rgb8(0xA0, 0xA1, 0xA7), + EditorColor::Focus => Color::BLACK, + EditorColor::Caret => Color::rgb8(0x52, 0x6F, 0xFF), + EditorColor::Selection => grey, + EditorColor::CurrentLine => Color::rgb8(0xF2, 0xF2, 0xF2), + EditorColor::Link => blue, + EditorColor::VisibleWhitespace => grey, + EditorColor::IndentGuide => grey, + EditorColor::StickyHeaderBackground => bg, + EditorColor::PreeditUnderline => fg, + } +} + +pub fn default_dark_color(color: EditorColor) -> Color { + let fg = Color::rgb8(0xAB, 0xB2, 0xBF); + let bg = Color::rgb8(0x28, 0x2C, 0x34); + let blue = Color::rgb8(0x61, 0xAF, 0xEF); + let grey = Color::rgb8(0x3E, 0x44, 0x51); + match color { + EditorColor::Background => bg, + EditorColor::Scrollbar => Color::rgba8(0x3E, 0x44, 0x51, 0xBB), + EditorColor::DropdownShadow => Color::BLACK, + EditorColor::Foreground => fg, + EditorColor::Dim => Color::rgb8(0x5C, 0x63, 0x70), + EditorColor::Focus => Color::rgb8(0xCC, 0xCC, 0xCC), + EditorColor::Caret => Color::rgb8(0x52, 0x8B, 0xFF), + EditorColor::Selection => grey, + EditorColor::CurrentLine => Color::rgb8(0x2C, 0x31, 0x3c), + EditorColor::Link => blue, + EditorColor::VisibleWhitespace => grey, + EditorColor::IndentGuide => grey, + EditorColor::StickyHeaderBackground => bg, + EditorColor::PreeditUnderline => fg, + } +} + +pub type DocumentRef = Rc; + +/// A document-wrapper for handling commands. +pub struct ExtCmdDocument { + pub doc: D, + /// Called whenever [`Document::run_command`] is called. + /// If `handler` returns [`CommandExecuted::Yes`] then the default handler on `doc: D` will not + /// be called. + pub handler: F, +} +impl< + D: Document, + F: Fn(&Editor, &Command, Option, ModifiersState) -> CommandExecuted + 'static, + > ExtCmdDocument +{ + pub fn new(doc: D, handler: F) -> ExtCmdDocument { + ExtCmdDocument { doc, handler } + } +} +// TODO: it'd be nice if there was some macro to wrap all of the `Document` methods +// but replace specific ones +impl Document for ExtCmdDocument +where + D: Document, + F: Fn(&Editor, &Command, Option, ModifiersState) -> CommandExecuted + 'static, +{ + fn text(&self) -> Rope { + self.doc.text() + } + + fn rope_text(&self) -> RopeTextVal { + self.doc.rope_text() + } + + fn cache_rev(&self) -> RwSignal { + self.doc.cache_rev() + } + + fn find_unmatched(&self, offset: usize, previous: bool, ch: char) -> usize { + self.doc.find_unmatched(offset, previous, ch) + } + + fn find_matching_pair(&self, offset: usize) -> usize { + self.doc.find_matching_pair(offset) + } + + fn preedit(&self) -> PreeditData { + self.doc.preedit() + } + + fn preedit_phantom(&self, under_line: Option, line: usize) -> Option { + self.doc.preedit_phantom(under_line, line) + } + + fn compute_screen_lines( + &self, + editor: &Editor, + base: RwSignal, + ) -> ScreenLines { + self.doc.compute_screen_lines(editor, base) + } + + fn run_command( + &self, + ed: &Editor, + cmd: &Command, + count: Option, + modifiers: ModifiersState, + ) -> CommandExecuted { + if (self.handler)(ed, cmd, count, modifiers) == CommandExecuted::Yes { + return CommandExecuted::Yes; + } + + self.doc.run_command(ed, cmd, count, modifiers) + } + + fn receive_char(&self, ed: &Editor, c: &str) { + self.doc.receive_char(ed, c) + } + + fn edit_single(&self, selection: Selection, content: &str, edit_type: EditType) { + self.doc.edit_single(selection, content, edit_type) + } + + fn edit(&self, iter: &mut dyn Iterator, edit_type: EditType) { + self.doc.edit(iter, edit_type) + } +} +impl DocumentPhantom for ExtCmdDocument +where + D: Document, + F: Fn(&Editor, &Command, Option, ModifiersState) -> CommandExecuted, +{ + fn phantom_text(&self, line: usize) -> PhantomTextLine { + self.doc.phantom_text(line) + } + + fn has_multiline_phantom(&self) -> bool { + self.doc.has_multiline_phantom() + } + + fn before_phantom_col(&self, line: usize, col: usize) -> usize { + self.doc.before_phantom_col(line, col) + } +} +impl CommonAction for ExtCmdDocument +where + D: Document + CommonAction, + F: Fn(&Editor, &Command, Option, ModifiersState) -> CommandExecuted, +{ + fn exec_motion_mode( + &self, + ed: &Editor, + cursor: &mut Cursor, + motion_mode: MotionMode, + range: Range, + is_vertical: bool, + register: &mut Register, + ) { + self.doc + .exec_motion_mode(ed, cursor, motion_mode, range, is_vertical, register) + } + + fn do_edit( + &self, + ed: &Editor, + cursor: &mut Cursor, + cmd: &EditCommand, + modal: bool, + register: &mut Register, + smart_tab: bool, + ) -> bool { + self.doc + .do_edit(ed, cursor, cmd, modal, register, smart_tab) + } +} + +pub const SCALE_OR_SIZE_LIMIT: f32 = 5.0; + +#[derive(Debug, Clone)] +pub struct SimpleStyling { + id: u64, + font_size: usize, + // TODO: should we really have this be a float? Shouldn't it just be a LineHeightValue? + /// If less than 5.0, line height will be a multiple of the font size + line_height: f32, + font_family: Vec, + weight: Weight, + italic_style: crate::cosmic_text::Style, + stretch: Stretch, + indent_style: IndentStyle, + tab_width: usize, + atomic_soft_tabs: bool, + wrap: WrapMethod, + color: C, +} +impl SimpleStyling { + pub fn builder() -> SimpleStylingBuilder { + SimpleStylingBuilder::default() + } +} +impl Color> SimpleStyling { + pub fn new(color: C) -> SimpleStyling { + SimpleStyling { + id: 0, + font_size: 16, + line_height: 1.5, + font_family: vec![FamilyOwned::SansSerif], + weight: Weight::NORMAL, + italic_style: crate::cosmic_text::Style::Normal, + stretch: Stretch::Normal, + indent_style: IndentStyle::Spaces(4), + tab_width: 4, + atomic_soft_tabs: false, + wrap: WrapMethod::EditorWidth, + color, + } + } +} +impl SimpleStyling Color> { + pub fn light() -> SimpleStyling Color> { + SimpleStyling::new(default_light_color) + } + + pub fn dark() -> SimpleStyling Color> { + SimpleStyling::new(default_dark_color) + } +} +impl Color> SimpleStyling { + pub fn increment_id(&mut self) { + self.id += 1; + } + + pub fn set_font_size(&mut self, font_size: usize) { + self.font_size = font_size; + self.increment_id(); + } + + pub fn set_line_height(&mut self, line_height: f32) { + self.line_height = line_height; + self.increment_id(); + } + + pub fn set_font_family(&mut self, font_family: Vec) { + self.font_family = font_family; + self.increment_id(); + } + + pub fn set_weight(&mut self, weight: Weight) { + self.weight = weight; + self.increment_id(); + } + + pub fn set_italic_style(&mut self, italic_style: crate::cosmic_text::Style) { + self.italic_style = italic_style; + self.increment_id(); + } + + pub fn set_stretch(&mut self, stretch: Stretch) { + self.stretch = stretch; + self.increment_id(); + } + + pub fn set_indent_style(&mut self, indent_style: IndentStyle) { + self.indent_style = indent_style; + self.increment_id(); + } + + pub fn set_tab_width(&mut self, tab_width: usize) { + self.tab_width = tab_width; + self.increment_id(); + } + + pub fn set_atomic_soft_tabs(&mut self, atomic_soft_tabs: bool) { + self.atomic_soft_tabs = atomic_soft_tabs; + self.increment_id(); + } + + pub fn set_wrap(&mut self, wrap: WrapMethod) { + self.wrap = wrap; + self.increment_id(); + } + + pub fn set_color(&mut self, color: C) { + self.color = color; + self.increment_id(); + } +} +impl Default for SimpleStyling Color> { + fn default() -> Self { + SimpleStyling::new(default_light_color) + } +} +impl Color> Styling for SimpleStyling { + fn id(&self) -> u64 { + 0 + } + + fn font_size(&self, _line: usize) -> usize { + self.font_size + } + + fn line_height(&self, _line: usize) -> f32 { + let line_height = if self.line_height < SCALE_OR_SIZE_LIMIT { + self.line_height * self.font_size as f32 + } else { + self.line_height + }; + + // Prevent overlapping lines + (line_height.round() as usize).max(self.font_size) as f32 + } + + fn font_family(&self, _line: usize) -> Cow<[FamilyOwned]> { + Cow::Borrowed(&self.font_family) + } + + fn weight(&self, _line: usize) -> Weight { + self.weight + } + + fn italic_style(&self, _line: usize) -> crate::cosmic_text::Style { + self.italic_style + } + + fn stretch(&self, _line: usize) -> Stretch { + self.stretch + } + + fn indent_style(&self) -> IndentStyle { + self.indent_style + } + + fn tab_width(&self, _line: usize) -> usize { + self.tab_width + } + + fn atomic_soft_tabs(&self, _line: usize) -> bool { + self.atomic_soft_tabs + } + + fn apply_attr_styles(&self, _line: usize, _default: Attrs, _attrs: &mut AttrsList) {} + + fn wrap(&self) -> WrapMethod { + self.wrap + } + + fn apply_layout_styles(&self, _line: usize, _layout_line: &mut TextLayoutLine) {} + + fn color(&self, color: EditorColor) -> Color { + (self.color)(color) + } +} + +#[derive(Default, Clone)] +pub struct SimpleStylingBuilder { + font_size: Option, + line_height: Option, + font_family: Option>, + weight: Option, + italic_style: Option, + stretch: Option, + indent_style: Option, + tab_width: Option, + atomic_soft_tabs: Option, + wrap: Option, +} +impl SimpleStylingBuilder { + /// Set the font size + /// Default: 16 + pub fn font_size(&mut self, font_size: usize) -> &mut Self { + self.font_size = Some(font_size); + self + } + + /// Set the line height + /// Default: 1.5 + pub fn line_height(&mut self, line_height: f32) -> &mut Self { + self.line_height = Some(line_height); + self + } + + /// Set the font families used + /// Default: `[FamilyOwned::SansSerif]` + pub fn font_family(&mut self, font_family: Vec) -> &mut Self { + self.font_family = Some(font_family); + self + } + + /// Set the font weight (such as boldness or thinness) + /// Default: `Weight::NORMAL` + pub fn weight(&mut self, weight: Weight) -> &mut Self { + self.weight = Some(weight); + self + } + + /// Set the italic style + /// Default: `Style::Normal` + pub fn italic_style(&mut self, italic_style: crate::cosmic_text::Style) -> &mut Self { + self.italic_style = Some(italic_style); + self + } + + /// Set the font stretch + /// Default: `Stretch::Normal` + pub fn stretch(&mut self, stretch: Stretch) -> &mut Self { + self.stretch = Some(stretch); + self + } + + /// Set the indent style + /// Default: `IndentStyle::Spaces(4)` + pub fn indent_style(&mut self, indent_style: IndentStyle) -> &mut Self { + self.indent_style = Some(indent_style); + self + } + + /// Set the tab width + /// Default: 4 + pub fn tab_width(&mut self, tab_width: usize) -> &mut Self { + self.tab_width = Some(tab_width); + self + } + + /// Set whether the cursor should treat leading soft tabs as if they are hard tabs + /// Default: false + pub fn atomic_soft_tabs(&mut self, atomic_soft_tabs: bool) -> &mut Self { + self.atomic_soft_tabs = Some(atomic_soft_tabs); + self + } + + /// Set the wrapping method + /// Default: `WrapMethod::EditorWidth` + pub fn wrap(&mut self, wrap: WrapMethod) -> &mut Self { + self.wrap = Some(wrap); + self + } + + /// Build the styling with the given color scheme + pub fn build Color>(&self, color: C) -> SimpleStyling { + let default = SimpleStyling::new(color); + SimpleStyling { + id: 0, + font_size: self.font_size.unwrap_or(default.font_size), + line_height: self.line_height.unwrap_or(default.line_height), + font_family: self.font_family.clone().unwrap_or(default.font_family), + weight: self.weight.unwrap_or(default.weight), + italic_style: self.italic_style.unwrap_or(default.italic_style), + stretch: self.stretch.unwrap_or(default.stretch), + indent_style: self.indent_style.unwrap_or(default.indent_style), + tab_width: self.tab_width.unwrap_or(default.tab_width), + atomic_soft_tabs: self.atomic_soft_tabs.unwrap_or(default.atomic_soft_tabs), + wrap: self.wrap.unwrap_or(default.wrap), + color: default.color, + } + } + + /// Build with the default light color scheme + pub fn build_light(&self) -> SimpleStyling Color> { + self.build(default_light_color) + } + + /// Build with the default dark color scheme + pub fn build_dark(&self) -> SimpleStyling Color> { + self.build(default_dark_color) + } +} diff --git a/src/views/editor/text_document.rs b/src/views/editor/text_document.rs new file mode 100644 index 00000000..c57a5f9f --- /dev/null +++ b/src/views/editor/text_document.rs @@ -0,0 +1,298 @@ +use std::{ + cell::{Cell, RefCell}, + collections::HashMap, + ops::Range, + rc::Rc, +}; + +use floem_editor_core::{ + buffer::{rope_text::RopeText, Buffer, InvalLines}, + command::EditCommand, + cursor::Cursor, + editor::{Action, EditConf, EditType}, + mode::{Mode, MotionMode}, + register::Register, + selection::Selection, + word::WordCursor, +}; +use floem_reactive::{RwSignal, Scope}; +use floem_winit::keyboard::ModifiersState; +use lapce_xi_rope::{Rope, RopeDelta}; +use smallvec::{smallvec, SmallVec}; + +use super::{ + actions::{handle_command_default, CommonAction}, + command::{Command, CommandExecuted}, + id::EditorId, + phantom_text::PhantomTextLine, + text::{Document, DocumentPhantom, PreeditData, SystemClipboard}, + Editor, +}; + +type PreCommandFn = Box CommandExecuted>; +#[derive(Debug, Clone)] +pub struct PreCommand<'a> { + pub editor: &'a Editor, + pub cmd: &'a Command, + pub count: Option, + pub mods: ModifiersState, +} + +type OnUpdateFn = Box; +#[derive(Debug, Clone)] +pub struct OnUpdate<'a> { + /// Optional because the document can be edited from outside any editor views + pub editor: Option<&'a Editor>, + deltas: &'a [(Rope, RopeDelta, InvalLines)], +} +impl<'a> OnUpdate<'a> { + pub fn deltas(&self) -> impl Iterator { + self.deltas.iter().map(|(_, delta, _)| delta) + } +} + +/// A simple text document that holds content in a rope. +/// This can be used as a base structure for common operations. +#[derive(Clone)] +pub struct TextDocument { + buffer: RwSignal, + cache_rev: RwSignal, + preedit: PreeditData, + + /// Whether to keep the indent of the previous line when inserting a new line + pub keep_indent: Cell, + /// Whether to automatically indent the new line via heuristics + pub auto_indent: Cell, + + /// (cmd: &Command, count: Option, modifiers: ModifierState) + /// Ran before a command is executed. If it says that it executed the command, then handlers + /// after it will not be called. + pre_command: Rc>>>, + + on_updates: Rc>>, +} +impl TextDocument { + pub fn new(cx: Scope, text: impl Into) -> TextDocument { + let text = text.into(); + let buffer = Buffer::new(text); + let preedit = PreeditData { + preedit: cx.create_rw_signal(None), + }; + + TextDocument { + buffer: cx.create_rw_signal(buffer), + cache_rev: cx.create_rw_signal(0), + preedit, + keep_indent: Cell::new(true), + auto_indent: Cell::new(false), + pre_command: Rc::new(RefCell::new(HashMap::new())), + on_updates: Rc::new(RefCell::new(SmallVec::new())), + } + } + + fn update_cache_rev(&self) { + self.cache_rev.try_update(|cache_rev| { + *cache_rev += 1; + }); + } + + fn on_update(&self, ed: Option<&Editor>, deltas: &[(Rope, RopeDelta, InvalLines)]) { + let on_updates = self.on_updates.borrow(); + let data = OnUpdate { editor: ed, deltas }; + for on_update in on_updates.iter() { + on_update(data.clone()); + } + } + + pub fn add_pre_command( + &self, + id: EditorId, + pre_command: impl Fn(PreCommand) -> CommandExecuted + 'static, + ) { + let pre_command: PreCommandFn = Box::new(pre_command); + self.pre_command + .borrow_mut() + .insert(id, smallvec![pre_command]); + } + + pub fn clear_pre_commands(&self) { + self.pre_command.borrow_mut().clear(); + } + + pub fn add_on_update(&self, on_update: impl Fn(OnUpdate) + 'static) { + self.on_updates.borrow_mut().push(Box::new(on_update)); + } + + pub fn clear_on_updates(&self) { + self.on_updates.borrow_mut().clear(); + } +} +impl Document for TextDocument { + fn text(&self) -> Rope { + self.buffer.with_untracked(|buffer| buffer.text().clone()) + } + + fn cache_rev(&self) -> RwSignal { + self.cache_rev + } + + fn preedit(&self) -> PreeditData { + self.preedit.clone() + } + + fn run_command( + &self, + ed: &Editor, + cmd: &Command, + count: Option, + modifiers: ModifiersState, + ) -> CommandExecuted { + let pre_commands = self.pre_command.borrow(); + let pre_commands = pre_commands.get(&ed.id()); + let pre_commands = pre_commands.iter().flat_map(|c| c.iter()); + let data = PreCommand { + editor: ed, + cmd, + count, + mods: modifiers, + }; + + for pre_command in pre_commands { + if pre_command(data.clone()) == CommandExecuted::Yes { + return CommandExecuted::Yes; + } + } + + handle_command_default(ed, self, cmd, count, modifiers) + } + + fn receive_char(&self, ed: &Editor, c: &str) { + if ed.read_only.get_untracked() { + return; + } + + let mode = ed.cursor.with_untracked(|c| c.get_mode()); + if mode == Mode::Insert { + let mut cursor = ed.cursor.get_untracked(); + { + let old_cursor_mode = cursor.mode.clone(); + let deltas = self + .buffer + .try_update(|buffer| { + Action::insert( + &mut cursor, + buffer, + c, + &|_, c, offset| { + WordCursor::new(&self.text(), offset).previous_unmatched(c) + }, + // TODO: ? + false, + false, + ) + }) + .unwrap(); + self.buffer.update(|buffer| { + buffer.set_cursor_before(old_cursor_mode); + buffer.set_cursor_after(cursor.mode.clone()); + }); + // TODO: line specific invalidation + self.update_cache_rev(); + self.on_update(Some(ed), &deltas); + } + ed.cursor.set(cursor); + } + } + + fn edit(&self, iter: &mut dyn Iterator, edit_type: EditType) { + let deltas = self + .buffer + .try_update(|buffer| buffer.edit(iter, edit_type)); + let deltas = deltas.map(|x| [x]); + let deltas = deltas.as_ref().map(|x| x as &[_]).unwrap_or(&[]); + + self.update_cache_rev(); + self.on_update(None, deltas); + } +} +impl DocumentPhantom for TextDocument { + fn phantom_text(&self, _line: usize) -> PhantomTextLine { + PhantomTextLine::default() + } + + fn has_multiline_phantom(&self) -> bool { + false + } +} +impl CommonAction for TextDocument { + fn exec_motion_mode( + &self, + _ed: &Editor, + cursor: &mut Cursor, + motion_mode: MotionMode, + range: Range, + is_vertical: bool, + register: &mut Register, + ) { + self.buffer.try_update(move |buffer| { + Action::execute_motion_mode(cursor, buffer, motion_mode, range, is_vertical, register) + }); + } + + fn do_edit( + &self, + ed: &Editor, + cursor: &mut Cursor, + cmd: &EditCommand, + modal: bool, + register: &mut Register, + smart_tab: bool, + ) -> bool { + if ed.read_only.get_untracked() && !cmd.not_changing_buffer() { + return false; + } + + let mut clipboard = SystemClipboard::new(); + let old_cursor = cursor.mode.clone(); + // TODO: configurable comment token + let deltas = self + .buffer + .try_update(|buffer| { + Action::do_edit( + cursor, + buffer, + cmd, + &mut clipboard, + register, + EditConf { + modal, + comment_token: "", + smart_tab, + keep_indent: self.keep_indent.get(), + auto_indent: self.auto_indent.get(), + }, + ) + }) + .unwrap(); + + if !deltas.is_empty() { + self.buffer.update(|buffer| { + buffer.set_cursor_before(old_cursor); + buffer.set_cursor_after(cursor.mode.clone()); + }); + } + + self.update_cache_rev(); + self.on_update(Some(ed), &deltas); + + !deltas.is_empty() + } +} + +impl std::fmt::Debug for TextDocument { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut s = f.debug_struct("TextDocument"); + s.field("text", &self.text()); + s.finish() + } +} diff --git a/src/views/editor/view.rs b/src/views/editor/view.rs new file mode 100644 index 00000000..10fb04b1 --- /dev/null +++ b/src/views/editor/view.rs @@ -0,0 +1,1291 @@ +use std::{collections::HashMap, ops::RangeInclusive, rc::Rc}; + +use crate::{ + action::{set_ime_allowed, set_ime_cursor_area}, + context::{LayoutCx, PaintCx, UpdateCx}, + cosmic_text::{Attrs, AttrsList, TextLayout}, + event::{Event, EventListener}, + id::Id, + keyboard::{Key, ModifiersState, NamedKey}, + kurbo::{BezPath, Line, Point, Rect, Size, Vec2}, + peniko::Color, + reactive::{batch, create_effect, create_memo, create_rw_signal, Memo, RwSignal, Scope}, + style::{CursorStyle, Style}, + taffy::node::Node, + view::{AnyWidget, View, ViewData, Widget}, + views::{clip, container, empty, label, scroll, stack, Decorators}, + EventPropagation, Renderer, +}; +use floem_editor_core::{ + cursor::{ColPosition, CursorAffinity, CursorMode}, + mode::{Mode, VisualMode}, +}; + +use crate::views::editor::{ + command::CommandExecuted, + gutter::editor_gutter_view, + keypress::{key::KeyInput, press::KeyPress}, + layout::LineExtraStyle, + phantom_text::PhantomTextKind, + visual_line::{RVLine, VLineInfo}, +}; + +use super::{color::EditorColor, Editor, CHAR_WIDTH}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum DiffSectionKind { + NoCode, + Added, + Removed, +} + +#[derive(Clone, PartialEq)] +pub struct DiffSection { + /// The y index that the diff section is at. + /// This is multiplied by the line height to get the y position. + /// So this can roughly be considered as the `VLine of the start of this diff section, but it + /// isn't necessarily convertable to a `VLine` due to jumping over empty code sections. + pub y_idx: usize, + pub height: usize, + pub kind: DiffSectionKind, +} + +// TODO(minor): We have diff sections in screen lines because Lapce uses them, but +// we don't really have support for diffs in floem-editor! Is there a better design for this? +// Possibly we should just move that out to a separate field on Lapce's editor. +#[derive(Clone, PartialEq)] +pub struct ScreenLines { + pub lines: Rc>, + /// Guaranteed to have an entry for each `VLine` in `lines` + /// You should likely use accessor functions rather than this directly. + pub info: Rc>, + pub diff_sections: Option>>, + /// The base y position that all the y positions inside `info` are relative to. + /// This exists so that if a text layout is created outside of the view, we don't have to + /// completely recompute the screen lines (or do somewhat intricate things to update them) + /// we simply have to update the `base_y`. + pub base: RwSignal, +} +impl ScreenLines { + pub fn new(cx: Scope, viewport: Rect) -> ScreenLines { + ScreenLines { + lines: Default::default(), + info: Default::default(), + diff_sections: Default::default(), + base: cx.create_rw_signal(ScreenLinesBase { + active_viewport: viewport, + }), + } + } + + pub fn is_empty(&self) -> bool { + self.lines.is_empty() + } + + pub fn clear(&mut self, viewport: Rect) { + self.lines = Default::default(); + self.info = Default::default(); + self.diff_sections = Default::default(); + self.base.set(ScreenLinesBase { + active_viewport: viewport, + }); + } + + /// Get the line info for the given rvline. + pub fn info(&self, rvline: RVLine) -> Option { + let info = self.info.get(&rvline)?; + let base = self.base.get(); + + Some(info.clone().with_base(base)) + } + + pub fn vline_info(&self, rvline: RVLine) -> Option> { + self.info.get(&rvline).map(|info| info.vline_info) + } + + pub fn rvline_range(&self) -> Option<(RVLine, RVLine)> { + self.lines.first().copied().zip(self.lines.last().copied()) + } + + /// Iterate over the line info, copying them with the full y positions. + pub fn iter_line_info(&self) -> impl Iterator + '_ { + self.lines.iter().map(|rvline| self.info(*rvline).unwrap()) + } + + /// Iterate over the line info within the range, copying them with the full y positions. + /// If the values are out of range, it is clamped to the valid lines within. + pub fn iter_line_info_r( + &self, + r: RangeInclusive, + ) -> impl Iterator + '_ { + // We search for the start/end indices due to not having a good way to iterate over + // successive rvlines without the view. + // This should be good enough due to lines being small. + let start_idx = self.lines.binary_search(r.start()).ok().or_else(|| { + if self.lines.first().map(|l| r.start() < l).unwrap_or(false) { + Some(0) + } else { + // The start is past the start of our lines + None + } + }); + + let end_idx = self.lines.binary_search(r.end()).ok().or_else(|| { + if self.lines.last().map(|l| r.end() > l).unwrap_or(false) { + Some(self.lines.len()) + } else { + // The end is before the end of our lines but not available + None + } + }); + + if let (Some(start_idx), Some(end_idx)) = (start_idx, end_idx) { + self.lines.get(start_idx..=end_idx) + } else { + // Hacky method to get an empty iterator of the same type + self.lines.get(0..0) + } + .into_iter() + .flatten() + .copied() + .map(|rvline| self.info(rvline).unwrap()) + } + + pub fn iter_vline_info(&self) -> impl Iterator> + '_ { + self.lines + .iter() + .map(|vline| &self.info[vline].vline_info) + .copied() + } + + pub fn iter_vline_info_r( + &self, + r: RangeInclusive, + ) -> impl Iterator> + '_ { + // TODO(minor): this should probably skip tracking? + self.iter_line_info_r(r).map(|x| x.vline_info) + } + + /// Iter the real lines underlying the visual lines on the screen + pub fn iter_lines(&self) -> impl Iterator + '_ { + // We can just assume that the lines stored are contiguous and thus just get the first + // buffer line and then the last buffer line. + let start_vline = self.lines.first().copied().unwrap_or_default(); + let end_vline = self.lines.last().copied().unwrap_or_default(); + + let start_line = self.info(start_vline).unwrap().vline_info.rvline.line; + let end_line = self.info(end_vline).unwrap().vline_info.rvline.line; + + start_line..=end_line + } + + /// Iterate over the real lines underlying the visual lines on the screen with the y position + /// of their layout. + /// (line, y) + pub fn iter_lines_y(&self) -> impl Iterator + '_ { + let mut last_line = None; + self.lines.iter().filter_map(move |vline| { + let info = self.info(*vline).unwrap(); + + let line = info.vline_info.rvline.line; + + if last_line == Some(line) { + // We've already considered this line. + return None; + } + + last_line = Some(line); + + Some((line, info.y)) + }) + } + + /// Get the earliest line info for a given line. + pub fn info_for_line(&self, line: usize) -> Option { + self.info(self.first_rvline_for_line(line)?) + } + + /// Get the earliest rvline for the given line + pub fn first_rvline_for_line(&self, line: usize) -> Option { + self.lines + .iter() + .find(|rvline| rvline.line == line) + .copied() + } + + /// Get the latest rvline for the given line + pub fn last_rvline_for_line(&self, line: usize) -> Option { + self.lines + .iter() + .rfind(|rvline| rvline.line == line) + .copied() + } + + /// Ran on [`LayoutEvent::CreatedLayout`] to update [`ScreenLinesBase`] & + /// the viewport if necessary. + /// + /// Returns `true` if [`ScreenLines`] needs to be completely updated in response + pub fn on_created_layout(&self, ed: &Editor, line: usize) -> bool { + // The default creation is empty, force an update if we're ever like this since it should + // not happen. + if self.is_empty() { + return true; + } + + let base = self.base.get_untracked(); + let vp = ed.viewport.get_untracked(); + + let is_before = self + .iter_vline_info() + .next() + .map(|l| line < l.rvline.line) + .unwrap_or(false); + + // If the line is created before the current screenlines, we can simply shift the + // base and viewport forward by the number of extra wrapped lines, + // without needing to recompute the screen lines. + if is_before { + // TODO: don't assume line height is constant + let line_height = f64::from(ed.line_height(0)); + + // We could use `try_text_layout` here, but I believe this guards against a rare + // crash (though it is hard to verify) wherein the style id has changed and so the + // layouts get cleared. + // However, the original trigger of the layout event was when a layout was created + // and it expects it to still exist. So we create it just in case, though we of course + // don't trigger another layout event. + let layout = ed.text_layout_trigger(line, false); + + // One line was already accounted for by treating it as an unwrapped line. + let new_lines = layout.line_count() - 1; + + let new_y0 = base.active_viewport.y0 + new_lines as f64 * line_height; + let new_y1 = new_y0 + vp.height(); + let new_viewport = Rect::new(vp.x0, new_y0, vp.x1, new_y1); + + batch(|| { + self.base.set(ScreenLinesBase { + active_viewport: new_viewport, + }); + ed.viewport.set(new_viewport); + }); + + // Ensure that it is created even after the base/viewport signals have been updated. + // (We need the `text_layout` to still have the layout) + // But we have to trigger an event still if it is created because it *would* alter the + // screenlines. + // TODO: this has some risk for infinite looping if we're unlucky. + let _layout = ed.text_layout_trigger(line, true); + + return false; + } + + let is_after = self + .iter_vline_info() + .last() + .map(|l| line > l.rvline.line) + .unwrap_or(false); + + // If the line created was after the current view, we don't need to update the screenlines + // at all, since the new line is not visible and has no effect on y positions + if is_after { + return false; + } + + // If the line is created within the current screenlines, we need to update the + // screenlines to account for the new line. + // That is handled by the caller. + true + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ScreenLinesBase { + /// The current/previous viewport. + /// Used for determining whether there were any changes, and the `y0` serves as the + /// base for positioning the lines. + pub active_viewport: Rect, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LineInfo { + // font_size: usize, + // line_height: f64, + // x: f64, + /// The starting y position of the overall line that this vline + /// is a part of. + pub y: f64, + /// The y position of the visual line + pub vline_y: f64, + pub vline_info: VLineInfo<()>, +} +impl LineInfo { + pub fn with_base(mut self, base: ScreenLinesBase) -> Self { + self.y += base.active_viewport.y0; + self.vline_y += base.active_viewport.y0; + self + } +} + +pub struct EditorView { + id: Id, + data: ViewData, + editor: RwSignal, + is_active: Memo, + inner_node: Option, +} +impl EditorView { + #[allow(clippy::too_many_arguments)] + fn paint_normal_selection( + cx: &mut PaintCx, + ed: &Editor, + color: Color, + screen_lines: &ScreenLines, + start_offset: usize, + end_offset: usize, + affinity: CursorAffinity, + is_block_cursor: bool, + ) { + // TODO: selections should have separate start/end affinity + let (start_rvline, start_col) = ed.rvline_col_of_offset(start_offset, affinity); + let (end_rvline, end_col) = ed.rvline_col_of_offset(end_offset, affinity); + + for LineInfo { + vline_y, + vline_info: info, + .. + } in screen_lines.iter_line_info_r(start_rvline..=end_rvline) + { + let rvline = info.rvline; + let line = rvline.line; + + let phantom_text = ed.phantom_text(line); + let left_col = if rvline == start_rvline { + start_col + } else { + ed.first_col(info) + }; + let right_col = if rvline == end_rvline { + end_col + } else { + ed.last_col(info, true) + }; + let left_col = phantom_text.col_after(left_col, is_block_cursor); + let right_col = phantom_text.col_after(right_col, false); + + // Skip over empty selections + if !info.is_empty() && left_col == right_col { + continue; + } + + // TODO: What affinity should these use? + let x0 = ed + .line_point_of_line_col(line, left_col, CursorAffinity::Forward) + .x; + let x1 = ed + .line_point_of_line_col(line, right_col, CursorAffinity::Backward) + .x; + // TODO(minor): Should this be line != end_line? + let x1 = if rvline != end_rvline { + x1 + CHAR_WIDTH + } else { + x1 + }; + + let (x0, width) = if info.is_empty() { + let text_layout = ed.text_layout(line); + let width = text_layout + .get_layout_x(rvline.line_index) + .map(|(_, x1)| x1) + .unwrap_or(0.0) + .into(); + (0.0, width) + } else { + (x0, x1 - x0) + }; + + let line_height = ed.line_height(line); + let rect = Rect::from_origin_size((x0, vline_y), (width, f64::from(line_height))); + cx.fill(&rect, color, 0.0); + } + } + + #[allow(clippy::too_many_arguments)] + pub fn paint_linewise_selection( + cx: &mut PaintCx, + ed: &Editor, + color: Color, + screen_lines: &ScreenLines, + start_offset: usize, + end_offset: usize, + affinity: CursorAffinity, + ) { + let viewport = ed.viewport.get_untracked(); + + let (start_rvline, _) = ed.rvline_col_of_offset(start_offset, affinity); + let (end_rvline, _) = ed.rvline_col_of_offset(end_offset, affinity); + // Linewise selection is by *line* so we move to the start/end rvlines of the line + let start_rvline = screen_lines + .first_rvline_for_line(start_rvline.line) + .unwrap_or(start_rvline); + let end_rvline = screen_lines + .last_rvline_for_line(end_rvline.line) + .unwrap_or(end_rvline); + + for LineInfo { + vline_info: info, + vline_y, + .. + } in screen_lines.iter_line_info_r(start_rvline..=end_rvline) + { + let rvline = info.rvline; + let line = rvline.line; + + // TODO: give ed a phantom_col_after + let phantom_text = ed.phantom_text(line); + + // The left column is always 0 for linewise selections. + let right_col = ed.last_col(info, true); + let right_col = phantom_text.col_after(right_col, false); + + // TODO: what affinity to use? + let x1 = ed + .line_point_of_line_col(line, right_col, CursorAffinity::Backward) + .x + + CHAR_WIDTH; + + let line_height = ed.line_height(line); + let rect = Rect::from_origin_size( + (viewport.x0, vline_y), + (x1 - viewport.x0, f64::from(line_height)), + ); + cx.fill(&rect, color, 0.0); + } + } + + #[allow(clippy::too_many_arguments)] + pub fn paint_blockwise_selection( + cx: &mut PaintCx, + ed: &Editor, + color: Color, + screen_lines: &ScreenLines, + start_offset: usize, + end_offset: usize, + affinity: CursorAffinity, + horiz: Option, + ) { + let (start_rvline, start_col) = ed.rvline_col_of_offset(start_offset, affinity); + let (end_rvline, end_col) = ed.rvline_col_of_offset(end_offset, affinity); + let left_col = start_col.min(end_col); + let right_col = start_col.max(end_col) + 1; + + let lines = screen_lines + .iter_line_info_r(start_rvline..=end_rvline) + .filter_map(|line_info| { + let max_col = ed.last_col(line_info.vline_info, true); + (max_col > left_col).then_some((line_info, max_col)) + }); + + for (line_info, max_col) in lines { + let line = line_info.vline_info.rvline.line; + let right_col = if let Some(ColPosition::End) = horiz { + max_col + } else { + right_col.min(max_col) + }; + let phantom_text = ed.phantom_text(line); + let left_col = phantom_text.col_after(left_col, true); + let right_col = phantom_text.col_after(right_col, false); + + // TODO: what affinity to use? + let x0 = ed + .line_point_of_line_col(line, left_col, CursorAffinity::Forward) + .x; + let x1 = ed + .line_point_of_line_col(line, right_col, CursorAffinity::Backward) + .x; + + let line_height = ed.line_height(line); + let rect = + Rect::from_origin_size((x0, line_info.vline_y), (x1 - x0, f64::from(line_height))); + cx.fill(&rect, color, 0.0); + } + } + + fn paint_cursor(cx: &mut PaintCx, ed: &Editor, is_active: bool, screen_lines: &ScreenLines) { + let cursor = ed.cursor; + + let viewport = ed.viewport.get_untracked(); + + let current_line_color = ed.color(EditorColor::CurrentLine); + + cursor.with_untracked(|cursor| { + let highlight_current_line = match cursor.mode { + CursorMode::Normal(_) | CursorMode::Insert(_) => true, + CursorMode::Visual { .. } => false, + }; + + // Highlight the current line + if highlight_current_line { + for (_, end) in cursor.regions_iter() { + // TODO: unsure if this is correct for wrapping lines + let rvline = ed.rvline_of_offset(end, cursor.affinity); + + if let Some(info) = screen_lines.info(rvline) { + let line_height = ed.line_height(info.vline_info.rvline.line); + let rect = Rect::from_origin_size( + (viewport.x0, info.vline_y), + (viewport.width(), f64::from(line_height)), + ); + + cx.fill(&rect, current_line_color, 0.0); + } + } + } + + EditorView::paint_selection(cx, ed, screen_lines); + + EditorView::paint_cursor_caret(cx, ed, is_active, screen_lines); + }); + } + + pub fn paint_selection(cx: &mut PaintCx, ed: &Editor, screen_lines: &ScreenLines) { + let cursor = ed.cursor; + + let selection_color = ed.color(EditorColor::Selection); + + cursor.with_untracked(|cursor| match cursor.mode { + CursorMode::Normal(_) => {} + CursorMode::Visual { + start, + end, + mode: VisualMode::Normal, + } => { + let start_offset = start.min(end); + let end_offset = ed.move_right(start.max(end), Mode::Insert, 1); + + EditorView::paint_normal_selection( + cx, + ed, + selection_color, + screen_lines, + start_offset, + end_offset, + cursor.affinity, + true, + ); + } + CursorMode::Visual { + start, + end, + mode: VisualMode::Linewise, + } => { + EditorView::paint_linewise_selection( + cx, + ed, + selection_color, + screen_lines, + start.min(end), + start.max(end), + cursor.affinity, + ); + } + CursorMode::Visual { + start, + end, + mode: VisualMode::Blockwise, + } => { + EditorView::paint_blockwise_selection( + cx, + ed, + selection_color, + screen_lines, + start.min(end), + start.max(end), + cursor.affinity, + cursor.horiz, + ); + } + CursorMode::Insert(_) => { + for (start, end) in cursor.regions_iter().filter(|(start, end)| start != end) { + EditorView::paint_normal_selection( + cx, + ed, + selection_color, + screen_lines, + start.min(end), + start.max(end), + cursor.affinity, + false, + ); + } + } + }); + } + + pub fn paint_cursor_caret( + cx: &mut PaintCx, + ed: &Editor, + is_active: bool, + screen_lines: &ScreenLines, + ) { + let cursor = ed.cursor; + let hide_cursor = ed.cursor_info.hidden; + let caret_color = ed.color(EditorColor::Caret); + + if !is_active || hide_cursor.get_untracked() { + return; + } + + cursor.with_untracked(|cursor| { + let style = ed.style(); + for (_, end) in cursor.regions_iter() { + let is_block = match cursor.mode { + CursorMode::Normal(_) | CursorMode::Visual { .. } => true, + CursorMode::Insert(_) => false, + }; + let LineRegion { x, width, rvline } = + cursor_caret(ed, end, is_block, cursor.affinity); + + if let Some(info) = screen_lines.info(rvline) { + if !style.paint_caret(ed, rvline.line) { + continue; + } + + let line_height = ed.line_height(info.vline_info.rvline.line); + let rect = + Rect::from_origin_size((x, info.vline_y), (width, f64::from(line_height))); + cx.fill(&rect, caret_color, 0.0); + } + } + }); + } + + pub fn paint_wave_line(cx: &mut PaintCx, width: f64, point: Point, color: Color) { + let radius = 2.0; + let origin = Point::new(point.x, point.y + radius); + let mut path = BezPath::new(); + path.move_to(origin); + + let mut x = 0.0; + let mut direction = -1.0; + while x < width { + let point = origin + (x, 0.0); + let p1 = point + (radius, -radius * direction); + let p2 = point + (radius * 2.0, 0.0); + path.quad_to(p1, p2); + x += radius * 2.0; + direction *= -1.0; + } + + cx.stroke(&path, color, 1.0); + } + + pub fn paint_extra_style( + cx: &mut PaintCx, + extra_styles: &[LineExtraStyle], + y: f64, + viewport: Rect, + ) { + for style in extra_styles { + let height = style.height; + if let Some(bg) = style.bg_color { + let width = style.width.unwrap_or_else(|| viewport.width()); + let base = if style.width.is_none() { + viewport.x0 + } else { + 0.0 + }; + let x = style.x + base; + let y = y + style.y; + cx.fill( + &Rect::ZERO + .with_size(Size::new(width, height)) + .with_origin(Point::new(x, y)), + bg, + 0.0, + ); + } + + if let Some(color) = style.under_line { + let width = style.width.unwrap_or_else(|| viewport.width()); + let base = if style.width.is_none() { + viewport.x0 + } else { + 0.0 + }; + let x = style.x + base; + let y = y + style.y + height; + cx.stroke( + &Line::new(Point::new(x, y), Point::new(x + width, y)), + color, + 1.0, + ); + } + + if let Some(color) = style.wave_line { + let width = style.width.unwrap_or_else(|| viewport.width()); + let y = y + style.y + height; + EditorView::paint_wave_line(cx, width, Point::new(style.x, y), color); + } + } + } + + pub fn paint_text(cx: &mut PaintCx, ed: &Editor, viewport: Rect, screen_lines: &ScreenLines) { + let style = ed.style(); + + // TODO: cache indent text layout width + let indent_unit = style.indent_style().as_str(); + // TODO: don't assume font family is the same for all lines? + let family = style.font_family(0); + let attrs = Attrs::new() + .family(&family) + .font_size(style.font_size(0) as f32); + let attrs_list = AttrsList::new(attrs); + + let mut indent_text = TextLayout::new(); + indent_text.set_text(&format!("{indent_unit}a"), attrs_list); + let indent_text_width = indent_text.hit_position(indent_unit.len()).point.x; + + for (line, y) in screen_lines.iter_lines_y() { + let text_layout = ed.text_layout(line); + + EditorView::paint_extra_style(cx, &text_layout.extra_style, y, viewport); + + if let Some(whitespaces) = &text_layout.whitespaces { + let family = style.font_family(line); + let font_size = style.font_size(line) as f32; + let attrs = Attrs::new() + .color(style.color(EditorColor::VisibleWhitespace)) + .family(&family) + .font_size(font_size); + let attrs_list = AttrsList::new(attrs); + let mut space_text = TextLayout::new(); + space_text.set_text("·", attrs_list.clone()); + let mut tab_text = TextLayout::new(); + tab_text.set_text("→", attrs_list); + + for (c, (x0, _x1)) in whitespaces.iter() { + match *c { + '\t' => { + cx.draw_text(&tab_text, Point::new(*x0, y)); + } + ' ' => { + cx.draw_text(&space_text, Point::new(*x0, y)); + } + _ => {} + } + } + } + + if ed.show_indent_guide.get_untracked() { + let line_height = f64::from(ed.line_height(line)); + let mut x = 0.0; + while x + 1.0 < text_layout.indent { + cx.stroke( + &Line::new(Point::new(x, y), Point::new(x, y + line_height)), + style.color(EditorColor::IndentGuide), + 1.0, + ); + x += indent_text_width; + } + } + + cx.draw_text(&text_layout.text, Point::new(0.0, y)); + } + } + + pub fn paint_scroll_bar(cx: &mut PaintCx, ed: &Editor, viewport: Rect) { + // TODO: let this be customized + const BAR_WIDTH: f64 = 10.0; + cx.fill( + &Rect::ZERO + .with_size(Size::new(1.0, viewport.height())) + .with_origin(Point::new( + viewport.x0 + viewport.width() - BAR_WIDTH, + viewport.y0, + )) + .inflate(0.0, 10.0), + ed.color(EditorColor::Scrollbar), + 0.0, + ); + } +} +impl View for EditorView { + fn id(&self) -> Id { + self.id + } + + fn view_data(&self) -> &ViewData { + &self.data + } + + fn view_data_mut(&mut self) -> &mut ViewData { + &mut self.data + } + + fn build(self) -> AnyWidget { + Box::new(self) + } +} +impl Widget for EditorView { + fn view_data(&self) -> &ViewData { + &self.data + } + + fn view_data_mut(&mut self) -> &mut ViewData { + &mut self.data + } + + fn update(&mut self, _cx: &mut UpdateCx, _state: Box) {} + + fn layout(&mut self, cx: &mut LayoutCx) -> crate::taffy::prelude::Node { + cx.layout_node(self.id, true, |cx| { + let editor = self.editor.get_untracked(); + + if self.inner_node.is_none() { + self.inner_node = Some(cx.new_node()); + } + + let screen_lines = editor.screen_lines.get_untracked(); + for (line, _) in screen_lines.iter_lines_y() { + // fill in text layout cache so that max width is correct. + editor.text_layout(line); + } + + let inner_node = self.inner_node.unwrap(); + + // TODO: don't assume there's a constant line height + let line_height = f64::from(editor.line_height(0)); + + let width = editor.max_line_width() + 20.0; + let height = line_height * editor.last_vline().get() as f64; + + let style = Style::new().width(width).height(height).to_taffy_style(); + cx.set_style(inner_node, style); + + vec![inner_node] + }) + } + + fn compute_layout(&mut self, cx: &mut crate::context::ComputeLayoutCx) -> Option { + let editor = self.editor.get_untracked(); + + let viewport = cx.current_viewport(); + if editor.viewport.with_untracked(|v| v != &viewport) { + editor.viewport.set(viewport); + } + None + } + + fn paint(&mut self, cx: &mut PaintCx) { + let ed = self.editor.get_untracked(); + let viewport = ed.viewport.get_untracked(); + + // We repeatedly get the screen lines because we don't currently carefully manage the + // paint functions to avoid potentially needing to recompute them, which could *maybe* + // make them invalid. + // TODO: One way to get around the above issue would be to more careful, since we + // technically don't need to stop it from *recomputing* just stop any possible changes, but + // avoiding recomputation seems easiest/clearest. + // I expect that most/all of the paint functions could restrict themselves to only what is + // within the active screen lines without issue. + let screen_lines = ed.screen_lines.get_untracked(); + EditorView::paint_cursor(cx, &ed, self.is_active.get_untracked(), &screen_lines); + let screen_lines = ed.screen_lines.get_untracked(); + EditorView::paint_text(cx, &ed, viewport, &screen_lines); + EditorView::paint_scroll_bar(cx, &ed, viewport); + } +} + +pub fn editor_view( + editor: RwSignal, + is_active: impl Fn(bool) -> bool + 'static + Copy, +) -> EditorView { + let id = Id::next(); + let is_active = create_memo(move |_| is_active(true)); + + let ed = editor.get_untracked(); + + let data = ViewData::new(id); + + let doc = ed.doc; + let style = ed.style; + create_effect(move |_| { + doc.track(); + style.track(); + id.request_layout(); + }); + + let hide_cursor = ed.cursor_info.hidden; + create_effect(move |_| { + hide_cursor.track(); + id.request_paint(); + }); + + let editor_window_origin = ed.window_origin; + let cursor = ed.cursor; + let ime_allowed = ed.ime_allowed; + let editor_viewport = ed.viewport; + create_effect(move |_| { + let active = is_active.get(); + if active { + if !cursor.with(|c| c.is_insert()) { + if ime_allowed.get_untracked() { + ime_allowed.set(false); + set_ime_allowed(false); + } + } else { + if !ime_allowed.get_untracked() { + ime_allowed.set(true); + set_ime_allowed(true); + } + let (offset, affinity) = cursor.with(|c| (c.offset(), c.affinity)); + let (_, point_below) = ed.points_of_offset(offset, affinity); + let window_origin = editor_window_origin.get(); + let viewport = editor_viewport.get(); + let pos = + window_origin + (point_below.x - viewport.x0, point_below.y - viewport.y0); + set_ime_cursor_area(pos, Size::new(800.0, 600.0)); + } + } + }); + + EditorView { + id, + data, + editor, + is_active, + inner_node: None, + } + .on_event(EventListener::ImePreedit, move |event| { + if !is_active.get_untracked() { + return EventPropagation::Continue; + } + + if let Event::ImePreedit { text, cursor } = event { + editor.with_untracked(|ed| { + if text.is_empty() { + ed.clear_preedit(); + } else { + let offset = ed.cursor.with_untracked(|c| c.offset()); + ed.set_preedit(text.clone(), *cursor, offset); + } + }); + } + EventPropagation::Stop + }) + .on_event(EventListener::ImeCommit, move |event| { + if !is_active.get_untracked() { + return EventPropagation::Continue; + } + + if let Event::ImeCommit(text) = event { + editor.with_untracked(|ed| { + ed.clear_preedit(); + ed.receive_char(text); + }); + } + EventPropagation::Stop + }) +} + +#[derive(Clone, Debug)] +pub struct LineRegion { + pub x: f64, + pub width: f64, + pub rvline: RVLine, +} + +/// Get the render information for a caret cursor at the given `offset`. +pub fn cursor_caret( + ed: &Editor, + offset: usize, + block: bool, + affinity: CursorAffinity, +) -> LineRegion { + let info = ed.rvline_info_of_offset(offset, affinity); + let (_, col) = ed.offset_to_line_col(offset); + let after_last_char = col == ed.line_end_col(info.rvline.line, true); + + let doc = ed.doc(); + let preedit_start = doc + .preedit() + .preedit + .with_untracked(|preedit| { + preedit.as_ref().and_then(|preedit| { + let preedit_line = ed.line_of_offset(preedit.offset); + preedit.cursor.map(|x| (preedit_line, x)) + }) + }) + .filter(|(preedit_line, _)| *preedit_line == info.rvline.line) + .map(|(_, (start, _))| start); + + let phantom_text = ed.phantom_text(info.rvline.line); + + let (_, col) = ed.offset_to_line_col(offset); + let ime_kind = preedit_start.map(|_| PhantomTextKind::Ime); + // The cursor should be after phantom text if the affinity is forward, or it is a block cursor. + // - if we have a relevant preedit we skip over IMEs + // - we skip over completion lens, as the text should be after the cursor + let col = phantom_text.col_after_ignore( + col, + affinity == CursorAffinity::Forward || (block && !after_last_char), + |p| p.kind == PhantomTextKind::Completion || Some(p.kind) == ime_kind, + ); + // We shift forward by the IME's start. This is due to the cursor potentially being in the + // middle of IME phantom text while editing it. + let col = col + preedit_start.unwrap_or(0); + + let point = ed.line_point_of_line_col(info.rvline.line, col, affinity); + + let rvline = if preedit_start.is_some() { + // If there's an IME edit, then we need to use the point's y to get the actual y position + // that the IME cursor is at. Since it could be in the middle of the IME phantom text + let y = point.y; + + // TODO: I don't think this is handling varying line heights properly + let line_height = ed.line_height(info.rvline.line); + + let line_index = (y / f64::from(line_height)).floor() as usize; + RVLine::new(info.rvline.line, line_index) + } else { + info.rvline + }; + + let x0 = point.x; + if block { + let width = if after_last_char { + CHAR_WIDTH + } else { + let x1 = ed + .line_point_of_line_col(info.rvline.line, col + 1, affinity) + .x; + x1 - x0 + }; + + LineRegion { + x: x0, + width, + rvline, + } + } else { + LineRegion { + x: x0 - 1.0, + width: 2.0, + rvline, + } + } +} + +pub fn editor_container_view( + editor: RwSignal, + is_active: impl Fn(bool) -> bool + 'static + Copy, + handle_key_event: impl Fn(&KeyPress, ModifiersState) -> CommandExecuted + 'static, +) -> impl View { + let editor_rect = create_rw_signal(Rect::ZERO); + + stack(( + // editor_breadcrumbs(workspace, editor.get_untracked(), config), + container( + stack(( + editor_gutter(editor), + container(editor_content(editor, is_active, handle_key_event)) + .style(move |s| s.size_pct(100.0, 100.0)), + empty().style(move |s| s.absolute().width_pct(100.0)), + )) + .on_resize(move |rect| { + editor_rect.set(rect); + }) + .style(|s| s.absolute().size_pct(100.0, 100.0)), + ) + .style(|s| s.size_pct(100.0, 100.0)), + )) + .on_cleanup(move || { + // TODO: should we have some way for doc to tell us if we're allowed to cleanup the editor? + let editor = editor.get_untracked(); + editor.cx.get().dispose(); + }) + // TODO(minor): only depend on style + .style(move |s| { + s.flex_col() + .size_pct(100.0, 100.0) + .background(editor.get().color(EditorColor::Background)) + }) +} + +/// Default editor gutter +/// Simply shows line numbers +pub fn editor_gutter(editor: RwSignal) -> impl View { + // TODO: these are probably tuned for lapce? + let padding_left = 25.0; + let padding_right = 30.0; + + let ed = editor.get_untracked(); + + let scroll_delta = ed.scroll_delta; + + let gutter_rect = create_rw_signal(Rect::ZERO); + + stack(( + stack(( + empty().style(move |s| s.width(padding_left)), + // TODO(minor): this could just track purely Doc + label(move || (editor.get().last_line() + 1).to_string()), + empty().style(move |s| s.width(padding_right)), + )) + .style(|s| s.height_pct(100.0)), + clip( + stack((editor_gutter_view(editor) + .on_resize(move |rect| { + gutter_rect.set(rect); + }) + .on_event_stop(EventListener::PointerWheel, move |event| { + if let Event::PointerWheel(pointer_event) = event { + scroll_delta.set(pointer_event.delta); + } + }) + .style(|s| s.size_pct(100.0, 100.0)),)) + .style(|s| s.size_pct(100.0, 100.0)), + ) + .style(move |s| { + s.absolute() + .size_pct(100.0, 100.0) + .padding_left(padding_left) + .padding_right(padding_right) + }), + )) + .style(|s| s.height_pct(100.0)) +} + +fn editor_content( + editor: RwSignal, + is_active: impl Fn(bool) -> bool + 'static + Copy, + handle_key_event: impl Fn(&KeyPress, ModifiersState) -> CommandExecuted + 'static, +) -> impl View { + let ed = editor.get_untracked(); + let cursor = ed.cursor; + let scroll_delta = ed.scroll_delta; + let scroll_to = ed.scroll_to; + let window_origin = ed.window_origin; + let viewport = ed.viewport; + let scroll_beyond_last_line = ed.scroll_beyond_last_line; + + scroll({ + let editor_content_view = editor_view(editor, is_active).style(move |s| { + let padding_bottom = if scroll_beyond_last_line.get() { + // TODO: don't assume line height is constant? + // just use the last line's line height maybe, or just make + // scroll beyond last line a f32 + // TODO: we shouldn't be using `get` on editor here, isn't this more of a 'has the + // style cache changed'? + let line_height = editor.get().line_height(0); + viewport.get().height() as f32 - line_height + } else { + 0.0 + }; + + s.absolute() + .padding_bottom(padding_bottom) + .cursor(CursorStyle::Text) + .min_size_pct(100.0, 100.0) + }); + + let id = editor_content_view.id(); + + editor_content_view + .on_event_cont(EventListener::PointerDown, move |event| { + // TODO: + if let Event::PointerDown(pointer_event) = event { + id.request_active(); + id.request_focus(); + editor.get_untracked().pointer_down(pointer_event); + } + }) + .on_event_stop(EventListener::PointerMove, move |event| { + if let Event::PointerMove(pointer_event) = event { + editor.get_untracked().pointer_move(pointer_event); + } + }) + .on_event_stop(EventListener::PointerUp, move |event| { + if let Event::PointerUp(pointer_event) = event { + editor.get_untracked().pointer_up(pointer_event); + } + }) + .on_event_stop(EventListener::KeyDown, move |event| { + let Event::KeyDown(key_event) = event else { + return; + }; + + let Ok(keypress) = KeyPress::try_from(key_event) else { + return; + }; + + handle_key_event(&keypress, key_event.modifiers); + + let mut mods = key_event.modifiers; + mods.set(ModifiersState::SHIFT, false); + #[cfg(target_os = "macos")] + mods.set(ModifiersState::ALT, false); + + if mods.is_empty() { + if let KeyInput::Keyboard(Key::Character(c), _) = keypress.key { + editor.get_untracked().receive_char(&c); + } else if let KeyInput::Keyboard(Key::Named(NamedKey::Space), _) = keypress.key + { + editor.get_untracked().receive_char(" "); + } + } + }) + }) + .on_move(move |point| { + window_origin.set(point); + }) + .scroll_to(move || scroll_to.get().map(Vec2::to_point)) + .scroll_delta(move || scroll_delta.get()) + .ensure_visible(move || { + let editor = editor.get_untracked(); + let cursor = cursor.get(); + let offset = cursor.offset(); + editor.doc.track(); + // TODO:? + // editor.kind.track(); + + let LineRegion { x, width, rvline } = + cursor_caret(&editor, offset, !cursor.is_insert(), cursor.affinity); + + // TODO: don't assume line-height is constant + let line_height = f64::from(editor.line_height(0)); + + // TODO: is there a good way to avoid the calculation of the vline here? + let vline = editor.vline_of_rvline(rvline); + let rect = + Rect::from_origin_size((x, vline.get() as f64 * line_height), (width, line_height)) + .inflate(10.0, 0.0); + + let viewport = viewport.get_untracked(); + let smallest_distance = (viewport.y0 - rect.y0) + .abs() + .min((viewport.y1 - rect.y0).abs()) + .min((viewport.y0 - rect.y1).abs()) + .min((viewport.y1 - rect.y1).abs()); + let biggest_distance = (viewport.y0 - rect.y0) + .abs() + .max((viewport.y1 - rect.y0).abs()) + .max((viewport.y0 - rect.y1).abs()) + .max((viewport.y1 - rect.y1).abs()); + let jump_to_middle = + biggest_distance > viewport.height() && smallest_distance > viewport.height() / 2.0; + + if jump_to_middle { + rect.inflate(0.0, viewport.height() / 2.0) + } else { + let mut rect = rect; + let cursor_surrounding_lines = editor.cursor_surrounding_lines.get_untracked() as f64; + rect.y0 -= cursor_surrounding_lines * line_height; + rect.y1 += cursor_surrounding_lines * line_height; + rect + } + }) + .style(|s| s.absolute().size_pct(100.0, 100.0)) +} diff --git a/src/views/editor/visual_line.rs b/src/views/editor/visual_line.rs new file mode 100644 index 00000000..5a7f9b84 --- /dev/null +++ b/src/views/editor/visual_line.rs @@ -0,0 +1,3352 @@ +//! Visual Line implementation +//! +//! Files are easily broken up into buffer lines by just spliiting on `\n` or `\r\n`. +//! However, editors require features like wrapping and multiline phantom text. These break the +//! nice one-to-one correspondence between buffer lines and visual lines. +//! +//! When rendering with those, we have to display based on visual lines rather than the +//! underlying buffer lines. As well, it is expected for interaction - like movement and clicking - +//! to work in a similar intuitive manner as it would be if there was no wrapping or phantom text. +//! Ex: Moving down a line should move to the next visual line, not the next buffer line by +//! default. +//! (Sometimes! Some vim defaults are to move to the next buffer line, or there might be other +//! differences) +//! +//! There's two types of ways of talking about Visual Lines: +//! - [`VLine`]: Variables are often written with `vline` in the name +//! - [`RVLine`]: Variables are often written with `rvline` in the name +//! +//! [`VLine`] is an absolute visual line within the file. This is useful for some positioning tasks +//! but is more expensive to calculate due to the nontriviality of the `buffer line <-> visual line` +//! conversion when the file has any wrapping or multiline phantom text. +//! +//! Typically, code should prefer to use [`RVLine`]. This simply stores the underlying +//! buffer line, and a line index. This is not enough for absolute positioning within the display, +//! but it is enough for most other things (like movement). This is easier to calculate since it +//! only needs to find the right (potentially wrapped or multiline) layout for the easy-to-work +//! with buffer line. +//! +//! [`VLine`] is a single `usize` internally which can be multiplied by the line-height to get the +//! absolute position. This means that it is not stable across text layouts being changed. +//! An [`RVLine`] holds the buffer line and the 'line index' within the layout. The line index +//! would be `0` for the first line, `1` if it is on the second wrapped line, etc. This is more +//! stable across text layouts being changed, as it is only relative to a specific line. +//! +//! ----- +//! +//! [`Lines`] is the main structure. It is responsible for holding the text layouts, as well as +//! providing the functions to convert between (r)vlines and buffer lines. +//! +//! ---- +//! +//! Many of [`Lines`] functions are passed a [`TextLayoutProvider`]. +//! This serves the dual-purpose of giving us the text of the underlying file, as well as +//! for constructing the text layouts that we use for rendering. +//! Having a trait that is passed in simplifies the logic, since the caller is the one who tracks +//! the text in whatever manner they chose. + +// TODO: This file is getting long. Possibly it should be broken out into multiple files. +// Especially as it will only grow with more utility functions. + +// TODO(minor): We use a lot of `impl TextLayoutProvider`. +// This has the desired benefit of inlining the functions, so that the compiler can optimize the +// logic better than a naive for-loop or whatnot. +// However it does have the issue that it overuses generics, and we sometimes end up instantiating +// multiple versions of the same function. `T: TextLayoutProvider`, `&T`... +// - It would be better to standardize on one way of doing that, probably `&impl TextLayoutProvider` + +use std::{ + cell::{Cell, RefCell}, + cmp::Ordering, + collections::HashMap, + rc::Rc, + sync::Arc, +}; + +use floem_editor_core::{ + buffer::rope_text::{RopeText, RopeTextRef}, + cursor::CursorAffinity, + word::WordCursor, +}; +use floem_reactive::Scope; +use floem_renderer::cosmic_text::{HitPosition, LayoutGlyph, TextLayout}; +use kurbo::Point; +use lapce_xi_rope::{Interval, Rope}; + +use super::{layout::TextLayoutLine, listener::Listener}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ResolvedWrap { + None, + Column(usize), + Width(f32), +} +impl ResolvedWrap { + pub fn is_different_kind(self, other: ResolvedWrap) -> bool { + !matches!( + (self, other), + (ResolvedWrap::None, ResolvedWrap::None) + | (ResolvedWrap::Column(_), ResolvedWrap::Column(_)) + | (ResolvedWrap::Width(_), ResolvedWrap::Width(_)) + ) + } +} + +/// A line within the editor view. +/// This gives the absolute position of the visual line. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct VLine(pub usize); +impl VLine { + pub fn get(&self) -> usize { + self.0 + } +} + +/// A visual line relative to some other line within the editor view. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RVLine { + /// The buffer line this is for + pub line: usize, + /// The index of the actual visual line's layout + pub line_index: usize, +} +impl RVLine { + pub fn new(line: usize, line_index: usize) -> RVLine { + RVLine { line, line_index } + } + + /// Is this the first visual line for the buffer line? + pub fn is_first(&self) -> bool { + self.line_index == 0 + } +} + +/// (Font Size -> (Buffer Line Number -> Text Layout)) +pub type Layouts = HashMap>>; + +#[derive(Default)] +pub struct TextLayoutCache { + /// The id of the last config so that we can clear when the config changes + config_id: u64, + /// The most recent cache revision of the document. + cache_rev: u64, + /// (Font Size -> (Buffer Line Number -> Text Layout)) + /// Different font-sizes are cached separately, which is useful for features like code lens + /// where the font-size can rapidly change. + /// It would also be useful for a prospective minimap feature. + pub layouts: Layouts, + /// The maximum width seen so far, used to determine if we need to show horizontal scrollbar + pub max_width: f64, +} +impl TextLayoutCache { + pub fn clear(&mut self, cache_rev: u64, config_id: Option) { + self.layouts.clear(); + if let Some(config_id) = config_id { + self.config_id = config_id; + } + self.cache_rev = cache_rev; + self.max_width = 0.0; + } + + /// Clear the layouts without changing the document cache revision. + /// Ex: Wrapping width changed, which does not change what the document holds. + pub fn clear_unchanged(&mut self) { + self.layouts.clear(); + self.max_width = 0.0; + } + + pub fn get(&self, font_size: usize, line: usize) -> Option<&Arc> { + self.layouts.get(&font_size).and_then(|c| c.get(&line)) + } + + pub fn get_mut(&mut self, font_size: usize, line: usize) -> Option<&mut Arc> { + self.layouts + .get_mut(&font_size) + .and_then(|c| c.get_mut(&line)) + } + + /// Get the (start, end) columns of the (line, line_index) + pub fn get_layout_col( + &self, + text_prov: &impl TextLayoutProvider, + font_size: usize, + line: usize, + line_index: usize, + ) -> Option<(usize, usize)> { + self.get(font_size, line) + .and_then(|l| l.layout_cols(text_prov, line).nth(line_index)) + } + + /// Check whether the config id has changed, clearing the cache if it has. + pub fn check_attributes(&mut self, config_id: u64) { + if self.config_id != config_id { + self.clear(self.cache_rev + 1, Some(config_id)); + } + } +} + +// TODO(minor): Should we rename this? It does more than just providing the text layout. It provides the text, text layouts, phantom text, and whether it has multiline phantom text. It is more of an outside state. +/// The [`TextLayoutProvider`] serves two primary roles: +/// - Providing the [`Rope`] text of the underlying file +/// - Constructing the text layout for a given line +/// +/// Note: `text` does not necessarily include every piece of text. The obvious example is phantom +/// text, which is not in the underlying buffer. +/// +/// Using this trait rather than passing around something like [`Document`] allows the backend to +/// be swapped out if needed. This would be useful if we ever wanted to reuse it across different +/// views that did not naturally fit into our 'document' model. As well as when we want to extract +/// the editor view code int a separate crate for Floem. +pub trait TextLayoutProvider { + fn text(&self) -> &Rope; + + /// Shorthand for getting a rope text version of `text`. + /// This MUST hold the same rope that `text` would return. + fn rope_text(&self) -> RopeTextRef { + RopeTextRef::new(self.text()) + } + + // TODO(minor): Do we really need to pass font size to this? The outer-api is providing line + // font size provider already, so it should be able to just use that. + fn new_text_layout( + &self, + line: usize, + font_size: usize, + wrap: ResolvedWrap, + ) -> Arc; + + /// Translate a column position into the postiion it would be before combining with the phantom + /// text + fn before_phantom_col(&self, line: usize, col: usize) -> usize; + + /// Whether the text has *any* multiline phantom text. + /// This is used to determine whether we can use the fast route where the lines are linear, + /// which also requires no wrapping. + /// This should be a conservative estimate, so if you aren't bothering to check all of your + /// phantom text then just return true. + fn has_multiline_phantom(&self) -> bool; +} +impl TextLayoutProvider for &T { + fn text(&self) -> &Rope { + (**self).text() + } + + fn new_text_layout( + &self, + line: usize, + font_size: usize, + wrap: ResolvedWrap, + ) -> Arc { + (**self).new_text_layout(line, font_size, wrap) + } + + fn before_phantom_col(&self, line: usize, col: usize) -> usize { + (**self).before_phantom_col(line, col) + } + + fn has_multiline_phantom(&self) -> bool { + (**self).has_multiline_phantom() + } +} + +pub type FontSizeCacheId = u64; +pub trait LineFontSizeProvider { + /// Get the 'general' font size for a specific buffer line. + /// This is typically the editor font size. + /// There might be alternate font-sizes within the line, like for phantom text, but those are + /// not considered here. + fn font_size(&self, line: usize) -> usize; + + /// An identifier used to mark when the font size info has changed. + /// This lets us update information. + fn cache_id(&self) -> FontSizeCacheId; +} + +/// Layout events. This is primarily needed for logic which tracks visual lines intelligently, like +/// `ScreenLines` in Lapce. +/// This is currently limited to only a `CreatedLayout` event, as changed to the cache rev would +/// capture the idea of all the layouts being cleared. In the future it could be expanded to more +/// events, especially if cache rev gets more specific than clearing everything. +#[derive(Debug, Clone, PartialEq)] +pub enum LayoutEvent { + CreatedLayout { font_size: usize, line: usize }, +} + +/// The main structure for tracking visual line information. +pub struct Lines { + /// This is inside out from the usual way of writing Arc-RefCells due to sometimes wanting to + /// swap out font sizes, while also grabbing an `Arc` to hold. + /// An `Arc>` has the issue that with a `dyn` it can't know they're the same size + /// if you were to assign. So this allows us to swap out the `Arc`, though it does mean that + /// the other holders of the `Arc` don't get the new version. That is fine currently. + pub font_sizes: RefCell>, + text_layouts: Rc>, + wrap: Cell, + font_size_cache_id: Cell, + last_vline: Rc>>, + pub layout_event: Listener, +} +impl Lines { + pub fn new(cx: Scope, font_sizes: RefCell>) -> Lines { + let id = font_sizes.borrow().cache_id(); + Lines { + font_sizes, + text_layouts: Rc::new(RefCell::new(TextLayoutCache::default())), + wrap: Cell::new(ResolvedWrap::None), + font_size_cache_id: Cell::new(id), + last_vline: Rc::new(Cell::new(None)), + layout_event: Listener::new_empty(cx), + } + } + + /// The current wrapping style + pub fn wrap(&self) -> ResolvedWrap { + self.wrap.get() + } + + /// Set the wrapping style + /// Does nothing if the wrapping style is the same as the current one. + /// Will trigger a clear of the text layouts if the wrapping style is different. + pub fn set_wrap(&self, wrap: ResolvedWrap) { + if wrap == self.wrap.get() { + return; + } + + // TODO(perf): We could improve this by only clearing the lines that would actually change + // Ex: Single vline lines don't need to be cleared if the wrapping changes from + // some width to None, or from some width to some larger width. + self.clear_unchanged(); + + self.wrap.set(wrap); + } + + /// The max width of the text layouts displayed + pub fn max_width(&self) -> f64 { + self.text_layouts.borrow().max_width + } + + /// Check if the lines can be modelled as a purely linear file. + /// If `true` this makes various operations simpler because there is a one-to-one + /// correspondence between visual lines and buffer lines. + /// However, if there is wrapping or any multiline phantom text, then we can't rely on that. + /// + /// TODO:? + /// We could be smarter about various pieces. + /// - If there was no lines that exceeded the wrap width then we could do the fast path + /// - Would require tracking that but might not be too hard to do it whenever we create a + /// text layout + /// - `is_linear` could be up to some line, which allows us to make at least the earliest parts + /// before any wrapping were faster. However, early lines are faster to calculate anyways. + pub fn is_linear(&self, text_prov: impl TextLayoutProvider) -> bool { + self.wrap.get() == ResolvedWrap::None && !text_prov.has_multiline_phantom() + } + + /// Get the font size that [`Self::font_sizes`] provides + pub fn font_size(&self, line: usize) -> usize { + self.font_sizes.borrow().font_size(line) + } + + /// Get the last visual line of the file. + /// Cached. + pub fn last_vline(&self, text_prov: impl TextLayoutProvider) -> VLine { + let current_id = self.font_sizes.borrow().cache_id(); + if current_id != self.font_size_cache_id.get() { + self.last_vline.set(None); + self.font_size_cache_id.set(current_id); + } + + if let Some(last_vline) = self.last_vline.get() { + last_vline + } else { + // For most files this should easily be fast enough. + // Though it could still be improved. + let rope_text = text_prov.rope_text(); + let hard_line_count = rope_text.num_lines(); + + let line_count = if self.is_linear(text_prov) { + hard_line_count + } else { + let mut soft_line_count = 0; + + let layouts = self.text_layouts.borrow(); + for i in 0..hard_line_count { + let font_size = self.font_size(i); + if let Some(text_layout) = layouts.get(font_size, i) { + let line_count = text_layout.line_count(); + soft_line_count += line_count; + } else { + soft_line_count += 1; + } + } + + soft_line_count + }; + + let last_vline = line_count.saturating_sub(1); + self.last_vline.set(Some(VLine(last_vline))); + VLine(last_vline) + } + } + + /// Clear the cache for the last vline + pub fn clear_last_vline(&self) { + self.last_vline.set(None); + } + + /// The last relative visual line. + /// Cheap, so not cached + pub fn last_rvline(&self, text_prov: impl TextLayoutProvider) -> RVLine { + let rope_text = text_prov.rope_text(); + let last_line = rope_text.last_line(); + let layouts = self.text_layouts.borrow(); + let font_size = self.font_size(last_line); + + if let Some(layout) = layouts.get(font_size, last_line) { + let line_count = layout.line_count(); + + RVLine::new(last_line, line_count - 1) + } else { + RVLine::new(last_line, 0) + } + } + + /// 'len' version of [`Lines::last_vline`] + /// Cached. + pub fn num_vlines(&self, text_prov: impl TextLayoutProvider) -> usize { + self.last_vline(text_prov).get() + 1 + } + + /// Get the text layout for the given buffer line number. + /// This will create the text layout if it doesn't exist. + /// + /// `trigger` (default to true) decides whether the creation of the text layout should trigger + /// the [`LayoutEvent::CreatedLayout`] event. + /// + /// This will check the `config_id`, which decides whether it should clear out the text layout + /// cache. + pub fn get_init_text_layout( + &self, + config_id: u64, + text_prov: impl TextLayoutProvider, + line: usize, + trigger: bool, + ) -> Arc { + self.check_config_id(config_id); + + let font_size = self.font_size(line); + get_init_text_layout( + &self.text_layouts, + trigger.then_some(self.layout_event), + text_prov, + line, + font_size, + self.wrap.get(), + &self.last_vline, + ) + } + + /// Try to get the text layout for the given line number. + /// + /// This will check the `config_id`, which decides whether it should clear out the text layout + /// cache. + pub fn try_get_text_layout(&self, config_id: u64, line: usize) -> Option> { + self.check_config_id(config_id); + + let font_size = self.font_size(line); + + self.text_layouts + .borrow() + .layouts + .get(&font_size) + .and_then(|f| f.get(&line)) + .cloned() + } + + /// Initialize the text layout of every line in the real line interval. + /// + /// `trigger` (default to true) decides whether the creation of the text layout should trigger + /// the [`LayoutEvent::CreatedLayout`] event. + pub fn init_line_interval( + &self, + config_id: u64, + text_prov: &impl TextLayoutProvider, + lines: impl Iterator, + trigger: bool, + ) { + for line in lines { + self.get_init_text_layout(config_id, text_prov, line, trigger); + } + } + + /// Initialize the text layout of every line in the file. + /// This should typically not be used. + /// + /// `trigger` (default to true) decides whether the creation of the text layout should trigger + /// the [`LayoutEvent::CreatedLayout`] event. + pub fn init_all(&self, config_id: u64, text_prov: &impl TextLayoutProvider, trigger: bool) { + let text = text_prov.text(); + let last_line = text.line_of_offset(text.len()); + self.init_line_interval(config_id, text_prov, 0..=last_line, trigger); + } + + /// Iterator over [`VLineInfo`]s, starting at `start_line`. + pub fn iter_vlines( + &self, + text_prov: impl TextLayoutProvider, + backwards: bool, + start: VLine, + ) -> impl Iterator { + VisualLines::new(self, text_prov, backwards, start) + } + + /// Iterator over [`VLineInfo`]s, starting at `start_line` and ending at `end_line`. + /// `start_line..end_line` + pub fn iter_vlines_over( + &self, + text_prov: impl TextLayoutProvider, + backwards: bool, + start: VLine, + end: VLine, + ) -> impl Iterator { + self.iter_vlines(text_prov, backwards, start) + .take_while(move |info| info.vline < end) + } + + /// Iterator over *relative* [`VLineInfo`]s, starting at the rvline, `start_line`. + /// This is preferable over `iter_vlines` if you do not need to absolute visual line value and + /// can provide the buffer line. + pub fn iter_rvlines( + &self, + text_prov: impl TextLayoutProvider, + backwards: bool, + start: RVLine, + ) -> impl Iterator> { + VisualLinesRelative::new(self, text_prov, backwards, start) + } + + /// Iterator over *relative* [`VLineInfo`]s, starting at the rvline `start_line` and + /// ending at the buffer line `end_line`. + /// `start_line..end_line` + /// This is preferable over `iter_vlines` if you do not need the absolute visual line value and + /// you can provide the buffer line. + pub fn iter_rvlines_over( + &self, + text_prov: impl TextLayoutProvider, + backwards: bool, + start: RVLine, + end_line: usize, + ) -> impl Iterator> { + self.iter_rvlines(text_prov, backwards, start) + .take_while(move |info| info.rvline.line < end_line) + } + + // TODO(minor): Get rid of the clone bound. + /// Initialize the text layouts as you iterate over them. + pub fn iter_vlines_init( + &self, + text_prov: impl TextLayoutProvider + Clone, + config_id: u64, + start: VLine, + trigger: bool, + ) -> impl Iterator { + self.check_config_id(config_id); + + if start <= self.last_vline(&text_prov) { + // We initialize the text layout for the line that start line is for + let (_, rvline) = find_vline_init_info(self, &text_prov, start).unwrap(); + self.get_init_text_layout(config_id, &text_prov, rvline.line, trigger); + // If the start line was past the last vline then we don't need to initialize anything + // since it won't get anything. + } + + let text_layouts = self.text_layouts.clone(); + let font_sizes = self.font_sizes.clone(); + let wrap = self.wrap.get(); + let last_vline = self.last_vline.clone(); + let layout_event = trigger.then_some(self.layout_event); + self.iter_vlines(text_prov.clone(), false, start) + .map(move |v| { + if v.is_first() { + // For every (first) vline we initialize the next buffer line's text layout + // This ensures it is ready for when re reach it. + let next_line = v.rvline.line + 1; + let font_size = font_sizes.borrow().font_size(next_line); + // `init_iter_vlines` is the reason `get_init_text_layout` is split out. + // Being split out lets us avoid attaching lifetimes to the iterator, since it + // only uses Rc/Arcs it is given. + // This is useful since `Lines` would be in a + // `Rc>` which would make iterators with lifetimes referring to + // `Lines` a pain. + get_init_text_layout( + &text_layouts, + layout_event, + &text_prov, + next_line, + font_size, + wrap, + &last_vline, + ); + } + v + }) + } + + /// Iterator over [`VLineInfo`]s, starting at `start_line` and ending at `end_line`. + /// `start_line..end_line` + /// Initializes the text layouts as you iterate over them. + /// + /// `trigger` (default to true) decides whether the creation of the text layout should trigger + /// the [`LayoutEvent::CreatedLayout`] event. + pub fn iter_vlines_init_over( + &self, + text_prov: impl TextLayoutProvider + Clone, + config_id: u64, + start: VLine, + end: VLine, + trigger: bool, + ) -> impl Iterator { + self.iter_vlines_init(text_prov, config_id, start, trigger) + .take_while(move |info| info.vline < end) + } + + /// Iterator over *relative* [`VLineInfo`]s, starting at the rvline, `start_line` and + /// ending at the buffer line `end_line`. + /// `start_line..end_line` + /// + /// `trigger` (default to true) decides whether the creation of the text layout should trigger + /// the [`LayoutEvent::CreatedLayout`] event. + pub fn iter_rvlines_init( + &self, + text_prov: impl TextLayoutProvider + Clone, + config_id: u64, + start: RVLine, + trigger: bool, + ) -> impl Iterator> { + self.check_config_id(config_id); + + if start.line <= text_prov.rope_text().last_line() { + // Initialize the text layout for the line that start line is for + self.get_init_text_layout(config_id, &text_prov, start.line, trigger); + } + + let text_layouts = self.text_layouts.clone(); + let font_sizes = self.font_sizes.clone(); + let wrap = self.wrap.get(); + let last_vline = self.last_vline.clone(); + let layout_event = trigger.then_some(self.layout_event); + self.iter_rvlines(text_prov.clone(), false, start) + .map(move |v| { + if v.is_first() { + // For every (first) vline we initialize the next buffer line's text layout + // This ensures it is ready for when re reach it. + let next_line = v.rvline.line + 1; + let font_size = font_sizes.borrow().font_size(next_line); + // `init_iter_lines` is the reason `get_init_text_layout` is split out. + // Being split out lets us avoid attaching lifetimes to the iterator, since it + // only uses Rc/Arcs that it. This is useful since `Lines` would be in a + // `Rc>` which would make iterators with lifetimes referring to + // `Lines` a pain. + get_init_text_layout( + &text_layouts, + layout_event, + &text_prov, + next_line, + font_size, + wrap, + &last_vline, + ); + } + v + }) + } + + /// Get the visual line of the offset. + /// + /// `affinity` decides whether an offset at a soft line break is considered to be on the + /// previous line or the next line. + /// If `affinity` is `CursorAffinity::Forward` and is at the very end of the wrapped line, then + /// the offset is considered to be on the next vline. + pub fn vline_of_offset( + &self, + text_prov: &impl TextLayoutProvider, + offset: usize, + affinity: CursorAffinity, + ) -> VLine { + let text = text_prov.text(); + + let offset = offset.min(text.len()); + + if self.is_linear(text_prov) { + let buffer_line = text.line_of_offset(offset); + return VLine(buffer_line); + } + + let Some((vline, _line_index)) = find_vline_of_offset(self, text_prov, offset, affinity) + else { + // We assume it is out of bounds + return self.last_vline(text_prov); + }; + + vline + } + + /// Get the visual line and column of the given offset. + /// + /// The column is before phantom text is applied and is into the overall line, not the + /// individual visual line. + pub fn vline_col_of_offset( + &self, + text_prov: &impl TextLayoutProvider, + offset: usize, + affinity: CursorAffinity, + ) -> (VLine, usize) { + let vline = self.vline_of_offset(text_prov, offset, affinity); + let last_col = self + .iter_vlines(text_prov, false, vline) + .next() + .map(|info| info.last_col(text_prov, true)) + .unwrap_or(0); + + let line = text_prov.text().line_of_offset(offset); + let line_offset = text_prov.text().offset_of_line(line); + + let col = offset - line_offset; + let col = col.min(last_col); + + (vline, col) + } + + /// Get the nearest offset to the start of the visual line + pub fn offset_of_vline(&self, text_prov: &impl TextLayoutProvider, vline: VLine) -> usize { + find_vline_init_info(self, text_prov, vline) + .map(|x| x.0) + .unwrap_or_else(|| text_prov.text().len()) + } + + /// Get the first visual line of the buffer line. + pub fn vline_of_line(&self, text_prov: &impl TextLayoutProvider, line: usize) -> VLine { + if self.is_linear(text_prov) { + return VLine(line); + } + + find_vline_of_line(self, text_prov, line).unwrap_or_else(|| self.last_vline(text_prov)) + } + + /// Find the matching visual line for the given relative visual line. + pub fn vline_of_rvline(&self, text_prov: &impl TextLayoutProvider, rvline: RVLine) -> VLine { + if self.is_linear(text_prov) { + debug_assert_eq!( + rvline.line_index, 0, + "Got a nonzero line index despite being linear, old RVLine was used." + ); + return VLine(rvline.line); + } + + let vline = self.vline_of_line(text_prov, rvline.line); + + // TODO(minor): There may be edge cases with this, like when you have a bunch of multiline + // phantom text at the same offset + VLine(vline.get() + rvline.line_index) + } + + /// Get the relative visual line of the offset. + /// + /// `affinity` decides whether an offset at a soft line break is considered to be on the + /// previous line or the next line. + /// If `affinity` is `CursorAffinity::Forward` and is at the very end of the wrapped line, then + /// the offset is considered to be on the next rvline. + pub fn rvline_of_offset( + &self, + text_prov: &impl TextLayoutProvider, + offset: usize, + affinity: CursorAffinity, + ) -> RVLine { + let text = text_prov.text(); + + let offset = offset.min(text.len()); + + if self.is_linear(text_prov) { + let buffer_line = text.line_of_offset(offset); + return RVLine::new(buffer_line, 0); + } + + find_rvline_of_offset(self, text_prov, offset, affinity) + .unwrap_or_else(|| self.last_rvline(text_prov)) + } + + /// Get the relative visual line and column of the given offset + /// + /// The column is before phantom text is applied and is into the overall line, not the + /// individual visual line. + pub fn rvline_col_of_offset( + &self, + text_prov: &impl TextLayoutProvider, + offset: usize, + affinity: CursorAffinity, + ) -> (RVLine, usize) { + let rvline = self.rvline_of_offset(text_prov, offset, affinity); + let info = self.iter_rvlines(text_prov, false, rvline).next().unwrap(); + let line_offset = text_prov.text().offset_of_line(rvline.line); + + let col = offset - line_offset; + let col = col.min(info.last_col(text_prov, true)); + + (rvline, col) + } + + /// Get the offset of a relative visual line + pub fn offset_of_rvline( + &self, + text_prov: &impl TextLayoutProvider, + RVLine { line, line_index }: RVLine, + ) -> usize { + let rope_text = text_prov.rope_text(); + let font_size = self.font_size(line); + let layouts = self.text_layouts.borrow(); + + let base_offset = rope_text.offset_of_line(line); + + // We could remove the debug asserts and allow invalid line indices. However I think it is + // desirable to avoid those since they are probably indicative of bugs. + if let Some(text_layout) = layouts.get(font_size, line) { + debug_assert!( + line_index < text_layout.line_count(), + "Line index was out of bounds. This likely indicates keeping an rvline past when it was valid." + ); + + let line_index = line_index.min(text_layout.line_count() - 1); + + let col = text_layout + .start_layout_cols(text_prov, line) + .nth(line_index) + .unwrap_or(0); + let col = text_prov.before_phantom_col(line, col); + + base_offset + col + } else { + // There was no text layout for this line, so we treat it like if line index is zero + // even if it is not. + + debug_assert_eq!(line_index, 0, "Line index was zero. This likely indicates keeping an rvline past when it was valid."); + + base_offset + } + } + + /// Get the relative visual line of the buffer line + pub fn rvline_of_line(&self, text_prov: &impl TextLayoutProvider, line: usize) -> RVLine { + if self.is_linear(text_prov) { + return RVLine::new(line, 0); + } + + let offset = text_prov.rope_text().offset_of_line(line); + + find_rvline_of_offset(self, text_prov, offset, CursorAffinity::Backward) + .unwrap_or_else(|| self.last_rvline(text_prov)) + } + + /// Check whether the config id has changed, clearing the cache if it has. + pub fn check_config_id(&self, config_id: u64) { + // Check if the text layout needs to update due to the config being changed + if config_id != self.text_layouts.borrow().config_id { + let cache_rev = self.text_layouts.borrow().cache_rev + 1; + self.clear(cache_rev, Some(config_id)); + } + } + + /// Check whether the text layout cache revision is different. + /// Clears the layouts and updates the cache rev if it was different. + pub fn check_cache_rev(&self, cache_rev: u64) { + if cache_rev != self.text_layouts.borrow().cache_rev { + self.clear(cache_rev, None); + } + } + + /// Clear the text layouts with a given cache revision + pub fn clear(&self, cache_rev: u64, config_id: Option) { + self.text_layouts.borrow_mut().clear(cache_rev, config_id); + self.last_vline.set(None); + } + + /// Clear the layouts and vline without changing the cache rev or config id. + pub fn clear_unchanged(&self) { + self.text_layouts.borrow_mut().clear_unchanged(); + self.last_vline.set(None); + } +} + +/// This is a separate function as a hacky solution to lifetimes. +/// While it being on `Lines` makes the most sense, it being separate lets us only have +/// `text_layouts` and `wrap` from the original to then initialize a text layout. This simplifies +/// lifetime issues in some functions, since they can just have an `Arc`/`Rc`. +/// +/// Note: This does not clear the cache or check via config id. That should be done outside this +/// as `Lines` does require knowing when the cache is invalidated. +fn get_init_text_layout( + text_layouts: &RefCell, + layout_event: Option>, + text_prov: impl TextLayoutProvider, + line: usize, + font_size: usize, + wrap: ResolvedWrap, + last_vline: &Cell>, +) -> Arc { + // If we don't have a second layer of the hashmap initialized for this specific font size, + // do it now + if text_layouts.borrow().layouts.get(&font_size).is_none() { + let mut cache = text_layouts.borrow_mut(); + cache.layouts.insert(font_size, HashMap::new()); + } + + // Get whether there's an entry for this specific font size and line + let cache_exists = text_layouts + .borrow() + .layouts + .get(&font_size) + .unwrap() + .get(&line) + .is_some(); + // If there isn't an entry then we actually have to create it + if !cache_exists { + let text_layout = text_prov.new_text_layout(line, font_size, wrap); + + // Update last vline + if let Some(vline) = last_vline.get() { + let last_line = text_prov.rope_text().last_line(); + if line <= last_line { + // We can get rid of the old line count and add our new count. + // This lets us typically avoid having to calculate the last visual line. + let vline = vline.get(); + let new_vline = vline + (text_layout.line_count() - 1); + + last_vline.set(Some(VLine(new_vline))); + } + // If the line is past the end of the file, then we don't need to update the last + // visual line. It is garbage. + } + // Otherwise last vline was already None. + + { + // Add the text layout to the cache. + let mut cache = text_layouts.borrow_mut(); + let width = text_layout.text.size().width; + if width > cache.max_width { + cache.max_width = width; + } + cache + .layouts + .get_mut(&font_size) + .unwrap() + .insert(line, text_layout); + } + + if let Some(layout_event) = layout_event { + layout_event.send(LayoutEvent::CreatedLayout { font_size, line }); + } + } + + // Just get the entry, assuming it has been created because we initialize it above. + text_layouts + .borrow() + .layouts + .get(&font_size) + .unwrap() + .get(&line) + .cloned() + .unwrap() +} + +/// Returns (visual line, line_index) +fn find_vline_of_offset( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + offset: usize, + affinity: CursorAffinity, +) -> Option<(VLine, usize)> { + let layouts = lines.text_layouts.borrow(); + + let rope_text = text_prov.rope_text(); + + let buffer_line = rope_text.line_of_offset(offset); + let line_start_offset = rope_text.offset_of_line(buffer_line); + let vline = find_vline_of_line(lines, text_prov, buffer_line)?; + + let font_size = lines.font_size(buffer_line); + let Some(text_layout) = layouts.get(font_size, buffer_line) else { + // No text layout for this line, so the vline we found is definitely correct. + // As well, there is no previous soft line to consider + return Some((vline, 0)); + }; + + let col = offset - line_start_offset; + + let (vline, line_index) = find_start_line_index(text_prov, text_layout, buffer_line, col) + .map(|line_index| (VLine(vline.get() + line_index), line_index))?; + + // If the most recent line break was due to a soft line break, + if line_index > 0 { + if let CursorAffinity::Backward = affinity { + // TODO: This can definitely be smarter. We're doing a vline search, and then this is + // practically doing another! + let line_end = lines.offset_of_vline(text_prov, vline); + // then if we're right at that soft line break, a backwards affinity + // means that we are on the previous visual line. + if line_end == offset && vline.get() != 0 { + return Some((VLine(vline.get() - 1), line_index - 1)); + } + } + } + + Some((vline, line_index)) +} + +fn find_rvline_of_offset( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + offset: usize, + affinity: CursorAffinity, +) -> Option { + let layouts = lines.text_layouts.borrow(); + + let rope_text = text_prov.rope_text(); + + let buffer_line = rope_text.line_of_offset(offset); + let line_start_offset = rope_text.offset_of_line(buffer_line); + + let font_size = lines.font_size(buffer_line); + let Some(text_layout) = layouts.get(font_size, buffer_line) else { + // There is no text layout for this line so the line index is always zero. + return Some(RVLine::new(buffer_line, 0)); + }; + + let col = offset - line_start_offset; + + let rv = find_start_line_index(text_prov, text_layout, buffer_line, col) + .map(|line_index| RVLine::new(buffer_line, line_index))?; + + // If the most recent line break was due to a soft line break, + if rv.line_index > 0 { + if let CursorAffinity::Backward = affinity { + let line_end = lines.offset_of_rvline(text_prov, rv); + // then if we're right at that soft line break, a backwards affinity + // means that we are on the previous visual line. + if line_end == offset { + if rv.line_index > 0 { + return Some(RVLine::new(rv.line, rv.line_index - 1)); + } else if rv.line == 0 { + // There is no previous line, we do nothing. + } else { + // We have to get rvline info for that rvline, so we can get the last line index + // This should aways have at least one rvline in it. + let font_sizes = lines.font_sizes.borrow(); + let (prev, _) = prev_rvline(&layouts, text_prov, &**font_sizes, rv)?; + return Some(prev); + } + } + } + } + + Some(rv) +} + +// TODO: a lot of these just take lines, so should possibly just be put on it. + +/// Find the line index which contains the column. +fn find_start_line_index( + text_prov: &impl TextLayoutProvider, + text_layout: &TextLayoutLine, + line: usize, + col: usize, +) -> Option { + let mut starts = text_layout + .layout_cols(text_prov, line) + .enumerate() + .peekable(); + + while let Some((i, (layout_start, _))) = starts.next() { + // TODO: we should just apply after_col to col to do this transformation once + let layout_start = text_prov.before_phantom_col(line, layout_start); + if layout_start >= col { + return Some(i); + } + + let next_start = starts + .peek() + .map(|(_, (next_start, _))| text_prov.before_phantom_col(line, *next_start)); + + if let Some(next_start) = next_start { + if next_start > col { + // The next layout starts *past* our column, so we're on the previous line. + return Some(i); + } + } else { + // There was no next glyph, which implies that we are either on this line or not at all + return Some(i); + } + } + + None +} + +/// Get the first visual line of a buffer line. +fn find_vline_of_line( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + line: usize, +) -> Option { + let rope = text_prov.rope_text(); + + let last_line = rope.last_line(); + + if line > last_line / 2 { + // Often the last vline will already be cached, which lets us half the search time. + // The compiler may or may not be smart enough to combine the last vline calculation with + // our calculation of the vline of the line we're looking for, but it might not. + // If it doesn't, we could write a custom version easily. + let last_vline = lines.last_vline(text_prov); + let last_rvline = lines.last_rvline(text_prov); + let last_start_vline = VLine(last_vline.get() - last_rvline.line_index); + find_vline_of_line_backwards(lines, (last_start_vline, last_line), line) + } else { + find_vline_of_line_forwards(lines, (VLine(0), 0), line) + } +} + +/// Get the first visual line of a buffer line. +/// This searches backwards from `pivot`, so it should be *after* the given line. +/// This requires that the `pivot` is the first line index of the line it is for. +fn find_vline_of_line_backwards( + lines: &Lines, + (start, s_line): (VLine, usize), + line: usize, +) -> Option { + if line > s_line { + return None; + } else if line == s_line { + return Some(start); + } else if line == 0 { + return Some(VLine(0)); + } + + let layouts = lines.text_layouts.borrow(); + + let mut cur_vline = start.get(); + + for cur_line in line..s_line { + let font_size = lines.font_size(cur_line); + + let Some(text_layout) = layouts.get(font_size, cur_line) else { + // no text layout, so its just a normal line + cur_vline -= 1; + continue; + }; + + let line_count = text_layout.line_count(); + + cur_vline -= line_count; + } + + Some(VLine(cur_vline)) +} + +fn find_vline_of_line_forwards( + lines: &Lines, + (start, s_line): (VLine, usize), + line: usize, +) -> Option { + match line.cmp(&s_line) { + Ordering::Equal => return Some(start), + Ordering::Less => return None, + Ordering::Greater => (), + } + + let layouts = lines.text_layouts.borrow(); + + let mut cur_vline = start.get(); + + for cur_line in s_line..line { + let font_size = lines.font_size(cur_line); + + let Some(text_layout) = layouts.get(font_size, cur_line) else { + // no text layout, so its just a normal line + cur_vline += 1; + continue; + }; + + let line_count = text_layout.line_count(); + cur_vline += line_count; + } + + Some(VLine(cur_vline)) +} + +/// Find the (start offset, buffer line, layout line index) of a given visual line. +/// +/// start offset is into the file, rather than the text layouts string, so it does not include +/// phantom text. +/// +/// Returns `None` if the visual line is out of bounds. +fn find_vline_init_info( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + vline: VLine, +) -> Option<(usize, RVLine)> { + let rope_text = text_prov.rope_text(); + + if vline.get() == 0 { + return Some((0, RVLine::new(0, 0))); + } + + if lines.is_linear(text_prov) { + // If lines is linear then we can trivially convert the visual line to a buffer line + let line = vline.get(); + if line > rope_text.last_line() { + return None; + } + + return Some((rope_text.offset_of_line(line), RVLine::new(line, 0))); + } + + let last_vline = lines.last_vline(text_prov); + + if vline > last_vline { + return None; + } + + if vline.get() < last_vline.get() / 2 { + let last_rvline = lines.last_rvline(text_prov); + find_vline_init_info_rv_backward(lines, text_prov, (last_vline, last_rvline), vline) + } else { + find_vline_init_info_forward(lines, text_prov, (VLine(0), 0), vline) + } +} + +// TODO(minor): should we package (VLine, buffer line) into a struct since we use it for these +// pseudo relative calculations often? +/// Find the `(start offset, rvline)` of a given [`VLine`] +/// +/// start offset is into the file, rather than text layout's string, so it does not include +/// phantom text. +/// +/// Returns `None` if the visual line is out of bounds, or if the start is past our target. +fn find_vline_init_info_forward( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + (start, start_line): (VLine, usize), + vline: VLine, +) -> Option<(usize, RVLine)> { + if start > vline { + return None; + } + + let rope_text = text_prov.rope_text(); + + let mut cur_line = start_line; + let mut cur_vline = start.get(); + + let layouts = lines.text_layouts.borrow(); + while cur_vline < vline.get() { + let font_size = lines.font_size(cur_line); + let line_count = if let Some(text_layout) = layouts.get(font_size, cur_line) { + let line_count = text_layout.line_count(); + + // We can then check if the visual line is in this intervening range. + if cur_vline + line_count > vline.get() { + // We found the line that contains the visual line. + // We can now find the offset of the visual line within the line. + let line_index = vline.get() - cur_vline; + // TODO: is it fine to unwrap here? + let col = text_layout + .start_layout_cols(text_prov, cur_line) + .nth(line_index) + .unwrap_or(0); + let col = text_prov.before_phantom_col(cur_line, col); + + let base_offset = rope_text.offset_of_line(cur_line); + return Some((base_offset + col, RVLine::new(cur_line, line_index))); + } + + // The visual line is not in this line, so we have to keep looking. + line_count + } else { + // There was no text layout so we only have to consider the line breaks in this line. + // Which, since we don't handle phantom text, is just one. + + 1 + }; + + cur_line += 1; + cur_vline += line_count; + } + + // We've reached the visual line we're looking for, we can return the offset. + // This also handles the case where the vline is past the end of the text. + if cur_vline == vline.get() { + if cur_line > rope_text.last_line() { + return None; + } + + // We use cur_line because if our target vline is out of bounds + // then the result should be len + Some((rope_text.offset_of_line(cur_line), RVLine::new(cur_line, 0))) + } else { + // We've gone past the visual line we're looking for, so it is out of bounds. + None + } +} + +/// Find the `(start offset, rvline)` of a given [`VLine`] +/// +/// `start offset` is into the file, rather than the text layout's content, so it does not +/// include phantom text. +/// +/// Returns `None` if the visual line is out of bounds or if the start is before our target. +/// This iterates backwards. +fn find_vline_init_info_rv_backward( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + (start, start_rvline): (VLine, RVLine), + vline: VLine, +) -> Option<(usize, RVLine)> { + if start < vline { + // The start was before the target. + return None; + } + + // This would the vline at the very start of the buffer line + let shifted_start = VLine(start.get() - start_rvline.line_index); + match shifted_start.cmp(&vline) { + // The shifted start was equivalent to the vline, which makes it easy to compute + Ordering::Equal => { + let offset = text_prov.rope_text().offset_of_line(start_rvline.line); + Some((offset, RVLine::new(start_rvline.line, 0))) + } + // The new start is before the vline, that means the vline is on the same line. + Ordering::Less => { + let line_index = vline.get() - shifted_start.get(); + let layouts = lines.text_layouts.borrow(); + let font_size = lines.font_size(start_rvline.line); + if let Some(text_layout) = layouts.get(font_size, start_rvline.line) { + vline_init_info_b( + text_prov, + text_layout, + RVLine::new(start_rvline.line, line_index), + ) + } else { + // There was no text layout so we only have to consider the line breaks in this line. + + let base_offset = text_prov.rope_text().offset_of_line(start_rvline.line); + Some((base_offset, RVLine::new(start_rvline.line, 0))) + } + } + Ordering::Greater => find_vline_init_info_backward( + lines, + text_prov, + (shifted_start, start_rvline.line), + vline, + ), + } +} + +fn find_vline_init_info_backward( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + (mut start, mut start_line): (VLine, usize), + vline: VLine, +) -> Option<(usize, RVLine)> { + loop { + let (prev_vline, prev_line) = prev_line_start(lines, start, start_line)?; + + match prev_vline.cmp(&vline) { + // We found the target, and it was at the start + Ordering::Equal => { + let offset = text_prov.rope_text().offset_of_line(prev_line); + return Some((offset, RVLine::new(prev_line, 0))); + } + // The target is on this line, so we can just search for it + Ordering::Less => { + let font_size = lines.font_size(prev_line); + let layouts = lines.text_layouts.borrow(); + if let Some(text_layout) = layouts.get(font_size, prev_line) { + return vline_init_info_b( + text_prov, + text_layout, + RVLine::new(prev_line, vline.get() - prev_vline.get()), + ); + } else { + // There was no text layout so we only have to consider the line breaks in this line. + // Which, since we don't handle phantom text, is just one. + + let base_offset = text_prov.rope_text().offset_of_line(prev_line); + return Some((base_offset, RVLine::new(prev_line, 0))); + } + } + // The target is before this line, so we have to keep searching + Ordering::Greater => { + start = prev_vline; + start_line = prev_line; + } + } + } +} + +/// Get the previous (line, start visual line) from a (line, start visual line). +fn prev_line_start(lines: &Lines, vline: VLine, line: usize) -> Option<(VLine, usize)> { + if line == 0 { + return None; + } + + let layouts = lines.text_layouts.borrow(); + + let prev_line = line - 1; + let font_size = lines.font_size(line); + if let Some(layout) = layouts.get(font_size, prev_line) { + let line_count = layout.line_count(); + let prev_vline = vline.get() - line_count; + Some((VLine(prev_vline), prev_line)) + } else { + // There's no layout for the previous line which makes this easy + Some((VLine(vline.get() - 1), prev_line)) + } +} + +fn vline_init_info_b( + text_prov: &impl TextLayoutProvider, + text_layout: &TextLayoutLine, + rv: RVLine, +) -> Option<(usize, RVLine)> { + let rope_text = text_prov.rope_text(); + let col = text_layout + .start_layout_cols(text_prov, rv.line) + .nth(rv.line_index) + .unwrap_or(0); + let col = text_prov.before_phantom_col(rv.line, col); + + let base_offset = rope_text.offset_of_line(rv.line); + + Some((base_offset + col, rv)) +} + +/// Information about the visual line and how it relates to the underlying buffer line. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub struct VLineInfo { + /// Start offset to end offset in the buffer that this visual line covers. + /// Note that this is obviously not including phantom text. + pub interval: Interval, + /// The total number of lines in this buffer line. Always at least 1. + pub line_count: usize, + pub rvline: RVLine, + /// The actual visual line this is for. + /// For relative visual line iteration, this is empty. + pub vline: L, +} +impl VLineInfo { + fn new>(iv: I, rvline: RVLine, line_count: usize, vline: L) -> Self { + Self { + interval: iv.into(), + line_count, + rvline, + vline, + } + } + + pub fn to_blank(&self) -> VLineInfo<()> { + VLineInfo::new(self.interval, self.rvline, self.line_count, ()) + } + + /// Check whether the interval is empty. + /// Note that there could still be phantom text on this line. + pub fn is_empty(&self) -> bool { + self.interval.is_empty() + } + + pub fn is_first(&self) -> bool { + self.rvline.is_first() + } + + // TODO: is this correct for phantom lines? + // TODO: can't we just use the line count field now? + /// Is this the last visual line for the relevant buffer line? + pub fn is_last(&self, text_prov: &impl TextLayoutProvider) -> bool { + let rope_text = text_prov.rope_text(); + let line_end = rope_text.line_end_offset(self.rvline.line, false); + let vline_end = self.line_end_offset(text_prov, false); + + line_end == vline_end + } + + /// Get the first column of the overall line of the visual line + pub fn first_col(&self, text_prov: &impl TextLayoutProvider) -> usize { + let line_start = self.interval.start; + let start_offset = text_prov.text().offset_of_line(self.rvline.line); + line_start - start_offset + } + + /// Get the last column in the overall line of this visual line + /// The caret decides whether it is after the last character, or before it. + /// ```rust,ignore + /// // line content = "conf = Config::default();\n" + /// // wrapped breakup = ["conf = ", "Config::default();\n"] + /// + /// // when vline_info is for "conf = " + /// assert_eq!(vline_info.last_col(text_prov, false), 6) // "conf =| " + /// assert_eq!(vline_info.last_col(text_prov, true), 7) // "conf = |" + /// // when vline_info is for "Config::default();\n" + /// // Notice that the column is in the overall line, not the wrapped line. + /// assert_eq!(vline_info.last_col(text_prov, false), 24) // "Config::default()|;" + /// assert_eq!(vline_info.last_col(text_prov, true), 25) // "Config::default();|" + /// ``` + pub fn last_col(&self, text_prov: &impl TextLayoutProvider, caret: bool) -> usize { + let vline_end = self.interval.end; + let start_offset = text_prov.text().offset_of_line(self.rvline.line); + // If these subtractions crash, then it is likely due to a bad vline being kept around + // somewhere + if !caret && !self.interval.is_empty() { + let vline_pre_end = text_prov.rope_text().prev_grapheme_offset(vline_end, 1, 0); + vline_pre_end - start_offset + } else { + vline_end - start_offset + } + } + + // TODO: we could generalize `RopeText::line_end_offset` to any interval, and then just use it here instead of basically reimplementing it. + pub fn line_end_offset(&self, text_prov: &impl TextLayoutProvider, caret: bool) -> usize { + let text = text_prov.text(); + let rope_text = RopeTextRef::new(text); + + let mut offset = self.interval.end; + let mut line_content: &str = &text.slice_to_cow(self.interval); + if line_content.ends_with("\r\n") { + offset -= 2; + line_content = &line_content[..line_content.len() - 2]; + } else if line_content.ends_with('\n') { + offset -= 1; + line_content = &line_content[..line_content.len() - 1]; + } + if !caret && !line_content.is_empty() { + offset = rope_text.prev_grapheme_offset(offset, 1, 0); + } + offset + } + + /// Returns the offset of the first non-blank character in the line. + pub fn first_non_blank_character(&self, text_prov: &impl TextLayoutProvider) -> usize { + WordCursor::new(text_prov.text(), self.interval.start).next_non_blank_char() + } +} + +/// Iterator of the visual lines in a [`Lines`]. +/// This only considers wrapped and phantom text lines that have been rendered into a text layout. +/// +/// In principle, we could consider the newlines in phantom text for lines that have not been +/// rendered. However, that is more expensive to compute and is probably not actually *useful*. +struct VisualLines { + v: VisualLinesRelative, + vline: VLine, +} +impl VisualLines { + pub fn new(lines: &Lines, text_prov: T, backwards: bool, start: VLine) -> VisualLines { + // TODO(minor): If we aren't using offset here then don't calculate it. + let Some((_offset, rvline)) = find_vline_init_info(lines, &text_prov, start) else { + return VisualLines::empty(lines, text_prov, backwards); + }; + + VisualLines { + v: VisualLinesRelative::new(lines, text_prov, backwards, rvline), + vline: start, + } + } + + pub fn empty(lines: &Lines, text_prov: T, backwards: bool) -> VisualLines { + VisualLines { + v: VisualLinesRelative::empty(lines, text_prov, backwards), + vline: VLine(0), + } + } +} +impl Iterator for VisualLines { + type Item = VLineInfo; + + fn next(&mut self) -> Option { + let was_first_iter = self.v.is_first_iter; + let info = self.v.next()?; + + if !was_first_iter { + if self.v.backwards { + // This saturation isn't really needed, but just in case. + debug_assert!( + self.vline.get() != 0, + "Expected VLine to always be nonzero if we were going backwards" + ); + self.vline = VLine(self.vline.get().saturating_sub(1)); + } else { + self.vline = VLine(self.vline.get() + 1); + } + } + + Some(VLineInfo { + interval: info.interval, + line_count: info.line_count, + rvline: info.rvline, + vline: self.vline, + }) + } +} + +/// Iterator of the visual lines in a [`Lines`] relative to some starting buffer line. +/// This only considers wrapped and phantom text lines that have been rendered into a text layout. +struct VisualLinesRelative { + font_sizes: Rc, + text_layouts: Rc>, + text_prov: T, + + is_done: bool, + + rvline: RVLine, + /// Our current offset into the rope. + offset: usize, + + /// Which direction we should move in. + backwards: bool, + /// Whether there is a one-to-one mapping between buffer lines and visual lines. + linear: bool, + + is_first_iter: bool, +} +impl VisualLinesRelative { + pub fn new( + lines: &Lines, + text_prov: T, + backwards: bool, + start: RVLine, + ) -> VisualLinesRelative { + // Empty iterator if we're past the end of the possible lines + if start > lines.last_rvline(&text_prov) { + return VisualLinesRelative::empty(lines, text_prov, backwards); + } + + let layouts = lines.text_layouts.borrow(); + let font_size = lines.font_size(start.line); + let offset = rvline_offset(&layouts, &text_prov, font_size, start); + + let linear = lines.is_linear(&text_prov); + + VisualLinesRelative { + font_sizes: lines.font_sizes.borrow().clone(), + text_layouts: lines.text_layouts.clone(), + text_prov, + is_done: false, + rvline: start, + offset, + backwards, + linear, + is_first_iter: true, + } + } + + pub fn empty(lines: &Lines, text_prov: T, backwards: bool) -> VisualLinesRelative { + VisualLinesRelative { + font_sizes: lines.font_sizes.borrow().clone(), + text_layouts: lines.text_layouts.clone(), + text_prov, + is_done: true, + rvline: RVLine::new(0, 0), + offset: 0, + backwards, + linear: true, + is_first_iter: true, + } + } +} +impl Iterator for VisualLinesRelative { + type Item = VLineInfo<()>; + + fn next(&mut self) -> Option { + if self.is_done { + return None; + } + + let layouts = self.text_layouts.borrow(); + if self.is_first_iter { + // This skips the next line call on the first line. + self.is_first_iter = false; + } else { + let v = shift_rvline( + &layouts, + &self.text_prov, + &*self.font_sizes, + self.rvline, + self.backwards, + self.linear, + ); + let Some((new_rel_vline, offset)) = v else { + self.is_done = true; + return None; + }; + + self.rvline = new_rel_vline; + self.offset = offset; + + if self.rvline.line > self.text_prov.rope_text().last_line() { + self.is_done = true; + return None; + } + } + + let line = self.rvline.line; + let line_index = self.rvline.line_index; + let vline = self.rvline; + + let start = self.offset; + + let font_size = self.font_sizes.font_size(line); + let end = end_of_rvline(&layouts, &self.text_prov, font_size, self.rvline); + + let line_count = if let Some(text_layout) = layouts.get(font_size, line) { + text_layout.line_count() + } else { + 1 + }; + debug_assert!(start <= end, "line: {line}, line_index: {line_index}, line_count: {line_count}, vline: {vline:?}, start: {start}, end: {end}, backwards: {} text_len: {}", self.backwards, self.text_prov.text().len()); + let info = VLineInfo::new(start..end, self.rvline, line_count, ()); + + Some(info) + } +} + +// TODO: This might skip spaces at the end of lines, which we probably don't want? +/// Get the end offset of the visual line from the file's line and the line index. +fn end_of_rvline( + layouts: &TextLayoutCache, + text_prov: &impl TextLayoutProvider, + font_size: usize, + RVLine { line, line_index }: RVLine, +) -> usize { + if line > text_prov.rope_text().last_line() { + return text_prov.text().len(); + } + + if let Some((_, end_col)) = layouts.get_layout_col(text_prov, font_size, line, line_index) { + let end_col = text_prov.before_phantom_col(line, end_col); + let base_offset = text_prov.text().offset_of_line(line); + + base_offset + end_col + } else { + let rope_text = text_prov.rope_text(); + + rope_text.line_end_offset(line, true) + } +} + +/// Shift a relative visual line forward or backwards based on the `backwards` parameter. +fn shift_rvline( + layouts: &TextLayoutCache, + text_prov: &impl TextLayoutProvider, + font_sizes: &dyn LineFontSizeProvider, + vline: RVLine, + backwards: bool, + linear: bool, +) -> Option<(RVLine, usize)> { + if linear { + let rope_text = text_prov.rope_text(); + debug_assert_eq!( + vline.line_index, 0, + "Line index should be zero if we're linearly working with lines" + ); + if backwards { + if vline.line == 0 { + return None; + } + + let prev_line = vline.line - 1; + let offset = rope_text.offset_of_line(prev_line); + Some((RVLine::new(prev_line, 0), offset)) + } else { + let next_line = vline.line + 1; + + if next_line > rope_text.last_line() { + return None; + } + + let offset = rope_text.offset_of_line(next_line); + Some((RVLine::new(next_line, 0), offset)) + } + } else if backwards { + prev_rvline(layouts, text_prov, font_sizes, vline) + } else { + let font_size = font_sizes.font_size(vline.line); + Some(next_rvline(layouts, text_prov, font_size, vline)) + } +} + +fn rvline_offset( + layouts: &TextLayoutCache, + text_prov: &impl TextLayoutProvider, + font_size: usize, + RVLine { line, line_index }: RVLine, +) -> usize { + let rope_text = text_prov.rope_text(); + if let Some((line_col, _)) = layouts.get_layout_col(text_prov, font_size, line, line_index) { + let line_offset = rope_text.offset_of_line(line); + let line_col = text_prov.before_phantom_col(line, line_col); + + line_offset + line_col + } else { + // There was no text layout line so this is a normal line. + debug_assert_eq!(line_index, 0); + + rope_text.offset_of_line(line) + } +} + +/// Move to the next visual line, giving the new information. +/// Returns `(new rel vline, offset)` +fn next_rvline( + layouts: &TextLayoutCache, + text_prov: &impl TextLayoutProvider, + font_size: usize, + RVLine { line, line_index }: RVLine, +) -> (RVLine, usize) { + let rope_text = text_prov.rope_text(); + if let Some(layout_line) = layouts.get(font_size, line) { + if let Some((line_col, _)) = layout_line.layout_cols(text_prov, line).nth(line_index + 1) { + let line_offset = rope_text.offset_of_line(line); + let line_col = text_prov.before_phantom_col(line, line_col); + let offset = line_offset + line_col; + + (RVLine::new(line, line_index + 1), offset) + } else { + // There was no next layout/vline on this buffer line. + // So we can simply move to the start of the next buffer line. + + (RVLine::new(line + 1, 0), rope_text.offset_of_line(line + 1)) + } + } else { + // There was no text layout line, so this is a normal line. + debug_assert_eq!(line_index, 0); + + (RVLine::new(line + 1, 0), rope_text.offset_of_line(line + 1)) + } +} + +/// Move to the previous visual line, giving the new information. +/// Returns `(new line, new line_index, offset)` +/// Returns `None` if the line and line index are zero and thus there is no previous visual line. +fn prev_rvline( + layouts: &TextLayoutCache, + text_prov: &impl TextLayoutProvider, + font_sizes: &dyn LineFontSizeProvider, + RVLine { line, line_index }: RVLine, +) -> Option<(RVLine, usize)> { + let rope_text = text_prov.rope_text(); + if line_index == 0 { + // Line index was zero so we must be moving back a buffer line + if line == 0 { + return None; + } + + let prev_line = line - 1; + let font_size = font_sizes.font_size(prev_line); + if let Some(layout_line) = layouts.get(font_size, prev_line) { + let line_offset = rope_text.offset_of_line(prev_line); + let (i, line_col) = layout_line + .start_layout_cols(text_prov, prev_line) + .enumerate() + .last() + .unwrap_or((0, 0)); + let line_col = text_prov.before_phantom_col(prev_line, line_col); + let offset = line_offset + line_col; + + Some((RVLine::new(prev_line, i), offset)) + } else { + // There was no text layout line, so the previous line is a normal line. + let prev_line_offset = rope_text.offset_of_line(prev_line); + Some((RVLine::new(prev_line, 0), prev_line_offset)) + } + } else { + // We're still on the same buffer line, so we can just move to the previous layout/vline. + + let prev_line_index = line_index - 1; + let font_size = font_sizes.font_size(line); + if let Some(layout_line) = layouts.get(font_size, line) { + if let Some((line_col, _)) = layout_line + .layout_cols(text_prov, line) + .nth(prev_line_index) + { + let line_offset = rope_text.offset_of_line(line); + let line_col = text_prov.before_phantom_col(line, line_col); + let offset = line_offset + line_col; + + Some((RVLine::new(line, prev_line_index), offset)) + } else { + // There was no previous layout/vline on this buffer line. + // So we can simply move to the end of the previous buffer line. + + let prev_line_offset = rope_text.offset_of_line(line - 1); + Some((RVLine::new(line - 1, 0), prev_line_offset)) + } + } else { + debug_assert!( + false, + "line_index was nonzero but there was no text layout line" + ); + // Despite that this shouldn't happen we default to just giving the start of this + // normal line + let line_offset = rope_text.offset_of_line(line); + Some((RVLine::new(line, 0), line_offset)) + } + } +} + +// FIXME: Put this in our cosmic-text fork. + +/// Hit position but decides wether it should go to the next line based on the `before` bool. +/// (Hit position should be equivalent to `before=false`). +/// This is needed when we have an idx at the end of, for example, a wrapped line which could be on +/// the first or second line. +pub fn hit_position_aff(this: &TextLayout, idx: usize, before: bool) -> HitPosition { + let mut last_line = 0; + let mut last_end: usize = 0; + let mut offset = 0; + let mut last_glyph: Option<&LayoutGlyph> = None; + let mut last_line_width = 0.0; + let mut last_glyph_width = 0.0; + let mut last_position = HitPosition { + line: 0, + point: Point::ZERO, + glyph_ascent: 0.0, + glyph_descent: 0.0, + }; + for (line, run) in this.layout_runs().enumerate() { + if run.line_i > last_line { + last_line = run.line_i; + offset += last_end + 1; + } + + // Handles wrapped lines, like: + // ```rust + // let config_path = | + // dirs::config_dir(); + // ``` + // The glyphs won't contain the space at the end of the first part, and the position right + // after the space is the same column as at `|dirs`, which is what before is letting us + // distinguish. + // So essentially, if the next run has a glyph that is at the same idx as the end of the + // previous run, *and* it is at `idx` itself, then we know to position it on the previous. + if let Some(last_glyph) = last_glyph { + if let Some(first_glyph) = run.glyphs.first() { + let end = last_glyph.end + offset + 1; + if before && end == idx && end == first_glyph.start + offset { + last_position.point.x = (last_line_width + last_glyph.w) as f64; + return last_position; + } + } + } + + for glyph in run.glyphs { + if glyph.start + offset > idx { + last_position.point.x += last_glyph_width as f64; + return last_position; + } + last_end = glyph.end; + last_glyph_width = glyph.w; + last_position = HitPosition { + line, + point: Point::new(glyph.x as f64, run.line_y as f64), + glyph_ascent: run.glyph_ascent as f64, + glyph_descent: run.glyph_descent as f64, + }; + if (glyph.start + offset..glyph.end + offset).contains(&idx) { + return last_position; + } + } + + last_glyph = run.glyphs.last(); + last_line_width = run.line_w; + } + + if idx > 0 { + last_position.point.x += last_glyph_width as f64; + return last_position; + } + + HitPosition { + line: 0, + point: Point::ZERO, + glyph_ascent: 0.0, + glyph_descent: 0.0, + } +} + +#[cfg(test)] +mod tests { + use std::{borrow::Cow, cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; + + use floem_editor_core::{ + buffer::rope_text::{RopeText, RopeTextRef}, + cursor::CursorAffinity, + }; + use floem_reactive::Scope; + use floem_renderer::cosmic_text::{Attrs, AttrsList, FamilyOwned, TextLayout, Wrap}; + use lapce_xi_rope::Rope; + use smallvec::smallvec; + + use crate::views::editor::{ + layout::TextLayoutLine, + phantom_text::{PhantomText, PhantomTextKind, PhantomTextLine}, + visual_line::{find_vline_of_line_backwards, find_vline_of_line_forwards}, + }; + + use super::{ + find_vline_init_info_forward, find_vline_init_info_rv_backward, FontSizeCacheId, + LineFontSizeProvider, Lines, RVLine, ResolvedWrap, TextLayoutProvider, VLine, + }; + + /// For most of the logic we standardize on a specific font size. + const FONT_SIZE: usize = 12; + + struct TestTextLayoutProvider<'a> { + text: &'a Rope, + phantom: HashMap, + font_family: Vec, + #[allow(dead_code)] + wrap: Wrap, + } + impl<'a> TestTextLayoutProvider<'a> { + fn new(text: &'a Rope, ph: HashMap, wrap: Wrap) -> Self { + Self { + text, + phantom: ph, + // we use a specific font to make width calculations consistent between platforms. + // TODO(minor): Is there a more common font that we can use? + font_family: FamilyOwned::parse_list("Cascadia Code").collect(), + wrap, + } + } + } + impl<'a> TextLayoutProvider for TestTextLayoutProvider<'a> { + fn text(&self) -> &Rope { + self.text + } + + // An implementation relatively close to the actual new text layout impl but simplified. + // TODO(minor): It would be nice to just use the same impl as view's + fn new_text_layout( + &self, + line: usize, + font_size: usize, + wrap: ResolvedWrap, + ) -> Arc { + let rope_text = RopeTextRef::new(self.text); + let line_content_original = rope_text.line_content(line); + + // Get the line content with newline characters replaced with spaces + // and the content without the newline characters + let (line_content, _line_content_original) = + if let Some(s) = line_content_original.strip_suffix("\r\n") { + ( + format!("{s} "), + &line_content_original[..line_content_original.len() - 2], + ) + } else if let Some(s) = line_content_original.strip_suffix('\n') { + ( + format!("{s} ",), + &line_content_original[..line_content_original.len() - 1], + ) + } else { + ( + line_content_original.to_string(), + &line_content_original[..], + ) + }; + + let phantom_text = self.phantom.get(&line).cloned().unwrap_or_default(); + let line_content = phantom_text.combine_with_text(&line_content); + + // let color + + let attrs = Attrs::new() + .family(&self.font_family) + .font_size(font_size as f32); + let mut attrs_list = AttrsList::new(attrs); + + // We don't do line styles, since they aren't relevant + + // Apply phantom text specific styling + for (offset, size, col, phantom) in phantom_text.offset_size_iter() { + let start = col + offset; + let end = start + size; + + let mut attrs = attrs; + if let Some(fg) = phantom.fg { + attrs = attrs.color(fg); + } + if let Some(phantom_font_size) = phantom.font_size { + attrs = attrs.font_size(phantom_font_size.min(font_size) as f32); + } + attrs_list.add_span(start..end, attrs); + // if let Some(font_family) = phantom.font_family.clone() { + // layout_builder = layout_builder.range_attribute( + // start..end, + // TextAttribute::FontFamily(font_family), + // ); + // } + } + + let mut text_layout = TextLayout::new(); + text_layout.set_wrap(Wrap::Word); + match wrap { + // We do not have to set the wrap mode if we do not set the width + ResolvedWrap::None => {} + ResolvedWrap::Column(_col) => todo!(), + ResolvedWrap::Width(px) => { + text_layout.set_size(px, f32::MAX); + } + } + text_layout.set_text(&line_content, attrs_list); + + // skip phantom text background styling because it doesn't shift positions + // skip severity styling + // skip diagnostic background styling + + Arc::new(TextLayoutLine { + extra_style: Vec::new(), + text: text_layout, + whitespaces: None, + indent: 0.0, + }) + } + + fn before_phantom_col(&self, line: usize, col: usize) -> usize { + self.phantom + .get(&line) + .map(|x| x.before_col(col)) + .unwrap_or(col) + } + + fn has_multiline_phantom(&self) -> bool { + // Conservatively, yes. + true + } + } + + struct TestFontSize { + font_size: usize, + } + impl LineFontSizeProvider for TestFontSize { + fn font_size(&self, _line: usize) -> usize { + self.font_size + } + + fn cache_id(&self) -> FontSizeCacheId { + 0 + } + } + + fn make_lines(text: &Rope, width: f32, init: bool) -> (TestTextLayoutProvider<'_>, Lines) { + make_lines_ph(text, width, init, HashMap::new()) + } + + fn make_lines_ph( + text: &Rope, + width: f32, + init: bool, + ph: HashMap, + ) -> (TestTextLayoutProvider<'_>, Lines) { + let wrap = Wrap::Word; + let r_wrap = ResolvedWrap::Width(width); + let font_sizes = TestFontSize { + font_size: FONT_SIZE, + }; + let text = TestTextLayoutProvider::new(text, ph, wrap); + let cx = Scope::new(); + let lines = Lines::new(cx, RefCell::new(Rc::new(font_sizes))); + lines.set_wrap(r_wrap); + + if init { + let config_id = 0; + lines.init_all(config_id, &text, true); + } + + (text, lines) + } + + fn render_breaks<'a>(text: &'a Rope, lines: &mut Lines, font_size: usize) -> Vec> { + // TODO: line_content on ropetextref would have the lifetime reference rope_text + // rather than the held &'a Rope. + // I think this would require an alternate trait for those functions to avoid incorrect lifetimes. Annoying but workable. + let rope_text = RopeTextRef::new(text); + let mut result = Vec::new(); + let layouts = lines.text_layouts.borrow(); + + for line in 0..rope_text.num_lines() { + if let Some(text_layout) = layouts.get(font_size, line) { + let lines = &text_layout.text.lines; + for line in lines { + let layouts = line.layout_opt().as_deref().unwrap(); + for layout in layouts { + // Spacing + if layout.glyphs.is_empty() { + continue; + } + let start_idx = layout.glyphs[0].start; + let end_idx = layout.glyphs.last().unwrap().end; + // Hacky solution to include the ending space/newline since those get trimmed off + let line_content = line + .text() + .get(start_idx..=end_idx) + .unwrap_or(&line.text()[start_idx..end_idx]); + result.push(Cow::Owned(line_content.to_string())); + } + } + } else { + let line_content = rope_text.line_content(line); + + let line_content = match line_content { + Cow::Borrowed(x) => { + if let Some(x) = x.strip_suffix('\n') { + // Cow::Borrowed(x) + Cow::Owned(x.to_string()) + } else { + // Cow::Borrowed(x) + Cow::Owned(x.to_string()) + } + } + Cow::Owned(x) => { + if let Some(x) = x.strip_suffix('\n') { + Cow::Owned(x.to_string()) + } else { + Cow::Owned(x) + } + } + }; + result.push(line_content); + } + } + result + } + + /// Utility fn to quickly create simple phantom text + fn mph(kind: PhantomTextKind, col: usize, text: &str) -> PhantomText { + PhantomText { + kind, + col, + text: text.to_string(), + font_size: None, + fg: None, + bg: None, + under_line: None, + } + } + + fn ffvline_info( + lines: &Lines, + text_prov: impl TextLayoutProvider, + vline: VLine, + ) -> Option<(usize, RVLine)> { + find_vline_init_info_forward(lines, &text_prov, (VLine(0), 0), vline) + } + + fn fbvline_info( + lines: &Lines, + text_prov: impl TextLayoutProvider, + vline: VLine, + ) -> Option<(usize, RVLine)> { + let last_vline = lines.last_vline(&text_prov); + let last_rvline = lines.last_rvline(&text_prov); + find_vline_init_info_rv_backward(lines, &text_prov, (last_vline, last_rvline), vline) + } + + #[test] + fn find_vline_init_info_empty() { + // Test empty buffer + let text = Rope::from(""); + let (text_prov, lines) = make_lines(&text, 50.0, false); + + assert_eq!( + ffvline_info(&lines, &text_prov, VLine(0)), + Some((0, RVLine::new(0, 0))) + ); + assert_eq!( + fbvline_info(&lines, &text_prov, VLine(0)), + Some((0, RVLine::new(0, 0))) + ); + assert_eq!(ffvline_info(&lines, &text_prov, VLine(1)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(1)), None); + + // Test empty buffer with phantom text and no wrapping + let text = Rope::from(""); + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 0, "hello world abc")], + }, + ); + let (text_prov, lines) = make_lines_ph(&text, 20.0, false, ph); + + assert_eq!( + ffvline_info(&lines, &text_prov, VLine(0)), + Some((0, RVLine::new(0, 0))) + ); + assert_eq!( + fbvline_info(&lines, &text_prov, VLine(0)), + Some((0, RVLine::new(0, 0))) + ); + assert_eq!(ffvline_info(&lines, &text_prov, VLine(1)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(1)), None); + + // Test empty buffer with phantom text and wrapping + lines.init_all(0, &text_prov, true); + + assert_eq!( + ffvline_info(&lines, &text_prov, VLine(0)), + Some((0, RVLine::new(0, 0))) + ); + assert_eq!( + fbvline_info(&lines, &text_prov, VLine(0)), + Some((0, RVLine::new(0, 0))) + ); + assert_eq!( + ffvline_info(&lines, &text_prov, VLine(1)), + Some((0, RVLine::new(0, 1))) + ); + assert_eq!( + fbvline_info(&lines, &text_prov, VLine(1)), + Some((0, RVLine::new(0, 1))) + ); + assert_eq!( + ffvline_info(&lines, &text_prov, VLine(2)), + Some((0, RVLine::new(0, 2))) + ); + assert_eq!( + fbvline_info(&lines, &text_prov, VLine(2)), + Some((0, RVLine::new(0, 2))) + ); + // Going outside bounds only ends up with None + assert_eq!(ffvline_info(&lines, &text_prov, VLine(3)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(3)), None); + // The affinity would shift from the front/end of the phantom line + // TODO: test affinity of logic behind clicking past the last vline? + } + + #[test] + fn find_vline_init_info_unwrapping() { + // Multiple lines with too large width for there to be any wrapping. + let text = Rope::from("hello\nworld toast and jam\nthe end\nhi"); + let rope_text = RopeTextRef::new(&text); + let (text_prov, mut lines) = make_lines(&text, 500.0, false); + + // Assert that with no text layouts (aka no wrapping and no phantom text) the function + // works + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["hello", "world toast and jam", "the end", "hi"] + ); + + lines.init_all(0, &text_prov, true); + + // Assert that even with text layouts, if it has no wrapping applied (because the width is large in this case) and no phantom text then it produces the same offsets as before. + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["hello ", "world toast and jam ", "the end ", "hi"] + ); + } + + #[test] + fn find_vline_init_info_phantom_unwrapping() { + let text = Rope::from("hello\nworld toast and jam\nthe end\nhi"); + let rope_text = RopeTextRef::new(&text); + + // Multiple lines with too large width for there to be any wrapping and phantom text + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 0, "greet world")], + }, + ); + + let (text_prov, lines) = make_lines_ph(&text, 500.0, false, ph); + + // With no text layouts, phantom text isn't initialized so it has no affect. + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + lines.init_all(0, &text_prov, true); + + // With text layouts, the phantom text is applied. + // But with a single line of phantom text, it doesn't affect the offsets. + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + // Multiple lines with too large width and a phantom text that takes up multiple lines. + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 0, "greet\nworld"),], + }, + ); + + let (text_prov, mut lines) = make_lines_ph(&text, 500.0, false, ph); + + // With no text layouts, phantom text isn't initialized so it has no affect. + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + lines.init_all(0, &text_prov, true); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + [ + "greet", + "worldhello ", + "world toast and jam ", + "the end ", + "hi" + ] + ); + + // With text layouts, the phantom text is applied. + // With a phantom text that takes up multiple lines, it does not affect the offsets + // but it does affect the valid visual lines. + let info = ffvline_info(&lines, &text_prov, VLine(0)); + assert_eq!(info, Some((0, RVLine::new(0, 0)))); + let info = fbvline_info(&lines, &text_prov, VLine(0)); + assert_eq!(info, Some((0, RVLine::new(0, 0)))); + let info = ffvline_info(&lines, &text_prov, VLine(1)); + assert_eq!(info, Some((0, RVLine::new(0, 1)))); + let info = fbvline_info(&lines, &text_prov, VLine(1)); + assert_eq!(info, Some((0, RVLine::new(0, 1)))); + + for line in 2..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line - 1); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!( + info, + (line_offset, RVLine::new(line - 1, 0)), + "vline {}", + line + ); + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!( + info, + (line_offset, RVLine::new(line - 1, 0)), + "vline {}", + line + ); + } + + // Then there's one extra vline due to the phantom text wrapping + let line_offset = rope_text.offset_of_line(rope_text.last_line()); + + let info = ffvline_info(&lines, &text_prov, VLine(rope_text.last_line() + 1)); + assert_eq!( + info, + Some((line_offset, RVLine::new(rope_text.last_line(), 0))), + "line {}", + rope_text.last_line() + 1, + ); + let info = fbvline_info(&lines, &text_prov, VLine(rope_text.last_line() + 1)); + assert_eq!( + info, + Some((line_offset, RVLine::new(rope_text.last_line(), 0))), + "line {}", + rope_text.last_line() + 1, + ); + + // Multiple lines with too large width and a phantom text that takes up multiple lines. + // But the phantom text is not at the start of the first line. + let mut ph = HashMap::new(); + ph.insert( + 2, // "the end" + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 3, "greet\nworld"),], + }, + ); + + let (text_prov, mut lines) = make_lines_ph(&text, 500.0, false, ph); + + // With no text layouts, phantom text isn't initialized so it has no affect. + for line in 0..rope_text.num_lines() { + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + + let line_offset = rope_text.offset_of_line(line); + + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + lines.init_all(0, &text_prov, true); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + [ + "hello ", + "world toast and jam ", + "thegreet", + "world end ", + "hi" + ] + ); + + // With text layouts, the phantom text is applied. + // With a phantom text that takes up multiple lines, it does not affect the offsets + // but it does affect the valid visual lines. + for line in 0..3 { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + // ' end' + let info = ffvline_info(&lines, &text_prov, VLine(3)); + assert_eq!(info, Some((29, RVLine::new(2, 1)))); + let info = fbvline_info(&lines, &text_prov, VLine(3)); + assert_eq!(info, Some((29, RVLine::new(2, 1)))); + + let info = ffvline_info(&lines, &text_prov, VLine(4)); + assert_eq!(info, Some((34, RVLine::new(3, 0)))); + let info = fbvline_info(&lines, &text_prov, VLine(4)); + assert_eq!(info, Some((34, RVLine::new(3, 0)))); + } + + #[test] + fn find_vline_init_info_basic_wrapping() { + // Tests with more mixes of text layout lines and uninitialized lines + + // Multiple lines with a small enough width for there to be a bunch of wrapping + let text = Rope::from("hello\nworld toast and jam\nthe end\nhi"); + let rope_text = RopeTextRef::new(&text); + let (text_prov, mut lines) = make_lines(&text, 30.0, false); + + // Assert that with no text layouts (aka no wrapping and no phantom text) the function + // works + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "line {}", line); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "line {}", line); + } + + assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["hello", "world toast and jam", "the end", "hi"] + ); + + lines.init_all(0, &text_prov, true); + + { + let layouts = lines.text_layouts.borrow(); + + assert!(layouts.get(FONT_SIZE, 0).is_some()); + assert!(layouts.get(FONT_SIZE, 1).is_some()); + assert!(layouts.get(FONT_SIZE, 2).is_some()); + assert!(layouts.get(FONT_SIZE, 3).is_some()); + assert!(layouts.get(FONT_SIZE, 4).is_none()); + } + + // start offset, start buffer line, layout line index) + let line_data = [ + (0, 0, 0), + (6, 1, 0), + (12, 1, 1), + (18, 1, 2), + (22, 1, 3), + (26, 2, 0), + (30, 2, 1), + (34, 3, 0), + ]; + assert_eq!(lines.last_vline(&text_prov), VLine(7)); + assert_eq!(lines.last_rvline(&text_prov), RVLine::new(3, 0)); + #[allow(clippy::needless_range_loop)] + for line in 0..8 { + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!( + (info.0, info.1.line, info.1.line_index), + line_data[line], + "vline {}", + line + ); + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!( + (info.0, info.1.line, info.1.line_index), + line_data[line], + "vline {}", + line + ); + } + + // Directly out of bounds + assert_eq!(ffvline_info(&lines, &text_prov, VLine(9)), None,); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(9)), None,); + + assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["hello ", "world ", "toast ", "and ", "jam ", "the ", "end ", "hi"] + ); + + let vline_line_data = [0, 1, 5, 7]; + + let rope = text_prov.rope_text(); + let last_start_vline = + VLine(lines.last_vline(&text_prov).get() - lines.last_rvline(&text_prov).line_index); + #[allow(clippy::needless_range_loop)] + for line in 0..4 { + let vline = VLine(vline_line_data[line]); + assert_eq!( + find_vline_of_line_forwards(&lines, Default::default(), line), + Some(vline) + ); + assert_eq!( + find_vline_of_line_backwards(&lines, (last_start_vline, rope.last_line()), line), + Some(vline), + "line: {line}" + ); + } + + let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); + let (text_prov, mut lines) = make_lines(&text, 2., true); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["aaaa ", "bb ", "bb ", "cc ", "cc ", "dddd ", "eeee ", "ff ", "ff ", "gggg"] + ); + + // (start offset, start buffer line, layout line index) + let line_data = [ + (0, 0, 0), + (5, 1, 0), + (8, 1, 1), + (11, 1, 2), + (14, 2, 0), + (17, 2, 1), + (22, 2, 2), + (27, 2, 3), + (30, 3, 0), + (33, 3, 1), + ]; + #[allow(clippy::needless_range_loop)] + for vline in 0..10 { + let info = ffvline_info(&lines, &text_prov, VLine(vline)).unwrap(); + assert_eq!( + (info.0, info.1.line, info.1.line_index), + line_data[vline], + "vline {}", + vline + ); + let info = fbvline_info(&lines, &text_prov, VLine(vline)).unwrap(); + assert_eq!( + (info.0, info.1.line, info.1.line_index), + line_data[vline], + "vline {}", + vline + ); + } + + let vline_line_data = [0, 1, 4, 8]; + + let rope = text_prov.rope_text(); + let last_start_vline = + VLine(lines.last_vline(&text_prov).get() - lines.last_rvline(&text_prov).line_index); + #[allow(clippy::needless_range_loop)] + for line in 0..4 { + let vline = VLine(vline_line_data[line]); + assert_eq!( + find_vline_of_line_forwards(&lines, Default::default(), line), + Some(vline) + ); + assert_eq!( + find_vline_of_line_backwards(&lines, (last_start_vline, rope.last_line()), line), + Some(vline), + "line: {line}" + ); + } + + // TODO: tests that have less line wrapping + } + + #[test] + fn find_vline_init_info_basic_wrapping_phantom() { + // Single line Phantom text at the very start + let text = Rope::from("hello\nworld toast and jam\nthe end\nhi"); + let rope_text = RopeTextRef::new(&text); + + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 0, "greet world")], + }, + ); + + let (text_prov, mut lines) = make_lines_ph(&text, 30.0, false, ph); + + // Assert that with no text layouts there is no change in behavior from having no phantom + // text + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)); + assert_eq!( + info, + Some((line_offset, RVLine::new(line, 0))), + "line {}", + line + ); + + let info = fbvline_info(&lines, &text_prov, VLine(line)); + assert_eq!( + info, + Some((line_offset, RVLine::new(line, 0))), + "line {}", + line + ); + } + + assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["hello", "world toast and jam", "the end", "hi"] + ); + + lines.init_all(0, &text_prov, true); + + { + let layouts = lines.text_layouts.borrow(); + + assert!(layouts.get(FONT_SIZE, 0).is_some()); + assert!(layouts.get(FONT_SIZE, 1).is_some()); + assert!(layouts.get(FONT_SIZE, 2).is_some()); + assert!(layouts.get(FONT_SIZE, 3).is_some()); + assert!(layouts.get(FONT_SIZE, 4).is_none()); + } + + // start offset, start buffer line, layout line index) + let line_data = [ + (0, 0, 0), + (0, 0, 1), + (6, 1, 0), + (12, 1, 1), + (18, 1, 2), + (22, 1, 3), + (26, 2, 0), + (30, 2, 1), + (34, 3, 0), + ]; + + #[allow(clippy::needless_range_loop)] + for line in 0..9 { + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!( + (info.0, info.1.line, info.1.line_index), + line_data[line], + "vline {}", + line + ); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!( + (info.0, info.1.line, info.1.line_index), + line_data[line], + "vline {}", + line + ); + } + + // Directly out of bounds + assert_eq!(ffvline_info(&lines, &text_prov, VLine(9)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(9)), None); + + assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + + // TODO: Currently the way we join phantom text and how cosmic wraps lines, + // the phantom text will be joined with whatever the word next to it is - if there is no + // spaces. It might be desirable to always separate them to let it wrap independently. + // An easy way to do this is to always include a space, and then manually cut the glyph + // margin in the text layout. + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + [ + "greet ", + "worldhello ", + "world ", + "toast ", + "and ", + "jam ", + "the ", + "end ", + "hi" + ] + ); + + // TODO: multiline phantom text in the middle + // TODO: test at the end + } + + #[test] + fn num_vlines() { + let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); + let (text_prov, lines) = make_lines(&text, 2., true); + assert_eq!(lines.num_vlines(&text_prov), 10); + + // With phantom text + let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 0, "greet\nworld")], + }, + ); + + let (text_prov, lines) = make_lines_ph(&text, 2., true, ph); + + // Only one increase because the second line of the phantom text is directly attached to + // the word at the start of the next line. + assert_eq!(lines.num_vlines(&text_prov), 11); + } + + #[test] + fn offset_to_line() { + let text = "a b c d ".into(); + let (text_prov, lines) = make_lines(&text, 1., true); + assert_eq!(lines.num_vlines(&text_prov), 4); + + let vlines = [0, 0, 1, 1, 2, 2, 3, 3]; + for (i, v) in vlines.iter().enumerate() { + assert_eq!( + lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward), + VLine(*v), + "offset: {i}" + ); + } + + assert_eq!(lines.offset_of_vline(&text_prov, VLine(0)), 0); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(1)), 2); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(2)), 4); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(3)), 6); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(10)), 8); + + for offset in 0..text.len() { + let line = lines.vline_of_offset(&text_prov, offset, CursorAffinity::Forward); + let line_offset = lines.offset_of_vline(&text_prov, line); + assert!( + line_offset <= offset, + "{} <= {} L{:?} O{}", + line_offset, + offset, + line, + offset + ); + } + + let text = "blah\n\n\nhi\na b c d e".into(); + let (text_prov, lines) = make_lines(&text, 12.0 * 3.0, true); + let vlines = [0, 0, 0, 0, 0]; + for (i, v) in vlines.iter().enumerate() { + assert_eq!( + lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward), + VLine(*v), + "offset: {i}" + ); + } + assert_eq!( + lines + .vline_of_offset(&text_prov, 4, CursorAffinity::Backward) + .get(), + 0 + ); + // Test that cursor affinity has no effect for hard line breaks + assert_eq!( + lines + .vline_of_offset(&text_prov, 5, CursorAffinity::Forward) + .get(), + 1 + ); + assert_eq!( + lines + .vline_of_offset(&text_prov, 5, CursorAffinity::Backward) + .get(), + 1 + ); + // starts at 'd'. Tests that cursor affinity works for soft line breaks + assert_eq!( + lines + .vline_of_offset(&text_prov, 16, CursorAffinity::Forward) + .get(), + 5 + ); + assert_eq!( + lines + .vline_of_offset(&text_prov, 16, CursorAffinity::Backward) + .get(), + 4 + ); + + assert_eq!( + lines.vline_of_offset(&text_prov, 20, CursorAffinity::Forward), + lines.last_vline(&text_prov) + ); + + let text = "a\nb\nc\n".into(); + let (text_prov, lines) = make_lines(&text, 1., true); + assert_eq!(lines.num_vlines(&text_prov), 4); + + // let vlines = [(0, 0), (0, 0), (1, 1), (1, 1), (2, 2), (2, 2), (3, 3)]; + let vlines = [0, 0, 1, 1, 2, 2, 3, 3]; + for (i, v) in vlines.iter().enumerate() { + assert_eq!( + lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward), + VLine(*v), + "offset: {i}" + ); + assert_eq!( + lines.vline_of_offset(&text_prov, i, CursorAffinity::Backward), + VLine(*v), + "offset: {i}" + ); + } + + let text = + Rope::from("asdf\nposition: Some(EditorPosition::Offset(self.offset))\nasdf\nasdf"); + let (text_prov, mut lines) = make_lines(&text, 1., true); + println!("Breaks: {:?}", render_breaks(&text, &mut lines, FONT_SIZE)); + + let rvline = lines.rvline_of_offset(&text_prov, 3, CursorAffinity::Backward); + assert_eq!(rvline, RVLine::new(0, 0)); + let rvline_info = lines + .iter_rvlines(&text_prov, false, rvline) + .next() + .unwrap(); + assert_eq!(rvline_info.rvline, rvline); + let offset = lines.offset_of_rvline(&text_prov, rvline); + assert_eq!(offset, 0); + assert_eq!( + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Backward), + VLine(0) + ); + assert_eq!(lines.vline_of_rvline(&text_prov, rvline), VLine(0)); + + let rvline = lines.rvline_of_offset(&text_prov, 7, CursorAffinity::Backward); + assert_eq!(rvline, RVLine::new(1, 0)); + let rvline_info = lines + .iter_rvlines(&text_prov, false, rvline) + .next() + .unwrap(); + assert_eq!(rvline_info.rvline, rvline); + let offset = lines.offset_of_rvline(&text_prov, rvline); + assert_eq!(offset, 5); + assert_eq!( + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Backward), + VLine(1) + ); + assert_eq!(lines.vline_of_rvline(&text_prov, rvline), VLine(1)); + + let rvline = lines.rvline_of_offset(&text_prov, 17, CursorAffinity::Backward); + assert_eq!(rvline, RVLine::new(1, 1)); + let rvline_info = lines + .iter_rvlines(&text_prov, false, rvline) + .next() + .unwrap(); + assert_eq!(rvline_info.rvline, rvline); + let offset = lines.offset_of_rvline(&text_prov, rvline); + assert_eq!(offset, 15); + assert_eq!( + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Backward), + VLine(1) + ); + assert_eq!( + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Forward), + VLine(2) + ); + assert_eq!(lines.vline_of_rvline(&text_prov, rvline), VLine(2)); + } + + #[test] + fn offset_to_line_phantom() { + let text = "a b c d ".into(); + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 1, "hi")], + }, + ); + + let (text_prov, mut lines) = make_lines_ph(&text, 1., true, ph); + + // The 'hi' is joined with the 'a' so it's not wrapped to a separate line + assert_eq!(lines.num_vlines(&text_prov), 4); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["ahi ", "b ", "c ", "d "] + ); + + let vlines = [0, 0, 1, 1, 2, 2, 3, 3]; + // Unchanged. The phantom text has no effect in the position. It doesn't shift a line with + // the affinity due to its position and it isn't multiline. + for (i, v) in vlines.iter().enumerate() { + assert_eq!( + lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward), + VLine(*v), + "offset: {i}" + ); + } + + assert_eq!(lines.offset_of_vline(&text_prov, VLine(0)), 0); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(1)), 2); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(2)), 4); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(3)), 6); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(10)), 8); + + for offset in 0..text.len() { + let line = lines.vline_of_offset(&text_prov, offset, CursorAffinity::Forward); + let line_offset = lines.offset_of_vline(&text_prov, line); + assert!( + line_offset <= offset, + "{} <= {} L{:?} O{}", + line_offset, + offset, + line, + offset + ); + } + + // Same as above but with a slightly shifted to make the affinity change the resulting vline + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 2, "hi")], + }, + ); + + let (text_prov, mut lines) = make_lines_ph(&text, 1., true, ph); + + // The 'hi' is joined with the 'a' so it's not wrapped to a separate line + assert_eq!(lines.num_vlines(&text_prov), 4); + + // TODO: Should this really be forward rendered? + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["a ", "hib ", "c ", "d "] + ); + + for (i, v) in vlines.iter().enumerate() { + assert_eq!( + lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward), + VLine(*v), + "offset: {i}" + ); + } + assert_eq!( + lines.vline_of_offset(&text_prov, 2, CursorAffinity::Backward), + VLine(0) + ); + + assert_eq!(lines.offset_of_vline(&text_prov, VLine(0)), 0); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(1)), 2); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(2)), 4); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(3)), 6); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(10)), 8); + + for offset in 0..text.len() { + let line = lines.vline_of_offset(&text_prov, offset, CursorAffinity::Forward); + let line_offset = lines.offset_of_vline(&text_prov, line); + assert!( + line_offset <= offset, + "{} <= {} L{:?} O{}", + line_offset, + offset, + line, + offset + ); + } + } + + #[test] + fn iter_lines() { + let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); + let (text_prov, lines) = make_lines(&text, 2., true); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(0)) + .take(2) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec!["aaaa", "bb "]); + + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(1)) + .take(2) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec!["bb ", "bb "]); + + let v = lines.get_init_text_layout(0, &text_prov, 2, true); + let v = v.layout_cols(&text_prov, 2).collect::>(); + assert_eq!(v, [(0, 3), (3, 8), (8, 13), (13, 15)]); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(3)) + .take(3) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec!["cc", "cc ", "dddd "]); + + let mut r: Vec<_> = lines.iter_vlines(&text_prov, false, VLine(0)).collect(); + r.reverse(); + let r1: Vec<_> = lines + .iter_vlines(&text_prov, true, lines.last_vline(&text_prov)) + .collect(); + assert_eq!(r, r1); + + let rel1: Vec<_> = lines + .iter_rvlines(&text_prov, false, RVLine::new(0, 0)) + .map(|i| i.rvline) + .collect(); + r.reverse(); // revert back + assert!(r.iter().map(|i| i.rvline).eq(rel1)); + + // Empty initialized + let text: Rope = "".into(); + let (text_prov, lines) = make_lines(&text, 2., true); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(0)) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec![""]); + // Empty initialized - Out of bounds + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(1)) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, Vec::<&str>::new()); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(2)) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, Vec::<&str>::new()); + + let mut r: Vec<_> = lines.iter_vlines(&text_prov, false, VLine(0)).collect(); + r.reverse(); + let r1: Vec<_> = lines + .iter_vlines(&text_prov, true, lines.last_vline(&text_prov)) + .collect(); + assert_eq!(r, r1); + + let rel1: Vec<_> = lines + .iter_rvlines(&text_prov, false, RVLine::new(0, 0)) + .map(|i| i.rvline) + .collect(); + r.reverse(); // revert back + assert!(r.iter().map(|i| i.rvline).eq(rel1)); + + // Empty uninitialized + let text: Rope = "".into(); + let (text_prov, lines) = make_lines(&text, 2., false); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(0)) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec![""]); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(1)) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, Vec::<&str>::new()); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(2)) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, Vec::<&str>::new()); + + let mut r: Vec<_> = lines.iter_vlines(&text_prov, false, VLine(0)).collect(); + r.reverse(); + let r1: Vec<_> = lines + .iter_vlines(&text_prov, true, lines.last_vline(&text_prov)) + .collect(); + assert_eq!(r, r1); + + let rel1: Vec<_> = lines + .iter_rvlines(&text_prov, false, RVLine::new(0, 0)) + .map(|i| i.rvline) + .collect(); + r.reverse(); // revert back + assert!(r.iter().map(|i| i.rvline).eq(rel1)); + + // TODO: clean up the above tests with some helper function. Very noisy at the moment. + // TODO: phantom text iter lines tests? + } + + // TODO(minor): Deduplicate the test code between this and iter_lines + // We're just testing whether it has equivalent behavior to iter lines (when lines are + // initialized) + #[test] + fn init_iter_vlines() { + let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); + let (text_prov, lines) = make_lines(&text, 2., false); + let r: Vec<_> = lines + .iter_vlines_init(&text_prov, 0, VLine(0), true) + .take(2) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec!["aaaa", "bb "]); + + let r: Vec<_> = lines + .iter_vlines_init(&text_prov, 0, VLine(1), true) + .take(2) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec!["bb ", "bb "]); + + let r: Vec<_> = lines + .iter_vlines_init(&text_prov, 0, VLine(3), true) + .take(3) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec!["cc", "cc ", "dddd "]); + + // Empty initialized + let text: Rope = "".into(); + let (text_prov, lines) = make_lines(&text, 2., false); + let r: Vec<_> = lines + .iter_vlines_init(&text_prov, 0, VLine(0), true) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec![""]); + let r: Vec<_> = lines + .iter_vlines_init(&text_prov, 0, VLine(1), true) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, Vec::<&str>::new()); + let r: Vec<_> = lines + .iter_vlines_init(&text_prov, 0, VLine(2), true) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, Vec::<&str>::new()); + } + + #[test] + fn line_numbers() { + let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); + let (text_prov, lines) = make_lines(&text, 12.0 * 2.0, true); + let get_nums = |start_vline: usize| { + lines + .iter_vlines(&text_prov, false, VLine(start_vline)) + .map(|l| { + ( + l.rvline.line, + l.vline.get(), + l.is_first(), + text.slice_to_cow(l.interval), + ) + }) + .collect::>() + }; + // (line, vline, is_first, text) + let x = vec![ + (0, 0, true, "aaaa".into()), + (1, 1, true, "bb ".into()), + (1, 2, false, "bb ".into()), + (1, 3, false, "cc\n".into()), // TODO: why does this have \n but the first line doesn't?? + (2, 4, true, "cc ".into()), + (2, 5, false, "dddd ".into()), + (2, 6, false, "eeee ".into()), + (2, 7, false, "ff\n".into()), + (3, 8, true, "ff ".into()), + (3, 9, false, "gggg".into()), + ]; + + // This ensures that there's no inconsistencies between starting at a specific index + // vs starting at zero and iterating to that index. + for i in 0..x.len() { + let nums = get_nums(i); + println!("i: {i}, #nums: {}, #&x[i..]: {}", nums.len(), x[i..].len()); + assert_eq!(nums, &x[i..], "failed at #{i}"); + } + + // TODO: test this without any wrapping + } + + #[test] + fn last_col() { + let text: Rope = Rope::from("conf = Config::default();"); + let (text_prov, lines) = make_lines(&text, 24.0 * 2.0, true); + + let mut iter = lines.iter_rvlines(&text_prov, false, RVLine::default()); + + // "conf = " + let v = iter.next().unwrap(); + assert_eq!(v.last_col(&text_prov, false), 6); + assert_eq!(v.last_col(&text_prov, true), 7); + + // "Config::default();" + let v = iter.next().unwrap(); + assert_eq!(v.last_col(&text_prov, false), 24); + assert_eq!(v.last_col(&text_prov, true), 25); + } +} diff --git a/src/views/mod.rs b/src/views/mod.rs index a3680e8c..c385b427 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -68,3 +68,11 @@ pub use drag_resize_window_area::*; mod img; pub use img::*; + +#[cfg(feature = "editor")] +pub mod editor; + +#[cfg(feature = "editor")] +pub mod text_editor; +#[cfg(feature = "editor")] +pub use text_editor::*; diff --git a/src/views/text_editor.rs b/src/views/text_editor.rs new file mode 100644 index 00000000..c33800bd --- /dev/null +++ b/src/views/text_editor.rs @@ -0,0 +1,231 @@ +use std::rc::Rc; + +use floem_editor_core::buffer::rope_text::RopeTextVal; +use floem_reactive::{with_scope, Scope}; + +use lapce_xi_rope::Rope; + +use crate::{ + id::Id, + view::{View, ViewData, Widget}, + views::editor::{ + command::CommandExecuted, + id::EditorId, + keypress::default_key_handler, + text::{Document, SimpleStyling, Styling}, + text_document::{OnUpdate, PreCommand, TextDocument}, + view::editor_container_view, + Editor, + }, +}; + +/// A text editor view. +/// Note: this requires that the document underlying it is a [`TextDocument`] for the use of some +/// logic. +pub struct TextEditor { + data: ViewData, + /// The scope this view was created in, used for creating the final view + cx: Scope, + + editor: Editor, +} + +pub fn text_editor(text: impl Into) -> TextEditor { + let id = Id::next(); + let cx = Scope::current(); + + let doc = Rc::new(TextDocument::new(cx, text)); + let style = Rc::new(SimpleStyling::light()); + let editor = Editor::new(cx, doc, style); + + TextEditor { + data: ViewData::new(id), + cx, + editor, + } +} + +impl View for TextEditor { + fn view_data(&self) -> &ViewData { + &self.data + } + + fn view_data_mut(&mut self) -> &mut ViewData { + &mut self.data + } + + fn build(self) -> Box { + let cx = self.cx; + + let editor = cx.create_rw_signal(self.editor); + let view = with_scope(self.cx, || { + editor_container_view(editor, |_| true, default_key_handler(editor)) + }); + view.build() + } +} + +impl TextEditor { + /// Note: this requires that the document underlying it is a [`TextDocument`] for the use of + /// some logic. You should usually not swap this out without good reason. + pub fn with_editor(self, f: impl FnOnce(&Editor)) -> Self { + f(&self.editor); + self + } + + /// Note: this requires that the document underlying it is a [`TextDocument`] for the use of + /// some logic. You should usually not swap this out without good reason. + pub fn with_editor_mut(mut self, f: impl FnOnce(&mut Editor)) -> Self { + f(&mut self.editor); + self + } + + pub fn editor_id(&self) -> EditorId { + self.editor.id() + } + + pub fn with_doc(self, f: impl FnOnce(&dyn Document)) -> Self { + self.editor.doc.with_untracked(|doc| { + f(doc.as_ref()); + }); + self + } + + pub fn doc(&self) -> Rc { + self.editor.doc() + } + + // TODO(minor): should this be named `text`? Ideally most users should use the rope text version + pub fn rope_text(&self) -> RopeTextVal { + self.editor.rope_text() + } + + /// Use a different document in the text editor + pub fn use_doc(self, doc: Rc) -> Self { + self.editor.update_doc(doc, None); + self + } + + /// Use the same document as another text editor view. + /// ```rust,ignore + /// let primary = text_editor(); + /// let secondary = text_editor().share_document(&primary); + /// + /// stack(( + /// primary, + /// secondary, + /// )) + /// ``` + /// If you wish for it to also share the styling, consider using [`TextEditor::shared_editor`] + /// instead. + pub fn share_doc(self, other: &TextEditor) -> Self { + self.use_doc(other.editor.doc()) + } + + /// Create a new [`TextEditor`] instance from this instance, sharing the document and styling. + /// ```rust,ignore + /// let primary = text_editor(); + /// let secondary = primary.shared_editor(); + /// ``` + pub fn shared_editor(&self) -> TextEditor { + let id = Id::next(); + + let doc = self.editor.doc(); + let style = self.editor.style(); + let editor = Editor::new(self.cx, doc, style); + + TextEditor { + data: ViewData::new(id), + cx: self.cx, + editor, + } + } + + /// Change the [`Styling`] used for the editor. + /// ```rust,ignore + /// let styling = SimpleStyling::builder() + /// .font_size(12) + /// .weight(Weight::BOLD); + /// text_editor().styling(styling); + /// ``` + pub fn styling(self, styling: impl Styling + 'static) -> Self { + self.styling_rc(Rc::new(styling)) + } + + /// Use an `Rc` to share between different editors. + pub fn styling_rc(self, styling: Rc) -> Self { + self.editor.update_styling(styling); + self + } + + /// Set the text editor to read only. + /// Equivalent to setting [`Editor::read_only`] + /// Default: `false` + pub fn read_only(self) -> Self { + self.editor.read_only.set(true); + self + } + + /// Allow scrolling beyond the last line of the document. + /// Equivalent to setting [`Editor::scroll_beyond_last_line`] + /// Default: `false` + pub fn scroll_beyond_last_line(self) -> Self { + self.editor.scroll_beyond_last_line.set(true); + self + } + + /// Set the number of lines to keep visible above and below the cursor. + /// Equivalent to setting [`Editor::cursor_surrounding_lines`] + /// Default: `1` + pub fn cursor_surrounding_lines(self, lines: usize) -> Self { + self.editor.cursor_surrounding_lines.set(lines); + self + } + + /// Insert the indent that is detected fror the file when tab is pressed. + /// Equivalent to setting [`Editor::smart_tab`] + /// Default: `false` + pub fn smart_tab(self) -> Self { + self.editor.smart_tab.set(true); + self + } + + /// When commands are run on the document, this function is called. + /// If it returns [`CommandExecuted::Yes`] then further handlers after it, including the + /// default handler, are not executed. + /// ```rust,ignore + /// text_editor("Hello") + /// .pre_command(|ev| { + /// if matches!(ev.cmd, Command::Edit(EditCommand::Undo)) { + /// // Sorry, no undoing allowed + /// CommandExecuted::Yes + /// } else { + /// CommandExecuted::No + /// } + /// }) + /// .pre_command(|_| { + /// // This will never be called if command was an undo + /// CommandExecuted::Yes + /// })) + /// .pre_command(|_| { + /// // This will never be called + /// CommandExecuted::No + /// }) + /// ``` + /// Note that these are specific to each text editor view. + pub fn pre_command(self, f: impl Fn(PreCommand) -> CommandExecuted + 'static) -> Self { + let doc: Result, _> = self.editor.doc().downcast_rc(); + if let Ok(doc) = doc { + doc.add_pre_command(self.editor.id(), f); + } + self + } + + pub fn update(self, f: impl Fn(OnUpdate) + 'static) -> Self { + let doc: Result, _> = self.editor.doc().downcast_rc(); + if let Ok(doc) = doc { + doc.add_on_update(f); + } + self + } +}