input: Use zed's Rope (#1214)

Now with 10K lines plain text, in release mode on macOS. The
`large-text` example can work with:

- 120 FPS for display.
- 90 FPS for update text.

```
MTL_HUD_ENABLED=1 cargo run --example large-text --release
```

- Fixed the cursor position at end of line, also support at start of
line.


https://github.com/user-attachments/assets/1de52f6c-e138-4ed3-a600-06372a807ed8

### Check list

- [x] Modifying Text with Rope
- [x] LineColumn, GoToLine
- [x] Code Highlight
- [x] Delete word, select word
- [x] Delete line
- [x] Move left, right, up, down
This commit is contained in:
Jason Lee 2025-09-08 16:44:52 +08:00 committed by GitHub
parent a9e6b49a15
commit a9953a9349
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 535 additions and 279 deletions

64
Cargo.lock generated
View file

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

View file

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

View file

@ -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<br>(Virtual Rows, Columns) | No | Yes<br>(Virtual Rows) | Yes<br>(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<br>(Virtual Rows, Columns) | No | Yes<br>(Virtual Rows) | Yes<br>(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

View file

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

View file

@ -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<InputState>,
go_to_line_state: Entity<InputState>,
soft_wrap: bool,
_subscribes: Vec<Subscription>,
}
impl Example {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> 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<Self> {
cx.new(|cx| Self::new(window, cx))
}
fn go_to_line(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
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::<usize>().ok())
.collect::<Vec<_>>()
.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>) {
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<Self>) -> 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);
});
}

View file

@ -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<AnyView>,
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<F, E>(
title: &str,
window_size: Option<Size<Pixels>>,
crate_view_fn: F,
cx: &mut App,
) where
E: Into<AnyView>,
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);

View file

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

View file

@ -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::Item> {
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<InputEdit>, 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;
};

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Item = Rope>;
/// 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<char>;
}
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<Item = Rope> {
(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<char> {
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);
}
}

View file

@ -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<SharedString>) -> 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<Self>,
) {
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>,
) {
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<Self>) {
@ -957,7 +954,7 @@ impl InputState {
window: &mut Window,
cx: &mut Context<Self>,
) {
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<Self>,
) {
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<Self>) -> 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<usize>,
new_text: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
fn push_history(&mut self, text: &Rope, range: &Range<usize>, 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<Self>) {
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<Self>) {
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<Self>,
) {
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<Range<usize>>) -> 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<String> {
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<Range<usize>>,
new_text: &str,
window: &mut Window,
_: &mut Window,
cx: &mut Context<Self>,
) {
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<Range<usize>>,
new_text: &str,
new_selected_range_utf16: Option<Range<usize>>,
window: &mut Window,
_: &mut Window,
cx: &mut Context<Self>,
) {
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<usize>,

View file

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

View file

@ -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<Pixels>, 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;
}

View file

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