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
This commit is contained in:
Jason Lee 2025-09-05 18:20:47 +08:00 committed by GitHub
parent 19be9b8d96
commit 344e4407ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 235 additions and 189 deletions

25
Cargo.lock generated
View file

@ -709,7 +709,7 @@ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools 0.11.0", "itertools 0.12.1",
"lazy_static", "lazy_static",
"lazycell", "lazycell",
"log", "log",
@ -732,7 +732,7 @@ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools 0.11.0", "itertools 0.13.0",
"log", "log",
"prettyplease", "prettyplease",
"proc-macro2", "proc-macro2",
@ -3160,6 +3160,7 @@ dependencies = [
"once_cell", "once_cell",
"paste", "paste",
"regex", "regex",
"ropey",
"rust-i18n", "rust-i18n",
"rust_decimal", "rust_decimal",
"schemars", "schemars",
@ -3624,7 +3625,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"wasm-bindgen", "wasm-bindgen",
"windows-core 0.58.0", "windows-core 0.61.2",
] ]
[[package]] [[package]]
@ -4901,7 +4902,7 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
dependencies = [ dependencies = [
"proc-macro-crate 1.3.1", "proc-macro-crate 3.3.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.105", "syn 2.0.105",
@ -6614,6 +6615,16 @@ dependencies = [
"syn 1.0.109", "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]] [[package]]
name = "roxmltree" name = "roxmltree"
version = "0.20.0" version = "0.20.0"
@ -7634,6 +7645,12 @@ dependencies = [
"unindent", "unindent",
] ]
[[package]]
name = "str_indices"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6"
[[package]] [[package]]
name = "streaming-iterator" name = "streaming-iterator"
version = "0.1.9" version = "0.1.9"

View file

@ -78,7 +78,7 @@ impl Render for ClipboardStory {
Clipboard::new("clipboard2") Clipboard::new("clipboard2")
.value_fn({ .value_fn({
let state = self.url_state.clone(); 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| { .on_copied(|value, window, cx| {
window.push_notification(format!("Copied value: {}", value), cx) window.push_notification(format!("Copied value: {}", value), cx)

View file

@ -70,6 +70,7 @@ paste = "1"
regex = "1" regex = "1"
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
uuid = "1.10" uuid = "1.10"
ropey = "1.6.1"
# WebView # WebView
wry = { version = "0.48.0", optional = true } wry = { version = "0.48.0", optional = true }

View file

@ -3,9 +3,11 @@ use crate::highlighter::LanguageRegistry;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use gpui::{App, HighlightStyle, SharedString}; use gpui::{App, HighlightStyle, SharedString};
use ropey::Rope;
use std::{ use std::{
collections::{BTreeSet, HashMap}, collections::{BTreeSet, HashMap},
ops::Range, ops::Range,
slice::Chunks,
usize, usize,
}; };
use sum_tree::{Bias, SumTree}; use sum_tree::{Bias, SumTree};
@ -22,7 +24,7 @@ pub struct SyntaxHighlighter {
injection_queries: HashMap<SharedString, Query>, injection_queries: HashMap<SharedString, Query>,
parser: Parser, parser: Parser,
old_tree: Option<Tree>, old_tree: Option<Tree>,
text: SharedString, text: Rope,
locals_pattern_index: usize, locals_pattern_index: usize,
highlights_pattern_index: usize, highlights_pattern_index: usize,
@ -39,6 +41,16 @@ pub struct SyntaxHighlighter {
cache: SumTree<HighlightItem>, cache: SumTree<HighlightItem>,
} }
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)] #[derive(Debug, Default, Clone)]
struct HighlightSummary { struct HighlightSummary {
count: usize, count: usize,
@ -256,7 +268,7 @@ impl SyntaxHighlighter {
injection_queries, injection_queries,
parser, parser,
old_tree: None, old_tree: None,
text: SharedString::new(""), text: Rope::new(),
cache: sum_tree::SumTree::new(&()), cache: sum_tree::SumTree::new(&()),
locals_pattern_index, locals_pattern_index,
highlights_pattern_index, highlights_pattern_index,
@ -271,42 +283,53 @@ impl SyntaxHighlighter {
} }
pub fn is_empty(&self) -> bool { 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. /// Highlight the given text, returning a map from byte ranges to highlight captures.
/// Uses incremental parsing, detects changed ranges, and caches unchanged results. /// Uses incremental parsing, detects changed ranges, and caches unchanged results.
pub fn update(&mut self, edit: Option<InputEdit>, full_text: &SharedString, cx: &App) { pub fn update(&mut self, edit: Option<InputEdit>, text: &Rope, cx: &App) {
if &self.text == full_text { if &self.text == text {
return; return;
} }
let new_tree = match &self.old_tree { let edit = edit.unwrap_or(InputEdit {
// NOTE: 10K lines, about 4.5ms start_byte: 0,
None => self.parser.parse(full_text.as_ref(), None), old_end_byte: 0,
Some(old) => { new_end_byte: text.len_bytes(),
let edit = edit.unwrap_or(InputEdit { start_position: Point::new(0, 0),
start_byte: 0, old_end_position: Point::new(0, 0),
old_end_byte: 0, new_end_position: Point::new(0, 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 mut old_tree = old.clone(); let mut old_tree = self
old_tree.edit(&edit); .old_tree
self.parser.parse(full_text.as_ref(), Some(&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 { let Some(new_tree) = new_tree else {
return; return;
}; };
// let changed_ranges = new_tree.changed_ranges(&old_tree);
// Update state // Update state
self.old_tree = Some(new_tree); self.old_tree = Some(new_tree);
self.text = full_text.clone(); self.text = text.clone();
// let measure = crate::Measure::new("build_styles"); // let measure = crate::Measure::new("build_styles");
self.build_styles(cx); self.build_styles(cx);
@ -325,22 +348,24 @@ impl SyntaxHighlighter {
return; return;
}; };
let source = self.text.as_bytes();
let root_node = tree.root_node(); let root_node = tree.root_node();
// Remove the changed items from the cache. // Remove the changed items from the cache.
let new_cache = sum_tree::SumTree::new(&()); let new_cache = sum_tree::SumTree::new(&());
self.cache = new_cache; self.cache = new_cache;
let mut query_cursor = QueryCursor::new(); let source = self.text.clone();
let mut matches = query_cursor.matches(&query, root_node, source);
while let Some(m) = matches.next() { let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, root_node, TextProvider(&source));
while let Some(query_match) = matches.next() {
// Ref: // Ref:
// https://github.com/tree-sitter/tree-sitter/blob/460118b4c82318b083b4d527c9c750426730f9c0/highlight/src/lib.rs#L556 // https://github.com/tree-sitter/tree-sitter/blob/460118b4c82318b083b4d527c9c750426730f9c0/highlight/src/lib.rs#L556
if let (Some(language_name), Some(content_node), _) = 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 { for (node_range, highlight_name) in styles {
self.cache self.cache
.push(HighlightItem::new(node_range.clone(), highlight_name), &()); .push(HighlightItem::new(node_range.clone(), highlight_name), &());
@ -349,7 +374,7 @@ impl SyntaxHighlighter {
continue; continue;
} }
for cap in m.captures { for cap in query_match.captures {
let node = cap.node; let node = cap.node;
let Some(highlight_name) = query.capture_names().get(cap.index as usize) else { let Some(highlight_name) = query.capture_names().get(cap.index as usize) else {
@ -403,7 +428,6 @@ impl SyntaxHighlighter {
&self, &self,
injection_language: &str, injection_language: &str,
node: Node, node: Node,
source: &[u8],
cx: &App, cx: &App,
) -> Vec<(Range<usize>, String)> { ) -> Vec<(Range<usize>, String)> {
let start_offset = node.start_byte(); let start_offset = node.start_byte();
@ -412,10 +436,9 @@ impl SyntaxHighlighter {
let Some(query) = &self.injection_queries.get(injection_language) else { let Some(query) = &self.injection_queries.get(injection_language) else {
return cache; return cache;
}; };
let Some(content) = source.get(node.start_byte()..node.end_byte()) else {
return cache; let content = self.text.byte_slice(node.start_byte()..node.end_byte());
}; if content.len_bytes() == 0 {
if content.is_empty() {
return cache; return cache;
}; };
let Some(config) = LanguageRegistry::global(cx).language(injection_language) else { let Some(config) = LanguageRegistry::global(cx).language(injection_language) else {
@ -425,12 +448,14 @@ impl SyntaxHighlighter {
if parser.set_language(&config.language).is_err() { if parser.set_language(&config.language).is_err() {
return cache; 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; return cache;
}; };
let mut query_cursor = QueryCursor::new(); 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; let mut last_end = start_offset;
while let Some(m) = matches.next() { while let Some(m) = matches.next() {
@ -469,24 +494,16 @@ impl SyntaxHighlighter {
parent_name: Option<SharedString>, parent_name: Option<SharedString>,
query: &'a Query, query: &'a Query,
query_match: &QueryMatch<'a, 'a>, query_match: &QueryMatch<'a, 'a>,
source: &'a [u8],
) -> (Option<SharedString>, Option<Node<'a>>, bool) { ) -> (Option<SharedString>, Option<Node<'a>>, bool) {
let content_capture_index = self.injection_content_capture_index; 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<SharedString> = None; let mut language_name: Option<SharedString> = None;
let mut content_node = None; let mut content_node = None;
for capture in query_match.captures { for capture in query_match.captures {
let index = Some(capture.index); let index = Some(capture.index);
if index == language_capture_index { if index == content_capture_index {
language_name = capture
.node
.utf8_text(source)
.ok()
.map(ToString::to_string)
.map(SharedString::from);
} else if index == content_capture_index {
content_node = Some(capture.node); content_node = Some(capture.node);
} }
} }

View file

@ -391,9 +391,9 @@ impl TextElement {
let mut skipped_offset = 0; let mut skipped_offset = 0;
let mut styles = vec![]; let mut styles = vec![];
for (ix, line) in state.text.split('\n').enumerate() { // The Rope line has includes `\n` and `\r`.
// +1 for last `\n`. for (ix, line) in state.text.lines().enumerate() {
let line_len = line.len() + 1; let line_len = line.len_bytes();
if ix < visible_range.start { if ix < visible_range.start {
offset += line_len; offset += line_len;
skipped_offset = offset; skipped_offset = offset;
@ -547,7 +547,7 @@ impl Element for TextElement {
let state = self.state.read(cx); let state = self.state.read(cx);
let multi_line = state.mode.is_multi_line(); let multi_line = state.mode.is_multi_line();
let text = state.text.clone(); let text = state.text.clone();
let is_empty = text.is_empty(); let is_empty = text.len_bytes() == 0;
let placeholder = self.placeholder.clone(); let placeholder = self.placeholder.clone();
let style = window.text_style(); let style = window.text_style();
let font_size = style.font_size.to_pixels(window.rem_size()); 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 { let (display_text, text_color) = if is_empty {
(placeholder, cx.theme().muted_foreground) (placeholder, cx.theme().muted_foreground)
} else if state.masked { } else if state.masked {
( ("*".repeat(text.len_chars()).into(), cx.theme().foreground)
"*".repeat(text.chars().count()).into(),
cx.theme().foreground,
)
} else { } else {
(text.clone(), cx.theme().foreground) (text.to_string().into(), cx.theme().foreground)
}; };
let text_style = window.text_style(); let text_style = window.text_style();

View file

@ -44,9 +44,7 @@ impl Marker {
return; return;
}; };
let Some(start_line_str) = state.text.get(start_line.range.clone()) else { let start_line_str = state.text.byte_slice(start_line.range.clone());
return;
};
let Some(end_line) = state let Some(end_line) = state
.text_wrapper .text_wrapper
@ -55,9 +53,7 @@ impl Marker {
else { else {
return; return;
}; };
let Some(end_line_str) = state.text.get(end_line.range.clone()) else { let end_line_str = state.text.byte_slice(end_line.range.clone());
return;
};
let start_byte = start_line.range.start let start_byte = start_line.range.start
+ start_line_str + start_line_str

View file

@ -9,6 +9,7 @@ mod mask_pattern;
mod mode; mod mode;
mod number_input; mod number_input;
mod otp_input; mod otp_input;
mod rope_ext;
mod state; mod state;
mod text_input; mod text_input;
mod text_wrapper; mod text_wrapper;
@ -20,5 +21,6 @@ pub use mask_pattern::MaskPattern;
pub use mode::TabSize; pub use mode::TabSize;
pub use number_input::{NumberInput, NumberInputEvent, StepAction}; pub use number_input::{NumberInput, NumberInputEvent, StepAction};
pub use otp_input::*; pub use otp_input::*;
pub(crate) use rope_ext::*;
pub use state::*; pub use state::*;
pub use text_input::*; pub use text_input::*;

View file

@ -2,8 +2,10 @@ use std::rc::Rc;
use std::{cell::RefCell, ops::Range}; use std::{cell::RefCell, ops::Range};
use gpui::{App, SharedString}; use gpui::{App, SharedString};
use ropey::Rope;
use tree_sitter::{InputEdit, Point}; use tree_sitter::{InputEdit, Point};
use crate::input::RopeExt as _;
use crate::{highlighter::SyntaxHighlighter, input::marker::Marker}; use crate::{highlighter::SyntaxHighlighter, input::marker::Marker};
use super::text_wrapper::TextWrapper; use super::text_wrapper::TextWrapper;
@ -161,9 +163,8 @@ impl InputMode {
pub(super) fn update_highlighter( pub(super) fn update_highlighter(
&mut self, &mut self,
selected_range: &Range<usize>, selected_range: &Range<usize>,
full_text: &SharedString, text: &Rope,
new_text: &str, new_text: &str,
text_wrapper: &TextWrapper,
force: bool, force: bool,
cx: &mut App, cx: &mut App,
) { ) {
@ -187,6 +188,10 @@ impl InputMode {
return; 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 insert a chart, this is 1.
// If backspace or delete, this is -1. // If backspace or delete, this is -1.
// If selected to delete, this is the length of the selected text. // 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 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 new_end = (selected_range.end as isize + changed_len) as usize;
// let start_pos = text_wrapper.line_column(selected_range.start); let start_pos = text.line_column(selected_range.start);
// let old_end_pos = text_wrapper.line_column(selected_range.end); let old_end_pos = text.line_column(selected_range.end);
// let new_end_pos = text_wrapper.line_column(new_end); let new_end_pos = text.line_column(new_end);
let edit = InputEdit { let edit = InputEdit {
start_byte: selected_range.start, start_byte: selected_range.start,
old_end_byte: selected_range.end, old_end_byte: selected_range.end,
new_end_byte: new_end, new_end_byte: new_end,
start_position: Point::new(0, 0), start_position: Point::new(start_pos.0, start_pos.1),
old_end_position: Point::new(0, 0), old_end_position: Point::new(old_end_pos.0, old_end_pos.1),
new_end_position: Point::new(0, 0), 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);
} }
_ => {} _ => {}
} }

View file

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

View file

@ -3,6 +3,7 @@
//! Based on the `Input` example from the `gpui` crate. //! Based on the `Input` example from the `gpui` crate.
//! https://github.com/zed-industries/zed/blob/main/crates/gpui/examples/input.rs //! https://github.com/zed-industries/zed/blob/main/crates/gpui/examples/input.rs
use gpui::Action; use gpui::Action;
use ropey::{Rope, RopeSlice};
use serde::Deserialize; use serde::Deserialize;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::cell::RefCell; use std::cell::RefCell;
@ -32,7 +33,7 @@ use super::{
}; };
use crate::input::hover_popover::DiagnosticPopover; use crate::input::hover_popover::DiagnosticPopover;
use crate::input::marker::Marker; 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}; use crate::{history::History, scroll::ScrollbarState, Root};
#[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[derive(Action, Clone, PartialEq, Eq, Deserialize)]
@ -240,7 +241,7 @@ impl Deref for LastLayout {
pub struct InputState { pub struct InputState {
pub(super) focus_handle: FocusHandle, pub(super) focus_handle: FocusHandle,
pub(super) mode: InputMode, pub(super) mode: InputMode,
pub(super) text: SharedString, pub(super) text: Rope,
pub(super) text_wrapper: TextWrapper, pub(super) text_wrapper: TextWrapper,
pub(super) history: History<Change>, pub(super) history: History<Change>,
pub(super) blink_cursor: Entity<BlinkCursor>, pub(super) blink_cursor: Entity<BlinkCursor>,
@ -647,7 +648,7 @@ impl InputState {
prev_lines_offset += line.len() + 1; 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); let new_cursor = Cursor::new(new_offset);
self.selected_range = (new_cursor..new_cursor).into(); self.selected_range = (new_cursor..new_cursor).into();
self.pause_blink_cursor(cx); self.pause_blink_cursor(cx);
@ -673,7 +674,7 @@ impl InputState {
// Ensure cursor to start when set text // Ensure cursor to start when set text
if self.mode.is_single_line() { if self.mode.is_single_line() {
self.selected_range = 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 { } else {
self.selected_range = (Cursor::new(0)..Cursor::new(0)).into(); self.selected_range = (Cursor::new(0)..Cursor::new(0)).into();
} }
@ -793,24 +794,31 @@ impl InputState {
/// Set the default value of the input field. /// Set the default value of the input field.
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self { pub fn default_value(mut self, value: impl Into<SharedString>) -> 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.text_wrapper.text = self.text.clone();
self self
} }
/// Return the value of the input field. /// Return the value of the input field.
pub fn value(&self) -> &SharedString { pub fn value(&self) -> SharedString {
&self.text SharedString::new(self.text.slice(..).as_str().unwrap_or_default())
} }
/// Return the value without mask. /// Return the value without mask.
pub fn unmask_value(&self) -> SharedString { 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. /// Return the (1-based) line and column of the cursor.
pub fn line_column(&self) -> LineColumn { 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. /// Set (1-based) line and column of the cursor.
@ -830,12 +838,11 @@ impl InputState {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if let Some(offset) = self let line_ix = line.saturating_sub(1);
.text_wrapper let column_ix = column.unwrap_or(1).saturating_sub(1);
.offset_for_line_column(line, column.unwrap_or(1)) let offset = self.text.line_column_to_byte(line_ix, column_ix);
{
self.move_to(Cursor::new(offset), window, cx); self.move_to(Cursor::new(offset), window, cx);
}
} }
/// Focus the input field. /// Focus the input field.
@ -976,7 +983,7 @@ impl InputState {
if self.mode.is_single_line() { if self.mode.is_single_line() {
return; 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); self.select_to(Cursor::new(self.next_boundary(offset)), window, cx);
} }
@ -987,7 +994,7 @@ impl InputState {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.move_to(Cursor::new(0), window, cx); 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<Self>) { pub(super) fn home(&mut self, _: &MoveHome, window: &mut Window, cx: &mut Context<Self>) {
@ -1017,7 +1024,7 @@ impl InputState {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let end = self.text.len(); let end = self.text.len_bytes();
self.move_to(Cursor::new(end), window, cx); self.move_to(Cursor::new(end), window, cx);
} }
@ -1056,7 +1063,7 @@ impl InputState {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let end = self.text.len(); let end = self.text.len_bytes();
self.select_to(Cursor::new(end), window, cx); self.select_to(Cursor::new(end), window, cx);
} }
@ -1103,7 +1110,10 @@ impl InputState {
/// Return the start offset of the previous word. /// Return the start offset of the previous word.
fn previous_start_of_word(&mut self) -> usize { fn previous_start_of_word(&mut self) -> usize {
let offset = self.selected_range.start.offset; 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) UnicodeSegmentation::split_word_bound_indices(prev_str)
.filter(|(_, s)| !s.trim_start().is_empty()) .filter(|(_, s)| !s.trim_start().is_empty())
.next_back() .next_back()
@ -1114,11 +1124,14 @@ impl InputState {
/// Return the next end offset of the next word. /// Return the next end offset of the next word.
fn next_end_of_word(&mut self) -> usize { fn next_end_of_word(&mut self) -> usize {
let offset = self.cursor().offset; 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) UnicodeSegmentation::split_word_bound_indices(next_str)
.find(|(_, s)| !s.trim_start().is_empty()) .find(|(_, s)| !s.trim_start().is_empty())
.map(|(i, s)| offset + i + s.len()) .map(|(i, s)| offset + i + s.len())
.unwrap_or(self.text.len()) .unwrap_or(self.text.len_bytes())
} }
/// Get start of line /// Get start of line
@ -1166,9 +1179,12 @@ impl InputState {
/// Get end of line /// Get end of line
fn end_of_line(&mut self, window: &mut Window, cx: &mut Context<Self>) -> usize { fn end_of_line(&mut self, window: &mut Window, cx: &mut Context<Self>) -> usize {
if self.mode.is_single_line() { 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); let offset = self.next_boundary(self.cursor().offset);
// ignore if offset is "\n" // ignore if offset is "\n"
if self if self
@ -1186,7 +1202,7 @@ impl InputState {
let line = self let line = self
.text_for_range( .text_for_range(
self.range_to_utf16(&(offset..self.text.len())), self.range_to_utf16(&(offset..self.text.len_bytes())),
&mut None, &mut None,
window, window,
cx, cx,
@ -1194,7 +1210,7 @@ impl InputState {
.unwrap_or_default() .unwrap_or_default()
.find('\n') .find('\n')
.map(|i| i + offset) .map(|i| i + offset)
.unwrap_or(self.text.len()); .unwrap_or(self.text.len_bytes());
line line
} }
@ -1293,7 +1309,7 @@ impl InputState {
) { ) {
let mut offset = self.end_of_line(window, cx); let mut offset = self.end_of_line(window, cx);
if offset == self.cursor().offset { 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( self.replace_text_in_range(
Some(self.range_to_utf16(&(self.cursor().offset..offset))), Some(self.range_to_utf16(&(self.cursor().offset..offset))),
@ -1502,7 +1518,9 @@ impl InputState {
let start_offset = self.selected_range.start; let start_offset = self.selected_range.start;
let offset = self.start_of_line_of_selection(window, cx); let offset = self.start_of_line_of_selection(window, cx);
if self 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()) .starts_with(tab_indent.as_ref())
{ {
self.replace_text_in_range( 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. /// 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<Self>) { fn move_to(&mut self, cursor: Cursor, _: &mut Window, cx: &mut Context<Self>) {
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.selected_range = (cursor..cursor).into();
self.pause_blink_cursor(cx); self.pause_blink_cursor(cx);
self.update_preferred_x_offset(cx); self.update_preferred_x_offset(cx);
@ -1752,7 +1770,7 @@ impl InputState {
_cx: &App, _cx: &App,
) -> usize { ) -> usize {
// If the text is empty, always return 0 // If the text is empty, always return 0
if self.text.is_empty() { if self.text.len_bytes() == 0 {
return 0; return 0;
} }
@ -1817,8 +1835,8 @@ impl InputState {
index += 1; index += 1;
} }
if index > self.text.len() { if index > self.text.len_bytes() {
self.text.len() self.text.len_bytes()
} else { } else {
index index
} }
@ -1851,7 +1869,7 @@ impl InputState {
/// ///
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset. /// 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<Self>) { fn select_to(&mut self, offset: Cursor, _: &mut Window, cx: &mut Context<Self>) {
let offset = offset.clamp(0, self.text.len()); let offset = offset.clamp(0, self.text.len_bytes());
if self.selection_reversed { if self.selection_reversed {
self.selected_range.start = Cursor::new(offset) self.selected_range.start = Cursor::new(offset)
} else { } else {
@ -1896,7 +1914,7 @@ impl InputState {
.unwrap_or_default(); .unwrap_or_default();
let next_text = self let next_text = self
.text_for_range( .text_for_range(
self.range_to_utf16(&(end..self.text.len())), self.range_to_utf16(&(end..self.text.len_bytes())),
&mut None, &mut None,
window, window,
cx, cx,
@ -1980,18 +1998,24 @@ impl InputState {
} }
fn previous_boundary(&self, offset: usize) -> usize { fn previous_boundary(&self, offset: usize) -> usize {
self.text let char_ix = self.text.byte_to_char(offset).saturating_sub(1);
.grapheme_indices(true) self.text.char_to_byte(char_ix)
.rev()
.find_map(|(idx, _)| (idx < offset).then_some(idx)) // self.text
.unwrap_or(0) // .grapheme_indices(true)
// .rev()
// .find_map(|(idx, _)| (idx < offset).then_some(idx))
// .unwrap_or(0)
} }
fn next_boundary(&self, offset: usize) -> usize { fn next_boundary(&self, offset: usize) -> usize {
self.text let char_ix = self.text.byte_to_char(offset) + 1;
.grapheme_indices(true) self.text.char_to_byte(char_ix).min(self.text.len_bytes())
.find_map(|(idx, _)| (idx > offset).then_some(idx))
.unwrap_or(self.text.len()) // 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. /// 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, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if self.text.is_empty() { if self.text.len_bytes() == 0 {
return; return;
} }
@ -2126,9 +2150,9 @@ impl InputState {
} }
} }
fn text_for_range_utf8(&mut self, range: impl Into<Range<usize>>) -> &str { fn text_for_range_utf8(&'_ self, range: impl Into<Range<usize>>) -> RopeSlice<'_> {
let range = self.range_from_utf16(&self.range_to_utf16(&range.into())); 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<String> { ) -> Option<String> {
let range = self.range_from_utf16(&range_utf16); let range = self.range_from_utf16(&range_utf16);
adjusted_range.replace(self.range_to_utf16(&range)); 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( fn selected_text_range(
@ -2191,10 +2215,12 @@ impl EntityInputHandler for InputState {
.or(self.marked_range.map(|range| range.into())) .or(self.marked_range.map(|range| range.into()))
.unwrap_or(self.selected_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 + new_text
+ self.text_for_range_utf8(range.end..self.text.len())) + &self
.into(); .text_for_range_utf8(range.end..self.text.len_bytes())
.to_string())
.into();
// Check if the new text is valid // Check if the new text is valid
if !self.is_valid_input(&pending_text, cx) { if !self.is_valid_input(&pending_text, cx) {
return; return;
@ -2205,18 +2231,18 @@ impl EntityInputHandler for InputState {
let new_offset = (range.start + new_text_len).min(mask_text.len()); let new_offset = (range.start + new_text_len).min(mask_text.len());
self.push_history(&range, &new_text, window, cx); 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.mode.clear_markers();
self.text_wrapper.update(&self.text, false, cx); self.text_wrapper.update(&self.text, false, cx);
self.mode 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.selected_range = (new_offset..new_offset).into();
self.marked_range.take(); self.marked_range.take();
self.update_preferred_x_offset(cx); self.update_preferred_x_offset(cx);
self.update_scroll_offset(None, cx); self.update_scroll_offset(None, cx);
self.mode.update_auto_grow(&self.text_wrapper); 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(); cx.notify();
} }
@ -2238,20 +2264,22 @@ impl EntityInputHandler for InputState {
.map(|range_utf16| self.range_from_utf16(range_utf16)) .map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.marked_range.map(|range| range.into())) .or(self.marked_range.map(|range| range.into()))
.unwrap_or(self.selected_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 + new_text
+ self.text_for_range_utf8(range.end..self.text.len())) + &self
.into(); .text_for_range_utf8(range.end..self.text.len_bytes())
.to_string())
.into();
if !self.is_valid_input(&pending_text, cx) { if !self.is_valid_input(&pending_text, cx) {
return; return;
} }
self.push_history(&range, new_text, window, cx); self.push_history(&range, new_text, window, cx);
self.text = pending_text; self.text = Rope::from_str(&pending_text);
self.mode.clear_markers(); self.mode.clear_markers();
self.text_wrapper.update(&self.text, false, cx); self.text_wrapper.update(&self.text, false, cx);
self.mode 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() { if new_text.is_empty() {
// Cancel selection, when cancel IME input. // Cancel selection, when cancel IME input.
self.selected_range = (range.start..range.start).into(); self.selected_range = (range.start..range.start).into();
@ -2266,7 +2294,7 @@ impl EntityInputHandler for InputState {
.into(); .into();
} }
self.mode.update_auto_grow(&self.text_wrapper); 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(); cx.notify();
} }
@ -2357,7 +2385,7 @@ impl Render for InputState {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.text_wrapper.update(&self.text, false, cx); self.text_wrapper.update(&self.text, false, cx);
self.mode self.mode
.update_highlighter(&(0..0), &self.text, "", &self.text_wrapper, false, cx); .update_highlighter(&(0..0), &self.text, "", false, cx);
div() div()
.id("input-state") .id("input-state")

View file

@ -176,7 +176,7 @@ impl RenderOnce for TextInput {
let suffix = self.suffix; let suffix = self.suffix;
let show_clear_button = self.cleanable let show_clear_button = self.cleanable
&& !state.loading && !state.loading
&& !state.text.is_empty() && state.text.len_bytes() > 0
&& state.mode.is_single_line(); && state.mode.is_single_line();
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button; let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;

View file

@ -1,7 +1,7 @@
use std::ops::Range; use std::ops::Range;
use crate::input::LineColumn; use gpui::{App, Font, LineFragment, Pixels};
use gpui::{App, Font, LineFragment, Pixels, SharedString}; use ropey::Rope;
#[allow(unused)] #[allow(unused)]
pub(super) struct LineWrap { pub(super) struct LineWrap {
@ -15,11 +15,11 @@ pub(super) struct LineWrap {
pub(super) range: Range<usize>, pub(super) range: Range<usize>,
} }
/// 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 /// After use lines to calculate the scroll size of the TextArea
pub(super) struct TextWrapper { pub(super) struct TextWrapper {
pub(super) text: SharedString, pub(super) text: Rope,
/// The wrapped lines, value is start and end index of the line. /// The wrapped lines, value is start and end index of the line.
pub(super) wrapped_lines: Vec<Range<usize>>, pub(super) wrapped_lines: Vec<Range<usize>>,
/// The lines by split \n /// The lines by split \n
@ -34,7 +34,7 @@ pub(super) struct TextWrapper {
impl TextWrapper { impl TextWrapper {
pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self { pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
Self { Self {
text: SharedString::default(), text: Rope::new(),
font, font,
font_size, font_size,
wrap_width, wrap_width,
@ -57,7 +57,7 @@ impl TextWrapper {
/// Update the text wrapper and recalculate the wrapped lines. /// Update the text wrapper and recalculate the wrapped lines.
/// ///
/// If the `text` is the same as the current text, do nothing. /// 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 { if &self.text == text && !force {
return; return;
} }
@ -69,7 +69,9 @@ impl TextWrapper {
.text_system() .text_system()
.line_wrapper(self.font.clone(), self.font_size); .line_wrapper(self.font.clone(), self.font_size);
let mut prev_line_ix = 0; 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 line_wraps = vec![];
let mut prev_boundary_ix = 0; let mut prev_boundary_ix = 0;
@ -100,47 +102,4 @@ impl TextWrapper {
self.wrapped_lines = wrapped_lines; self.wrapped_lines = wrapped_lines;
self.lines = 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<usize> {
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)
}
} }

View file

@ -7,6 +7,7 @@ use gpui::{
StatefulInteractiveElement, Styled, StyledImage as _, Window, StatefulInteractiveElement, Styled, StyledImage as _, Window,
}; };
use markdown::mdast; use markdown::mdast;
use ropey::Rope;
use crate::{ use crate::{
h_flex, h_flex,
@ -291,7 +292,7 @@ impl CodeBlock {
let mut styles = vec![]; let mut styles = vec![];
if let Some(lang) = &lang { if let Some(lang) = &lang {
let mut highlighter = SyntaxHighlighter::new(&lang, cx); 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); styles = highlighter.styles(&(0..code.len()), &theme);
}; };

View file

@ -71,7 +71,7 @@ impl Main {
move |state, event: &InputEvent, _, cx| { move |state, event: &InputEvent, _, cx| {
if let InputEvent::PressEnter { .. } = event { if let InputEvent::PressEnter { .. } = event {
let url = state.read(cx).value(); let url = state.read(cx).value();
webview.read(cx).browser().load_url(url); webview.read(cx).browser().load_url(&url);
} }
} }
}) })

View file

@ -318,7 +318,7 @@ impl BrowserHandler for WebViewHandler {
let callback = callback.clone(); let callback = callback.clone();
let input_state = input_state.clone(); let input_state = input_state.clone();
move |_, _, cx| { move |_, _, cx| {
callback.continue_(true, Some(input_state.read(cx).value())); callback.continue_(true, Some(&input_state.read(cx).value()));
true true
} }
}) })