From 1c3eca93b864dd212bcc7ceefeacde80d3f2dd6c Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 24 Sep 2025 20:52:18 +0800 Subject: [PATCH] editor: Revert to use ropey. (#1284) For the License reason. - The performance is slow down, before version can handle 200K lines, now 150K lines is good. --- Cargo.lock | 32 +-- Cargo.toml | 2 +- crates/story/examples/editor.rs | 4 +- crates/ui/Cargo.toml | 2 +- crates/ui/src/highlighter/diagnostics.rs | 4 +- crates/ui/src/highlighter/highlighter.rs | 35 ++- crates/ui/src/input/cursor.rs | 11 +- crates/ui/src/input/element.rs | 13 +- crates/ui/src/input/lsp/completions.rs | 2 +- crates/ui/src/input/lsp/definitions.rs | 2 +- crates/ui/src/input/lsp/hover.rs | 2 +- crates/ui/src/input/mod.rs | 2 +- crates/ui/src/input/mode.rs | 17 +- crates/ui/src/input/rope_ext.rs | 319 ++++++++++++++++------- crates/ui/src/input/search.rs | 10 +- crates/ui/src/input/state.rs | 42 ++- crates/ui/src/input/text_wrapper.rs | 52 +++- crates/ui/src/inspector.rs | 6 +- crates/ui/src/text/node.rs | 4 +- 19 files changed, 369 insertions(+), 192 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05f4b45e..cd9e2d3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -757,7 +757,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -780,7 +780,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -3225,7 +3225,7 @@ dependencies = [ "once_cell", "paste", "regex", - "rope", + "ropey", "rust-i18n", "rust_decimal", "schemars", @@ -3690,7 +3690,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.58.0", + "windows-core 0.61.2", ] [[package]] @@ -4980,7 +4980,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.3.0", "proc-macro2", "quote", "syn 2.0.105", @@ -6754,18 +6754,12 @@ dependencies = [ ] [[package]] -name = "rope" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5" +name = "ropey" +version = "2.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4045a00dc327d084a2bbf126976e14125b54f23bd30511d45b842eba76c52d74" dependencies = [ - "arrayvec", - "log", - "rayon", - "smallvec", - "sum_tree", - "unicode-segmentation", - "util", - "workspace-hack", + "str_indices", ] [[package]] @@ -7802,6 +7796,12 @@ dependencies = [ "unindent", ] +[[package]] +name = "str_indices" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + [[package]] name = "streaming-iterator" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index 471d637f..00908547 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,13 +17,13 @@ resolver = "2" [workspace.dependencies] gpui = { git = "https://github.com/zed-industries/zed.git" } gpui_macros = { git = "https://github.com/zed-industries/zed.git" } -rope = { git = "https://github.com/zed-industries/zed.git" } reqwest_client = { git = "https://github.com/zed-industries/zed.git" } sum_tree = { git = "https://github.com/zed-industries/zed.git" } gpui-component = { path = "crates/ui" } gpui-component-macros = { path = "crates/macros" } story = { path = "crates/story" } wef = { path = "crates/wef" } +ropey = { version = "=2.0.0-beta.1", features = ["metric_utf16"] } anyhow = "1" log = "0.4" diff --git a/crates/story/examples/editor.rs b/crates/story/examples/editor.rs index c4ee0345..45eb9ab1 100644 --- a/crates/story/examples/editor.rs +++ b/crates/story/examples/editor.rs @@ -912,7 +912,9 @@ impl Render for Example { .xsmall() .label(format!( "{}:{} ({} byte)", - position.line, position.character, cursor + position.line + 1, + position.character + 1, + cursor )) .on_click(cx.listener(Self::go_to_line)) }), diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index abd05bbc..1755801c 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -53,7 +53,7 @@ gpui = { workspace = true } gpui-component-macros.workspace = true gpui_macros.workspace = true notify.workspace = true -rope.workspace = true +ropey.workspace = true rust-i18n.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/ui/src/highlighter/diagnostics.rs b/crates/ui/src/highlighter/diagnostics.rs index 3446703a..2db4d331 100644 --- a/crates/ui/src/highlighter/diagnostics.rs +++ b/crates/ui/src/highlighter/diagnostics.rs @@ -5,7 +5,7 @@ use std::{ }; use gpui::{px, App, HighlightStyle, Hsla, SharedString, UnderlineStyle}; -use rope::Rope; +use ropey::Rope; use sum_tree::{Bias, SeekTarget, SumTree}; use crate::{ @@ -343,7 +343,7 @@ mod tests { #[test] fn test_diagnostic() { - use rope::Rope; + use ropey::Rope; use super::{Diagnostic, DiagnosticSet, DiagnosticSeverity}; diff --git a/crates/ui/src/highlighter/highlighter.rs b/crates/ui/src/highlighter/highlighter.rs index 21a71b06..65b3299a 100644 --- a/crates/ui/src/highlighter/highlighter.rs +++ b/crates/ui/src/highlighter/highlighter.rs @@ -1,9 +1,9 @@ -use crate::{highlighter::LanguageRegistry, input::RopeExt as _, ActiveTheme}; +use crate::{highlighter::LanguageRegistry, ActiveTheme}; use anyhow::{anyhow, Context, Result}; use gpui::{App, HighlightStyle, SharedString}; -use rope::Rope; +use ropey::{ChunkCursor, Rope}; use std::{ collections::{BTreeSet, HashMap}, ops::Range, @@ -40,12 +40,21 @@ pub struct SyntaxHighlighter { } struct TextProvider<'a>(&'a Rope); -struct ByteChunks<'a>(rope::Chunks<'a>); +struct ByteChunks<'a> { + cursor: ChunkCursor<'a>, + end: usize, +} impl<'a> tree_sitter::TextProvider<&'a [u8]> for TextProvider<'a> { type I = ByteChunks<'a>; fn text(&mut self, node: tree_sitter::Node) -> Self::I { - ByteChunks(self.0.chunks_in_range(node.byte_range())) + let range = node.byte_range(); + let cursor = self.0.chunk_cursor_at(range.start); + + ByteChunks { + cursor, + end: range.end, + } } } @@ -53,7 +62,14 @@ impl<'a> Iterator for ByteChunks<'a> { type Item = &'a [u8]; fn next(&mut self) -> Option { - self.0.next().map(str::as_bytes) + let cursor = &mut self.cursor; + let end = self.end; + + if cursor.next() && cursor.byte_offset() < end { + Some(cursor.chunk().as_bytes()) + } else { + None + } } } @@ -315,11 +331,14 @@ impl SyntaxHighlighter { .unwrap_or(self.parser.parse("", None).unwrap()); old_tree.edit(&edit); - let mut chunks = text.chunks(); let new_tree = self.parser.parse_with_options( &mut move |offset, _| { - chunks.seek(offset); - chunks.next().unwrap_or("").as_bytes() + if offset >= text.len() { + "" + } else { + let (chunk, chunk_byte_ix) = text.chunk(offset); + &chunk[offset - chunk_byte_ix..] + } }, Some(&old_tree), None, diff --git a/crates/ui/src/input/cursor.rs b/crates/ui/src/input/cursor.rs index c8d7fb84..e4a6a887 100644 --- a/crates/ui/src/input/cursor.rs +++ b/crates/ui/src/input/cursor.rs @@ -1,4 +1,4 @@ -use std::ops::Range; +use std::ops::{Range, RangeBounds}; /// A selection in the text, represented by start and end byte indices. #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] @@ -42,6 +42,15 @@ impl From for Range { value.start..value.end } } +impl RangeBounds for Selection { + fn start_bound(&self) -> std::ops::Bound<&usize> { + std::ops::Bound::Included(&self.start) + } + + fn end_bound(&self) -> std::ops::Bound<&usize> { + std::ops::Bound::Excluded(&self.end) + } +} pub type Position = lsp_types::Position; diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 7c135c4a..dabd03c7 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -6,7 +6,7 @@ use gpui::{ MouseMoveEvent, Path, Pixels, Point, ShapedLine, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window, }; -use rope::Rope; +use ropey::Rope; use smallvec::SmallVec; use crate::{ @@ -508,7 +508,7 @@ impl TextElement { let mut styles = vec![]; for line in text - .lines() + .rows() .skip(visible_range.start) .take(visible_range.len()) { @@ -781,7 +781,7 @@ impl Element for TextElement { // NOTE: Here 50 lines about 150µs // let measure = crate::Measure::new("shape_text"); let visible_text = display_text - .slice_rows(visible_range.start as u32..visible_range.end as u32) + .slice_rows(visible_range.start..visible_range.end) .to_string(); let lines = window @@ -792,11 +792,8 @@ impl Element for TextElement { let mut longest_line_width = wrap_width.unwrap_or(px(0.)); if state.mode.is_multi_line() && !state.soft_wrap && lines.len() > 1 { - let longtest_line: SharedString = state - .text - .line(state.text.summary().longest_row as usize) - .to_string() - .into(); + let longest_row = state.text_wrapper.longest_row.row; + let longtest_line: SharedString = state.text.slice_row(longest_row).to_string().into(); longest_line_width = window .text_system() .shape_line( diff --git a/crates/ui/src/input/lsp/completions.rs b/crates/ui/src/input/lsp/completions.rs index 395c8afb..108ccea5 100644 --- a/crates/ui/src/input/lsp/completions.rs +++ b/crates/ui/src/input/lsp/completions.rs @@ -1,7 +1,7 @@ use anyhow::Result; use gpui::{Context, EntityInputHandler, Task, Window}; use lsp_types::{request::Completion, CompletionContext, CompletionItem, CompletionResponse}; -use rope::Rope; +use ropey::Rope; use std::{cell::RefCell, ops::Range, rc::Rc}; use crate::input::{ diff --git a/crates/ui/src/input/lsp/definitions.rs b/crates/ui/src/input/lsp/definitions.rs index 4a21ed91..0a519529 100644 --- a/crates/ui/src/input/lsp/definitions.rs +++ b/crates/ui/src/input/lsp/definitions.rs @@ -2,7 +2,7 @@ use anyhow::Result; use gpui::{ px, App, Context, HighlightStyle, Hitbox, MouseDownEvent, Task, UnderlineStyle, Window, }; -use rope::Rope; +use ropey::Rope; use std::{ops::Range, rc::Rc}; use crate::{ diff --git a/crates/ui/src/input/lsp/hover.rs b/crates/ui/src/input/lsp/hover.rs index ffead6d0..37d81f2e 100644 --- a/crates/ui/src/input/lsp/hover.rs +++ b/crates/ui/src/input/lsp/hover.rs @@ -1,6 +1,6 @@ use anyhow::Result; use gpui::{App, Context, Task, Window}; -use rope::Rope; +use ropey::Rope; use crate::input::{popovers::HoverPopover, InputState, RopeExt}; diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs index 62f22bd6..c0750bc3 100644 --- a/crates/ui/src/input/mod.rs +++ b/crates/ui/src/input/mod.rs @@ -22,7 +22,7 @@ pub use mask_pattern::MaskPattern; pub use mode::TabSize; pub use number_input::{NumberInput, NumberInputEvent, StepAction}; pub use otp_input::*; -pub use rope::Rope; pub use rope_ext::*; +pub use ropey::Rope; pub use state::*; pub use text_input::*; diff --git a/crates/ui/src/input/mode.rs b/crates/ui/src/input/mode.rs index 051c00b1..7f85b34e 100644 --- a/crates/ui/src/input/mode.rs +++ b/crates/ui/src/input/mode.rs @@ -2,12 +2,13 @@ use std::rc::Rc; use std::{cell::RefCell, ops::Range}; use gpui::{App, SharedString}; -use rope::Rope; -use tree_sitter::{InputEdit, Point}; +use ropey::Rope; +use tree_sitter::InputEdit; use super::text_wrapper::TextWrapper; use crate::highlighter::DiagnosticSet; use crate::highlighter::SyntaxHighlighter; +use crate::input::RopeExt as _; #[derive(Debug, Copy, Clone)] pub struct TabSize { @@ -210,15 +211,9 @@ impl InputMode { start_byte: selected_range.start, old_end_byte: selected_range.end, new_end_byte: new_end, - start_position: Point::new(start_pos.row as usize, start_pos.column as usize), - old_end_position: Point::new( - old_end_pos.row as usize, - old_end_pos.column as usize, - ), - new_end_position: Point::new( - new_end_pos.row as usize, - new_end_pos.column as usize, - ), + start_position: start_pos, + old_end_position: old_end_pos, + new_end_position: new_end_pos, }; highlighter.update(Some(edit), text); diff --git a/crates/ui/src/input/rope_ext.rs b/crates/ui/src/input/rope_ext.rs index b0f7be9a..01237e51 100644 --- a/crates/ui/src/input/rope_ext.rs +++ b/crates/ui/src/input/rope_ext.rs @@ -1,19 +1,25 @@ use std::ops::Range; -use rope::{Point, Rope}; +use ropey::{LineType, Rope, RopeSlice}; +use sum_tree::Bias; +use tree_sitter::Point; use crate::input::Position; /// An extension trait for `Rope` to provide additional utility methods. pub trait RopeExt { - /// Get the line at the given row (0-based) index, including the `\r` at the end, but not `\n`. - /// - /// Return empty rope if the row (0-based) is out of bounds. - fn line(&self, row: usize) -> Rope; - /// Start offset of the line at the given row (0-based) index. fn line_start_offset(&self, row: usize) -> usize; + /// Return a line slice at the given row (0-based) index. including `\r` if present, but not `\n`. + fn slice_row(&self, row: usize) -> RopeSlice<'_>; + + fn slice_rows(&self, rows_range: Range) -> RopeSlice<'_>; + + fn rows(&self) -> impl Iterator> + '_ { + (0..self.lines_len()).map(|row| self.slice_row(row)) + } + /// Line the end offset (including `\n`) of the line at the given row (0-based) index. /// /// Return the end of the rope if the row is out of bounds. @@ -22,22 +28,19 @@ pub trait RopeExt { /// Return the number of lines in the rope. fn lines_len(&self) -> usize; - /// Return the lines iterator. - /// - /// Each line is including the `\r` at the end, but not `\n`. - fn lines(&self) -> RopeLines; - - /// Check is equal to another rope. - fn eq(&self, other: &Rope) -> bool; + /// Return the length of the row (0-based) in characters, including `\r` if present, but not `\n`. + fn line_len(&self, row: usize) -> usize; /// Total number of characters in the rope. fn chars_count(&self) -> usize; + /// Replace the text in the given byte range with new text. + fn replace(&mut self, range: Range, new_text: &str); + /// Get char at the given offset (byte). /// - /// If the offset is in the middle of a multi-byte character will panic. - /// - /// If the offset is out of bounds, return None. + /// - If the offset is in the middle of a multi-byte character will panic. + /// - If the offset is out of bounds, return None. fn char_at(&self, offset: usize) -> Option; /// Get the byte offset from the given line, column [`Position`] (0-based). @@ -46,76 +49,76 @@ pub trait RopeExt { /// Get the line, column [`Position`] (0-based) from the given byte offset. fn offset_to_position(&self, offset: usize) -> Position; + fn offset_to_point(&self, offset: usize) -> Point; + fn point_to_offset(&self, point: Point) -> usize; + /// Get the word byte range at the given offset (byte). fn word_range(&self, offset: usize) -> Option>; /// Get word at the given offset (byte). fn word_at(&self, offset: usize) -> String; + + /// Convert offset_utf16 to offset (byte). + fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize; + + /// Convert offset (byte) to offset_utf16. + fn offset_to_offset_utf16(&self, offset: usize) -> usize; + + /// Get a clipped offset (avoid in a char boundary). + /// + /// - If Bias::Left and inside the char boundary, return the ix - 1; + /// - If Bias::Right and in inside char boundary, return the ix + 1; + /// - Otherwise return the ix. + fn clip_offset(&self, offset: usize, bias: Bias) -> usize; } -/// An iterator over the lines of a `Rope`. -pub struct RopeLines { - row: usize, - end_row: usize, - rope: Rope, -} - -impl RopeLines { - /// Create a new `RopeLines` iterator. - pub fn new(rope: Rope) -> Self { - let end_row = rope.lines_len(); - Self { - row: 0, - end_row, - rope, - } - } -} - -impl Iterator for RopeLines { - type Item = Rope; - - #[inline] - fn next(&mut self) -> Option { - if self.row >= self.end_row { - return None; - } - - let line = self.rope.line(self.row); - self.row += 1; - Some(line) - } - - #[inline] - fn nth(&mut self, n: usize) -> Option { - self.row = self.row.saturating_add(n); - self.next() - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - let len = self.end_row - self.row; - (len, Some(len)) - } -} - -impl std::iter::ExactSizeIterator for RopeLines {} -impl std::iter::FusedIterator for RopeLines {} - impl RopeExt for Rope { - fn line(&self, row: usize) -> Rope { - let start = self.line_start_offset(row); - let end = start + self.line_len(row as u32) as usize; + fn slice_row(&self, row: usize) -> RopeSlice<'_> { + if row >= self.lines_len() { + return self.slice(0..0); + } + + let line = self.line(row, LineType::LF_CR); + if line.len() > 0 && line.chars().last() == Some('\n') { + line.slice(..line.len().saturating_sub(1)) + } else { + line + } + } + + fn slice_rows(&self, rows_range: Range) -> RopeSlice<'_> { + let start = self.line_start_offset(rows_range.start); + let end = self.line_end_offset(rows_range.end.saturating_sub(1)); self.slice(start..end) } + fn line_len(&self, row: usize) -> usize { + self.slice_row(row).len() + } + fn line_start_offset(&self, row: usize) -> usize { - let row = row as u32; self.point_to_offset(Point::new(row, 0)) } + fn offset_to_point(&self, offset: usize) -> Point { + let offset = self.clip_offset(offset, Bias::Left); + let row = self.byte_to_line_idx(offset, LineType::LF_CR); + let line_start = self.line_to_byte_idx(row, LineType::LF_CR); + let column = offset.saturating_sub(line_start); + Point::new(row, column) + } + + fn point_to_offset(&self, point: Point) -> usize { + if point.row >= self.lines_len() { + return self.len(); + } + + let line_start = self.line_to_byte_idx(point.row, LineType::LF_CR); + line_start + point.column + } + fn position_to_offset(&self, pos: &Position) -> usize { - let line = self.line(pos.line as usize); + let line = self.slice_row(pos.line as usize); self.line_start_offset(pos.line as usize) + line .chars() @@ -126,30 +129,22 @@ impl RopeExt for Rope { fn offset_to_position(&self, offset: usize) -> Position { let point = self.offset_to_point(offset); - let line = self.line(point.row as usize); - let column = line.clip_offset(point.column as usize, sum_tree::Bias::Left); - let character = line.slice(0..column).chars().count(); - Position::new(point.row, character as u32) + let line = self.slice_row(point.row); + let offset = line.utf16_to_byte_idx(line.byte_to_utf16_idx(point.column)); + let character = line.slice(..offset).chars().count(); + Position::new(point.row as u32, character as u32) } fn line_end_offset(&self, row: usize) -> usize { - if row > self.max_point().row as usize { + if row > self.lines_len() { return self.len(); } - self.line_start_offset(row) + self.line_len(row as u32) as usize + self.line_start_offset(row) + self.line_len(row) } fn lines_len(&self) -> usize { - self.max_point().row as usize + 1 - } - - fn lines(&self) -> RopeLines { - RopeLines::new(self.clone()) - } - - fn eq(&self, other: &Rope) -> bool { - self.summary() == other.summary() + self.len_lines(LineType::LF_CR) } fn chars_count(&self) -> usize { @@ -161,8 +156,7 @@ impl RopeExt for Rope { return None; } - let offset = self.clip_offset(offset, sum_tree::Bias::Left); - self.slice(offset..self.len()).chars().next() + self.get_char(offset).ok() } fn word_range(&self, offset: usize) -> Option> { @@ -170,10 +164,9 @@ impl RopeExt for Rope { return None; } - let offset = self.clip_offset(offset, sum_tree::Bias::Left); - let mut left = String::new(); - for c in self.reversed_chars_at(offset) { + let offset = self.clip_offset(offset, Bias::Left); + for c in self.chars_at(offset).reversed() { if c.is_alphanumeric() || c == '_' { left.insert(0, c); } else { @@ -203,24 +196,65 @@ impl RopeExt for Rope { String::new() } } + + #[inline] + fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize { + if offset_utf16 > self.len_utf16() { + return self.len(); + } + + self.utf16_to_byte_idx(offset_utf16) + } + + #[inline] + fn offset_to_offset_utf16(&self, offset: usize) -> usize { + if offset > self.len() { + return self.len_utf16(); + } + + self.byte_to_utf16_idx(offset) + } + + fn replace(&mut self, range: Range, new_text: &str) { + self.remove(range.clone()); + self.insert(range.start, new_text); + } + + fn clip_offset(&self, offset: usize, bias: Bias) -> usize { + if offset > self.len() { + return self.len(); + } + + if self.is_char_boundary(offset) { + return offset; + } + + if bias == Bias::Left { + self.floor_char_boundary(offset) + } else { + self.ceil_char_boundary(offset) + } + } } #[cfg(test)] mod tests { - use rope::Rope; + use ropey::Rope; + use sum_tree::Bias; + use tree_sitter::Point; - use crate::input::{Position, RopeExt as _}; + use crate::input::{Position, RopeExt}; #[test] fn test_line() { let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); - assert_eq!(rope.line(0).to_string(), "Hello"); - assert_eq!(rope.line(1).to_string(), "World\r"); - assert_eq!(rope.line(2).to_string(), "This is a test 中文"); - assert_eq!(rope.line(3).to_string(), "Rope"); + assert_eq!(rope.slice_row(0).to_string(), "Hello"); + assert_eq!(rope.slice_row(1).to_string(), "World\r"); + assert_eq!(rope.slice_row(2).to_string(), "This is a test 中文"); + assert_eq!(rope.slice_row(3).to_string(), "Rope"); // over bounds - assert_eq!(rope.line(6).to_string(), ""); + assert_eq!(rope.slice_row(6).to_string(), ""); } #[test] @@ -244,9 +278,9 @@ mod tests { } #[test] - fn test_lines() { + fn test_rows() { let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); - let lines: Vec<_> = rope.lines().map(|r| r.to_string()).collect(); + let lines: Vec<_> = rope.rows().map(|r| r.to_string()).collect(); assert_eq!( lines, vec!["Hello", "World\r", "This is a test 中文", "Rope"] @@ -305,6 +339,32 @@ mod tests { ); } + #[test] + fn test_offset_to_point() { + let rope = Rope::from("a 中文🎉 test\nRope"); + assert_eq!(rope.offset_to_point(0), Point::new(0, 0)); + assert_eq!(rope.offset_to_point(1), Point::new(0, 1)); + assert_eq!(rope.offset_to_point("a 中".len()), Point::new(0, 5)); + assert_eq!(rope.offset_to_point("a 中文🎉".len()), Point::new(0, 12)); + assert_eq!( + rope.offset_to_point("a 中文🎉 test\nR".len()), + Point::new(1, 1) + ); + } + + #[test] + fn test_point_to_offset() { + let rope = Rope::from("a 中文🎉 test\nRope"); + assert_eq!(rope.point_to_offset(Point::new(0, 0)), 0); + assert_eq!(rope.point_to_offset(Point::new(0, 1)), 1); + assert_eq!(rope.point_to_offset(Point::new(0, 5)), "a 中".len()); + assert_eq!(rope.point_to_offset(Point::new(0, 12)), "a 中文🎉".len()); + assert_eq!( + rope.point_to_offset(Point::new(1, 1)), + "a 中文🎉 test\nR".len() + ); + } + #[test] fn test_char_at() { let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文🎉\nRope"); @@ -336,4 +396,65 @@ mod tests { assert_eq!(rope.word_range(44), Some(42..46)); assert_eq!(rope.word_at(45), "Rope"); } + + #[test] + fn test_offset_utf16_conversion() { + let rope = Rope::from("hello 中文🎉 test\nRope"); + assert_eq!(rope.offset_to_offset_utf16("hello".len()), 5); + assert_eq!(rope.offset_to_offset_utf16("hello 中".len()), 7); + assert_eq!(rope.offset_to_offset_utf16("hello 中文".len()), 8); + assert_eq!(rope.offset_to_offset_utf16("hello 中文🎉".len()), 10); + assert_eq!(rope.offset_to_offset_utf16(100), 20); + + assert_eq!(rope.offset_utf16_to_offset(5), "hello".len()); + assert_eq!(rope.offset_utf16_to_offset(7), "hello 中".len()); + assert_eq!(rope.offset_utf16_to_offset(8), "hello 中文".len()); + assert_eq!(rope.offset_utf16_to_offset(10), "hello 中文🎉".len()); + assert_eq!(rope.offset_utf16_to_offset(100), rope.len()); + } + + #[test] + fn test_replace() { + let mut rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); + rope.replace(6..11, "Universe"); + assert_eq!( + rope.to_string(), + "Hello\nUniverse\r\nThis is a test 中文\nRope" + ); + + rope.replace(0..5, "Hi"); + assert_eq!( + rope.to_string(), + "Hi\nUniverse\r\nThis is a test 中文\nRope" + ); + + rope.replace(rope.len() - 4..rope.len(), "String"); + assert_eq!( + rope.to_string(), + "Hi\nUniverse\r\nThis is a test 中文\nString" + ); + } + + #[test] + fn test_clip_offset() { + let rope = Rope::from("Hello 中文🎉 test\nRope"); + // Inside multi-byte character '中' (3 bytes) + assert_eq!(rope.clip_offset(5, Bias::Left), 5); + assert_eq!(rope.clip_offset(7, Bias::Left), 6); + assert_eq!(rope.clip_offset(7, Bias::Right), 9); + assert_eq!(rope.clip_offset(9, Bias::Left), 9); + + // Inside multi-byte character '🎉' (4 bytes) + assert_eq!(rope.clip_offset(13, Bias::Left), 12); + assert_eq!(rope.clip_offset(13, Bias::Right), 16); + assert_eq!(rope.clip_offset(16, Bias::Left), 16); + + // At character boundary + assert_eq!(rope.clip_offset(5, Bias::Left), 5); + assert_eq!(rope.clip_offset(5, Bias::Right), 5); + + // Out of bounds + assert_eq!(rope.clip_offset(26, Bias::Left), 26); + assert_eq!(rope.clip_offset(100, Bias::Left), 26); + } } diff --git a/crates/ui/src/input/search.rs b/crates/ui/src/input/search.rs index f505e287..52754064 100644 --- a/crates/ui/src/input/search.rs +++ b/crates/ui/src/input/search.rs @@ -7,13 +7,13 @@ use gpui::{ Entity, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding, ParentElement as _, Pixels, Render, Styled, Subscription, Window, }; -use rope::Rope; +use ropey::Rope; use crate::{ actions::SelectPrev, button::{Button, ButtonVariants}, h_flex, - input::{Enter, Escape, IndentInline, InputEvent, InputState, RopeExt, Search, TextInput}, + input::{Enter, Escape, IndentInline, InputEvent, InputState, RopeExt as _, Search, TextInput}, label::Label, v_flex, ActiveTheme, Disableable, IconName, Selectable, Sizable, }; @@ -65,7 +65,9 @@ impl SearchMatcher { fn update_matches(&mut self) { let mut new_ranges = Vec::new(); if let Some(query) = &self.query { - let matches = query.stream_find_iter(self.text.bytes_in_range(0..self.text.len())); + let text = self.text.to_string(); + // FIXME: Use stream find + let matches = query.stream_find_iter(text.as_bytes()); for query_match in matches.into_iter() { let query_match = query_match.expect("query match for select all action"); @@ -201,7 +203,7 @@ impl InputState { let text = self.text.clone(); let editor = cx.entity(); - let selected_text = self.selected_text(); + let selected_text = Rope::from(self.selected_text()); search_panel.update(cx, |this, cx| { this.editor = editor; this.matcher.update(&text); diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 83994d69..726a5acc 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -11,7 +11,7 @@ use gpui::{ ScrollWheelEvent, SharedString, Styled as _, Subscription, Task, UTF16Selection, Window, WrappedLine, }; -use rope::{OffsetUtf16, Rope}; +use ropey::{Rope, RopeSlice}; use serde::Deserialize; use smallvec::SmallVec; use std::cell::RefCell; @@ -581,19 +581,18 @@ impl InputState { }; let point = self.text.offset_to_point(self.cursor()); - let row = (point.row as usize).saturating_sub(last_layout.visible_range.start); + let row = point.row.saturating_sub(last_layout.visible_range.start); let Some(line) = last_layout.lines.get(row) else { self.preferred_column = None; return; }; - let Some(pos) = line.position_for_index(point.column as usize, last_layout.line_height) - else { + let Some(pos) = line.position_for_index(point.column, last_layout.line_height) else { self.preferred_column = None; return; }; - self.preferred_column = Some((pos.x, point.column as usize)); + self.preferred_column = Some((pos.x, point.column)); } /// Find which line and sub-line the given offset belongs to, along with the position within that sub-line. @@ -644,17 +643,18 @@ impl InputState { let was_preferred_column = self.preferred_column; let row = self.text.offset_to_point(offset).row; - let new_row = row.saturating_add_signed(move_lines as i32); - let line_start_offset = self.text.point_to_offset(rope::Point::new(new_row, 0)); + let new_row = row.saturating_add_signed(move_lines); + let line_start_offset = self + .text + .point_to_offset(tree_sitter::Point::new(new_row, 0)); let mut new_offset = line_start_offset; if let Some((preferred_x, column)) = was_preferred_column { - let new_column = column.min(self.text.line(new_row as usize).len()); + let new_column = column.min(self.text.slice_row(new_row).len()); new_offset = line_start_offset + new_column; // If in visible range, prefer to use position to get column. - let new_row = new_row as usize; if new_row >= last_layout.visible_range.start { let visible_row = new_row.saturating_sub(last_layout.visible_range.start); if let Some(line) = last_layout.lines.get(visible_row) { @@ -889,13 +889,7 @@ impl InputState { cx: &mut Context, ) { let position: Position = position.into(); - let max_point = self.text.max_point(); - let row = position.line.min(max_point.row); - let col = position.character.min(self.text.line_len(row)); - - let offset = self - .text - .point_to_offset(rope::Point::new(row as u32, col as u32)); + let offset = self.text.position_to_offset(&position); self.move_to(offset, cx); self.update_preferred_column(); @@ -1164,7 +1158,7 @@ impl InputState { } let row = self.text.offset_to_point(self.cursor()).row; - self.text.line_start_offset(row as usize) + self.text.line_start_offset(row) } /// Get end of line byte offset of cursor @@ -1174,7 +1168,7 @@ impl InputState { } let row = self.text.offset_to_point(self.cursor()).row; - self.text.line_end_offset(row as usize) + self.text.line_end_offset(row) } /// Get start line of selection start or end (The min value). @@ -1668,7 +1662,7 @@ impl InputState { let line_height = last_layout.line_height; let point = self.text.offset_to_point(offset); - let row = point.row as usize; + let row = point.row; let mut row_offset_y = px(0.); for (ix, wrap_line) in self.text_wrapper.lines.iter().enumerate() { @@ -1710,7 +1704,7 @@ impl InputState { return; } - let selected_text = self.text.slice(self.selected_range.into()).to_string(); + let selected_text = self.text.slice(self.selected_range).to_string(); cx.write_to_clipboard(ClipboardItem::new_string(selected_text)); } @@ -1719,7 +1713,7 @@ impl InputState { return; } - let selected_text = self.text.slice(self.selected_range.into()).to_string(); + let selected_text = self.text.slice(self.selected_range).to_string(); cx.write_to_clipboard(ClipboardItem::new_string(selected_text)); self.replace_text_in_range_silent(None, "", window, cx); @@ -2014,12 +2008,12 @@ impl InputState { #[inline] pub(super) fn offset_from_utf16(&self, offset: usize) -> usize { - self.text.offset_utf16_to_offset(OffsetUtf16(offset)) + self.text.offset_utf16_to_offset(offset) } #[inline] pub(super) fn offset_to_utf16(&self, offset: usize) -> usize { - self.text.offset_to_offset_utf16(offset).0 + self.text.offset_to_offset_utf16(offset) } #[inline] @@ -2191,7 +2185,7 @@ impl InputState { } } - pub(super) fn selected_text(&self) -> Rope { + pub(super) fn selected_text(&self) -> RopeSlice<'_> { let range_utf16 = self.range_to_utf16(&self.selected_range.into()); let range = self.range_from_utf16(&range_utf16); self.text.slice(range) diff --git a/crates/ui/src/input/text_wrapper.rs b/crates/ui/src/input/text_wrapper.rs index e2cd7d78..c9f733eb 100644 --- a/crates/ui/src/input/text_wrapper.rs +++ b/crates/ui/src/input/text_wrapper.rs @@ -1,7 +1,7 @@ use std::ops::Range; use gpui::{App, Font, LineFragment, Pixels}; -use rope::Rope; +use ropey::{LineType, Rope}; use crate::input::RopeExt; @@ -37,6 +37,14 @@ impl LineItem { } } +#[derive(Debug, Default)] +pub(super) struct LongestRow { + /// The 0-based row index. + pub row: usize, + /// The bytes length of the longest line. + pub len: usize, +} + /// Used to prepare the text with soft wrap to be get lines to displayed in the Editor. /// /// After use lines to calculate the scroll size of the Editor. @@ -48,6 +56,8 @@ pub(super) struct TextWrapper { font_size: Pixels, /// If is none, it means the text is not wrapped wrap_width: Option, + /// The longest (row, bytes len) in characters, used to calculate the horizontal scroll width. + pub(super) longest_row: LongestRow, /// The lines by split \n pub(super) lines: Vec, } @@ -61,6 +71,7 @@ impl TextWrapper { font_size, wrap_width, soft_lines: 0, + longest_row: LongestRow::default(), lines: Vec::new(), } } @@ -149,18 +160,25 @@ impl TextWrapper { } // Remove the old changed lines. - let start_row = self.text.offset_to_point(range.start).row as usize; + let start_row = self.text.offset_to_point(range.start).row; let start_row = start_row.min(self.lines.len().saturating_sub(1)); - let end_row = self.text.offset_to_point(range.end).row as usize; + let end_row = self.text.offset_to_point(range.end).row; let end_row = end_row.min(self.lines.len().saturating_sub(1)); let rows_range = start_row..=end_row; + if rows_range.contains(&self.longest_row.row) { + self.longest_row = LongestRow::default(); + } + + let mut longest_row_ix = self.longest_row.row; + let mut longest_row_len = self.longest_row.len; + // To add the new lines. - let new_start_row = changed_text.offset_to_point(range.start).row as usize; + let new_start_row = changed_text.offset_to_point(range.start).row; let new_start_offset = changed_text.line_start_offset(new_start_row); let new_end_row = changed_text .offset_to_point(range.start + new_text.len()) - .row as usize; + .row; let new_end_offset = changed_text.line_end_offset(new_end_row); let new_range = new_start_offset..new_end_offset; @@ -168,11 +186,27 @@ impl TextWrapper { let wrap_width = self.wrap_width; - for line in changed_text.slice(new_range).lines() { + for (ix, line) in changed_text + .slice(new_range) + .lines(LineType::LF_CR) + .enumerate() + { + // Remove the last `\n` + let line = if line.len() > 0 && line.chars().last() == Some('\n') { + line.slice(..line.len().saturating_sub(1)) + } else { + line + }; + let line_str = line.to_string(); let mut wrapped_lines = vec![]; let mut prev_boundary_ix = 0; + if line_str.len() > longest_row_len { + longest_row_ix = new_start_row + ix; + longest_row_len = line_str.len(); + } + // If wrap_width is Pixels::MAX, skip wrapping to disable word wrap if let Some(wrap_width) = wrap_width { // Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty. @@ -188,7 +222,7 @@ impl TextWrapper { } new_lines.push(LineItem { - line: line.clone(), + line: Rope::from(line), wrapped_lines, }); } @@ -204,6 +238,10 @@ impl TextWrapper { // dbg!(self.lines.len()); self.text = changed_text.clone(); self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum(); + self.longest_row = LongestRow { + row: longest_row_ix, + len: longest_row_len, + } } /// Update the text wrapper and recalculate the wrapped lines. diff --git a/crates/ui/src/inspector.rs b/crates/ui/src/inspector.rs index c9c095a1..1a77dff0 100644 --- a/crates/ui/src/inspector.rs +++ b/crates/ui/src/inspector.rs @@ -11,7 +11,7 @@ use lsp_types::{ CompletionItem, CompletionItemKind, CompletionResponse, CompletionTextEdit, Diagnostic, DiagnosticSeverity, Position, TextEdit, }; -use rope::Rope; +use ropey::Rope; use crate::{ alert::Alert, @@ -348,7 +348,7 @@ fn rust_to_style(mut style: StyleRefinement, source: &str) -> (StyleRefinement, let mut offset = 0; let mut method_offset = 0; let mut method = String::new(); - for line in rope.lines() { + for line in rope.rows() { if line.to_string().trim().starts_with("//") { offset += line.len() + 1; continue; @@ -564,7 +564,7 @@ struct LspProvider {} impl CompletionProvider for LspProvider { fn completions( &self, - rope: &rope::Rope, + rope: &ropey::Rope, offset: usize, _: lsp_types::CompletionContext, _: &mut Window, diff --git a/crates/ui/src/text/node.rs b/crates/ui/src/text/node.rs index 97a99b0f..37ac22c1 100644 --- a/crates/ui/src/text/node.rs +++ b/crates/ui/src/text/node.rs @@ -7,7 +7,7 @@ use gpui::{ StatefulInteractiveElement, Styled, StyledImage as _, Window, }; use markdown::mdast; -use rope::Rope; +use ropey::Rope; use crate::{ h_flex, @@ -291,7 +291,7 @@ impl CodeBlock { let mut styles = vec![]; if let Some(lang) = &lang { let mut highlighter = SyntaxHighlighter::new(&lang, cx); - highlighter.update(None, &Rope::from(code.as_str())); + highlighter.update(None, &Rope::from_str(code.as_str())); styles = highlighter.styles(&(0..code.len()), cx); };