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

View file

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

View file

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

View file

@ -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<SharedString, Query>,
parser: Parser,
old_tree: Option<Tree>,
text: SharedString,
text: Rope,
locals_pattern_index: usize,
highlights_pattern_index: usize,
@ -39,6 +41,16 @@ pub struct SyntaxHighlighter {
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)]
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<InputEdit>, full_text: &SharedString, cx: &App) {
if &self.text == full_text {
pub fn update(&mut self, edit: Option<InputEdit>, 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<usize>, 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<SharedString>,
query: &'a Query,
query_match: &QueryMatch<'a, 'a>,
source: &'a [u8],
) -> (Option<SharedString>, Option<Node<'a>>, 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<SharedString> = 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);
}
}

View file

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

View file

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

View file

@ -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::*;

View file

@ -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<usize>,
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);
}
_ => {}
}

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.
//! 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<Change>,
pub(super) blink_cursor: Entity<BlinkCursor>,
@ -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<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
}
/// 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<Self>,
) {
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>,
) {
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>) {
@ -1017,7 +1024,7 @@ impl InputState {
window: &mut Window,
cx: &mut Context<Self>,
) {
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<Self>,
) {
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<Self>) -> 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<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.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<Self>) {
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<Self>,
) {
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<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()));
&self.text[range]
self.text.byte_slice(range)
}
}
@ -2142,7 +2166,7 @@ impl EntityInputHandler for InputState {
) -> Option<String> {
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<Self>) -> 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")

View file

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

View file

@ -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<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
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<Range<usize>>,
/// 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<Pixels>) -> 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<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,
};
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);
};

View file

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

View file

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