From 344e4407ffab7a755eecd01a3e34994f885ca6d5 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 5 Sep 2025 18:20:47 +0800 Subject: [PATCH] input: Use Rope for InputState. (#1208) This PR to change the Input to use [ropey](https://github.com/cessen/ropey) to store the text. ## Break Changes - The `value` method of `InputState` now returns a `SharedString` type. ```diff - pub fn value(&self) -> &SharedString + pub fn value(&self) -> SharedString ``` Ref links: - https://github.com/cessen/ropey - https://zed.dev/blog/zed-decoded-rope-sumtree - https://github.com/helix-editor/helix/blob/master/docs/architecture.md --- Cargo.lock | 25 ++- crates/story/src/clipboard_story.rs | 2 +- crates/ui/Cargo.toml | 1 + crates/ui/src/highlighter/highlighter.rs | 111 +++++++------ crates/ui/src/input/element.rs | 15 +- crates/ui/src/input/marker.rs | 8 +- crates/ui/src/input/mod.rs | 2 + crates/ui/src/input/mode.rs | 23 +-- crates/ui/src/input/rope_ext.rs | 23 +++ crates/ui/src/input/state.rs | 146 +++++++++++------- crates/ui/src/input/text_input.rs | 2 +- crates/ui/src/input/text_wrapper.rs | 59 ++----- crates/ui/src/text/node.rs | 3 +- .../webview/examples/wef-example/src/main.rs | 2 +- crates/webview/src/browser_handler.rs | 2 +- 15 files changed, 235 insertions(+), 189 deletions(-) create mode 100644 crates/ui/src/input/rope_ext.rs diff --git a/Cargo.lock b/Cargo.lock index 72910fcc..4213401a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -709,7 +709,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -732,7 +732,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -3160,6 +3160,7 @@ dependencies = [ "once_cell", "paste", "regex", + "ropey", "rust-i18n", "rust_decimal", "schemars", @@ -3624,7 +3625,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.58.0", + "windows-core 0.61.2", ] [[package]] @@ -4901,7 +4902,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", @@ -6614,6 +6615,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ropey" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5" +dependencies = [ + "smallvec", + "str_indices", +] + [[package]] name = "roxmltree" version = "0.20.0" @@ -7634,6 +7645,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/crates/story/src/clipboard_story.rs b/crates/story/src/clipboard_story.rs index 6467ba86..304aee10 100644 --- a/crates/story/src/clipboard_story.rs +++ b/crates/story/src/clipboard_story.rs @@ -78,7 +78,7 @@ impl Render for ClipboardStory { Clipboard::new("clipboard2") .value_fn({ let state = self.url_state.clone(); - move |_, cx| state.read(cx).value().clone() + move |_, cx| state.read(cx).value().into() }) .on_copied(|value, window, cx| { window.push_notification(format!("Copied value: {}", value), cx) diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 1b4b777b..ffa02480 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -70,6 +70,7 @@ paste = "1" regex = "1" unicode-segmentation = "1.12.0" uuid = "1.10" +ropey = "1.6.1" # WebView wry = { version = "0.48.0", optional = true } diff --git a/crates/ui/src/highlighter/highlighter.rs b/crates/ui/src/highlighter/highlighter.rs index e15d7a83..d1aacd43 100644 --- a/crates/ui/src/highlighter/highlighter.rs +++ b/crates/ui/src/highlighter/highlighter.rs @@ -3,9 +3,11 @@ use crate::highlighter::LanguageRegistry; use anyhow::{anyhow, Context, Result}; use gpui::{App, HighlightStyle, SharedString}; +use ropey::Rope; use std::{ collections::{BTreeSet, HashMap}, ops::Range, + slice::Chunks, usize, }; use sum_tree::{Bias, SumTree}; @@ -22,7 +24,7 @@ pub struct SyntaxHighlighter { injection_queries: HashMap, parser: Parser, old_tree: Option, - text: SharedString, + text: Rope, locals_pattern_index: usize, highlights_pattern_index: usize, @@ -39,6 +41,16 @@ pub struct SyntaxHighlighter { cache: SumTree, } +struct TextProvider<'a>(&'a Rope); +impl<'a> tree_sitter::TextProvider<&'a [u8]> for TextProvider<'a> { + type I = Chunks<'a, u8>; + + fn text(&mut self, node: tree_sitter::Node) -> Self::I { + let slice = self.0.byte_slice(node.start_byte()..node.end_byte()); + slice.as_str().unwrap_or_default().as_bytes().chunks(64) + } +} + #[derive(Debug, Default, Clone)] struct HighlightSummary { count: usize, @@ -256,7 +268,7 @@ impl SyntaxHighlighter { injection_queries, parser, old_tree: None, - text: SharedString::new(""), + text: Rope::new(), cache: sum_tree::SumTree::new(&()), locals_pattern_index, highlights_pattern_index, @@ -271,42 +283,53 @@ impl SyntaxHighlighter { } pub fn is_empty(&self) -> bool { - self.text.is_empty() + self.text.len_bytes() == 0 } /// Highlight the given text, returning a map from byte ranges to highlight captures. /// Uses incremental parsing, detects changed ranges, and caches unchanged results. - pub fn update(&mut self, edit: Option, full_text: &SharedString, cx: &App) { - if &self.text == full_text { + pub fn update(&mut self, edit: Option, text: &Rope, cx: &App) { + if &self.text == text { return; } - let new_tree = match &self.old_tree { - // NOTE: 10K lines, about 4.5ms - None => self.parser.parse(full_text.as_ref(), None), - Some(old) => { - let edit = edit.unwrap_or(InputEdit { - start_byte: 0, - old_end_byte: 0, - new_end_byte: 0, - start_position: Point::new(0, 0), - old_end_position: Point::new(0, 0), - new_end_position: Point::new(0, 0), - }); + let edit = edit.unwrap_or(InputEdit { + start_byte: 0, + old_end_byte: 0, + new_end_byte: text.len_bytes(), + start_position: Point::new(0, 0), + old_end_position: Point::new(0, 0), + new_end_position: Point::new(0, 0), + }); - let mut old_tree = old.clone(); - old_tree.edit(&edit); - self.parser.parse(full_text.as_ref(), Some(&old_tree)) - } - }; + let mut old_tree = self + .old_tree + .take() + .unwrap_or(self.parser.parse("", None).unwrap()); + old_tree.edit(&edit); + + let new_tree = self.parser.parse_with_options( + &mut |offset, _| { + if offset >= text.len_bytes() { + "" + } else { + let (chunk, chunk_byte_ix, _, _) = text.chunk_at_byte(offset); + &chunk[offset - chunk_byte_ix..] + } + }, + Some(&old_tree), + None, + ); let Some(new_tree) = new_tree else { return; }; + // let changed_ranges = new_tree.changed_ranges(&old_tree); + // Update state self.old_tree = Some(new_tree); - self.text = full_text.clone(); + self.text = text.clone(); // let measure = crate::Measure::new("build_styles"); self.build_styles(cx); @@ -325,22 +348,24 @@ impl SyntaxHighlighter { return; }; - let source = self.text.as_bytes(); let root_node = tree.root_node(); // Remove the changed items from the cache. let new_cache = sum_tree::SumTree::new(&()); self.cache = new_cache; - let mut query_cursor = QueryCursor::new(); - let mut matches = query_cursor.matches(&query, root_node, source); - while let Some(m) = matches.next() { + let source = self.text.clone(); + + let mut cursor = QueryCursor::new(); + let mut matches = cursor.matches(&query, root_node, TextProvider(&source)); + + while let Some(query_match) = matches.next() { // Ref: // https://github.com/tree-sitter/tree-sitter/blob/460118b4c82318b083b4d527c9c750426730f9c0/highlight/src/lib.rs#L556 if let (Some(language_name), Some(content_node), _) = - self.injection_for_match(None, query, m, source) + self.injection_for_match(None, query, query_match) { - let styles = self.handle_injection(&language_name, content_node, source, cx); + let styles = self.handle_injection(&language_name, content_node, cx); for (node_range, highlight_name) in styles { self.cache .push(HighlightItem::new(node_range.clone(), highlight_name), &()); @@ -349,7 +374,7 @@ impl SyntaxHighlighter { continue; } - for cap in m.captures { + for cap in query_match.captures { let node = cap.node; let Some(highlight_name) = query.capture_names().get(cap.index as usize) else { @@ -403,7 +428,6 @@ impl SyntaxHighlighter { &self, injection_language: &str, node: Node, - source: &[u8], cx: &App, ) -> Vec<(Range, String)> { let start_offset = node.start_byte(); @@ -412,10 +436,9 @@ impl SyntaxHighlighter { let Some(query) = &self.injection_queries.get(injection_language) else { return cache; }; - let Some(content) = source.get(node.start_byte()..node.end_byte()) else { - return cache; - }; - if content.is_empty() { + + let content = self.text.byte_slice(node.start_byte()..node.end_byte()); + if content.len_bytes() == 0 { return cache; }; let Some(config) = LanguageRegistry::global(cx).language(injection_language) else { @@ -425,12 +448,14 @@ impl SyntaxHighlighter { if parser.set_language(&config.language).is_err() { return cache; } - let Some(tree) = parser.parse(content, None) else { + + let source = content.as_str().unwrap_or_default().as_bytes(); + let Some(tree) = parser.parse(source, None) else { return cache; }; let mut query_cursor = QueryCursor::new(); - let mut matches = query_cursor.matches(query, tree.root_node(), content); + let mut matches = query_cursor.matches(query, tree.root_node(), source); let mut last_end = start_offset; while let Some(m) = matches.next() { @@ -469,24 +494,16 @@ impl SyntaxHighlighter { parent_name: Option, query: &'a Query, query_match: &QueryMatch<'a, 'a>, - source: &'a [u8], ) -> (Option, Option>, bool) { let content_capture_index = self.injection_content_capture_index; - let language_capture_index = self.injection_language_capture_index; + // let language_capture_index = self.injection_language_capture_index; let mut language_name: Option = None; let mut content_node = None; for capture in query_match.captures { let index = Some(capture.index); - if index == language_capture_index { - language_name = capture - .node - .utf8_text(source) - .ok() - .map(ToString::to_string) - .map(SharedString::from); - } else if index == content_capture_index { + if index == content_capture_index { content_node = Some(capture.node); } } diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 1f21b4ae..943b8ea4 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -391,9 +391,9 @@ impl TextElement { let mut skipped_offset = 0; let mut styles = vec![]; - for (ix, line) in state.text.split('\n').enumerate() { - // +1 for last `\n`. - let line_len = line.len() + 1; + // The Rope line has includes `\n` and `\r`. + for (ix, line) in state.text.lines().enumerate() { + let line_len = line.len_bytes(); if ix < visible_range.start { offset += line_len; skipped_offset = offset; @@ -547,7 +547,7 @@ impl Element for TextElement { let state = self.state.read(cx); let multi_line = state.mode.is_multi_line(); let text = state.text.clone(); - let is_empty = text.is_empty(); + let is_empty = text.len_bytes() == 0; let placeholder = self.placeholder.clone(); let style = window.text_style(); let font_size = style.font_size.to_pixels(window.rem_size()); @@ -556,12 +556,9 @@ impl Element for TextElement { let (display_text, text_color) = if is_empty { (placeholder, cx.theme().muted_foreground) } else if state.masked { - ( - "*".repeat(text.chars().count()).into(), - cx.theme().foreground, - ) + ("*".repeat(text.len_chars()).into(), cx.theme().foreground) } else { - (text.clone(), cx.theme().foreground) + (text.to_string().into(), cx.theme().foreground) }; let text_style = window.text_style(); diff --git a/crates/ui/src/input/marker.rs b/crates/ui/src/input/marker.rs index 1aff08d6..c2fba985 100644 --- a/crates/ui/src/input/marker.rs +++ b/crates/ui/src/input/marker.rs @@ -44,9 +44,7 @@ impl Marker { return; }; - let Some(start_line_str) = state.text.get(start_line.range.clone()) else { - return; - }; + let start_line_str = state.text.byte_slice(start_line.range.clone()); let Some(end_line) = state .text_wrapper @@ -55,9 +53,7 @@ impl Marker { else { return; }; - let Some(end_line_str) = state.text.get(end_line.range.clone()) else { - return; - }; + let end_line_str = state.text.byte_slice(end_line.range.clone()); let start_byte = start_line.range.start + start_line_str diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs index 2defa36c..0a02d6c1 100644 --- a/crates/ui/src/input/mod.rs +++ b/crates/ui/src/input/mod.rs @@ -9,6 +9,7 @@ mod mask_pattern; mod mode; mod number_input; mod otp_input; +mod rope_ext; mod state; mod text_input; mod text_wrapper; @@ -20,5 +21,6 @@ pub use mask_pattern::MaskPattern; pub use mode::TabSize; pub use number_input::{NumberInput, NumberInputEvent, StepAction}; pub use otp_input::*; +pub(crate) use rope_ext::*; pub use state::*; pub use text_input::*; diff --git a/crates/ui/src/input/mode.rs b/crates/ui/src/input/mode.rs index 5ce5a7f7..168b284b 100644 --- a/crates/ui/src/input/mode.rs +++ b/crates/ui/src/input/mode.rs @@ -2,8 +2,10 @@ use std::rc::Rc; use std::{cell::RefCell, ops::Range}; use gpui::{App, SharedString}; +use ropey::Rope; use tree_sitter::{InputEdit, Point}; +use crate::input::RopeExt as _; use crate::{highlighter::SyntaxHighlighter, input::marker::Marker}; use super::text_wrapper::TextWrapper; @@ -161,9 +163,8 @@ impl InputMode { pub(super) fn update_highlighter( &mut self, selected_range: &Range, - full_text: &SharedString, + text: &Rope, new_text: &str, - text_wrapper: &TextWrapper, force: bool, cx: &mut App, ) { @@ -187,6 +188,10 @@ impl InputMode { return; }; + // When full text changed, the selected_range may be out of bound (The before version). + let mut selected_range = selected_range.clone(); + selected_range.end = selected_range.end.min(text.len_bytes()); + // If insert a chart, this is 1. // If backspace or delete, this is -1. // If selected to delete, this is the length of the selected text. @@ -194,20 +199,20 @@ impl InputMode { let changed_len = new_text.len() as isize - selected_range.len() as isize; let new_end = (selected_range.end as isize + changed_len) as usize; - // let start_pos = text_wrapper.line_column(selected_range.start); - // let old_end_pos = text_wrapper.line_column(selected_range.end); - // let new_end_pos = text_wrapper.line_column(new_end); + let start_pos = text.line_column(selected_range.start); + let old_end_pos = text.line_column(selected_range.end); + let new_end_pos = text.line_column(new_end); let edit = InputEdit { start_byte: selected_range.start, old_end_byte: selected_range.end, new_end_byte: new_end, - start_position: Point::new(0, 0), - old_end_position: Point::new(0, 0), - new_end_position: Point::new(0, 0), + start_position: Point::new(start_pos.0, start_pos.1), + old_end_position: Point::new(old_end_pos.0, old_end_pos.1), + new_end_position: Point::new(new_end_pos.0, new_end_pos.1), }; - highlighter.update(Some(edit), full_text, cx); + highlighter.update(Some(edit), text, cx); } _ => {} } diff --git a/crates/ui/src/input/rope_ext.rs b/crates/ui/src/input/rope_ext.rs new file mode 100644 index 00000000..76ad9538 --- /dev/null +++ b/crates/ui/src/input/rope_ext.rs @@ -0,0 +1,23 @@ +pub(crate) trait RopeExt { + /// Get the index of (line, column) (0-based) from the byte offset (0-based). + /// If the offset is out of bounds, return the last line and column. + fn line_column(&self, byte_offset: usize) -> (usize, usize); + /// Get the byte offset (0-based) from the line, column (0-based). + fn line_column_to_byte(&self, line_ix: usize, column_ix: usize) -> usize; +} + +impl RopeExt for ropey::Rope { + fn line_column(&self, offset: usize) -> (usize, usize) { + let line_ix = self.byte_to_line(offset); + let line_offset = offset.saturating_sub(self.line_to_byte(line_ix)); + let line = self.line(line_ix); + let column_ix = line.byte_to_char(line_offset); + + (line_ix, column_ix) + } + + fn line_column_to_byte(&self, line_ix: usize, column_ix: usize) -> usize { + let line = self.line(line_ix); + self.line_to_byte(line_ix) + line.char_to_byte(column_ix) + } +} diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index e8327834..e9f38646 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -3,6 +3,7 @@ //! Based on the `Input` example from the `gpui` crate. //! https://github.com/zed-industries/zed/blob/main/crates/gpui/examples/input.rs use gpui::Action; +use ropey::{Rope, RopeSlice}; use serde::Deserialize; use smallvec::SmallVec; use std::cell::RefCell; @@ -32,7 +33,7 @@ use super::{ }; use crate::input::hover_popover::DiagnosticPopover; use crate::input::marker::Marker; -use crate::input::{Cursor, LineColumn, Selection}; +use crate::input::{Cursor, LineColumn, RopeExt, Selection}; use crate::{history::History, scroll::ScrollbarState, Root}; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] @@ -240,7 +241,7 @@ impl Deref for LastLayout { pub struct InputState { pub(super) focus_handle: FocusHandle, pub(super) mode: InputMode, - pub(super) text: SharedString, + pub(super) text: Rope, pub(super) text_wrapper: TextWrapper, pub(super) history: History, pub(super) blink_cursor: Entity, @@ -647,7 +648,7 @@ impl InputState { prev_lines_offset += line.len() + 1; } - let new_offset = (prev_lines_offset + new_local_index).min(self.text.len()); + let new_offset = (prev_lines_offset + new_local_index).min(self.text.len_bytes()); let new_cursor = Cursor::new(new_offset); self.selected_range = (new_cursor..new_cursor).into(); self.pause_blink_cursor(cx); @@ -673,7 +674,7 @@ impl InputState { // Ensure cursor to start when set text if self.mode.is_single_line() { self.selected_range = - (Cursor::new(self.text.len())..Cursor::new(self.text.len())).into(); + (Cursor::new(self.text.len_bytes())..Cursor::new(self.text.len_bytes())).into(); } else { self.selected_range = (Cursor::new(0)..Cursor::new(0)).into(); } @@ -793,24 +794,31 @@ impl InputState { /// Set the default value of the input field. pub fn default_value(mut self, value: impl Into) -> Self { - self.text = value.into(); + let text: SharedString = value.into(); + self.text = Rope::from_str(text.as_str()); self.text_wrapper.text = self.text.clone(); self } /// Return the value of the input field. - pub fn value(&self) -> &SharedString { - &self.text + pub fn value(&self) -> SharedString { + SharedString::new(self.text.slice(..).as_str().unwrap_or_default()) } /// Return the value without mask. pub fn unmask_value(&self) -> SharedString { - self.mask_pattern.unmask(&self.text).into() + self.mask_pattern.unmask(&self.text.to_string()).into() } /// Return the (1-based) line and column of the cursor. pub fn line_column(&self) -> LineColumn { - self.text_wrapper.line_column(self.cursor().offset) + let offset = self.cursor().offset; + let (line_ix, column_ix) = self.text.line_column(offset); + + LineColumn { + line: line_ix + 1, + column: column_ix + 1, + } } /// Set (1-based) line and column of the cursor. @@ -830,12 +838,11 @@ impl InputState { window: &mut Window, cx: &mut Context, ) { - if let Some(offset) = self - .text_wrapper - .offset_for_line_column(line, column.unwrap_or(1)) - { - self.move_to(Cursor::new(offset), window, cx); - } + let line_ix = line.saturating_sub(1); + let column_ix = column.unwrap_or(1).saturating_sub(1); + let offset = self.text.line_column_to_byte(line_ix, column_ix); + + self.move_to(Cursor::new(offset), window, cx); } /// Focus the input field. @@ -976,7 +983,7 @@ impl InputState { if self.mode.is_single_line() { return; } - let offset = (self.end_of_line(window, cx) + 1).min(self.text.len()); + let offset = (self.end_of_line(window, cx) + 1).min(self.text.len_bytes()); self.select_to(Cursor::new(self.next_boundary(offset)), window, cx); } @@ -987,7 +994,7 @@ impl InputState { cx: &mut Context, ) { self.move_to(Cursor::new(0), window, cx); - self.select_to(Cursor::new(self.text.len()), window, cx) + self.select_to(Cursor::new(self.text.len_bytes()), window, cx) } pub(super) fn home(&mut self, _: &MoveHome, window: &mut Window, cx: &mut Context) { @@ -1017,7 +1024,7 @@ impl InputState { window: &mut Window, cx: &mut Context, ) { - let end = self.text.len(); + let end = self.text.len_bytes(); self.move_to(Cursor::new(end), window, cx); } @@ -1056,7 +1063,7 @@ impl InputState { window: &mut Window, cx: &mut Context, ) { - let end = self.text.len(); + let end = self.text.len_bytes(); self.select_to(Cursor::new(end), window, cx); } @@ -1103,7 +1110,10 @@ impl InputState { /// Return the start offset of the previous word. fn previous_start_of_word(&mut self) -> usize { let offset = self.selected_range.start.offset; - let prev_str = self.text_for_range_utf8(0..offset); + let prev_str = self + .text_for_range_utf8(0..offset) + .as_str() + .unwrap_or_default(); UnicodeSegmentation::split_word_bound_indices(prev_str) .filter(|(_, s)| !s.trim_start().is_empty()) .next_back() @@ -1114,11 +1124,14 @@ impl InputState { /// Return the next end offset of the next word. fn next_end_of_word(&mut self) -> usize { let offset = self.cursor().offset; - let next_str = self.text_for_range_utf8(offset..self.text.len()); + let next_str = self + .text_for_range_utf8(offset..self.text.len_bytes()) + .as_str() + .unwrap_or_default(); UnicodeSegmentation::split_word_bound_indices(next_str) .find(|(_, s)| !s.trim_start().is_empty()) .map(|(i, s)| offset + i + s.len()) - .unwrap_or(self.text.len()) + .unwrap_or(self.text.len_bytes()) } /// Get start of line @@ -1166,9 +1179,12 @@ impl InputState { /// Get end of line fn end_of_line(&mut self, window: &mut Window, cx: &mut Context) -> usize { if self.mode.is_single_line() { - return self.text.len(); + return self.text.len_bytes(); } + // let line = self.text.byte_to_line(self.cursor().offset); + // let offset = self.text.line_to_byte(line) + self.text.line(line).len_bytes(); + let offset = self.next_boundary(self.cursor().offset); // ignore if offset is "\n" if self @@ -1186,7 +1202,7 @@ impl InputState { let line = self .text_for_range( - self.range_to_utf16(&(offset..self.text.len())), + self.range_to_utf16(&(offset..self.text.len_bytes())), &mut None, window, cx, @@ -1194,7 +1210,7 @@ impl InputState { .unwrap_or_default() .find('\n') .map(|i| i + offset) - .unwrap_or(self.text.len()); + .unwrap_or(self.text.len_bytes()); line } @@ -1293,7 +1309,7 @@ impl InputState { ) { let mut offset = self.end_of_line(window, cx); if offset == self.cursor().offset { - offset = (offset + 1).clamp(0, self.text.len()); + offset = (offset + 1).clamp(0, self.text.len_bytes()); } self.replace_text_in_range( Some(self.range_to_utf16(&(self.cursor().offset..offset))), @@ -1502,7 +1518,9 @@ impl InputState { let start_offset = self.selected_range.start; let offset = self.start_of_line_of_selection(window, cx); if self - .text_for_range_utf8(offset..self.text.len()) + .text_for_range_utf8(offset..self.text.len_bytes()) + .as_str() + .unwrap_or("") .starts_with(tab_indent.as_ref()) { self.replace_text_in_range( @@ -1723,7 +1741,7 @@ impl InputState { /// /// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset. fn move_to(&mut self, cursor: Cursor, _: &mut Window, cx: &mut Context) { - let cursor = Cursor::new(cursor.offset.clamp(0, self.text.len())); + let cursor = Cursor::new(cursor.offset.clamp(0, self.text.len_bytes())); self.selected_range = (cursor..cursor).into(); self.pause_blink_cursor(cx); self.update_preferred_x_offset(cx); @@ -1752,7 +1770,7 @@ impl InputState { _cx: &App, ) -> usize { // If the text is empty, always return 0 - if self.text.is_empty() { + if self.text.len_bytes() == 0 { return 0; } @@ -1817,8 +1835,8 @@ impl InputState { index += 1; } - if index > self.text.len() { - self.text.len() + if index > self.text.len_bytes() { + self.text.len_bytes() } else { index } @@ -1851,7 +1869,7 @@ impl InputState { /// /// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset. fn select_to(&mut self, offset: Cursor, _: &mut Window, cx: &mut Context) { - let offset = offset.clamp(0, self.text.len()); + let offset = offset.clamp(0, self.text.len_bytes()); if self.selection_reversed { self.selected_range.start = Cursor::new(offset) } else { @@ -1896,7 +1914,7 @@ impl InputState { .unwrap_or_default(); let next_text = self .text_for_range( - self.range_to_utf16(&(end..self.text.len())), + self.range_to_utf16(&(end..self.text.len_bytes())), &mut None, window, cx, @@ -1980,18 +1998,24 @@ impl InputState { } fn previous_boundary(&self, offset: usize) -> usize { - self.text - .grapheme_indices(true) - .rev() - .find_map(|(idx, _)| (idx < offset).then_some(idx)) - .unwrap_or(0) + let char_ix = self.text.byte_to_char(offset).saturating_sub(1); + self.text.char_to_byte(char_ix) + + // self.text + // .grapheme_indices(true) + // .rev() + // .find_map(|(idx, _)| (idx < offset).then_some(idx)) + // .unwrap_or(0) } fn next_boundary(&self, offset: usize) -> usize { - self.text - .grapheme_indices(true) - .find_map(|(idx, _)| (idx > offset).then_some(idx)) - .unwrap_or(self.text.len()) + let char_ix = self.text.byte_to_char(offset) + 1; + self.text.char_to_byte(char_ix).min(self.text.len_bytes()) + + // self.text + // .grapheme_indices(true) + // .find_map(|(idx, _)| (idx > offset).then_some(idx)) + // .unwrap_or(self.text.len()) } /// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible. @@ -2034,7 +2058,7 @@ impl InputState { window: &mut Window, cx: &mut Context, ) { - if self.text.is_empty() { + if self.text.len_bytes() == 0 { return; } @@ -2126,9 +2150,9 @@ impl InputState { } } - fn text_for_range_utf8(&mut self, range: impl Into>) -> &str { + fn text_for_range_utf8(&'_ self, range: impl Into>) -> RopeSlice<'_> { let range = self.range_from_utf16(&self.range_to_utf16(&range.into())); - &self.text[range] + self.text.byte_slice(range) } } @@ -2142,7 +2166,7 @@ impl EntityInputHandler for InputState { ) -> Option { let range = self.range_from_utf16(&range_utf16); adjusted_range.replace(self.range_to_utf16(&range)); - Some(self.text[range].to_string()) + Some(self.text.byte_slice(range).to_string()) } fn selected_text_range( @@ -2191,10 +2215,12 @@ impl EntityInputHandler for InputState { .or(self.marked_range.map(|range| range.into())) .unwrap_or(self.selected_range.into()); - let pending_text: SharedString = (self.text_for_range_utf8(0..range.start).to_owned() + let pending_text: SharedString = (self.text_for_range_utf8(0..range.start).to_string() + new_text - + self.text_for_range_utf8(range.end..self.text.len())) - .into(); + + &self + .text_for_range_utf8(range.end..self.text.len_bytes()) + .to_string()) + .into(); // Check if the new text is valid if !self.is_valid_input(&pending_text, cx) { return; @@ -2205,18 +2231,18 @@ impl EntityInputHandler for InputState { let new_offset = (range.start + new_text_len).min(mask_text.len()); self.push_history(&range, &new_text, window, cx); - self.text = mask_text.clone(); + self.text = Rope::from_str(&mask_text); self.mode.clear_markers(); self.text_wrapper.update(&self.text, false, cx); self.mode - .update_highlighter(&range, &self.text, &new_text, &self.text_wrapper, true, cx); + .update_highlighter(&range, &self.text, &new_text, true, cx); self.selected_range = (new_offset..new_offset).into(); self.marked_range.take(); self.update_preferred_x_offset(cx); self.update_scroll_offset(None, cx); self.mode.update_auto_grow(&self.text_wrapper); - cx.emit(InputEvent::Change(self.unmask_value())); + cx.emit(InputEvent::Change(self.unmask_value().into())); cx.notify(); } @@ -2238,20 +2264,22 @@ impl EntityInputHandler for InputState { .map(|range_utf16| self.range_from_utf16(range_utf16)) .or(self.marked_range.map(|range| range.into())) .unwrap_or(self.selected_range.into()); - let pending_text: SharedString = (self.text_for_range_utf8(0..range.start).to_owned() + let pending_text: SharedString = (self.text_for_range_utf8(0..range.start).to_string() + new_text - + self.text_for_range_utf8(range.end..self.text.len())) - .into(); + + &self + .text_for_range_utf8(range.end..self.text.len_bytes()) + .to_string()) + .into(); if !self.is_valid_input(&pending_text, cx) { return; } self.push_history(&range, new_text, window, cx); - self.text = pending_text; + self.text = Rope::from_str(&pending_text); self.mode.clear_markers(); self.text_wrapper.update(&self.text, false, cx); self.mode - .update_highlighter(&range, &self.text, &new_text, &self.text_wrapper, true, cx); + .update_highlighter(&range, &self.text, &new_text, true, cx); if new_text.is_empty() { // Cancel selection, when cancel IME input. self.selected_range = (range.start..range.start).into(); @@ -2266,7 +2294,7 @@ impl EntityInputHandler for InputState { .into(); } self.mode.update_auto_grow(&self.text_wrapper); - cx.emit(InputEvent::Change(self.unmask_value())); + cx.emit(InputEvent::Change(self.unmask_value().into())); cx.notify(); } @@ -2357,7 +2385,7 @@ impl Render for InputState { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { self.text_wrapper.update(&self.text, false, cx); self.mode - .update_highlighter(&(0..0), &self.text, "", &self.text_wrapper, false, cx); + .update_highlighter(&(0..0), &self.text, "", false, cx); div() .id("input-state") diff --git a/crates/ui/src/input/text_input.rs b/crates/ui/src/input/text_input.rs index 405accf1..c2fc305f 100644 --- a/crates/ui/src/input/text_input.rs +++ b/crates/ui/src/input/text_input.rs @@ -176,7 +176,7 @@ impl RenderOnce for TextInput { let suffix = self.suffix; let show_clear_button = self.cleanable && !state.loading - && !state.text.is_empty() + && state.text.len_bytes() > 0 && state.mode.is_single_line(); let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button; diff --git a/crates/ui/src/input/text_wrapper.rs b/crates/ui/src/input/text_wrapper.rs index d30f3f94..38c2a848 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 crate::input::LineColumn; -use gpui::{App, Font, LineFragment, Pixels, SharedString}; +use gpui::{App, Font, LineFragment, Pixels}; +use ropey::Rope; #[allow(unused)] pub(super) struct LineWrap { @@ -15,11 +15,11 @@ pub(super) struct LineWrap { pub(super) range: Range, } -/// Used to prepare the text with soft_wrap to be get lines to displayed in the TextArea +/// Used to prepare the text with soft wrap to be get lines to displayed in the TextArea /// /// After use lines to calculate the scroll size of the TextArea pub(super) struct TextWrapper { - pub(super) text: SharedString, + pub(super) text: Rope, /// The wrapped lines, value is start and end index of the line. pub(super) wrapped_lines: Vec>, /// The lines by split \n @@ -34,7 +34,7 @@ pub(super) struct TextWrapper { impl TextWrapper { pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option) -> Self { Self { - text: SharedString::default(), + text: Rope::new(), font, font_size, wrap_width, @@ -57,7 +57,7 @@ impl TextWrapper { /// Update the text wrapper and recalculate the wrapped lines. /// /// If the `text` is the same as the current text, do nothing. - pub(super) fn update(&mut self, text: &SharedString, force: bool, cx: &mut App) { + pub(super) fn update(&mut self, text: &Rope, force: bool, cx: &mut App) { if &self.text == text && !force { return; } @@ -69,7 +69,9 @@ impl TextWrapper { .text_system() .line_wrapper(self.font.clone(), self.font_size); let mut prev_line_ix = 0; - for line in text.split('\n') { + + // FIXME: here may need use from Rope + for line in text.to_string().split('\n') { let mut line_wraps = vec![]; let mut prev_boundary_ix = 0; @@ -100,47 +102,4 @@ impl TextWrapper { self.wrapped_lines = wrapped_lines; self.lines = lines; } - - /// Returns the line and column (1-based) of the given offset (Entire text). - pub(super) fn line_column(&self, offset: usize) -> LineColumn { - if self.lines.is_empty() { - return LineColumn::default(); - } - - let line = self - .lines - .binary_search_by_key(&offset, |line| line.range.end) - .unwrap_or_else(|i| i); - let column = offset.saturating_sub(self.lines[line].range.start); - - (line + 1, column + 1).into() - } - - /// Returns the offset of the given line and column (1-based). - /// - /// - If the `line` is 0, it will return 0. - /// - If the `line` is greater than the number of lines, it will return - /// the length of the text. - pub(super) fn offset_for_line_column(&self, line: usize, column: usize) -> Option { - if line == 0 || self.lines.is_empty() { - return None; - } - - let line = line.saturating_sub(1); - if line >= self.lines.len() { - return Some(self.text.len()); - } - - let Some(line_wrap) = &self.lines.get(line) else { - return None; - }; - - let offset = line_wrap.range.start; - if column == 0 { - return Some(offset); - } - let offset = offset + column.saturating_sub(1).min(line_wrap.range.len()); - - Some(offset) - } } diff --git a/crates/ui/src/text/node.rs b/crates/ui/src/text/node.rs index c2689ec1..da266144 100644 --- a/crates/ui/src/text/node.rs +++ b/crates/ui/src/text/node.rs @@ -7,6 +7,7 @@ use gpui::{ StatefulInteractiveElement, Styled, StyledImage as _, Window, }; use markdown::mdast; +use ropey::Rope; use crate::{ h_flex, @@ -291,7 +292,7 @@ impl CodeBlock { let mut styles = vec![]; if let Some(lang) = &lang { let mut highlighter = SyntaxHighlighter::new(&lang, cx); - highlighter.update(None, &code, cx); + highlighter.update(None, &Rope::from_str(code.as_str()), cx); styles = highlighter.styles(&(0..code.len()), &theme); }; diff --git a/crates/webview/examples/wef-example/src/main.rs b/crates/webview/examples/wef-example/src/main.rs index 5ecdcef9..4f0ce048 100644 --- a/crates/webview/examples/wef-example/src/main.rs +++ b/crates/webview/examples/wef-example/src/main.rs @@ -71,7 +71,7 @@ impl Main { move |state, event: &InputEvent, _, cx| { if let InputEvent::PressEnter { .. } = event { let url = state.read(cx).value(); - webview.read(cx).browser().load_url(url); + webview.read(cx).browser().load_url(&url); } } }) diff --git a/crates/webview/src/browser_handler.rs b/crates/webview/src/browser_handler.rs index 083461e7..be692342 100644 --- a/crates/webview/src/browser_handler.rs +++ b/crates/webview/src/browser_handler.rs @@ -318,7 +318,7 @@ impl BrowserHandler for WebViewHandler { let callback = callback.clone(); let input_state = input_state.clone(); move |_, _, cx| { - callback.continue_(true, Some(input_state.read(cx).value())); + callback.continue_(true, Some(&input_state.read(cx).value())); true } })