diff --git a/Cargo.lock b/Cargo.lock
index 4213401a..ed9deeed 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -709,7 +709,7 @@ dependencies = [
"bitflags 2.9.1",
"cexpr",
"clang-sys",
- "itertools 0.12.1",
+ "itertools 0.11.0",
"lazy_static",
"lazycell",
"log",
@@ -732,7 +732,7 @@ dependencies = [
"bitflags 2.9.1",
"cexpr",
"clang-sys",
- "itertools 0.13.0",
+ "itertools 0.11.0",
"log",
"prettyplease",
"proc-macro2",
@@ -797,7 +797,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.6.0"
-source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
+source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"ash",
"ash-window",
@@ -830,7 +830,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.3.0"
-source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
+source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"proc-macro2",
"quote",
@@ -840,7 +840,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.2.0"
-source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
+source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -1401,7 +1401,7 @@ dependencies = [
[[package]]
name = "collections"
version = "0.1.0"
-source = "git+https://github.com/zed-industries/zed.git#a852bcc09410b47dcabbe9b089725777024d125e"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
"indexmap 2.10.0",
"rustc-hash 2.1.1",
@@ -1870,7 +1870,7 @@ dependencies = [
[[package]]
name = "derive_refineable"
version = "0.1.0"
-source = "git+https://github.com/zed-industries/zed.git#a852bcc09410b47dcabbe9b089725777024d125e"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
"proc-macro2",
"quote",
@@ -3049,7 +3049,7 @@ dependencies = [
[[package]]
name = "gpui"
version = "0.1.0"
-source = "git+https://github.com/zed-industries/zed.git#a852bcc09410b47dcabbe9b089725777024d125e"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
"anyhow",
"as-raw-xcb-connection",
@@ -3101,7 +3101,7 @@ dependencies = [
"pathfinder_geometry",
"postage",
"profiling",
- "rand 0.8.5",
+ "rand 0.9.2",
"raw-window-handle",
"refineable",
"resvg",
@@ -3160,7 +3160,7 @@ dependencies = [
"once_cell",
"paste",
"regex",
- "ropey",
+ "rope",
"rust-i18n",
"rust_decimal",
"schemars",
@@ -3232,7 +3232,7 @@ dependencies = [
[[package]]
name = "gpui_macros"
version = "0.1.0"
-source = "git+https://github.com/zed-industries/zed.git#a852bcc09410b47dcabbe9b089725777024d125e"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -3501,7 +3501,7 @@ dependencies = [
[[package]]
name = "http_client"
version = "0.1.0"
-source = "git+https://github.com/zed-industries/zed.git#a852bcc09410b47dcabbe9b089725777024d125e"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
"anyhow",
"bytes",
@@ -3521,7 +3521,7 @@ dependencies = [
[[package]]
name = "http_client_tls"
version = "0.1.0"
-source = "git+https://github.com/zed-industries/zed.git#a852bcc09410b47dcabbe9b089725777024d125e"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
"rustls",
"rustls-platform-verifier",
@@ -3625,7 +3625,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
- "windows-core 0.61.2",
+ "windows-core 0.58.0",
]
[[package]]
@@ -4466,7 +4466,7 @@ dependencies = [
[[package]]
name = "media"
version = "0.1.0"
-source = "git+https://github.com/zed-industries/zed.git#a852bcc09410b47dcabbe9b089725777024d125e"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
"anyhow",
"bindgen 0.71.1",
@@ -4902,7 +4902,7 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
dependencies = [
- "proc-macro-crate 3.3.0",
+ "proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"syn 2.0.105",
@@ -6380,7 +6380,7 @@ dependencies = [
[[package]]
name = "refineable"
version = "0.1.0"
-source = "git+https://github.com/zed-industries/zed.git#a852bcc09410b47dcabbe9b089725777024d125e"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
"derive_refineable",
"workspace-hack",
@@ -6533,7 +6533,7 @@ dependencies = [
[[package]]
name = "reqwest_client"
version = "0.1.0"
-source = "git+https://github.com/zed-industries/zed.git#a852bcc09410b47dcabbe9b089725777024d125e"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
"anyhow",
"bytes",
@@ -6544,7 +6544,6 @@ dependencies = [
"regex",
"reqwest 0.12.15",
"serde",
- "smol 2.0.2",
"tokio",
"workspace-hack",
]
@@ -6616,13 +6615,18 @@ dependencies = [
]
[[package]]
-name = "ropey"
-version = "1.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5"
+name = "rope"
+version = "0.1.0"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
+ "arrayvec",
+ "log",
+ "rayon",
"smallvec",
- "str_indices",
+ "sum_tree",
+ "unicode-segmentation",
+ "util",
+ "workspace-hack",
]
[[package]]
@@ -7120,7 +7124,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]]
name = "semantic_version"
version = "0.1.0"
-source = "git+https://github.com/zed-industries/zed.git#a852bcc09410b47dcabbe9b089725777024d125e"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
"anyhow",
"serde",
@@ -7645,12 +7649,6 @@ 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"
@@ -7749,7 +7747,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "sum_tree"
version = "0.1.0"
-source = "git+https://github.com/zed-industries/zed.git#a852bcc09410b47dcabbe9b089725777024d125e"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
"arrayvec",
"log",
@@ -9059,7 +9057,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "util"
version = "0.1.0"
-source = "git+https://github.com/zed-industries/zed.git#a852bcc09410b47dcabbe9b089725777024d125e"
+source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
dependencies = [
"anyhow",
"async-fs 2.1.3",
diff --git a/Cargo.toml b/Cargo.toml
index 7a08103e..5ee4a4b1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,7 @@ resolver = "2"
[workspace.dependencies]
gpui = { git = "https://github.com/zed-industries/zed.git" }
gpui_macros = { git = "https://github.com/zed-industries/zed.git" }
+rope = { git = "https://github.com/zed-industries/zed.git" }
reqwest_client = { git = "https://github.com/zed-industries/zed.git" }
sum_tree = { git = "https://github.com/zed-industries/zed.git" }
gpui-component = { path = "crates/ui" }
diff --git a/README.md b/README.md
index fa5d030b..dc19189b 100644
--- a/README.md
+++ b/README.md
@@ -67,29 +67,30 @@ Check out [CONTRIBUTING.md](CONTRIBUTING.md) for more details.
## Compare to others
-| Features | GPUI Component | [Iced] | [egui] | [QT 6] |
-| --------------------- | ------------------------------ | --------- | --------------------- | ------------------------------ |
-| Language | Rust | Rust | Rust | C++/QML |
-| Core Render | GPUI | wgpu | wgpu | QT |
-| License | Apache 2.0 | MIT | MIT/Apache 2.0 | Commercial |
-| Min Binary Size [^1] | 12MB | 11MB | 5M | 20MB [^2] |
-| Cross-Platform | Yes | Yes | Yes | Yes |
-| Documentation | No | Simple | Simple | Good |
-| Web | No | Yes | Yes | Yes |
-| UI Style | Modern | Basic | Basic | Basic |
-| CJK Support | Yes | Yes | Bad | Yes |
-| Chart | Yes | No | No | Yes |
-| Table (Large dataset) | Yes
(Virtual Rows, Columns) | No | Yes
(Virtual Rows) | Yes
(Virtual Rows, Columns) |
-| Table Column Resize | Yes | No | Yes | Yes |
-| CodeEditor | Simple | Simple | Simple | Basic API |
-| Dock Layout | Yes | Yes | Yes | Yes |
-| Syntax Highlight | [Tree Sitter] | [Syntect] | [Syntect] | [QSyntaxHighlighter] |
-| Markdown Rendering | Yes | Yes | Basic | No |
-| Markdown mix HTML | Yes | No | No | No |
-| HTML Rendering | Basic | No | No | Yes |
-| Text Selection | TextView | No | Any Label | No |
-| Themes | Yes | No | No | No |
-| I18n | Yes | Yes | Yes | Yes |
+| Features | GPUI Component | [Iced] | [egui] | [QT 6] |
+| --------------------- | ------------------------------ | ----------- | --------------------- | ------------------------------ |
+| Language | Rust | Rust | Rust | C++/QML |
+| Core Render | GPUI | wgpu | wgpu | QT |
+| License | Apache 2.0 | MIT | MIT/Apache 2.0 | Commercial |
+| Min Binary Size [^1] | 12MB | 11MB | 5M | 20MB [^2] |
+| Cross-Platform | Yes | Yes | Yes | Yes |
+| Documentation | No | Simple | Simple | Good |
+| Web | No | Yes | Yes | Yes |
+| UI Style | Modern | Basic | Basic | Basic |
+| CJK Support | Yes | Yes | Bad | Yes |
+| Chart | Yes | No | No | Yes |
+| Table (Large dataset) | Yes
(Virtual Rows, Columns) | No | Yes
(Virtual Rows) | Yes
(Virtual Rows, Columns) |
+| Table Column Resize | Yes | No | Yes | Yes |
+| Text base | Rope | String [^3] | trait TextBuffer [^4] | [QTextDocument] |
+| CodeEditor | Simple | Simple | Simple | Basic API |
+| Dock Layout | Yes | Yes | Yes | Yes |
+| Syntax Highlight | [Tree Sitter] | [Syntect] | [Syntect] | [QSyntaxHighlighter] |
+| Markdown Rendering | Yes | Yes | Basic | No |
+| Markdown mix HTML | Yes | No | No | No |
+| HTML Rendering | Basic | No | No | Yes |
+| Text Selection | TextView | No | Any Label | No |
+| Themes | Yes | No | No | No |
+| I18n | Yes | Yes | Yes | Yes |
> Please submit an issue or PR if any mistakes or outdated are found.
@@ -99,11 +100,16 @@ Check out [CONTRIBUTING.md](CONTRIBUTING.md) for more details.
[Tree Sitter]: https://tree-sitter.github.io/tree-sitter/
[Syntect]: https://github.com/trishume/syntect
[QSyntaxHighlighter]: https://doc.qt.io/qt-6/qsyntaxhighlighter.html
+[QTextDocument]: https://doc.qt.io/qt-6/qtextdocument.html
[^1]: Release builds by use simple hello world example.
[^2]: [Reducing Binary Size of Qt Applications](https://www.qt.io/blog/reducing-binary-size-of-qt-applications-part-3-more-platforms)
+[^3]: Iced TextInput: https://github.com/iced-rs/iced/blob/c65bfd1afb399a74eddb3f66853f3cc385a08826/widget/src/text_input.rs#L111
+
+[^4]: egui TextBuffer: https://github.com/emilk/egui/blob/0a81372cfd3a4deda640acdecbbaf24bf78bb6a2/crates/egui/src/widgets/text_edit/text_buffer.rs#L20
+
## License
Apache-2.0
diff --git a/crates/story/examples/code-editor.rs b/crates/story/examples/code-editor.rs
index 5a82ad7c..cc0fa154 100644
--- a/crates/story/examples/code-editor.rs
+++ b/crates/story/examples/code-editor.rs
@@ -331,8 +331,9 @@ fn main() {
init(cx);
cx.activate(true);
- story::create_new_window(
+ story::create_new_window_with_size(
"Code Editor",
+ Some(size(px(1200.), px(960.))),
|window, cx| cx.new(|cx| Example::new(name, window, cx)),
cx,
);
diff --git a/crates/story/examples/large-text.rs b/crates/story/examples/large-text.rs
new file mode 100644
index 00000000..28ae23f5
--- /dev/null
+++ b/crates/story/examples/large-text.rs
@@ -0,0 +1,155 @@
+use gpui::*;
+use gpui_component::{
+ button::{Button, ButtonVariants as _},
+ h_flex,
+ input::{InputEvent, InputState, TabSize, TextInput},
+ v_flex, ActiveTheme, ContextModal, Selectable, Sizable,
+};
+use story::Assets;
+
+pub struct Example {
+ editor: Entity,
+ go_to_line_state: Entity,
+ soft_wrap: bool,
+ _subscribes: Vec,
+}
+
+impl Example {
+ pub fn new(window: &mut Window, cx: &mut Context) -> Self {
+ // 10K lines
+ let text = "这是一个中文演示段落,用于展示更多的 [Markdown GFM] 内容。您可以在此尝试使用使用**粗体**、*斜体*和`代码`等样式。これは日本語のデモ段落です。Markdown の多言語サポートを示すためのテキストが含まれています。例えば、、**ボールド**、_イタリック_、および`コード`のスタイルなどを試すことができます。\n".repeat(10000);
+
+ let editor = cx.new(|cx| {
+ InputState::new(window, cx)
+ .multi_line()
+ .line_number(true)
+ .tab_size(TabSize {
+ tab_size: 4,
+ hard_tabs: false,
+ })
+ .soft_wrap(false)
+ .placeholder("Enter your code here...")
+ .default_value(text)
+ });
+ let go_to_line_state = cx.new(|cx| InputState::new(window, cx));
+
+ let _subscribes = vec![cx.subscribe(&editor, |_, _, _: &InputEvent, cx| {
+ cx.notify();
+ })];
+
+ Self {
+ editor,
+ go_to_line_state,
+ soft_wrap: false,
+ _subscribes,
+ }
+ }
+
+ fn view(window: &mut Window, cx: &mut App) -> Entity {
+ cx.new(|cx| Self::new(window, cx))
+ }
+
+ fn go_to_line(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) {
+ let editor = self.editor.clone();
+ let input_state = self.go_to_line_state.clone();
+
+ window.open_modal(cx, move |modal, window, cx| {
+ input_state.update(cx, |state, cx| {
+ state.set_placeholder(format!("{}", editor.read(cx).line_column()), window, cx);
+ state.focus(window, cx);
+ });
+
+ modal
+ .title("Go to line")
+ .child(TextInput::new(&input_state))
+ .confirm()
+ .on_ok({
+ let editor = editor.clone();
+ let input_state = input_state.clone();
+ move |_, window, cx| {
+ let query = input_state.read(cx).value();
+ let mut parts = query
+ .split(':')
+ .map(|s| s.trim().parse::().ok())
+ .collect::>()
+ .into_iter();
+ let Some(line) = parts.next().and_then(|l| l) else {
+ return false;
+ };
+ let column = parts.next().and_then(|c| c);
+
+ editor.update(cx, |state, cx| {
+ state.go_to_line(line, column, window, cx);
+ });
+
+ true
+ }
+ })
+ });
+ }
+
+ fn toggle_soft_wrap(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) {
+ self.soft_wrap = !self.soft_wrap;
+ self.editor.update(cx, |state, cx| {
+ state.set_soft_wrap(self.soft_wrap, window, cx);
+ });
+ cx.notify();
+ }
+}
+
+impl Render for Example {
+ fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
+ v_flex().size_full().child(
+ v_flex()
+ .id("source")
+ .w_full()
+ .flex_1()
+ .child(
+ TextInput::new(&self.editor)
+ .bordered(false)
+ .h_full()
+ .focus_bordered(false),
+ )
+ .child(
+ h_flex()
+ .justify_between()
+ .text_sm()
+ .bg(cx.theme().secondary)
+ .py_1p5()
+ .px_4()
+ .border_t_1()
+ .border_color(cx.theme().border)
+ .text_color(cx.theme().muted_foreground)
+ .child(h_flex().gap_3().child({
+ Button::new("soft-wrap")
+ .ghost()
+ .xsmall()
+ .label("Soft Wrap")
+ .selected(self.soft_wrap)
+ .on_click(cx.listener(Self::toggle_soft_wrap))
+ }))
+ .child({
+ let loc = self.editor.read(cx).line_column();
+ let cursor = self.editor.read(cx).cursor();
+
+ Button::new("line-column")
+ .ghost()
+ .xsmall()
+ .label(format!("{} ({} c)", loc, cursor.offset()))
+ .on_click(cx.listener(Self::go_to_line))
+ }),
+ ),
+ )
+ }
+}
+
+fn main() {
+ let app = Application::new().with_assets(Assets);
+
+ app.run(move |cx| {
+ story::init(cx);
+ cx.activate(true);
+
+ story::create_new_window("Large Text Editor", Example::view, cx);
+ });
+}
diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs
index e7e3e6a5..9d09246a 100644
--- a/crates/story/src/lib.rs
+++ b/crates/story/src/lib.rs
@@ -52,9 +52,9 @@ pub use assets::Assets;
use gpui::{
actions, div, prelude::FluentBuilder as _, px, rems, size, Action, AnyElement, AnyView, App,
AppContext, Bounds, Context, Div, Entity, EventEmitter, Focusable, Global, Hsla,
- InteractiveElement, IntoElement, KeyBinding, Menu, MenuItem, ParentElement, Render, RenderOnce,
- SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window, WindowBounds,
- WindowKind, WindowOptions,
+ InteractiveElement, IntoElement, KeyBinding, Menu, MenuItem, ParentElement, Pixels, Render,
+ RenderOnce, SharedString, Size, StatefulInteractiveElement, StyleRefinement, Styled, Window,
+ WindowBounds, WindowKind, WindowOptions,
};
pub use accordion_story::AccordionStory;
@@ -166,7 +166,19 @@ where
E: Into,
F: FnOnce(&mut Window, &mut App) -> E + Send + 'static,
{
- let mut window_size = size(px(1600.0), px(1200.0));
+ create_new_window_with_size(title, None, crate_view_fn, cx);
+}
+
+pub fn create_new_window_with_size(
+ title: &str,
+ window_size: Option>,
+ crate_view_fn: F,
+ cx: &mut App,
+) where
+ E: Into,
+ F: FnOnce(&mut Window, &mut App) -> E + Send + 'static,
+{
+ let mut window_size = window_size.unwrap_or(size(px(1600.0), px(1200.0)));
if let Some(display) = cx.primary_display() {
let display_size = display.bounds().size;
window_size.width = window_size.width.min(display_size.width * 0.85);
diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml
index ffa02480..4e3f98f0 100644
--- a/crates/ui/Cargo.toml
+++ b/crates/ui/Cargo.toml
@@ -51,6 +51,7 @@ tree-sitter-languages = [
gpui.workspace = true
sum_tree.workspace = true
gpui_macros.workspace = true
+rope.workspace = true
gpui-component-macros.workspace = true
rust-i18n.workspace = true
schemars.workspace = true
@@ -70,7 +71,6 @@ paste = "1"
regex = "1"
unicode-segmentation = "1.12.0"
uuid = "1.10"
-ropey = "1.6.1"
# WebView
wry = { version = "0.48.0", optional = true }
diff --git a/crates/ui/src/highlighter/highlighter.rs b/crates/ui/src/highlighter/highlighter.rs
index b8eb1104..746edf2f 100644
--- a/crates/ui/src/highlighter/highlighter.rs
+++ b/crates/ui/src/highlighter/highlighter.rs
@@ -1,13 +1,13 @@
use super::HighlightTheme;
-use crate::highlighter::LanguageRegistry;
+use crate::{highlighter::LanguageRegistry, input::RopeExt as _};
use anyhow::{anyhow, Context, Result};
use gpui::{App, HighlightStyle, SharedString};
-use ropey::Rope;
+
+use rope::Rope;
use std::{
collections::{BTreeSet, HashMap},
ops::Range,
- slice::Chunks,
usize,
};
use sum_tree::{Bias, SumTree};
@@ -42,12 +42,20 @@ pub struct SyntaxHighlighter {
}
struct TextProvider<'a>(&'a Rope);
+struct ByteChunks<'a>(rope::Chunks<'a>);
impl<'a> tree_sitter::TextProvider<&'a [u8]> for TextProvider<'a> {
- type I = Chunks<'a, u8>;
+ type I = ByteChunks<'a>;
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)
+ ByteChunks(self.0.chunks_in_range(node.byte_range()))
+ }
+}
+
+impl<'a> Iterator for ByteChunks<'a> {
+ type Item = &'a [u8];
+
+ fn next(&mut self) -> Option {
+ self.0.next().map(str::as_bytes)
}
}
@@ -283,20 +291,20 @@ impl SyntaxHighlighter {
}
pub fn is_empty(&self) -> bool {
- self.text.len_bytes() == 0
+ self.text.len() == 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, text: &Rope, cx: &App) {
- if &self.text == text {
+ if self.text.eq(text) {
return;
}
let edit = edit.unwrap_or(InputEdit {
start_byte: 0,
old_end_byte: 0,
- new_end_byte: text.len_bytes(),
+ new_end_byte: text.len(),
start_position: Point::new(0, 0),
old_end_position: Point::new(0, 0),
new_end_position: Point::new(0, 0),
@@ -308,14 +316,11 @@ impl SyntaxHighlighter {
.unwrap_or(self.parser.parse("", None).unwrap());
old_tree.edit(&edit);
+ let mut chunks = text.chunks();
let new_tree = self.parser.parse_with_options(
- &mut |offset, _| {
- if offset >= text.len_bytes() {
- ""
- } else {
- let (chunk, chunk_byte_ix, _, _) = text.chunk_at_byte(offset);
- &chunk[offset - chunk_byte_ix..]
- }
+ &mut move |offset, _| {
+ chunks.seek(offset);
+ chunks.next().unwrap_or("").as_bytes()
},
Some(&old_tree),
None,
@@ -437,10 +442,13 @@ impl SyntaxHighlighter {
return cache;
};
- let content = self.text.byte_slice(node.start_byte()..node.end_byte());
- if content.len_bytes() == 0 {
+ let content = self.text.slice(node.byte_range());
+ if content.len() == 0 {
return cache;
};
+ // FIXME: Avoid to_string.
+ let content = content.to_string();
+
let Some(config) = LanguageRegistry::global(cx).language(injection_language) else {
return cache;
};
@@ -449,7 +457,7 @@ impl SyntaxHighlighter {
return cache;
}
- let source = content.as_str().unwrap_or_default().as_bytes();
+ let source = content.as_bytes();
let Some(tree) = parser.parse(source, None) else {
return cache;
};
diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs
index 943b8ea4..b39259bc 100644
--- a/crates/ui/src/input/element.rs
+++ b/crates/ui/src/input/element.rs
@@ -9,7 +9,9 @@ use gpui::{
use smallvec::SmallVec;
use crate::{
- highlighter::SyntaxHighlighter, input::blink_cursor::CURSOR_WIDTH, ActiveTheme as _, Root,
+ highlighter::SyntaxHighlighter,
+ input::{blink_cursor::CURSOR_WIDTH, RopeExt as _},
+ ActiveTheme as _, Root,
};
use super::{mode::InputMode, InputState, LastLayout};
@@ -391,9 +393,9 @@ impl TextElement {
let mut skipped_offset = 0;
let mut styles = vec![];
- // The Rope line has includes `\n` and `\r`.
for (ix, line) in state.text.lines().enumerate() {
- let line_len = line.len_bytes();
+ // +1 for `\n`
+ let line_len = line.len() + 1;
if ix < visible_range.start {
offset += line_len;
skipped_offset = offset;
@@ -547,7 +549,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.len_bytes() == 0;
+ let is_empty = text.len() == 0;
let placeholder = self.placeholder.clone();
let style = window.text_style();
let font_size = style.font_size.to_pixels(window.rem_size());
@@ -556,7 +558,7 @@ impl Element for TextElement {
let (display_text, text_color) = if is_empty {
(placeholder, cx.theme().muted_foreground)
} else if state.masked {
- ("*".repeat(text.len_chars()).into(), cx.theme().foreground)
+ ("*".repeat(text.chars_count()).into(), cx.theme().foreground)
} else {
(text.to_string().into(), cx.theme().foreground)
};
@@ -564,25 +566,21 @@ impl Element for TextElement {
let text_style = window.text_style();
// Calculate the width of the line numbers
- let empty_line_number = window
- .text_system()
- .shape_text(
- "++++".into(),
- font_size,
- &[TextRun {
- len: 4,
- font: style.font(),
- color: gpui::black(),
- background_color: None,
- underline: None,
- strikethrough: None,
- }],
- None,
- None,
- )
- .unwrap();
+ let empty_line_number = window.text_system().shape_line(
+ "++++".into(),
+ font_size,
+ &[TextRun {
+ len: 4,
+ font: style.font(),
+ color: gpui::black(),
+ background_color: None,
+ underline: None,
+ strikethrough: None,
+ }],
+ None,
+ );
let line_number_width = if state.mode.line_number() {
- empty_line_number.last().unwrap().width() + LINE_NUMBER_RIGHT_MARGIN
+ empty_line_number.width + LINE_NUMBER_RIGHT_MARGIN
} else {
px(0.)
};
diff --git a/crates/ui/src/input/marker.rs b/crates/ui/src/input/marker.rs
index c2fba985..eb0874aa 100644
--- a/crates/ui/src/input/marker.rs
+++ b/crates/ui/src/input/marker.rs
@@ -44,7 +44,7 @@ impl Marker {
return;
};
- let start_line_str = state.text.byte_slice(start_line.range.clone());
+ let start_line_str = state.text.slice(start_line.range.clone());
let Some(end_line) = state
.text_wrapper
@@ -53,7 +53,7 @@ impl Marker {
else {
return;
};
- let end_line_str = state.text.byte_slice(end_line.range.clone());
+ let end_line_str = state.text.slice(end_line.range.clone());
let start_byte = start_line.range.start
+ start_line_str
diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs
index 0a02d6c1..f9ea1c57 100644
--- a/crates/ui/src/input/mod.rs
+++ b/crates/ui/src/input/mod.rs
@@ -21,6 +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 rope_ext::*;
pub use state::*;
pub use text_input::*;
diff --git a/crates/ui/src/input/mode.rs b/crates/ui/src/input/mode.rs
index 168b284b..a13be6a1 100644
--- a/crates/ui/src/input/mode.rs
+++ b/crates/ui/src/input/mode.rs
@@ -2,10 +2,9 @@ use std::rc::Rc;
use std::{cell::RefCell, ops::Range};
use gpui::{App, SharedString};
-use ropey::Rope;
+use rope::Rope;
use tree_sitter::{InputEdit, Point};
-use crate::input::RopeExt as _;
use crate::{highlighter::SyntaxHighlighter, input::marker::Marker};
use super::text_wrapper::TextWrapper;
@@ -190,7 +189,7 @@ impl InputMode {
// 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());
+ selected_range.end = selected_range.end.min(text.len());
// If insert a chart, this is 1.
// If backspace or delete, this is -1.
@@ -199,17 +198,23 @@ 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.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 start_pos = text.offset_to_point(selected_range.start);
+ let old_end_pos = text.offset_to_point(selected_range.end);
+ let new_end_pos = text.offset_to_point(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(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),
+ start_position: Point::new(start_pos.row as usize, start_pos.column as usize),
+ old_end_position: Point::new(
+ old_end_pos.row as usize,
+ old_end_pos.column as usize,
+ ),
+ new_end_position: Point::new(
+ new_end_pos.row as usize,
+ new_end_pos.column as usize,
+ ),
};
highlighter.update(Some(edit), text, cx);
diff --git a/crates/ui/src/input/rope_ext.rs b/crates/ui/src/input/rope_ext.rs
index e334af92..44cf111c 100644
--- a/crates/ui/src/input/rope_ext.rs
+++ b/crates/ui/src/input/rope_ext.rs
@@ -1,40 +1,132 @@
-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.
+use rope::{Point, Rope};
+
+/// An extension trait for `Rope` to provide additional utility methods.
+pub trait RopeExt {
+ /// Get the line at the given row index, including the `\r` at the end, but not `\n`.
///
- /// If the `offset` is out of bounds, it returns (0, 0).
- fn line_column(&self, byte_offset: usize) -> (usize, usize);
- /// Get the byte offset (0-based) from the line, column (0-based).
+ /// Return empty rope if the row is out of bounds.
+ fn line(&self, row: usize) -> Rope;
+
+ /// Return the number of lines in the rope.
+ fn lines_len(&self) -> usize;
+
+ /// Return the lines iterator.
///
- /// Return the last line, if line is out of bounds.
- /// Return the end column of line, if the column is out of bounds.
- fn line_column_to_byte(&self, line_ix: usize, column_ix: usize) -> usize;
+ /// Each line is including the `\n` at the end, but not `\n`.
+ fn lines(&self) -> impl Iterator- ;
+
+ /// Check is equal to another rope.
+ fn eq(&self, other: &Rope) -> bool;
+
+ /// Total number of characters in the rope.
+ fn chars_count(&self) -> usize;
+
+ /// Get char at the given offset (byte).
+ ///
+ /// If the offset is in the middle of a multi-byte character will panic.
+ ///
+ /// If the offset is out of bounds, return None.
+ fn char_at(&self, offset: usize) -> Option;
}
-impl RopeExt for ropey::Rope {
- fn line_column(&self, offset: usize) -> (usize, usize) {
- let Ok(line_ix) = self.try_byte_to_line(offset) else {
- return (0, 0);
- };
-
- let line = self.line(line_ix);
- let line_start_byte = self.line_to_byte(line_ix);
- let line_offset = offset.saturating_sub(line_start_byte);
-
- let column_ix = line
- .try_byte_to_char(line_offset)
- .unwrap_or(line.len_chars());
-
- (line_ix, column_ix)
+impl RopeExt for Rope {
+ fn line(&self, row: usize) -> Rope {
+ let row = row as u32;
+ let start = self.point_to_offset(Point::new(row, 0));
+ let end = start + self.line_len(row) as usize;
+ self.slice(start..end)
}
- fn line_column_to_byte(&self, line_ix: usize, column_ix: usize) -> usize {
- let line_ix = self.len_lines().saturating_sub(1).min(line_ix);
- let line = self.line(line_ix);
+ fn lines_len(&self) -> usize {
+ self.max_point().row as usize + 1
+ }
- self.line_to_byte(line_ix)
- + line
- .try_char_to_byte(column_ix)
- .unwrap_or(line.len_bytes().saturating_sub(1))
+ fn lines(&self) -> impl Iterator
- {
+ (0..self.lines_len()).map(move |row| self.line(row))
+ }
+
+ fn eq(&self, other: &Rope) -> bool {
+ self.summary() == other.summary()
+ }
+
+ fn chars_count(&self) -> usize {
+ self.chars().count()
+ }
+
+ fn char_at(&self, offset: usize) -> Option {
+ if offset > self.len() {
+ return None;
+ }
+
+ self.slice(offset..self.len()).chars().next()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use rope::Rope;
+
+ use crate::input::RopeExt as _;
+
+ #[test]
+ fn test_line() {
+ let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
+ assert_eq!(rope.line(0).to_string(), "Hello");
+ assert_eq!(rope.line(1).to_string(), "World\r");
+ assert_eq!(rope.line(2).to_string(), "This is a test 中文");
+ assert_eq!(rope.line(3).to_string(), "Rope");
+ assert_eq!(rope.line(4).to_string(), "");
+ }
+
+ #[test]
+ fn test_lines_len() {
+ let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
+ assert_eq!(rope.lines_len(), 4);
+ let rope = Rope::from("");
+ assert_eq!(rope.lines_len(), 1);
+ let rope = Rope::from("Single line");
+ assert_eq!(rope.lines_len(), 1);
+ }
+
+ #[test]
+ fn test_eq() {
+ let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
+ assert!(rope.eq(&Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope")));
+ assert!(!rope.eq(&Rope::from("Hello\nWorld")));
+
+ let rope1 = rope.clone();
+ assert!(rope.eq(&rope1));
+ }
+
+ #[test]
+ fn test_lines() {
+ let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
+ let lines: Vec<_> = rope.lines().into_iter().map(|r| r.to_string()).collect();
+ assert_eq!(
+ lines,
+ vec!["Hello", "World\r", "This is a test 中文", "Rope"]
+ );
+ }
+
+ #[test]
+ fn test_chars_count() {
+ let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文🎉\nRope");
+ assert_eq!(rope.chars_count(), 36);
+ let rope = Rope::from("");
+ assert_eq!(rope.chars_count(), 0);
+ let rope = Rope::from("Single line");
+ assert_eq!(rope.chars_count(), 11);
+ }
+
+ #[test]
+ fn test_char_at() {
+ let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文🎉\nRope");
+ assert_eq!(rope.char_at(0), Some('H'));
+ assert_eq!(rope.char_at(5), Some('\n'));
+ assert_eq!(rope.char_at(13), Some('T'));
+ assert_eq!(rope.char_at(28), Some('中'));
+ assert_eq!(rope.char_at(34), Some('🎉'));
+ assert_eq!(rope.char_at(38), Some('\n'));
+ assert_eq!(rope.char_at(50), None);
}
}
diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs
index 3dbbbd20..997f411f 100644
--- a/crates/ui/src/input/state.rs
+++ b/crates/ui/src/input/state.rs
@@ -3,12 +3,13 @@
//! 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 rope::Rope;
use serde::Deserialize;
use smallvec::SmallVec;
use std::cell::RefCell;
use std::ops::{Deref, Range};
use std::rc::Rc;
+use sum_tree::Bias;
use unicode_segmentation::*;
use gpui::{
@@ -19,9 +20,6 @@ use gpui::{
ScrollWheelEvent, SharedString, Styled as _, Subscription, UTF16Selection, Window, WrappedLine,
};
-// TODO:
-// - Move cursor to skip line eof empty chars.
-
use super::{
blink_cursor::BlinkCursor,
change::Change,
@@ -33,7 +31,7 @@ use super::{
};
use crate::input::hover_popover::DiagnosticPopover;
use crate::input::marker::Marker;
-use crate::input::{Cursor, LineColumn, RopeExt, Selection};
+use crate::input::{Cursor, LineColumn, RopeExt as _, Selection};
use crate::{history::History, scroll::ScrollbarState, Root};
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
@@ -518,8 +516,8 @@ impl InputState {
/// Called after moving the cursor. Updates preferred_column if we know where the cursor now is.
fn update_preferred_column(&mut self) {
- let column_ix = self.text.line_column(self.cursor().offset).1;
- self.preferred_column = Some(column_ix);
+ let column_ix = self.text.offset_to_point(self.cursor().offset).column;
+ self.preferred_column = Some(column_ix as usize);
}
/// Find which line and sub-line the given offset belongs to, along with the position within that sub-line.
@@ -565,22 +563,12 @@ impl InputState {
let offset = self.cursor().offset;
let was_preferred_column = self.preferred_column;
- let Ok(line_ix) = self.text.try_byte_to_line(offset) else {
- return;
- };
+ let row = self.text.offset_to_point(offset).row;
+ let new_row = row.saturating_add_signed(move_lines as i32);
+ let line = self.text.line(new_row as usize);
+ let line_start_offset = self.text.point_to_offset(rope::Point::new(new_row, 0));
- let new_line_ix = line_ix.saturating_add_signed(move_lines);
- let Some(line) = self.text.get_line(new_line_ix) else {
- return;
- };
- let Ok(line_start_offset) = self.text.try_line_to_byte(new_line_ix) else {
- return;
- };
-
- let new_column = self
- .preferred_column
- .unwrap_or_default()
- .min(line.len_bytes().saturating_sub(1));
+ let new_column = self.preferred_column.unwrap_or_default().min(line.len());
let new_offset = line_start_offset + new_column;
self.pause_blink_cursor(cx);
@@ -607,7 +595,7 @@ impl InputState {
// Ensure cursor to start when set text
if self.mode.is_single_line() {
self.selected_range =
- (Cursor::new(self.text.len_bytes())..Cursor::new(self.text.len_bytes())).into();
+ (Cursor::new(self.text.len())..Cursor::new(self.text.len())).into();
} else {
self.selected_range = (Cursor::new(0)..Cursor::new(0)).into();
}
@@ -728,14 +716,14 @@ impl InputState {
/// Set the default value of the input field.
pub fn default_value(mut self, value: impl Into) -> Self {
let text: SharedString = value.into();
- self.text = Rope::from_str(text.as_str());
+ self.text = Rope::from(text.as_str());
self.text_wrapper.text = self.text.clone();
self
}
/// Return the value of the input field.
pub fn value(&self) -> SharedString {
- SharedString::new(self.text.slice(..).as_str().unwrap_or_default())
+ SharedString::new(self.text.to_string())
}
/// Return the value without mask.
@@ -746,11 +734,11 @@ impl InputState {
/// Return the (1-based) line and column of the cursor.
pub fn line_column(&self) -> LineColumn {
let offset = self.cursor().offset;
- let (line_ix, column_ix) = self.text.line_column(offset);
+ let point = self.text.offset_to_point(offset);
LineColumn {
- line: line_ix + 1,
- column: column_ix + 1,
+ line: point.row as usize + 1,
+ column: point.column as usize + 1,
}
}
@@ -771,11 +759,20 @@ impl InputState {
window: &mut Window,
cx: &mut Context,
) {
- 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);
+ let max_point = self.text.max_point();
+ let line_ix = line.saturating_sub(1).min(max_point.row as usize);
+ let column_ix = column
+ .unwrap_or(1)
+ .saturating_sub(1)
+ .min(self.text.line_len(line_ix as u32) as usize);
+
+ let offset = self
+ .text
+ .point_to_offset(rope::Point::new(line_ix as u32, column_ix as u32));
self.move_to(Cursor::new(offset), window, cx);
+ self.update_preferred_column();
+ self.focus(window, cx);
}
/// Focus the input field.
@@ -916,7 +913,7 @@ impl InputState {
if self.mode.is_single_line() {
return;
}
- let offset = (self.end_of_line(window, cx) + 1).min(self.text.len_bytes());
+ let offset = (self.end_of_line(window, cx) + 1).min(self.text.len());
self.select_to(Cursor::new(self.next_boundary(offset)), window, cx);
}
@@ -927,7 +924,7 @@ impl InputState {
cx: &mut Context,
) {
self.move_to(Cursor::new(0), window, cx);
- self.select_to(Cursor::new(self.text.len_bytes()), window, cx)
+ self.select_to(Cursor::new(self.text.len()), window, cx)
}
pub(super) fn home(&mut self, _: &MoveHome, window: &mut Window, cx: &mut Context) {
@@ -957,7 +954,7 @@ impl InputState {
window: &mut Window,
cx: &mut Context,
) {
- let end = self.text.len_bytes();
+ let end = self.text.len();
self.move_to(Cursor::new(end), window, cx);
}
@@ -996,7 +993,7 @@ impl InputState {
window: &mut Window,
cx: &mut Context,
) {
- let end = self.text.len_bytes();
+ let end = self.text.len();
self.select_to(Cursor::new(end), window, cx);
}
@@ -1043,11 +1040,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)
- .as_str()
- .unwrap_or_default();
- UnicodeSegmentation::split_word_bound_indices(prev_str)
+ // FIXME: Avoid to_string
+ let left_part = self.text.slice(0..offset).to_string();
+
+ UnicodeSegmentation::split_word_bound_indices(left_part.as_str())
.filter(|(_, s)| !s.trim_start().is_empty())
.next_back()
.map(|(i, _)| i)
@@ -1057,14 +1053,12 @@ 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_bytes())
- .as_str()
- .unwrap_or_default();
- UnicodeSegmentation::split_word_bound_indices(next_str)
+ let right_part = self.text.slice(offset..self.text.len()).to_string();
+
+ UnicodeSegmentation::split_word_bound_indices(right_part.as_str())
.find(|(_, s)| !s.trim_start().is_empty())
.map(|(i, s)| offset + i + s.len())
- .unwrap_or(self.text.len_bytes())
+ .unwrap_or(self.text.len())
}
/// Get start of line
@@ -1096,7 +1090,7 @@ impl InputState {
.start
.min(self.selected_range.end.offset),
);
- if self.text.chars().nth(offset) == Some('\r') {
+ if self.text.char_at(offset) == Some('\r') {
offset += 1;
}
@@ -1112,11 +1106,11 @@ impl InputState {
/// Get end of line
fn end_of_line(&mut self, window: &mut Window, cx: &mut Context) -> usize {
if self.mode.is_single_line() {
- return self.text.len_bytes();
+ return self.text.len();
}
// 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.text.line_to_byte(line) + self.text.line(line).len();
let offset = self.next_boundary(self.cursor().offset);
// ignore if offset is "\n"
@@ -1135,7 +1129,7 @@ impl InputState {
let line = self
.text_for_range(
- self.range_to_utf16(&(offset..self.text.len_bytes())),
+ self.range_to_utf16(&(offset..self.text.len())),
&mut None,
window,
cx,
@@ -1143,7 +1137,7 @@ impl InputState {
.unwrap_or_default()
.find('\n')
.map(|i| i + offset)
- .unwrap_or(self.text.len_bytes());
+ .unwrap_or(self.text.len());
line
}
@@ -1242,7 +1236,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_bytes());
+ offset = (offset + 1).clamp(0, self.text.len());
}
self.replace_text_in_range(
Some(self.range_to_utf16(&(self.cursor().offset..offset))),
@@ -1450,10 +1444,11 @@ impl InputState {
// Selected none
let start_offset = self.selected_range.start;
let offset = self.start_of_line_of_selection(window, cx);
+ // FIXME: To improve performance
if self
- .text_for_range_utf8(offset..self.text.len_bytes())
- .as_str()
- .unwrap_or("")
+ .text
+ .slice(offset..self.text.len())
+ .to_string()
.starts_with(tab_indent.as_ref())
{
self.replace_text_in_range(
@@ -1600,7 +1595,7 @@ impl InputState {
return;
}
- let selected_text = self.text_for_range_utf8(self.selected_range).to_string();
+ let selected_text = self.text.slice(self.selected_range.into()).to_string();
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
}
@@ -1609,7 +1604,7 @@ impl InputState {
return;
}
- let selected_text = self.text_for_range_utf8(self.selected_range).to_string();
+ let selected_text = self.text.slice(self.selected_range.into()).to_string();
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
self.replace_text_in_range(None, "", window, cx);
}
@@ -1625,20 +1620,12 @@ impl InputState {
}
}
- fn push_history(
- &mut self,
- range: &Range,
- new_text: &str,
- window: &mut Window,
- cx: &mut Context,
- ) {
+ fn push_history(&mut self, text: &Rope, range: &Range, new_text: &str) {
if self.history.ignore {
return;
}
- let old_text = self
- .text_for_range(self.range_to_utf16(&range), &mut None, window, cx)
- .unwrap_or("".to_string());
+ let old_text = text.slice(range.clone()).to_string();
let new_range = range.start..range.start + new_text.len();
@@ -1674,7 +1661,7 @@ impl InputState {
///
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
fn move_to(&mut self, cursor: Cursor, _: &mut Window, cx: &mut Context) {
- let cursor = Cursor::new(cursor.offset.clamp(0, self.text.len_bytes()));
+ let cursor = Cursor::new(cursor.offset.clamp(0, self.text.len()));
self.selected_range = (cursor..cursor).into();
self.pause_blink_cursor(cx);
self.update_preferred_column();
@@ -1703,7 +1690,7 @@ impl InputState {
_cx: &App,
) -> usize {
// If the text is empty, always return 0
- if self.text.len_bytes() == 0 {
+ if self.text.len() == 0 {
return 0;
}
@@ -1768,8 +1755,8 @@ impl InputState {
index += 1;
}
- if index > self.text.len_bytes() {
- self.text.len_bytes()
+ if index > self.text.len() {
+ self.text.len()
} else {
index
}
@@ -1802,7 +1789,7 @@ impl InputState {
///
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
fn select_to(&mut self, offset: Cursor, _: &mut Window, cx: &mut Context) {
- let offset = offset.clamp(0, self.text.len_bytes());
+ let offset = offset.clamp(0, self.text.len());
if self.selection_reversed {
self.selected_range.start = Cursor::new(offset)
} else {
@@ -1847,7 +1834,7 @@ impl InputState {
.unwrap_or_default();
let next_text = self
.text_for_range(
- self.range_to_utf16(&(end..self.text.len_bytes())),
+ self.range_to_utf16(&(end..self.text.len())),
&mut None,
window,
cx,
@@ -1931,24 +1918,11 @@ impl InputState {
}
fn previous_boundary(&self, offset: usize) -> usize {
- 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)
+ self.text.clip_offset(offset.saturating_sub(1), Bias::Left)
}
fn next_boundary(&self, offset: usize) -> usize {
- 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())
+ self.text.clip_offset(offset + 1, Bias::Right)
}
/// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible.
@@ -1991,7 +1965,7 @@ impl InputState {
window: &mut Window,
cx: &mut Context,
) {
- if self.text.len_bytes() == 0 {
+ if self.text.len() == 0 {
return;
}
@@ -2026,10 +2000,11 @@ impl InputState {
return false;
}
- self.pattern
- .as_ref()
- .map(|p| p.is_match(new_text))
- .unwrap_or(true)
+ let Some(pattern) = &self.pattern else {
+ return true;
+ };
+
+ pattern.is_match(new_text)
}
/// Set the mask pattern for formatting the input text.
@@ -2082,11 +2057,6 @@ impl InputState {
}
}
}
-
- fn text_for_range_utf8(&'_ self, range: impl Into>) -> RopeSlice<'_> {
- let range = self.range_from_utf16(&self.range_to_utf16(&range.into()));
- self.text.byte_slice(range)
- }
}
impl EntityInputHandler for InputState {
@@ -2099,7 +2069,7 @@ impl EntityInputHandler for InputState {
) -> Option {
let range = self.range_from_utf16(&range_utf16);
adjusted_range.replace(self.range_to_utf16(&range));
- Some(self.text.byte_slice(range).to_string())
+ Some(self.text.slice(range).to_string())
}
fn selected_text_range(
@@ -2135,7 +2105,7 @@ impl EntityInputHandler for InputState {
&mut self,
range_utf16: Option>,
new_text: &str,
- window: &mut Window,
+ _: &mut Window,
cx: &mut Context,
) {
if self.disabled {
@@ -2148,23 +2118,26 @@ 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_string()
- + new_text
- + &self
- .text_for_range_utf8(range.end..self.text.len_bytes())
- .to_string())
- .into();
+ let old_text = self.text.clone();
+ self.text.replace(range.clone(), new_text);
+
+ let pending_text = self.text.to_string();
// Check if the new text is valid
if !self.is_valid_input(&pending_text, cx) {
+ self.text = old_text;
return;
}
- let mask_text = self.mask_pattern.mask(&pending_text);
- let new_text_len = (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
- let new_offset = (range.start + new_text_len).min(mask_text.len());
+ let mut new_offset = (range.start + new_text.len()).min(self.text.len());
+ if !self.mask_pattern.is_none() {
+ let mask_text = self.mask_pattern.mask(&pending_text);
+ self.text = Rope::from(mask_text.as_str());
+ let new_text_len =
+ (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
+ new_offset = (range.start + new_text_len).min(mask_text.len());
+ }
- self.push_history(&range, &new_text, window, cx);
- self.text = Rope::from_str(&mask_text);
+ self.push_history(&old_text, &range, &new_text);
self.mode.clear_markers();
self.text_wrapper.update(&self.text, false, cx);
@@ -2185,7 +2158,7 @@ impl EntityInputHandler for InputState {
range_utf16: Option>,
new_text: &str,
new_selected_range_utf16: Option>,
- window: &mut Window,
+ _: &mut Window,
cx: &mut Context,
) {
if self.disabled {
@@ -2197,18 +2170,17 @@ 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_string()
- + new_text
- + &self
- .text_for_range_utf8(range.end..self.text.len_bytes())
- .to_string())
- .into();
+
+ let old_text = self.text.clone();
+ self.text.replace(range.clone(), new_text);
+ let pending_text = self.text.to_string();
+
if !self.is_valid_input(&pending_text, cx) {
+ self.text = old_text;
return;
}
- self.push_history(&range, new_text, window, cx);
- self.text = Rope::from_str(&pending_text);
+ self.push_history(&old_text, &range, new_text);
self.mode.clear_markers();
self.text_wrapper.update(&self.text, false, cx);
self.mode
@@ -2232,7 +2204,6 @@ impl EntityInputHandler for InputState {
}
/// Used to position IME candidates.
- /// TODO: Fix position of IME candidates in multi-line text input.
fn bounds_for_range(
&mut self,
range_utf16: Range,
diff --git a/crates/ui/src/input/text_input.rs b/crates/ui/src/input/text_input.rs
index c2fc305f..d1f73a14 100644
--- a/crates/ui/src/input/text_input.rs
+++ b/crates/ui/src/input/text_input.rs
@@ -174,10 +174,8 @@ impl RenderOnce for TextInput {
let prefix = self.prefix;
let suffix = self.suffix;
- let show_clear_button = self.cleanable
- && !state.loading
- && state.text.len_bytes() > 0
- && state.mode.is_single_line();
+ let show_clear_button =
+ self.cleanable && !state.loading && state.text.len() > 0 && state.mode.is_single_line();
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
div()
diff --git a/crates/ui/src/input/text_wrapper.rs b/crates/ui/src/input/text_wrapper.rs
index 38c2a848..823b4627 100644
--- a/crates/ui/src/input/text_wrapper.rs
+++ b/crates/ui/src/input/text_wrapper.rs
@@ -1,7 +1,9 @@
use std::ops::Range;
use gpui::{App, Font, LineFragment, Pixels};
-use ropey::Rope;
+use rope::Rope;
+
+use crate::input::RopeExt as _;
#[allow(unused)]
pub(super) struct LineWrap {
@@ -44,11 +46,19 @@ impl TextWrapper {
}
pub(super) fn set_wrap_width(&mut self, wrap_width: Option, cx: &mut App) {
+ if wrap_width == self.wrap_width {
+ return;
+ }
+
self.wrap_width = wrap_width;
self.update(&self.text.clone(), true, cx);
}
pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
+ if self.font.eq(&font) && self.font_size == font_size {
+ return;
+ }
+
self.font = font;
self.font_size = font_size;
self.update(&self.text.clone(), true, cx);
@@ -58,7 +68,7 @@ impl TextWrapper {
///
/// If the `text` is the same as the current text, do nothing.
pub(super) fn update(&mut self, text: &Rope, force: bool, cx: &mut App) {
- if &self.text == text && !force {
+ if self.text.eq(text) && !force {
return;
}
@@ -70,15 +80,15 @@ impl TextWrapper {
.line_wrapper(self.font.clone(), self.font_size);
let mut prev_line_ix = 0;
- // FIXME: here may need use from Rope
- for line in text.to_string().split('\n') {
+ for line in text.lines() {
+ let line = line.to_string();
let mut line_wraps = vec![];
let mut prev_boundary_ix = 0;
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
if let Some(wrap_width) = wrap_width {
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
- for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) {
+ for boundary in line_wrapper.wrap_line(&[LineFragment::text(&line)], wrap_width) {
line_wraps.push(prev_boundary_ix..boundary.ix);
prev_boundary_ix = boundary.ix;
}
@@ -95,6 +105,7 @@ impl TextWrapper {
wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len());
}
+ // +1 for \n
prev_line_ix += line.len() + 1;
}
diff --git a/crates/ui/src/text/node.rs b/crates/ui/src/text/node.rs
index da266144..46ea1232 100644
--- a/crates/ui/src/text/node.rs
+++ b/crates/ui/src/text/node.rs
@@ -7,7 +7,7 @@ use gpui::{
StatefulInteractiveElement, Styled, StyledImage as _, Window,
};
use markdown::mdast;
-use ropey::Rope;
+use rope::Rope;
use crate::{
h_flex,
@@ -292,7 +292,7 @@ impl CodeBlock {
let mut styles = vec![];
if let Some(lang) = &lang {
let mut highlighter = SyntaxHighlighter::new(&lang, cx);
- highlighter.update(None, &Rope::from_str(code.as_str()), cx);
+ highlighter.update(None, &Rope::from(code.as_str()), cx);
styles = highlighter.styles(&(0..code.len()), &theme);
};