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:
parent
19be9b8d96
commit
344e4407ff
15 changed files with 235 additions and 189 deletions
25
Cargo.lock
generated
25
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
23
crates/ui/src/input/rope_ext.rs
Normal file
23
crates/ui/src/input/rope_ext.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue