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",
|
"bitflags 2.9.1",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.12.1",
|
"itertools 0.11.0",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"lazycell",
|
"lazycell",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -732,7 +732,7 @@ dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.13.0",
|
"itertools 0.11.0",
|
||||||
"log",
|
"log",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
@ -797,7 +797,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blade-graphics"
|
name = "blade-graphics"
|
||||||
version = "0.6.0"
|
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 = [
|
dependencies = [
|
||||||
"ash",
|
"ash",
|
||||||
"ash-window",
|
"ash-window",
|
||||||
|
|
@ -830,7 +830,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blade-macros"
|
name = "blade-macros"
|
||||||
version = "0.3.0"
|
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 = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -840,7 +840,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blade-util"
|
name = "blade-util"
|
||||||
version = "0.2.0"
|
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 = [
|
dependencies = [
|
||||||
"blade-graphics",
|
"blade-graphics",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
|
|
@ -1401,7 +1401,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"indexmap 2.10.0",
|
"indexmap 2.10.0",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
|
|
@ -1870,7 +1870,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_refineable"
|
name = "derive_refineable"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -3049,7 +3049,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
|
|
@ -3101,7 +3101,7 @@ dependencies = [
|
||||||
"pathfinder_geometry",
|
"pathfinder_geometry",
|
||||||
"postage",
|
"postage",
|
||||||
"profiling",
|
"profiling",
|
||||||
"rand 0.8.5",
|
"rand 0.9.2",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"refineable",
|
"refineable",
|
||||||
"resvg",
|
"resvg",
|
||||||
|
|
@ -3160,7 +3160,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"paste",
|
"paste",
|
||||||
"regex",
|
"regex",
|
||||||
"ropey",
|
"rope",
|
||||||
"rust-i18n",
|
"rust-i18n",
|
||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"schemars",
|
"schemars",
|
||||||
|
|
@ -3232,7 +3232,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_macros"
|
name = "gpui_macros"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
@ -3501,7 +3501,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http_client"
|
name = "http_client"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -3521,7 +3521,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http_client_tls"
|
name = "http_client_tls"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
|
|
@ -3625,7 +3625,7 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.58.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4466,7 +4466,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
|
|
@ -4902,7 +4902,7 @@ version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
|
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate 3.3.0",
|
"proc-macro-crate 1.3.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.105",
|
"syn 2.0.105",
|
||||||
|
|
@ -6380,7 +6380,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
|
|
@ -6533,7 +6533,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest_client"
|
name = "reqwest_client"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -6544,7 +6544,6 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest 0.12.15",
|
"reqwest 0.12.15",
|
||||||
"serde",
|
"serde",
|
||||||
"smol 2.0.2",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
]
|
]
|
||||||
|
|
@ -6616,13 +6615,18 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ropey"
|
name = "rope"
|
||||||
version = "1.6.1"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5"
|
||||||
checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"log",
|
||||||
|
"rayon",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"str_indices",
|
"sum_tree",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"util",
|
||||||
|
"workspace-hack",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -7120,7 +7124,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semantic_version"
|
name = "semantic_version"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -7645,12 +7649,6 @@ dependencies = [
|
||||||
"unindent",
|
"unindent",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "str_indices"
|
|
||||||
version = "0.4.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "streaming-iterator"
|
name = "streaming-iterator"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
|
|
@ -7749,7 +7747,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sum_tree"
|
name = "sum_tree"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -9059,7 +9057,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "util"
|
name = "util"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs 2.1.3",
|
"async-fs 2.1.3",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ resolver = "2"
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
gpui = { git = "https://github.com/zed-industries/zed.git" }
|
gpui = { git = "https://github.com/zed-industries/zed.git" }
|
||||||
gpui_macros = { 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" }
|
reqwest_client = { git = "https://github.com/zed-industries/zed.git" }
|
||||||
sum_tree = { git = "https://github.com/zed-industries/zed.git" }
|
sum_tree = { git = "https://github.com/zed-industries/zed.git" }
|
||||||
gpui-component = { path = "crates/ui" }
|
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
|
## Compare to others
|
||||||
|
|
||||||
| Features | GPUI Component | [Iced] | [egui] | [QT 6] |
|
| Features | GPUI Component | [Iced] | [egui] | [QT 6] |
|
||||||
| --------------------- | ------------------------------ | --------- | --------------------- | ------------------------------ |
|
| --------------------- | ------------------------------ | ----------- | --------------------- | ------------------------------ |
|
||||||
| Language | Rust | Rust | Rust | C++/QML |
|
| Language | Rust | Rust | Rust | C++/QML |
|
||||||
| Core Render | GPUI | wgpu | wgpu | QT |
|
| Core Render | GPUI | wgpu | wgpu | QT |
|
||||||
| License | Apache 2.0 | MIT | MIT/Apache 2.0 | Commercial |
|
| License | Apache 2.0 | MIT | MIT/Apache 2.0 | Commercial |
|
||||||
| Min Binary Size [^1] | 12MB | 11MB | 5M | 20MB [^2] |
|
| Min Binary Size [^1] | 12MB | 11MB | 5M | 20MB [^2] |
|
||||||
| Cross-Platform | Yes | Yes | Yes | Yes |
|
| Cross-Platform | Yes | Yes | Yes | Yes |
|
||||||
| Documentation | No | Simple | Simple | Good |
|
| Documentation | No | Simple | Simple | Good |
|
||||||
| Web | No | Yes | Yes | Yes |
|
| Web | No | Yes | Yes | Yes |
|
||||||
| UI Style | Modern | Basic | Basic | Basic |
|
| UI Style | Modern | Basic | Basic | Basic |
|
||||||
| CJK Support | Yes | Yes | Bad | Yes |
|
| CJK Support | Yes | Yes | Bad | Yes |
|
||||||
| Chart | Yes | No | No | 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 (Large dataset) | Yes<br>(Virtual Rows, Columns) | No | Yes<br>(Virtual Rows) | Yes<br>(Virtual Rows, Columns) |
|
||||||
| Table Column Resize | Yes | No | Yes | Yes |
|
| Table Column Resize | Yes | No | Yes | Yes |
|
||||||
| CodeEditor | Simple | Simple | Simple | Basic API |
|
| Text base | Rope | String [^3] | trait TextBuffer [^4] | [QTextDocument] |
|
||||||
| Dock Layout | Yes | Yes | Yes | Yes |
|
| CodeEditor | Simple | Simple | Simple | Basic API |
|
||||||
| Syntax Highlight | [Tree Sitter] | [Syntect] | [Syntect] | [QSyntaxHighlighter] |
|
| Dock Layout | Yes | Yes | Yes | Yes |
|
||||||
| Markdown Rendering | Yes | Yes | Basic | No |
|
| Syntax Highlight | [Tree Sitter] | [Syntect] | [Syntect] | [QSyntaxHighlighter] |
|
||||||
| Markdown mix HTML | Yes | No | No | No |
|
| Markdown Rendering | Yes | Yes | Basic | No |
|
||||||
| HTML Rendering | Basic | No | No | Yes |
|
| Markdown mix HTML | Yes | No | No | No |
|
||||||
| Text Selection | TextView | No | Any Label | No |
|
| HTML Rendering | Basic | No | No | Yes |
|
||||||
| Themes | Yes | No | No | No |
|
| Text Selection | TextView | No | Any Label | No |
|
||||||
| I18n | Yes | Yes | Yes | Yes |
|
| Themes | Yes | No | No | No |
|
||||||
|
| I18n | Yes | Yes | Yes | Yes |
|
||||||
|
|
||||||
> Please submit an issue or PR if any mistakes or outdated are found.
|
> 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/
|
[Tree Sitter]: https://tree-sitter.github.io/tree-sitter/
|
||||||
[Syntect]: https://github.com/trishume/syntect
|
[Syntect]: https://github.com/trishume/syntect
|
||||||
[QSyntaxHighlighter]: https://doc.qt.io/qt-6/qsyntaxhighlighter.html
|
[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.
|
[^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)
|
[^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
|
## License
|
||||||
|
|
||||||
Apache-2.0
|
Apache-2.0
|
||||||
|
|
|
||||||
|
|
@ -331,8 +331,9 @@ fn main() {
|
||||||
init(cx);
|
init(cx);
|
||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
|
|
||||||
story::create_new_window(
|
story::create_new_window_with_size(
|
||||||
"Code Editor",
|
"Code Editor",
|
||||||
|
Some(size(px(1200.), px(960.))),
|
||||||
|window, cx| cx.new(|cx| Example::new(name, window, cx)),
|
|window, cx| cx.new(|cx| Example::new(name, window, cx)),
|
||||||
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::{
|
use gpui::{
|
||||||
actions, div, prelude::FluentBuilder as _, px, rems, size, Action, AnyElement, AnyView, App,
|
actions, div, prelude::FluentBuilder as _, px, rems, size, Action, AnyElement, AnyView, App,
|
||||||
AppContext, Bounds, Context, Div, Entity, EventEmitter, Focusable, Global, Hsla,
|
AppContext, Bounds, Context, Div, Entity, EventEmitter, Focusable, Global, Hsla,
|
||||||
InteractiveElement, IntoElement, KeyBinding, Menu, MenuItem, ParentElement, Render, RenderOnce,
|
InteractiveElement, IntoElement, KeyBinding, Menu, MenuItem, ParentElement, Pixels, Render,
|
||||||
SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window, WindowBounds,
|
RenderOnce, SharedString, Size, StatefulInteractiveElement, StyleRefinement, Styled, Window,
|
||||||
WindowKind, WindowOptions,
|
WindowBounds, WindowKind, WindowOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use accordion_story::AccordionStory;
|
pub use accordion_story::AccordionStory;
|
||||||
|
|
@ -166,7 +166,19 @@ where
|
||||||
E: Into<AnyView>,
|
E: Into<AnyView>,
|
||||||
F: FnOnce(&mut Window, &mut App) -> E + Send + 'static,
|
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() {
|
if let Some(display) = cx.primary_display() {
|
||||||
let display_size = display.bounds().size;
|
let display_size = display.bounds().size;
|
||||||
window_size.width = window_size.width.min(display_size.width * 0.85);
|
window_size.width = window_size.width.min(display_size.width * 0.85);
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ tree-sitter-languages = [
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
sum_tree.workspace = true
|
sum_tree.workspace = true
|
||||||
gpui_macros.workspace = true
|
gpui_macros.workspace = true
|
||||||
|
rope.workspace = true
|
||||||
gpui-component-macros.workspace = true
|
gpui-component-macros.workspace = true
|
||||||
rust-i18n.workspace = true
|
rust-i18n.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
|
|
@ -70,7 +71,6 @@ paste = "1"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
uuid = "1.10"
|
uuid = "1.10"
|
||||||
ropey = "1.6.1"
|
|
||||||
|
|
||||||
# WebView
|
# WebView
|
||||||
wry = { version = "0.48.0", optional = true }
|
wry = { version = "0.48.0", optional = true }
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
use super::HighlightTheme;
|
use super::HighlightTheme;
|
||||||
use crate::highlighter::LanguageRegistry;
|
use crate::{highlighter::LanguageRegistry, input::RopeExt as _};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use gpui::{App, HighlightStyle, SharedString};
|
use gpui::{App, HighlightStyle, SharedString};
|
||||||
use ropey::Rope;
|
|
||||||
|
use rope::Rope;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeSet, HashMap},
|
collections::{BTreeSet, HashMap},
|
||||||
ops::Range,
|
ops::Range,
|
||||||
slice::Chunks,
|
|
||||||
usize,
|
usize,
|
||||||
};
|
};
|
||||||
use sum_tree::{Bias, SumTree};
|
use sum_tree::{Bias, SumTree};
|
||||||
|
|
@ -42,12 +42,20 @@ pub struct SyntaxHighlighter {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TextProvider<'a>(&'a Rope);
|
struct TextProvider<'a>(&'a Rope);
|
||||||
|
struct ByteChunks<'a>(rope::Chunks<'a>);
|
||||||
impl<'a> tree_sitter::TextProvider<&'a [u8]> for TextProvider<'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 {
|
fn text(&mut self, node: tree_sitter::Node) -> Self::I {
|
||||||
let slice = self.0.byte_slice(node.start_byte()..node.end_byte());
|
ByteChunks(self.0.chunks_in_range(node.byte_range()))
|
||||||
slice.as_str().unwrap_or_default().as_bytes().chunks(64)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
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.
|
/// Highlight the given text, returning a map from byte ranges to highlight captures.
|
||||||
/// Uses incremental parsing, detects changed ranges, and caches unchanged results.
|
/// Uses incremental parsing, detects changed ranges, and caches unchanged results.
|
||||||
pub fn update(&mut self, edit: Option<InputEdit>, text: &Rope, cx: &App) {
|
pub fn update(&mut self, edit: Option<InputEdit>, text: &Rope, cx: &App) {
|
||||||
if &self.text == text {
|
if self.text.eq(text) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let edit = edit.unwrap_or(InputEdit {
|
let edit = edit.unwrap_or(InputEdit {
|
||||||
start_byte: 0,
|
start_byte: 0,
|
||||||
old_end_byte: 0,
|
old_end_byte: 0,
|
||||||
new_end_byte: text.len_bytes(),
|
new_end_byte: text.len(),
|
||||||
start_position: Point::new(0, 0),
|
start_position: Point::new(0, 0),
|
||||||
old_end_position: Point::new(0, 0),
|
old_end_position: Point::new(0, 0),
|
||||||
new_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());
|
.unwrap_or(self.parser.parse("", None).unwrap());
|
||||||
old_tree.edit(&edit);
|
old_tree.edit(&edit);
|
||||||
|
|
||||||
|
let mut chunks = text.chunks();
|
||||||
let new_tree = self.parser.parse_with_options(
|
let new_tree = self.parser.parse_with_options(
|
||||||
&mut |offset, _| {
|
&mut move |offset, _| {
|
||||||
if offset >= text.len_bytes() {
|
chunks.seek(offset);
|
||||||
""
|
chunks.next().unwrap_or("").as_bytes()
|
||||||
} else {
|
|
||||||
let (chunk, chunk_byte_ix, _, _) = text.chunk_at_byte(offset);
|
|
||||||
&chunk[offset - chunk_byte_ix..]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Some(&old_tree),
|
Some(&old_tree),
|
||||||
None,
|
None,
|
||||||
|
|
@ -437,10 +442,13 @@ impl SyntaxHighlighter {
|
||||||
return cache;
|
return cache;
|
||||||
};
|
};
|
||||||
|
|
||||||
let content = self.text.byte_slice(node.start_byte()..node.end_byte());
|
let content = self.text.slice(node.byte_range());
|
||||||
if content.len_bytes() == 0 {
|
if content.len() == 0 {
|
||||||
return cache;
|
return cache;
|
||||||
};
|
};
|
||||||
|
// FIXME: Avoid to_string.
|
||||||
|
let content = content.to_string();
|
||||||
|
|
||||||
let Some(config) = LanguageRegistry::global(cx).language(injection_language) else {
|
let Some(config) = LanguageRegistry::global(cx).language(injection_language) else {
|
||||||
return cache;
|
return cache;
|
||||||
};
|
};
|
||||||
|
|
@ -449,7 +457,7 @@ impl SyntaxHighlighter {
|
||||||
return cache;
|
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 {
|
let Some(tree) = parser.parse(source, None) else {
|
||||||
return cache;
|
return cache;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ use gpui::{
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::{
|
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};
|
use super::{mode::InputMode, InputState, LastLayout};
|
||||||
|
|
@ -391,9 +393,9 @@ impl TextElement {
|
||||||
let mut skipped_offset = 0;
|
let mut skipped_offset = 0;
|
||||||
let mut styles = vec![];
|
let mut styles = vec![];
|
||||||
|
|
||||||
// The Rope line has includes `\n` and `\r`.
|
|
||||||
for (ix, line) in state.text.lines().enumerate() {
|
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 {
|
if ix < visible_range.start {
|
||||||
offset += line_len;
|
offset += line_len;
|
||||||
skipped_offset = offset;
|
skipped_offset = offset;
|
||||||
|
|
@ -547,7 +549,7 @@ impl Element for TextElement {
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let multi_line = state.mode.is_multi_line();
|
let multi_line = state.mode.is_multi_line();
|
||||||
let text = state.text.clone();
|
let text = state.text.clone();
|
||||||
let is_empty = text.len_bytes() == 0;
|
let is_empty = text.len() == 0;
|
||||||
let placeholder = self.placeholder.clone();
|
let placeholder = self.placeholder.clone();
|
||||||
let style = window.text_style();
|
let style = window.text_style();
|
||||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
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 {
|
let (display_text, text_color) = if is_empty {
|
||||||
(placeholder, cx.theme().muted_foreground)
|
(placeholder, cx.theme().muted_foreground)
|
||||||
} else if state.masked {
|
} else if state.masked {
|
||||||
("*".repeat(text.len_chars()).into(), cx.theme().foreground)
|
("*".repeat(text.chars_count()).into(), cx.theme().foreground)
|
||||||
} else {
|
} else {
|
||||||
(text.to_string().into(), cx.theme().foreground)
|
(text.to_string().into(), cx.theme().foreground)
|
||||||
};
|
};
|
||||||
|
|
@ -564,25 +566,21 @@ impl Element for TextElement {
|
||||||
let text_style = window.text_style();
|
let text_style = window.text_style();
|
||||||
|
|
||||||
// Calculate the width of the line numbers
|
// Calculate the width of the line numbers
|
||||||
let empty_line_number = window
|
let empty_line_number = window.text_system().shape_line(
|
||||||
.text_system()
|
"++++".into(),
|
||||||
.shape_text(
|
font_size,
|
||||||
"++++".into(),
|
&[TextRun {
|
||||||
font_size,
|
len: 4,
|
||||||
&[TextRun {
|
font: style.font(),
|
||||||
len: 4,
|
color: gpui::black(),
|
||||||
font: style.font(),
|
background_color: None,
|
||||||
color: gpui::black(),
|
underline: None,
|
||||||
background_color: None,
|
strikethrough: None,
|
||||||
underline: None,
|
}],
|
||||||
strikethrough: None,
|
None,
|
||||||
}],
|
);
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let line_number_width = if state.mode.line_number() {
|
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 {
|
} else {
|
||||||
px(0.)
|
px(0.)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ impl Marker {
|
||||||
return;
|
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
|
let Some(end_line) = state
|
||||||
.text_wrapper
|
.text_wrapper
|
||||||
|
|
@ -53,7 +53,7 @@ impl Marker {
|
||||||
else {
|
else {
|
||||||
return;
|
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
|
let start_byte = start_line.range.start
|
||||||
+ start_line_str
|
+ start_line_str
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,6 @@ pub use mask_pattern::MaskPattern;
|
||||||
pub use mode::TabSize;
|
pub use mode::TabSize;
|
||||||
pub use number_input::{NumberInput, NumberInputEvent, StepAction};
|
pub use number_input::{NumberInput, NumberInputEvent, StepAction};
|
||||||
pub use otp_input::*;
|
pub use otp_input::*;
|
||||||
pub(crate) use rope_ext::*;
|
pub use rope_ext::*;
|
||||||
pub use state::*;
|
pub use state::*;
|
||||||
pub use text_input::*;
|
pub use text_input::*;
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,9 @@ use std::rc::Rc;
|
||||||
use std::{cell::RefCell, ops::Range};
|
use std::{cell::RefCell, ops::Range};
|
||||||
|
|
||||||
use gpui::{App, SharedString};
|
use gpui::{App, SharedString};
|
||||||
use ropey::Rope;
|
use rope::Rope;
|
||||||
use tree_sitter::{InputEdit, Point};
|
use tree_sitter::{InputEdit, Point};
|
||||||
|
|
||||||
use crate::input::RopeExt as _;
|
|
||||||
use crate::{highlighter::SyntaxHighlighter, input::marker::Marker};
|
use crate::{highlighter::SyntaxHighlighter, input::marker::Marker};
|
||||||
|
|
||||||
use super::text_wrapper::TextWrapper;
|
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).
|
// When full text changed, the selected_range may be out of bound (The before version).
|
||||||
let mut selected_range = selected_range.clone();
|
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 insert a chart, this is 1.
|
||||||
// If backspace or delete, 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 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 new_end = (selected_range.end as isize + changed_len) as usize;
|
||||||
|
|
||||||
let start_pos = text.line_column(selected_range.start);
|
let start_pos = text.offset_to_point(selected_range.start);
|
||||||
let old_end_pos = text.line_column(selected_range.end);
|
let old_end_pos = text.offset_to_point(selected_range.end);
|
||||||
let new_end_pos = text.line_column(new_end);
|
let new_end_pos = text.offset_to_point(new_end);
|
||||||
|
|
||||||
let edit = InputEdit {
|
let edit = InputEdit {
|
||||||
start_byte: selected_range.start,
|
start_byte: selected_range.start,
|
||||||
old_end_byte: selected_range.end,
|
old_end_byte: selected_range.end,
|
||||||
new_end_byte: new_end,
|
new_end_byte: new_end,
|
||||||
start_position: Point::new(start_pos.0, start_pos.1),
|
start_position: Point::new(start_pos.row as usize, start_pos.column as usize),
|
||||||
old_end_position: Point::new(old_end_pos.0, old_end_pos.1),
|
old_end_position: Point::new(
|
||||||
new_end_position: Point::new(new_end_pos.0, new_end_pos.1),
|
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);
|
highlighter.update(Some(edit), text, cx);
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,132 @@
|
||||||
pub(crate) trait RopeExt {
|
use rope::{Point, Rope};
|
||||||
/// 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.
|
/// 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).
|
/// Return empty rope if the row is out of bounds.
|
||||||
fn line_column(&self, byte_offset: usize) -> (usize, usize);
|
fn line(&self, row: usize) -> Rope;
|
||||||
/// Get the byte offset (0-based) from the line, column (0-based).
|
|
||||||
|
/// 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.
|
/// Each line is including the `\n` at the end, but not `\n`.
|
||||||
/// Return the end column of line, if the column is out of bounds.
|
fn lines(&self) -> impl Iterator<Item = Rope>;
|
||||||
fn line_column_to_byte(&self, line_ix: usize, column_ix: usize) -> usize;
|
|
||||||
|
/// 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 {
|
impl RopeExt for Rope {
|
||||||
fn line_column(&self, offset: usize) -> (usize, usize) {
|
fn line(&self, row: usize) -> Rope {
|
||||||
let Ok(line_ix) = self.try_byte_to_line(offset) else {
|
let row = row as u32;
|
||||||
return (0, 0);
|
let start = self.point_to_offset(Point::new(row, 0));
|
||||||
};
|
let end = start + self.line_len(row) as usize;
|
||||||
|
self.slice(start..end)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_column_to_byte(&self, line_ix: usize, column_ix: usize) -> usize {
|
fn lines_len(&self) -> usize {
|
||||||
let line_ix = self.len_lines().saturating_sub(1).min(line_ix);
|
self.max_point().row as usize + 1
|
||||||
let line = self.line(line_ix);
|
}
|
||||||
|
|
||||||
self.line_to_byte(line_ix)
|
fn lines(&self) -> impl Iterator<Item = Rope> {
|
||||||
+ line
|
(0..self.lines_len()).map(move |row| self.line(row))
|
||||||
.try_char_to_byte(column_ix)
|
}
|
||||||
.unwrap_or(line.len_bytes().saturating_sub(1))
|
|
||||||
|
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.
|
//! Based on the `Input` example from the `gpui` crate.
|
||||||
//! https://github.com/zed-industries/zed/blob/main/crates/gpui/examples/input.rs
|
//! https://github.com/zed-industries/zed/blob/main/crates/gpui/examples/input.rs
|
||||||
use gpui::Action;
|
use gpui::Action;
|
||||||
use ropey::{Rope, RopeSlice};
|
use rope::Rope;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::ops::{Deref, Range};
|
use std::ops::{Deref, Range};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use sum_tree::Bias;
|
||||||
use unicode_segmentation::*;
|
use unicode_segmentation::*;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
|
@ -19,9 +20,6 @@ use gpui::{
|
||||||
ScrollWheelEvent, SharedString, Styled as _, Subscription, UTF16Selection, Window, WrappedLine,
|
ScrollWheelEvent, SharedString, Styled as _, Subscription, UTF16Selection, Window, WrappedLine,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// - Move cursor to skip line eof empty chars.
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
blink_cursor::BlinkCursor,
|
blink_cursor::BlinkCursor,
|
||||||
change::Change,
|
change::Change,
|
||||||
|
|
@ -33,7 +31,7 @@ use super::{
|
||||||
};
|
};
|
||||||
use crate::input::hover_popover::DiagnosticPopover;
|
use crate::input::hover_popover::DiagnosticPopover;
|
||||||
use crate::input::marker::Marker;
|
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};
|
use crate::{history::History, scroll::ScrollbarState, Root};
|
||||||
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
#[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.
|
/// Called after moving the cursor. Updates preferred_column if we know where the cursor now is.
|
||||||
fn update_preferred_column(&mut self) {
|
fn update_preferred_column(&mut self) {
|
||||||
let column_ix = self.text.line_column(self.cursor().offset).1;
|
let column_ix = self.text.offset_to_point(self.cursor().offset).column;
|
||||||
self.preferred_column = Some(column_ix);
|
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.
|
/// 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 offset = self.cursor().offset;
|
||||||
let was_preferred_column = self.preferred_column;
|
let was_preferred_column = self.preferred_column;
|
||||||
|
|
||||||
let Ok(line_ix) = self.text.try_byte_to_line(offset) else {
|
let row = self.text.offset_to_point(offset).row;
|
||||||
return;
|
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 new_column = self.preferred_column.unwrap_or_default().min(line.len());
|
||||||
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_offset = line_start_offset + new_column;
|
let new_offset = line_start_offset + new_column;
|
||||||
|
|
||||||
self.pause_blink_cursor(cx);
|
self.pause_blink_cursor(cx);
|
||||||
|
|
@ -607,7 +595,7 @@ impl InputState {
|
||||||
// Ensure cursor to start when set text
|
// Ensure cursor to start when set text
|
||||||
if self.mode.is_single_line() {
|
if self.mode.is_single_line() {
|
||||||
self.selected_range =
|
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 {
|
} else {
|
||||||
self.selected_range = (Cursor::new(0)..Cursor::new(0)).into();
|
self.selected_range = (Cursor::new(0)..Cursor::new(0)).into();
|
||||||
}
|
}
|
||||||
|
|
@ -728,14 +716,14 @@ impl InputState {
|
||||||
/// Set the default value of the input field.
|
/// Set the default value of the input field.
|
||||||
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
|
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
|
||||||
let text: SharedString = value.into();
|
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.text_wrapper.text = self.text.clone();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the value of the input field.
|
/// Return the value of the input field.
|
||||||
pub fn value(&self) -> SharedString {
|
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.
|
/// Return the value without mask.
|
||||||
|
|
@ -746,11 +734,11 @@ impl InputState {
|
||||||
/// Return the (1-based) line and column of the cursor.
|
/// Return the (1-based) line and column of the cursor.
|
||||||
pub fn line_column(&self) -> LineColumn {
|
pub fn line_column(&self) -> LineColumn {
|
||||||
let offset = self.cursor().offset;
|
let offset = self.cursor().offset;
|
||||||
let (line_ix, column_ix) = self.text.line_column(offset);
|
let point = self.text.offset_to_point(offset);
|
||||||
|
|
||||||
LineColumn {
|
LineColumn {
|
||||||
line: line_ix + 1,
|
line: point.row as usize + 1,
|
||||||
column: column_ix + 1,
|
column: point.column as usize + 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -771,11 +759,20 @@ impl InputState {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let line_ix = line.saturating_sub(1);
|
let max_point = self.text.max_point();
|
||||||
let column_ix = column.unwrap_or(1).saturating_sub(1);
|
let line_ix = line.saturating_sub(1).min(max_point.row as usize);
|
||||||
let offset = self.text.line_column_to_byte(line_ix, column_ix);
|
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.move_to(Cursor::new(offset), window, cx);
|
||||||
|
self.update_preferred_column();
|
||||||
|
self.focus(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Focus the input field.
|
/// Focus the input field.
|
||||||
|
|
@ -916,7 +913,7 @@ impl InputState {
|
||||||
if self.mode.is_single_line() {
|
if self.mode.is_single_line() {
|
||||||
return;
|
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);
|
self.select_to(Cursor::new(self.next_boundary(offset)), window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -927,7 +924,7 @@ impl InputState {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.move_to(Cursor::new(0), window, cx);
|
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>) {
|
pub(super) fn home(&mut self, _: &MoveHome, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
|
@ -957,7 +954,7 @@ impl InputState {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let end = self.text.len_bytes();
|
let end = self.text.len();
|
||||||
self.move_to(Cursor::new(end), window, cx);
|
self.move_to(Cursor::new(end), window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -996,7 +993,7 @@ impl InputState {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let end = self.text.len_bytes();
|
let end = self.text.len();
|
||||||
self.select_to(Cursor::new(end), window, cx);
|
self.select_to(Cursor::new(end), window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1043,11 +1040,10 @@ impl InputState {
|
||||||
/// Return the start offset of the previous word.
|
/// Return the start offset of the previous word.
|
||||||
fn previous_start_of_word(&mut self) -> usize {
|
fn previous_start_of_word(&mut self) -> usize {
|
||||||
let offset = self.selected_range.start.offset;
|
let offset = self.selected_range.start.offset;
|
||||||
let prev_str = self
|
// FIXME: Avoid to_string
|
||||||
.text_for_range_utf8(0..offset)
|
let left_part = self.text.slice(0..offset).to_string();
|
||||||
.as_str()
|
|
||||||
.unwrap_or_default();
|
UnicodeSegmentation::split_word_bound_indices(left_part.as_str())
|
||||||
UnicodeSegmentation::split_word_bound_indices(prev_str)
|
|
||||||
.filter(|(_, s)| !s.trim_start().is_empty())
|
.filter(|(_, s)| !s.trim_start().is_empty())
|
||||||
.next_back()
|
.next_back()
|
||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
|
|
@ -1057,14 +1053,12 @@ impl InputState {
|
||||||
/// Return the next end offset of the next word.
|
/// Return the next end offset of the next word.
|
||||||
fn next_end_of_word(&mut self) -> usize {
|
fn next_end_of_word(&mut self) -> usize {
|
||||||
let offset = self.cursor().offset;
|
let offset = self.cursor().offset;
|
||||||
let next_str = self
|
let right_part = self.text.slice(offset..self.text.len()).to_string();
|
||||||
.text_for_range_utf8(offset..self.text.len_bytes())
|
|
||||||
.as_str()
|
UnicodeSegmentation::split_word_bound_indices(right_part.as_str())
|
||||||
.unwrap_or_default();
|
|
||||||
UnicodeSegmentation::split_word_bound_indices(next_str)
|
|
||||||
.find(|(_, s)| !s.trim_start().is_empty())
|
.find(|(_, s)| !s.trim_start().is_empty())
|
||||||
.map(|(i, s)| offset + i + s.len())
|
.map(|(i, s)| offset + i + s.len())
|
||||||
.unwrap_or(self.text.len_bytes())
|
.unwrap_or(self.text.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get start of line
|
/// Get start of line
|
||||||
|
|
@ -1096,7 +1090,7 @@ impl InputState {
|
||||||
.start
|
.start
|
||||||
.min(self.selected_range.end.offset),
|
.min(self.selected_range.end.offset),
|
||||||
);
|
);
|
||||||
if self.text.chars().nth(offset) == Some('\r') {
|
if self.text.char_at(offset) == Some('\r') {
|
||||||
offset += 1;
|
offset += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1112,11 +1106,11 @@ impl InputState {
|
||||||
/// Get end of line
|
/// Get end of line
|
||||||
fn end_of_line(&mut self, window: &mut Window, cx: &mut Context<Self>) -> usize {
|
fn end_of_line(&mut self, window: &mut Window, cx: &mut Context<Self>) -> usize {
|
||||||
if self.mode.is_single_line() {
|
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 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);
|
let offset = self.next_boundary(self.cursor().offset);
|
||||||
// ignore if offset is "\n"
|
// ignore if offset is "\n"
|
||||||
|
|
@ -1135,7 +1129,7 @@ impl InputState {
|
||||||
|
|
||||||
let line = self
|
let line = self
|
||||||
.text_for_range(
|
.text_for_range(
|
||||||
self.range_to_utf16(&(offset..self.text.len_bytes())),
|
self.range_to_utf16(&(offset..self.text.len())),
|
||||||
&mut None,
|
&mut None,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -1143,7 +1137,7 @@ impl InputState {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.find('\n')
|
.find('\n')
|
||||||
.map(|i| i + offset)
|
.map(|i| i + offset)
|
||||||
.unwrap_or(self.text.len_bytes());
|
.unwrap_or(self.text.len());
|
||||||
line
|
line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1242,7 +1236,7 @@ impl InputState {
|
||||||
) {
|
) {
|
||||||
let mut offset = self.end_of_line(window, cx);
|
let mut offset = self.end_of_line(window, cx);
|
||||||
if offset == self.cursor().offset {
|
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(
|
self.replace_text_in_range(
|
||||||
Some(self.range_to_utf16(&(self.cursor().offset..offset))),
|
Some(self.range_to_utf16(&(self.cursor().offset..offset))),
|
||||||
|
|
@ -1450,10 +1444,11 @@ impl InputState {
|
||||||
// Selected none
|
// Selected none
|
||||||
let start_offset = self.selected_range.start;
|
let start_offset = self.selected_range.start;
|
||||||
let offset = self.start_of_line_of_selection(window, cx);
|
let offset = self.start_of_line_of_selection(window, cx);
|
||||||
|
// FIXME: To improve performance
|
||||||
if self
|
if self
|
||||||
.text_for_range_utf8(offset..self.text.len_bytes())
|
.text
|
||||||
.as_str()
|
.slice(offset..self.text.len())
|
||||||
.unwrap_or("")
|
.to_string()
|
||||||
.starts_with(tab_indent.as_ref())
|
.starts_with(tab_indent.as_ref())
|
||||||
{
|
{
|
||||||
self.replace_text_in_range(
|
self.replace_text_in_range(
|
||||||
|
|
@ -1600,7 +1595,7 @@ impl InputState {
|
||||||
return;
|
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));
|
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1609,7 +1604,7 @@ impl InputState {
|
||||||
return;
|
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));
|
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
|
||||||
self.replace_text_in_range(None, "", window, cx);
|
self.replace_text_in_range(None, "", window, cx);
|
||||||
}
|
}
|
||||||
|
|
@ -1625,20 +1620,12 @@ impl InputState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_history(
|
fn push_history(&mut self, text: &Rope, range: &Range<usize>, new_text: &str) {
|
||||||
&mut self,
|
|
||||||
range: &Range<usize>,
|
|
||||||
new_text: &str,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if self.history.ignore {
|
if self.history.ignore {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let old_text = self
|
let old_text = text.slice(range.clone()).to_string();
|
||||||
.text_for_range(self.range_to_utf16(&range), &mut None, window, cx)
|
|
||||||
.unwrap_or("".to_string());
|
|
||||||
|
|
||||||
let new_range = range.start..range.start + new_text.len();
|
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.
|
/// 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>) {
|
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.selected_range = (cursor..cursor).into();
|
||||||
self.pause_blink_cursor(cx);
|
self.pause_blink_cursor(cx);
|
||||||
self.update_preferred_column();
|
self.update_preferred_column();
|
||||||
|
|
@ -1703,7 +1690,7 @@ impl InputState {
|
||||||
_cx: &App,
|
_cx: &App,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
// If the text is empty, always return 0
|
// If the text is empty, always return 0
|
||||||
if self.text.len_bytes() == 0 {
|
if self.text.len() == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1768,8 +1755,8 @@ impl InputState {
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if index > self.text.len_bytes() {
|
if index > self.text.len() {
|
||||||
self.text.len_bytes()
|
self.text.len()
|
||||||
} else {
|
} else {
|
||||||
index
|
index
|
||||||
}
|
}
|
||||||
|
|
@ -1802,7 +1789,7 @@ impl InputState {
|
||||||
///
|
///
|
||||||
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
|
/// 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>) {
|
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 {
|
if self.selection_reversed {
|
||||||
self.selected_range.start = Cursor::new(offset)
|
self.selected_range.start = Cursor::new(offset)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1847,7 +1834,7 @@ impl InputState {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let next_text = self
|
let next_text = self
|
||||||
.text_for_range(
|
.text_for_range(
|
||||||
self.range_to_utf16(&(end..self.text.len_bytes())),
|
self.range_to_utf16(&(end..self.text.len())),
|
||||||
&mut None,
|
&mut None,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -1931,24 +1918,11 @@ impl InputState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn previous_boundary(&self, offset: usize) -> usize {
|
fn previous_boundary(&self, offset: usize) -> usize {
|
||||||
let char_ix = self.text.byte_to_char(offset).saturating_sub(1);
|
self.text.clip_offset(offset.saturating_sub(1), Bias::Left)
|
||||||
self.text.char_to_byte(char_ix)
|
|
||||||
|
|
||||||
// self.text
|
|
||||||
// .grapheme_indices(true)
|
|
||||||
// .rev()
|
|
||||||
// .find_map(|(idx, _)| (idx < offset).then_some(idx))
|
|
||||||
// .unwrap_or(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_boundary(&self, offset: usize) -> usize {
|
fn next_boundary(&self, offset: usize) -> usize {
|
||||||
let char_ix = self.text.byte_to_char(offset) + 1;
|
self.text.clip_offset(offset + 1, Bias::Right)
|
||||||
self.text.char_to_byte(char_ix).min(self.text.len_bytes())
|
|
||||||
|
|
||||||
// self.text
|
|
||||||
// .grapheme_indices(true)
|
|
||||||
// .find_map(|(idx, _)| (idx > offset).then_some(idx))
|
|
||||||
// .unwrap_or(self.text.len())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible.
|
/// 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,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if self.text.len_bytes() == 0 {
|
if self.text.len() == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2026,10 +2000,11 @@ impl InputState {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pattern
|
let Some(pattern) = &self.pattern else {
|
||||||
.as_ref()
|
return true;
|
||||||
.map(|p| p.is_match(new_text))
|
};
|
||||||
.unwrap_or(true)
|
|
||||||
|
pattern.is_match(new_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the mask pattern for formatting the input 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 {
|
impl EntityInputHandler for InputState {
|
||||||
|
|
@ -2099,7 +2069,7 @@ impl EntityInputHandler for InputState {
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let range = self.range_from_utf16(&range_utf16);
|
let range = self.range_from_utf16(&range_utf16);
|
||||||
adjusted_range.replace(self.range_to_utf16(&range));
|
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(
|
fn selected_text_range(
|
||||||
|
|
@ -2135,7 +2105,7 @@ impl EntityInputHandler for InputState {
|
||||||
&mut self,
|
&mut self,
|
||||||
range_utf16: Option<Range<usize>>,
|
range_utf16: Option<Range<usize>>,
|
||||||
new_text: &str,
|
new_text: &str,
|
||||||
window: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if self.disabled {
|
if self.disabled {
|
||||||
|
|
@ -2148,23 +2118,26 @@ impl EntityInputHandler for InputState {
|
||||||
.or(self.marked_range.map(|range| range.into()))
|
.or(self.marked_range.map(|range| range.into()))
|
||||||
.unwrap_or(self.selected_range.into());
|
.unwrap_or(self.selected_range.into());
|
||||||
|
|
||||||
let pending_text: SharedString = (self.text_for_range_utf8(0..range.start).to_string()
|
let old_text = self.text.clone();
|
||||||
+ new_text
|
self.text.replace(range.clone(), new_text);
|
||||||
+ &self
|
|
||||||
.text_for_range_utf8(range.end..self.text.len_bytes())
|
let pending_text = self.text.to_string();
|
||||||
.to_string())
|
|
||||||
.into();
|
|
||||||
// Check if the new text is valid
|
// Check if the new text is valid
|
||||||
if !self.is_valid_input(&pending_text, cx) {
|
if !self.is_valid_input(&pending_text, cx) {
|
||||||
|
self.text = old_text;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mask_text = self.mask_pattern.mask(&pending_text);
|
let mut new_offset = (range.start + new_text.len()).min(self.text.len());
|
||||||
let new_text_len = (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
|
if !self.mask_pattern.is_none() {
|
||||||
let new_offset = (range.start + new_text_len).min(mask_text.len());
|
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.push_history(&old_text, &range, &new_text);
|
||||||
self.text = Rope::from_str(&mask_text);
|
|
||||||
|
|
||||||
self.mode.clear_markers();
|
self.mode.clear_markers();
|
||||||
self.text_wrapper.update(&self.text, false, cx);
|
self.text_wrapper.update(&self.text, false, cx);
|
||||||
|
|
@ -2185,7 +2158,7 @@ impl EntityInputHandler for InputState {
|
||||||
range_utf16: Option<Range<usize>>,
|
range_utf16: Option<Range<usize>>,
|
||||||
new_text: &str,
|
new_text: &str,
|
||||||
new_selected_range_utf16: Option<Range<usize>>,
|
new_selected_range_utf16: Option<Range<usize>>,
|
||||||
window: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if self.disabled {
|
if self.disabled {
|
||||||
|
|
@ -2197,18 +2170,17 @@ impl EntityInputHandler for InputState {
|
||||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||||
.or(self.marked_range.map(|range| range.into()))
|
.or(self.marked_range.map(|range| range.into()))
|
||||||
.unwrap_or(self.selected_range.into());
|
.unwrap_or(self.selected_range.into());
|
||||||
let pending_text: SharedString = (self.text_for_range_utf8(0..range.start).to_string()
|
|
||||||
+ new_text
|
let old_text = self.text.clone();
|
||||||
+ &self
|
self.text.replace(range.clone(), new_text);
|
||||||
.text_for_range_utf8(range.end..self.text.len_bytes())
|
let pending_text = self.text.to_string();
|
||||||
.to_string())
|
|
||||||
.into();
|
|
||||||
if !self.is_valid_input(&pending_text, cx) {
|
if !self.is_valid_input(&pending_text, cx) {
|
||||||
|
self.text = old_text;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.push_history(&range, new_text, window, cx);
|
self.push_history(&old_text, &range, new_text);
|
||||||
self.text = Rope::from_str(&pending_text);
|
|
||||||
self.mode.clear_markers();
|
self.mode.clear_markers();
|
||||||
self.text_wrapper.update(&self.text, false, cx);
|
self.text_wrapper.update(&self.text, false, cx);
|
||||||
self.mode
|
self.mode
|
||||||
|
|
@ -2232,7 +2204,6 @@ impl EntityInputHandler for InputState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used to position IME candidates.
|
/// Used to position IME candidates.
|
||||||
/// TODO: Fix position of IME candidates in multi-line text input.
|
|
||||||
fn bounds_for_range(
|
fn bounds_for_range(
|
||||||
&mut self,
|
&mut self,
|
||||||
range_utf16: Range<usize>,
|
range_utf16: Range<usize>,
|
||||||
|
|
|
||||||
|
|
@ -174,10 +174,8 @@ impl RenderOnce for TextInput {
|
||||||
|
|
||||||
let prefix = self.prefix;
|
let prefix = self.prefix;
|
||||||
let suffix = self.suffix;
|
let suffix = self.suffix;
|
||||||
let show_clear_button = self.cleanable
|
let show_clear_button =
|
||||||
&& !state.loading
|
self.cleanable && !state.loading && state.text.len() > 0 && state.mode.is_single_line();
|
||||||
&& state.text.len_bytes() > 0
|
|
||||||
&& state.mode.is_single_line();
|
|
||||||
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
|
let has_suffix = suffix.is_some() || state.loading || self.mask_toggle || show_clear_button;
|
||||||
|
|
||||||
div()
|
div()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use gpui::{App, Font, LineFragment, Pixels};
|
use gpui::{App, Font, LineFragment, Pixels};
|
||||||
use ropey::Rope;
|
use rope::Rope;
|
||||||
|
|
||||||
|
use crate::input::RopeExt as _;
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub(super) struct LineWrap {
|
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) {
|
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.wrap_width = wrap_width;
|
||||||
self.update(&self.text.clone(), true, cx);
|
self.update(&self.text.clone(), true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn set_font(&mut self, font: Font, font_size: Pixels, cx: &mut App) {
|
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 = font;
|
||||||
self.font_size = font_size;
|
self.font_size = font_size;
|
||||||
self.update(&self.text.clone(), true, cx);
|
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.
|
/// 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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,15 +80,15 @@ impl TextWrapper {
|
||||||
.line_wrapper(self.font.clone(), self.font_size);
|
.line_wrapper(self.font.clone(), self.font_size);
|
||||||
let mut prev_line_ix = 0;
|
let mut prev_line_ix = 0;
|
||||||
|
|
||||||
// FIXME: here may need use from Rope
|
for line in text.lines() {
|
||||||
for line in text.to_string().split('\n') {
|
let line = line.to_string();
|
||||||
let mut line_wraps = vec![];
|
let mut line_wraps = vec![];
|
||||||
let mut prev_boundary_ix = 0;
|
let mut prev_boundary_ix = 0;
|
||||||
|
|
||||||
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
|
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
|
||||||
if let Some(wrap_width) = wrap_width {
|
if let Some(wrap_width) = wrap_width {
|
||||||
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
|
// 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);
|
line_wraps.push(prev_boundary_ix..boundary.ix);
|
||||||
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());
|
wrapped_lines.push(prev_line_ix + prev_boundary_ix..prev_line_ix + line.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// +1 for \n
|
||||||
prev_line_ix += line.len() + 1;
|
prev_line_ix += line.len() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use gpui::{
|
||||||
StatefulInteractiveElement, Styled, StyledImage as _, Window,
|
StatefulInteractiveElement, Styled, StyledImage as _, Window,
|
||||||
};
|
};
|
||||||
use markdown::mdast;
|
use markdown::mdast;
|
||||||
use ropey::Rope;
|
use rope::Rope;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
h_flex,
|
h_flex,
|
||||||
|
|
@ -292,7 +292,7 @@ impl CodeBlock {
|
||||||
let mut styles = vec![];
|
let mut styles = vec![];
|
||||||
if let Some(lang) = &lang {
|
if let Some(lang) = &lang {
|
||||||
let mut highlighter = SyntaxHighlighter::new(&lang, cx);
|
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);
|
styles = highlighter.styles(&(0..code.len()), &theme);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue