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:
parent
a9e6b49a15
commit
a9953a9349
17 changed files with 535 additions and 279 deletions
64
Cargo.lock
generated
64
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
52
README.md
52
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<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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
155
crates/story/examples/large-text.rs
Normal file
155
crates/story/examples/large-text.rs
Normal 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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue