diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a8e10eeff..b35fc6bc6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,21 @@ ## Unreleased +### Features/Changes +- [#2723](https://github.com/lapce/lapce/pull/2723): Line wrapping based on width (no column-based yet) +- [#1277](https://github.com/lapce/lapce/pull/1277): Error message prompted on missing git user.email and/or user.name +- [#2910](https://github.com/lapce/lapce/pull/2910): Files can be compared in the diff editor + +### Bug Fixes +- [#2779](https://github.com/lapce/lapce/pull/2779): Fix files detection on fresh git/VCS repository + +## 0.3.1 + ### Features/Changes ### Bug Fixes - [#2754](https://github.com/lapce/lapce/pull/2754): Don't mark nonexistent files as read only (fix saving new files) -- [#2819](https://github.com/lapce/lapce/issues/2819): `Save Witohut Formatting` doesn't save the file +- [#2819](https://github.com/lapce/lapce/issues/2819): `Save Without Formatting` doesn't save the file ## 0.3.0 diff --git a/Cargo.lock b/Cargo.lock index 618b0f5ee7..5d7ae658c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,6 +179,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anyhow" version = "1.0.69" @@ -690,6 +696,12 @@ dependencies = [ "winx", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.0.83" @@ -733,6 +745,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half 1.8.2", +] + [[package]] name = "clap" version = "3.2.25" @@ -772,25 +811,13 @@ dependencies = [ "os_str_bytes", ] -[[package]] -name = "clipboard" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" -dependencies = [ - "clipboard-win", - "objc", - "objc-foundation", - "objc_id", - "x11-clipboard", -] - [[package]] name = "clipboard-win" -version = "2.2.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" dependencies = [ + "lazy-bytes-cast", "winapi", ] @@ -889,6 +916,20 @@ dependencies = [ "toml 0.5.9", ] +[[package]] +name = "copypasta" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d35364349bf9e9e1c3a035ddcb00d188d23a3c40c50244c03c27a99fc6a65ae" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -933,7 +974,7 @@ dependencies = [ [[package]] name = "cosmic-text" version = "0.7.0" -source = "git+https://github.com/lapce/cosmic-text?rev=f7a20704d6ebbe8fb82d0bb579c37c53e7ae9747#f7a20704d6ebbe8fb82d0bb579c37c53e7ae9747" +source = "git+https://github.com/lapce/cosmic-text?rev=fdc5165f79bb24c76bfeb335dbfa133094636e25#fdc5165f79bb24c76bfeb335dbfa133094636e25" dependencies = [ "fontdb 0.16.0", "libm", @@ -1096,6 +1137,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -1179,9 +1256,9 @@ dependencies = [ [[package]] name = "cursor-icon" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740bb192a8e2d1350119916954f4409ee7f62f149b536911eeb78ba5a20526bf" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" [[package]] name = "d3d12" @@ -1510,7 +1587,7 @@ checksum = "832a761f35ab3e6664babfbdc6cef35a4860e816ec3916dcfd0882954e98a8a8" dependencies = [ "bit_field", "flume", - "half", + "half 2.2.1", "lebe", "miniz_oxide 0.7.1", "rayon-core", @@ -1596,10 +1673,10 @@ checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" [[package]] name = "floem" version = "0.1.0" -source = "git+https://github.com/lapce/floem?rev=4621c89a5f3d43ec7d8d0f51c0a0b7214eb03a56#4621c89a5f3d43ec7d8d0f51c0a0b7214eb03a56" +source = "git+https://github.com/lapce/floem?rev=6ae536237716d606e5546edb9f72867f2bc23946#6ae536237716d606e5546edb9f72867f2bc23946" dependencies = [ "bitflags 2.4.0", - "clipboard", + "copypasta", "crossbeam-channel", "educe", "floem_reactive", @@ -1626,7 +1703,7 @@ dependencies = [ [[package]] name = "floem_reactive" version = "0.1.0" -source = "git+https://github.com/lapce/floem?rev=4621c89a5f3d43ec7d8d0f51c0a0b7214eb03a56#4621c89a5f3d43ec7d8d0f51c0a0b7214eb03a56" +source = "git+https://github.com/lapce/floem?rev=6ae536237716d606e5546edb9f72867f2bc23946#6ae536237716d606e5546edb9f72867f2bc23946" dependencies = [ "smallvec", ] @@ -1634,7 +1711,7 @@ dependencies = [ [[package]] name = "floem_renderer" version = "0.1.0" -source = "git+https://github.com/lapce/floem?rev=4621c89a5f3d43ec7d8d0f51c0a0b7214eb03a56#4621c89a5f3d43ec7d8d0f51c0a0b7214eb03a56" +source = "git+https://github.com/lapce/floem?rev=6ae536237716d606e5546edb9f72867f2bc23946#6ae536237716d606e5546edb9f72867f2bc23946" dependencies = [ "cosmic-text", "image", @@ -1645,7 +1722,7 @@ dependencies = [ [[package]] name = "floem_tiny_skia" version = "0.1.0" -source = "git+https://github.com/lapce/floem?rev=4621c89a5f3d43ec7d8d0f51c0a0b7214eb03a56#4621c89a5f3d43ec7d8d0f51c0a0b7214eb03a56" +source = "git+https://github.com/lapce/floem?rev=6ae536237716d606e5546edb9f72867f2bc23946#6ae536237716d606e5546edb9f72867f2bc23946" dependencies = [ "anyhow", "bytemuck", @@ -1662,7 +1739,7 @@ dependencies = [ [[package]] name = "floem_vger" version = "0.1.0" -source = "git+https://github.com/lapce/floem?rev=4621c89a5f3d43ec7d8d0f51c0a0b7214eb03a56#4621c89a5f3d43ec7d8d0f51c0a0b7214eb03a56" +source = "git+https://github.com/lapce/floem?rev=6ae536237716d606e5546edb9f72867f2bc23946#6ae536237716d606e5546edb9f72867f2bc23946" dependencies = [ "anyhow", "floem_renderer", @@ -1974,6 +2051,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + [[package]] name = "getopts" version = "0.2.21" @@ -2212,6 +2299,12 @@ dependencies = [ "tracing 0.1.37", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "half" version = "2.2.1" @@ -2799,7 +2892,7 @@ dependencies = [ [[package]] name = "lapce" -version = "0.3.0" +version = "0.3.1" dependencies = [ "lapce-app", "lapce-proxy", @@ -2807,7 +2900,7 @@ dependencies = [ [[package]] name = "lapce-app" -version = "0.3.0" +version = "0.3.1" dependencies = [ "Inflector", "alacritty_terminal", @@ -2816,8 +2909,8 @@ dependencies = [ "bytemuck", "chrono", "clap", - "clipboard", "config", + "criterion", "crossbeam-channel", "directories", "dmg", @@ -2867,7 +2960,7 @@ dependencies = [ [[package]] name = "lapce-core" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "arc-swap", @@ -2901,7 +2994,7 @@ dependencies = [ [[package]] name = "lapce-proxy" -version = "0.3.0" +version = "0.3.1" dependencies = [ "alacritty_terminal", "anyhow", @@ -2954,7 +3047,7 @@ dependencies = [ [[package]] name = "lapce-rpc" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "crossbeam-channel", @@ -2982,6 +3075,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "lazy-bytes-cast" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + [[package]] name = "lazy_static" version = "1.4.0" @@ -3129,9 +3228,8 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.93.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3bcfee315dde785ba887edb540b08765fd7df75a7d948844be6bf5712246734" +version = "0.94.1" +source = "git+https://github.com/lapce/lsp-types?rev=3031a76c4452f46ed265eb0154d6bb1d10ddb9f6#3031a76c4452f46ed265eb0154d6bb1d10ddb9f6" dependencies = [ "bitflags 1.3.2", "serde", @@ -3633,6 +3731,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + [[package]] name = "open" version = "5.0.0" @@ -3904,6 +4008,34 @@ dependencies = [ "time 0.3.14", ] +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + [[package]] name = "png" version = "0.17.10" @@ -4836,6 +4968,17 @@ dependencies = [ "xkeysym", ] +[[package]] +name = "smithay-clipboard" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb62b280ce5a5cba847669933a0948d00904cf83845c944eae96a4738cea1a6" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend 0.3.2", +] + [[package]] name = "smol_str" version = "0.2.0" @@ -4894,7 +5037,7 @@ dependencies = [ "wayland-sys 0.30.1", "web-sys", "windows-sys 0.48.0", - "x11rb", + "x11rb 0.12.0", ] [[package]] @@ -5300,6 +5443,16 @@ dependencies = [ "tracing 0.1.37", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -5927,7 +6080,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vger" version = "0.2.7" -source = "git+https://github.com/lapce/vger-rs?rev=ed10537c72a732a03f782225a39da80e6f9acbbe#ed10537c72a732a03f782225a39da80e6f9acbbe" +source = "git+https://github.com/lapce/vger-rs?rev=1820c59e09fa731d0867a908d0ef094d27e1b3fb#1820c59e09fa731d0867a908d0ef094d27e1b3fb" dependencies = [ "cosmic-text", "euclid", @@ -7136,8 +7289,8 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winit" -version = "0.29.3" -source = "git+https://github.com/lapce/winit?rev=7608048ad91efceb6d97d03dcd74b33c60cc2072#7608048ad91efceb6d97d03dcd74b33c60cc2072" +version = "0.29.4" +source = "git+https://github.com/lapce/winit?rev=a75f4124c5f26990d061a49ae7451f4bbad8bc0e#a75f4124c5f26990d061a49ae7451f4bbad8bc0e" dependencies = [ "ahash 0.8.3", "android-activity", @@ -7178,7 +7331,7 @@ dependencies = [ "web-time", "windows-sys 0.48.0", "x11-dl", - "x11rb", + "x11rb 0.13.0", "xkbcommon-dl", ] @@ -7242,11 +7395,11 @@ dependencies = [ [[package]] name = "x11-clipboard" -version = "0.3.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" +checksum = "b41aca1115b1f195f21c541c5efb423470848d48143127d0f07f8b90c27440df" dependencies = [ - "xcb", + "x11rb 0.12.0", ] [[package]] @@ -7267,14 +7420,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" dependencies = [ "as-raw-xcb-connection", - "gethostname", + "gethostname 0.3.0", "libc", "libloading 0.7.3", "nix 0.26.4", "once_cell", "winapi", "winapi-wsapoll", - "x11rb-protocol", + "x11rb-protocol 0.12.0", +] + +[[package]] +name = "x11rb" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +dependencies = [ + "as-raw-xcb-connection", + "gethostname 0.4.3", + "libc", + "libloading 0.8.1", + "once_cell", + "rustix 0.38.20", + "x11rb-protocol 0.13.0", ] [[package]] @@ -7287,22 +7455,18 @@ dependencies = [ ] [[package]] -name = "xattr" -version = "0.2.3" +name = "x11rb-protocol" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" -dependencies = [ - "libc", -] +checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" [[package]] -name = "xcb" -version = "0.8.2" +name = "xattr" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" dependencies = [ "libc", - "log", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index de23504ae7..040b9553b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lapce" -version = "0.3.0" +version = "0.3.1" authors = ["Dongdong Zhou "] edition = "2021" rust-version = "1.65" @@ -27,7 +27,7 @@ members = [ ] [workspace.package] -version = "0.3.0" +version = "0.3.1" edition = "2021" rust-version = "1.64" homepage = "https://lapce.dev" @@ -63,7 +63,7 @@ toml = { version = "*" } toml_edit = { version = "0.19.14", features = ["serde"] } url = { version = "2.3.1" } -lsp-types = { version = "0.93", features = ["proposed"] } +lsp-types = { version = "0.94.1", features = ["proposed"] } # not following semver, so should be locked to patch version updates only psp-types = { git = "https://github.com/lapce/psp-types", rev = "f7fea28f59e7b2d6faa1034a21679ad49b3524ad" } lapce-xi-rope = { version = "0.3.2", features = ["serde"] } @@ -72,6 +72,10 @@ lapce-core = { path = "./lapce-core" } lapce-rpc = { path = "./lapce-rpc" } lapce-proxy = { path = "./lapce-proxy" } +[patch.crates-io] +# Temporarily patch lsp-types with a version that supports inline-completion +lsp-types = { git = "https://github.com/lapce/lsp-types", rev = "3031a76c4452f46ed265eb0154d6bb1d10ddb9f6" } + [workspace.dependencies.tracing] git = "https://github.com/tokio-rs/tracing" rev = "c14525e1610db88986f849d46bd3e9795878b012" diff --git a/defaults/keymaps-common.toml b/defaults/keymaps-common.toml index 0143e91805..227937efcf 100644 --- a/defaults/keymaps-common.toml +++ b/defaults/keymaps-common.toml @@ -205,7 +205,37 @@ mode = "n" [[keymaps]] key = "esc" command = "modal.close" -when = "modal_focus" +when = "modal_focus || completion_focus" + +[[keymaps]] +key = "tab" +command = "inline_completion.select" +when = "inline_completion_visible && !search_focus && !modal_focus && !list_focus && !search_active" +mode = "i" + +[[keymaps]] +key = "esc" +command = "inline_completion.cancel" +when = "inline_completion_visible && !search_focus && !modal_focus && !list_focus && !search_active" +mode = "i" + +[[keymaps]] +key = "alt+[" +command = "inline_completion.previous" +when = "inline_completion_visible && !search_focus && !modal_focus && !list_focus && !search_active" +mode = "i" + +[[keymaps]] +key = "alt+]" +command = "inline_completion.next" +when = "inline_completion_visible && !search_focus && !modal_focus && !list_focus && !search_active" +mode = "i" + +[[keymaps]] +key = "alt+\\" +command = "inline_completion.invoke" +when = "!inline_completion_visible && !search_focus && !modal_focus && !list_focus && !search_active" +mode = "i" [[keymaps]] key = "ctrl+b" @@ -299,7 +329,7 @@ mode = "i" [[keymaps]] key = "tab" command = "insert_tab" -when = "!in_snippet && !completion_focus && !search_focus && !replace_focus" +when = "!in_snippet && !completion_focus && !inline_completion_visible && !search_focus && !replace_focus" mode = "i" [[keymaps]] @@ -324,7 +354,7 @@ mode = "i" key = "esc" command = "normal_mode" mode = "niv" -when = "!search_focus && !modal_focus && !search_active" +when = "!search_focus && !modal_focus && !search_active && !inline_completion_visible" [[keymaps]] key = "ctrl+c" diff --git a/defaults/settings.toml b/defaults/settings.toml index c7062a564c..1ba286884e 100644 --- a/defaults/settings.toml +++ b/defaults/settings.toml @@ -17,6 +17,9 @@ show-tab = true show-bread-crumbs = true scroll-beyond-last-line = true cursor-surrounding-lines = 1 +wrap-style = "editor-width" +wrap-column = 80 +wrap-width = 600 # px sticky-header = true completion-show-documentation = true show-signature = true @@ -38,7 +41,9 @@ enable-error-lens = true error-lens-end-of-line = true error-lens-font-family = "" error-lens-font-size = 0 +error-lens-multiline = false enable-completion-lens = false +enable-inline-completion = true completion-lens-font-family = "" completion-lens-font-size = 0 blink-interval = 500 # ms @@ -49,8 +54,8 @@ show-indent-guide = true atomic-soft-tabs = false double-click = "single" move-focus-while-search = true -diff-context-lines=3 -scroll-speed-modifier=1 +diff-context-lines = 3 +scroll-speed-modifier = 1 [terminal] font-family = "" diff --git a/docs/installing-with-package-manager.md b/docs/installing-with-package-manager.md index 05cae2dbae..62db5161a8 100644 --- a/docs/installing-with-package-manager.md +++ b/docs/installing-with-package-manager.md @@ -1,87 +1,10 @@ ## Installation With Package Manager -### Arch Linux +Lapce is available in below software repositories: -There is an community package that can be installed with `pacman`: +[![Packaging status](https://repology.org/badge/vertical-allrepos/lapce.svg)](https://repology.org/project/lapce/versions) -```bash -sudo pacman -Syu lapce -``` - -### Fedora - -```bash -sudo dnf copr enable titaniumtown/lapce -sudo dnf install lapce -``` - -### Flatpak - -Lapce is available as a flatpak [here](https://flathub.org/apps/details/dev.lapce.lapce) - -```bash -flatpak install flathub dev.lapce.lapce -``` - -### Gentoo - -Lapce is available in Gentoos user repository GURU. -If the GURU is not activated, it can be with: - -```bash -emerge --ask app-eselect/eselect-repository # install eselect repository -eselect repository enable guru -emaint sync -r guru -``` - -After activating and syncing the GURU repository, lapce can be installed with - -```bash -emerge app-editors/lapce -``` - -### Homebrew - -```bash -brew install lapce -``` - -### nixpkgs - -You can find the packages [here](https://search.nixos.org/packages?channel=unstable&show=lapce&from=0&size=50&sort=relevance&type=packages&query=lapce): - -```bash -# try with nix-shell -nix-shell -p lapce - -# on NixOS -nix-env -iA nixos.lapce - -# on non-NixOS installs, including macOS -nix-env -iA nixpkgs.lapce - -# only if `nix.settings.experimental-features` includes both `nix-command` and `flakes`. -# WARNING: THIS BREAKS nix-env, PROCEED AT YOUR OWN RISK. THIS ALSO INSTALLS FROM UNSTABLE BRANCH. -nix profile install nixpkgs#lapce -``` - -### Scoop - -```bash -scoop install lapce -``` - -### Void Linux - -```bash -sudo xbps-install -S lapce -``` - -### winget - -You can find the packages [here](https://github.com/microsoft/winget-pkgs/tree/master/manifests/l/Lapce/Lapce): - -```bash -winget install lapce -``` +Lapce is also additionally available via: +- [Flatpak](https://flathub.org/apps/details/dev.lapce.lapce) +- [Scoop](https://scoop.sh/#/apps?q=lapce) diff --git a/extra/linux/dev.lapce.lapce.metainfo.xml b/extra/linux/dev.lapce.lapce.metainfo.xml index 6e97aa781a..12f8d45648 100644 --- a/extra/linux/dev.lapce.lapce.metainfo.xml +++ b/extra/linux/dev.lapce.lapce.metainfo.xml @@ -30,6 +30,6 @@ - + diff --git a/extra/macos/Lapce.app/Contents/Info.plist b/extra/macos/Lapce.app/Contents/Info.plist index ff59fcc0a6..f2374a26ae 100644 --- a/extra/macos/Lapce.app/Contents/Info.plist +++ b/extra/macos/Lapce.app/Contents/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.3.0 + 0.3.1 CFBundleSupportedPlatforms MacOSX diff --git a/extra/windows/wix/lapce.wxs b/extra/windows/wix/lapce.wxs index 3520f01070..64a9d27c32 100644 --- a/extra/windows/wix/lapce.wxs +++ b/extra/windows/wix/lapce.wxs @@ -1,6 +1,6 @@ - + diff --git a/lapce-app/Cargo.toml b/lapce-app/Cargo.toml index 1f5d1c18d6..442065e207 100644 --- a/lapce-app/Cargo.toml +++ b/lapce-app/Cargo.toml @@ -51,8 +51,7 @@ sled = "0.34.7" bytemuck = "1.14.0" tokio = { version = "1.21", features = ["full"] } futures = "0.3.26" -clipboard = "0.5.0" -floem = { git = "https://github.com/lapce/floem", rev = "4621c89a5f3d43ec7d8d0f51c0a0b7214eb03a56" } +floem = { git = "https://github.com/lapce/floem", rev = "6ae536237716d606e5546edb9f72867f2bc23946" } # floem = { path = "../../workspaces/floem" } config = { version = "0.13.2", default-features = false, features = ["toml"] } structdesc = { git = "https://github.com/lapce/structdesc" } @@ -86,3 +85,10 @@ all-languages = [ "lapce-core/lang-toml", "lapce-core/lang-yaml", ] + +[dev-dependencies] +criterion = "0.4" + +[[bench]] +name = "visual_line" +harness = false diff --git a/lapce-app/benches/visual_line.rs b/lapce-app/benches/visual_line.rs new file mode 100644 index 0000000000..90a8135cff --- /dev/null +++ b/lapce-app/benches/visual_line.rs @@ -0,0 +1,280 @@ +use std::{cell::RefCell, collections::HashMap, sync::Arc}; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use floem::{ + cosmic_text::{Attrs, AttrsList, FamilyOwned, TextLayout, Wrap}, + reactive::Scope, +}; +use lapce_app::{ + doc::phantom_text::PhantomTextLine, + editor::{ + view_data::TextLayoutLine, + visual_line::{ + FontSizeCacheId, LineFontSizeProvider, Lines, ResolvedWrap, + TextLayoutProvider, VLine, + }, + }, +}; +use lapce_core::{ + buffer::rope_text::{RopeText, RopeTextRef}, + cursor::CursorAffinity, +}; +use lapce_xi_rope::Rope; + +const FONT_SIZE: usize = 12; + +// TODO: use the editor data view structures! +struct TLProv<'a> { + text: &'a Rope, + phantom: HashMap, + font_family: Vec, + has_multiline_phantom: bool, +} +impl<'a> TextLayoutProvider for TLProv<'a> { + fn text(&self) -> &Rope { + self.text + } + + // An implementation relatively close to the actual new text layout impl but simplified. + // TODO(minor): It would be nice to just use the same impl as view's + fn new_text_layout( + &self, + line: usize, + font_size: usize, + wrap: ResolvedWrap, + ) -> Arc { + let rope_text = RopeTextRef::new(self.text); + let line_content_original = rope_text.line_content(line); + + // Get the line content with newline characters replaced with spaces + // and the content without the newline characters + let (line_content, _line_content_original) = + if let Some(s) = line_content_original.strip_suffix("\r\n") { + ( + format!("{s} "), + &line_content_original[..line_content_original.len() - 2], + ) + } else if let Some(s) = line_content_original.strip_suffix('\n') { + ( + format!("{s} ",), + &line_content_original[..line_content_original.len() - 1], + ) + } else { + ( + line_content_original.to_string(), + &line_content_original[..], + ) + }; + + let phantom_text = self.phantom.get(&line).cloned().unwrap_or_default(); + let line_content = phantom_text.combine_with_text(line_content); + + let attrs = Attrs::new() + .family(&self.font_family) + .font_size(font_size as f32); + let mut attrs_list = AttrsList::new(attrs); + + // We don't do line styles, since they aren't relevant + + // Apply phantom text specific styling + for (offset, size, col, phantom) in phantom_text.offset_size_iter() { + let start = col + offset; + let end = start + size; + + let mut attrs = attrs; + if let Some(fg) = phantom.fg { + attrs = attrs.color(fg); + } + if let Some(phantom_font_size) = phantom.font_size { + attrs = attrs.font_size(phantom_font_size.min(font_size) as f32); + } + attrs_list.add_span(start..end, attrs); + // if let Some(font_family) = phantom.font_family.clone() { + // layout_builder = layout_builder.range_attribute( + // start..end, + // TextAttribute::FontFamily(font_family), + // ); + // } + } + + let mut text_layout = TextLayout::new(); + text_layout.set_wrap(Wrap::Word); + match wrap { + // We do not have to set the wrap mode if we do not set the width + ResolvedWrap::None => {} + ResolvedWrap::Column(_col) => todo!(), + ResolvedWrap::Width(px) => { + text_layout.set_size(px, f32::MAX); + } + } + text_layout.set_text(&line_content, attrs_list); + + // skip phantom text background styling because it doesn't shift positions + // skip severity styling + // skip diagnostic background styling + + Arc::new(TextLayoutLine { + extra_style: Vec::new(), + text: text_layout, + whitespaces: None, + indent: 0.0, + }) + } + + fn before_phantom_col(&self, line: usize, col: usize) -> usize { + if let Some(phantom) = self.phantom.get(&line) { + phantom.before_col(col) + } else { + col + } + } + + fn has_multiline_phantom(&self) -> bool { + self.has_multiline_phantom + } +} +struct TestFontSize; +impl LineFontSizeProvider for TestFontSize { + fn font_size(&self, _line: usize) -> usize { + FONT_SIZE + } + + fn cache_id(&self) -> FontSizeCacheId { + 0 + } +} + +fn make_lines(text: &Rope, wrap: ResolvedWrap, init: bool) -> (TLProv<'_>, Lines) { + make_lines_ph(text, wrap, init, HashMap::new(), false) +} + +fn make_lines_ph( + text: &Rope, + wrap: ResolvedWrap, + init: bool, + ph: HashMap, + has_multiline_phantom: bool, +) -> (TLProv<'_>, Lines) { + // let wrap = Wrap::Word; + // let r_wrap = ResolvedWrap::Width(width); + let font_sizes = TestFontSize; + let text = TLProv { + text, + phantom: ph, + font_family: Vec::new(), + has_multiline_phantom, + }; + let cx = Scope::new(); + let lines = Lines::new(cx, RefCell::new(Arc::new(font_sizes))); + lines.set_wrap(wrap); + + if init { + let config_id = 0; + lines.init_all(config_id, &text, true); + } + + (text, lines) +} + +fn medium_rope() -> Rope { + let mut text = String::new(); + + // TODO: use some actual file's content. + for i in 0..3000 { + let content = if i % 2 == 0 { + "This is a roughly typical line of text\n" + } else if i % 3 == 0 { + "\n" + } else { + "A short line\n" + }; + + text.push_str(content); + } + + Rope::from(&text) +} + +fn visual_line(c: &mut Criterion) { + let text = medium_rope(); + + // Should be very fast because it is trivially linear and there's no multiline phantom + c.bench_function("last vline (uninit)", |b| { + let (text_prov, lines) = make_lines(&text, ResolvedWrap::None, false); + b.iter(|| { + lines.clear_last_vline(); + + let last_vline = lines.last_vline(&text_prov); + black_box(last_vline); + }) + }); + + // Unrealistic since the user will very rarely have all of the lines initialized + // Should still be fast because there's no wrapping or multiline phantom text + c.bench_function("last vline (all, no wrapping)", |b| { + let (text_prov, lines) = make_lines(&text, ResolvedWrap::None, true); + b.iter(|| { + lines.clear_last_vline(); + + let last_vline = lines.last_vline(&text_prov); + let _val = black_box(last_vline); + }) + }); + + // TODO: we could precompute line count on the text layout? + // Unrealistic since the user will very rarely have all of the lines initialized + // Still decently fast, though. <1ms + c.bench_function("last vline (all, wrapping)", |b| { + let width = 100.0; + let (text_prov, lines) = make_lines(&text, ResolvedWrap::Width(width), true); + + b.iter(|| { + // This should clear any other caching mechanisms that get added + lines.clear_last_vline(); + let last_vline = lines.last_vline(&text_prov); + let _val = black_box(last_vline); + }) + }); + + // Q: This seems like 1/5th the cost of last vline despite only being half the lines.. + c.bench_function("vline of offset (all, wrapping)", |b| { + let width = 100.0; + let (text_prov, lines) = make_lines(&text, ResolvedWrap::Width(width), true); + + // Not past the middle. If we were past the middle then it'd be just benching last vline + // calculation, which is admittedly relatively similar to this. + let offset = 1450; + + b.iter(|| { + // This should clear any other caching mechanisms that get added + lines.clear_last_vline(); + + let vline = + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Backward); + let _val = black_box(vline); + }) + }); + + c.bench_function("offset of vline (all, wrapping)", |b| { + let width = 100.0; + let (text_prov, lines) = make_lines(&text, ResolvedWrap::Width(width), true); + + let vline = VLine(3300); + + b.iter(|| { + // This should clear any other caching mechanisms that get added + lines.clear_last_vline(); + + let offset = lines.offset_of_vline(&text_prov, vline); + let _val = black_box(offset); + }) + }); + + // TODO: when we have the reverse search of vline for offset, we should have a separate instance where the last vline isn't cached, which would give us a range of 'worst case' (never reused, have to recopmute it everytime) and 'best case' (always reused) + + // TODO: bench common operations, like a single line changing or the (equivalent of) cache rev + // updating. +} + +criterion_group!(benches, visual_line); +criterion_main!(benches); diff --git a/lapce-app/src/about.rs b/lapce-app/src/about.rs index 59a4300d3f..7995be819f 100644 --- a/lapce-app/src/about.rs +++ b/lapce-app/src/about.rs @@ -100,56 +100,56 @@ pub fn about_popup(window_tab_data: Rc) -> impl View { exclusive_popup(window_tab_data, about_data.visible, move || { stack(( - svg(move || (*config.get()).logo_svg()).style(move |s| { + svg(move || (config.get()).logo_svg()).style(move |s| { s.size(logo_size, logo_size) - .color(*config.get().get_color(LapceColor::EDITOR_FOREGROUND)) + .color(config.get().color(LapceColor::EDITOR_FOREGROUND)) }), label(|| "Lapce".to_string()).style(move |s| { s.font_bold() .margin_top(10.0) - .color(*config.get().get_color(LapceColor::EDITOR_FOREGROUND)) + .color(config.get().color(LapceColor::EDITOR_FOREGROUND)) }), label(|| format!("Version: {}", VERSION)).style(move |s| { s.margin_top(10.0) - .color(*config.get().get_color(LapceColor::EDITOR_DIM)) + .color(config.get().color(LapceColor::EDITOR_DIM)) }), web_link( || "Website".to_string(), || AboutUri::LAPCE.to_string(), - move || *config.get().get_color(LapceColor::EDITOR_LINK), + move || config.get().color(LapceColor::EDITOR_LINK), internal_command, ) .style(|s| s.margin_top(20.0)), web_link( || "GitHub".to_string(), || AboutUri::GITHUB.to_string(), - move || *config.get().get_color(LapceColor::EDITOR_LINK), + move || config.get().color(LapceColor::EDITOR_LINK), internal_command, ) .style(|s| s.margin_top(10.0)), web_link( || "Discord".to_string(), || AboutUri::DISCORD.to_string(), - move || *config.get().get_color(LapceColor::EDITOR_LINK), + move || config.get().color(LapceColor::EDITOR_LINK), internal_command, ) .style(|s| s.margin_top(10.0)), web_link( || "Matrix".to_string(), || AboutUri::MATRIX.to_string(), - move || *config.get().get_color(LapceColor::EDITOR_LINK), + move || config.get().color(LapceColor::EDITOR_LINK), internal_command, ) .style(|s| s.margin_top(10.0)), label(|| "Attributions".to_string()).style(move |s| { s.font_bold() - .color(*config.get().get_color(LapceColor::EDITOR_DIM)) + .color(config.get().color(LapceColor::EDITOR_DIM)) .margin_top(40.0) }), web_link( || "Codicons (CC-BY-4.0)".to_string(), || AboutUri::CODICONS.to_string(), - move || *config.get().get_color(LapceColor::EDITOR_LINK), + move || config.get().color(LapceColor::EDITOR_LINK), internal_command, ) .style(|s| s.margin_top(10.0)), @@ -174,8 +174,8 @@ fn exclusive_popup( .padding_horiz(100.0) .border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) }) .on_event_stop(EventListener::PointerDown, move |_| {}), ) @@ -204,7 +204,7 @@ fn exclusive_popup( .background( config .get() - .get_color(LapceColor::LAPCE_DROPDOWN_SHADOW) + .color(LapceColor::LAPCE_DROPDOWN_SHADOW) .with_alpha_factor(0.5), ) }) diff --git a/lapce-app/src/alert.rs b/lapce-app/src/alert.rs index cb0428e879..0d117675e5 100644 --- a/lapce-app/src/alert.rs +++ b/lapce-app/src/alert.rs @@ -9,7 +9,7 @@ use floem::{ reactive::{ReadSignal, RwSignal, Scope}, style::CursorStyle, view::View, - views::{container, label, list, stack, svg, Decorators}, + views::{container, dyn_stack, label, stack, svg, Decorators}, }; use crate::{ @@ -66,7 +66,7 @@ pub fn alert_box(alert_data: AlertBoxData) -> impl View { svg(move || config.get().ui_svg(LapceIcons::WARNING)).style( move |s| { s.size(50.0, 50.0) - .color(*config.get().get_color(LapceColor::LAPCE_WARN)) + .color(config.get().color(LapceColor::LAPCE_WARN)) }, ), label(move || title.get()).style(move |s| { @@ -77,7 +77,7 @@ pub fn alert_box(alert_data: AlertBoxData) -> impl View { }), label(move || msg.get()) .style(move |s| s.width_pct(100.0).margin_top(10.0)), - list( + dyn_stack( move || buttons.get(), move |_button| { button_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed) @@ -97,17 +97,17 @@ pub fn alert_box(alert_data: AlertBoxData) -> impl View { .border(1.0) .border_radius(6.0) .border_color( - *config.get_color(LapceColor::LAPCE_BORDER), + config.color(LapceColor::LAPCE_BORDER), ) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get_color( + config.color( LapceColor::PANEL_HOVERED_BACKGROUND, ), ) }) .active(|s| { - s.background(*config.get_color( + s.background(config.color( LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND, )) }) @@ -128,18 +128,15 @@ pub fn alert_box(alert_data: AlertBoxData) -> impl View { .line_height(1.5) .border(1.0) .border_radius(6.0) - .border_color( - *config.get_color(LapceColor::LAPCE_BORDER), - ) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get_color( - LapceColor::PANEL_HOVERED_BACKGROUND, - ), + config + .color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) .active(|s| { - s.background(*config.get_color( + s.background(config.color( LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND, )) }) @@ -154,9 +151,9 @@ pub fn alert_box(alert_data: AlertBoxData) -> impl View { .width(250.0) .border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .color(*config.get_color(LapceColor::EDITOR_FOREGROUND)) - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) }) }) .on_event_stop(EventListener::PointerDown, move |_| {}) @@ -169,7 +166,7 @@ pub fn alert_box(alert_data: AlertBoxData) -> impl View { .background( config .get() - .get_color(LapceColor::LAPCE_DROPDOWN_SHADOW) + .color(LapceColor::LAPCE_DROPDOWN_SHADOW) .with_alpha_factor(0.5), ) }) diff --git a/lapce-app/src/app.rs b/lapce-app/src/app.rs index 3a0e7d844a..f8e9a3a596 100644 --- a/lapce-app/src/app.rs +++ b/lapce-app/src/app.rs @@ -32,9 +32,8 @@ use floem::{ view::View, views::{ clip, container, container_box, drag_resize_window_area, drag_window_area, - empty, label, list, rich_text, scroll::scroll, stack, svg, tab, text, - virtual_list, Decorators, VirtualListDirection, VirtualListItemSize, - VirtualListVector, + dyn_stack, empty, label, rich_text, scroll::scroll, stack, svg, tab, text, + virtual_stack, Decorators, VirtualDirection, VirtualItemSize, VirtualVector, }, window::{ResizeDirection, WindowConfig, WindowId}, EventPropagation, @@ -285,7 +284,9 @@ impl AppData { .is_empty() || !std::env::var("WSL_INTEROP").unwrap_or_default().is_empty() { - LapceWorkspaceType::RemoteWSL + LapceWorkspaceType::RemoteWSL(crate::workspace::WslHost { + host: String::new(), + }) } else { LapceWorkspaceType::Local }; @@ -670,7 +671,7 @@ fn editor_tab_header( s.items_center() .border_left(if i.get() == 0 { 1.0 } else { 0.0 }) .border_right(1.0) - .border_color(*config.get().get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) }; match tab_close_button_style { @@ -689,11 +690,17 @@ fn editor_tab_header( }; let confirmed = match local_child { - EditorTabChild::Editor(editor_id) => { - let editor_data = editors - .with_untracked(|editors| editors.get(&editor_id).cloned()); - editor_data.map(|editor_data| editor_data.confirmed) - } + EditorTabChild::Editor(editor_id) => editors.with_untracked(|editors| { + editors + .get(&editor_id) + .map(|editor_data| editor_data.confirmed) + }), + EditorTabChild::DiffEditor(diff_editor_id) => diff_editors + .with_untracked(|diff_editors| { + diff_editors + .get(&diff_editor_id) + .map(|diff_editor_data| diff_editor_data.confirmed) + }), _ => None, }; @@ -780,10 +787,10 @@ fn editor_tab_header( .border_radius(6.0) .background( config - .get_color(LapceColor::PANEL_BACKGROUND) + .color(LapceColor::PANEL_BACKGROUND) .with_alpha_factor(0.7), ) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) }) .style(|s| s.align_items(Some(AlignItems::Center)).height_full()), container(empty().style(move |s| { @@ -793,7 +800,7 @@ fn editor_tab_header( } else { 0.0 }) - .border_color(*config.get().get_color(if is_focused() { + .border_color(config.get().color(if is_focused() { LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE } else { LapceColor::LAPCE_TAB_INACTIVE_UNDERLINE @@ -825,7 +832,7 @@ fn editor_tab_header( .border_color( config .get() - .get_color(LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE) + .color(LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE) .with_alpha_factor(0.5), ) }), @@ -847,10 +854,10 @@ fn editor_tab_header( s.absolute() .height_full() .width(size.get().width as f32) - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) .box_shadow_blur(3.0) .box_shadow_color( - *config.get_color(LapceColor::LAPCE_DROPDOWN_SHADOW), + config.color(LapceColor::LAPCE_DROPDOWN_SHADOW), ) })) .style(move |s| { @@ -892,7 +899,7 @@ fn editor_tab_header( }), container({ scroll({ - list(items, key, view_fn) + dyn_stack(items, key, view_fn) .on_resize(move |rect| { let size = rect.size(); if content_size.get_untracked() != size { @@ -929,12 +936,10 @@ fn editor_tab_header( .height_full() .margin_left(30.0) .width(size.get().width as f32) - .background( - *config.get_color(LapceColor::PANEL_BACKGROUND), - ) + .background(config.color(LapceColor::PANEL_BACKGROUND)) .box_shadow_blur(3.0) .box_shadow_color( - *config.get_color(LapceColor::LAPCE_DROPDOWN_SHADOW), + config.color(LapceColor::LAPCE_DROPDOWN_SHADOW), ) }) }) @@ -992,8 +997,8 @@ fn editor_tab_header( let config = config.get(); s.items_center() .border_bottom(1.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) }) } @@ -1137,9 +1142,7 @@ fn editor_tab_content( .flex_basis(0.0) .border_right(1.0) .border_color( - *config - .get() - .get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }), container(editor_container_view( @@ -1282,9 +1285,7 @@ fn editor_tab( .margin_left(margin_left as f32) .apply_if(pos.is_none(), |s| s.hide()) .background( - *config - .get() - .get_color(LapceColor::EDITOR_DRAG_DROP_BACKGROUND), + config.get().color(LapceColor::EDITOR_DRAG_DROP_BACKGROUND), ) }), empty() @@ -1455,7 +1456,7 @@ fn split_resize_border( split.with_untracked(|split| split.direction) } }; - list( + dyn_stack( move || { let data = split.get(); data.children.into_iter().enumerate().skip(1) @@ -1564,18 +1565,14 @@ fn split_resize_border( SplitDirection::Vertical => CursorStyle::ColResize, SplitDirection::Horizontal => CursorStyle::RowResize, }) - .background( - *config.get().get_color(LapceColor::EDITOR_CARET), - ) + .background(config.get().color(LapceColor::EDITOR_CARET)) }) .hover(|s| { s.cursor(match direction { SplitDirection::Vertical => CursorStyle::ColResize, SplitDirection::Horizontal => CursorStyle::RowResize, }) - .background( - *config.get().get_color(LapceColor::EDITOR_CARET), - ) + .background(config.get().color(LapceColor::EDITOR_CARET)) }) }) }, @@ -1590,7 +1587,7 @@ fn split_border( config: ReadSignal>, ) -> impl View { let direction = move || split.with(|split| split.direction); - list( + dyn_stack( move || split.get().children.into_iter().skip(1), |(_, content)| content.id(), move |(_, content)| { @@ -1604,7 +1601,7 @@ fn split_border( SplitDirection::Vertical => PxPctAuto::Pct(100.0), SplitDirection::Horizontal => PxPctAuto::Px(1.0), }) - .background(*config.get().get_color(LapceColor::LAPCE_BORDER)) + .background(config.get().color(LapceColor::LAPCE_BORDER)) })) .style(move |s| { let rect = match &content { @@ -1755,7 +1752,7 @@ fn split_list( }; container_box( stack(( - list(items, key, view_fn).style(move |s| { + dyn_stack(items, key, view_fn).style(move |s| { s.flex_direction(match direction() { SplitDirection::Vertical => FlexDirection::Row, SplitDirection::Horizontal => FlexDirection::Column, @@ -1801,8 +1798,8 @@ fn main_split(window_tab_data: Rc) -> impl View { let config = config.get(); let is_hidden = panel.panel_bottom_maximized(true) && panel.is_container_shown(&PanelContainerPosition::Bottom, true); - s.border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + s.border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) .apply_if(is_hidden, |s| s.display(Display::None)) .width_full() .flex_grow(1.0) @@ -1824,12 +1821,10 @@ pub fn clickable_icon( let config = config.get(); let size = config.ui.icon_size() as f32; s.size(size, size) - .color(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) .disabled(|s| { - s.color( - *config.get_color(LapceColor::LAPCE_ICON_INACTIVE), - ) - .cursor(CursorStyle::Default) + s.color(config.color(LapceColor::LAPCE_ICON_INACTIVE)) + .cursor(CursorStyle::Default) }) }) .disabled(disabled_fn), @@ -1845,17 +1840,16 @@ pub fn clickable_icon( .border(1.0) .border_color(Color::TRANSPARENT) .apply_if(active_fn(), |s| { - s.border_color(*config.get_color(LapceColor::EDITOR_CARET)) + s.border_color(config.color(LapceColor::EDITOR_CARET)) }) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) .active(|s| { s.background( - *config - .get_color(LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND), + config.color(LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND), ) }) }), @@ -1910,15 +1904,15 @@ fn palette_item( | PaletteItemContent::Reference { path, .. } => { let file_name = path .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_string(); + .unwrap_or_default() + .to_string_lossy() + .into_owned(); // let (file_name, _) = create_signal(cx.scope, file_name); let folder = path .parent() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_string(); + .unwrap_or("".as_ref()) + .to_string_lossy() + .into_owned(); // let (folder, _) = create_signal(cx.scope, folder); let folder_len = folder.len(); @@ -1950,7 +1944,7 @@ fn palette_item( svg(move || config.get().file_svg(&path).0).style(move |s| { let config = config.get(); let size = config.ui.icon_size() as f32; - let color = config.file_svg(&style_path).1.copied(); + let color = config.file_svg(&style_path).1; s.min_width(size) .size(size, size) .margin_right(5.0) @@ -1959,16 +1953,16 @@ fn palette_item( focus_text( move || file_name.clone(), move || file_name_indices.clone(), - move || *config.get().get_color(LapceColor::EDITOR_FOCUS), + move || config.get().color(LapceColor::EDITOR_FOCUS), ) .style(|s| s.margin_right(6.0).max_width_full()), focus_text( move || folder.clone(), move || folder_indices.clone(), - move || *config.get().get_color(LapceColor::EDITOR_FOCUS), + move || config.get().color(LapceColor::EDITOR_FOCUS), ) .style(move |s| { - s.color(*config.get().get_color(LapceColor::EDITOR_DIM)) + s.color(config.get().color(LapceColor::EDITOR_DIM)) .min_width(0.0) .flex_grow(1.0) .flex_basis(0.0) @@ -2024,21 +2018,21 @@ fn palette_item( s.min_width(size) .size(size, size) .margin_right(5.0) - .color(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) }), focus_text( move || text.clone(), move || text_indices.clone(), - move || *config.get().get_color(LapceColor::EDITOR_FOCUS), + move || config.get().color(LapceColor::EDITOR_FOCUS), ) .style(|s| s.margin_right(6.0).max_width_full()), focus_text( move || hint.clone(), move || hint_indices.clone(), - move || *config.get().get_color(LapceColor::EDITOR_FOCUS), + move || config.get().color(LapceColor::EDITOR_FOCUS), ) .style(move |s| { - s.color(*config.get().get_color(LapceColor::EDITOR_DIM)) + s.color(config.get().color(LapceColor::EDITOR_DIM)) .min_width(0.0) .flex_grow(1.0) .flex_basis(0.0) @@ -2105,21 +2099,21 @@ fn palette_item( s.min_width(size) .size(size, size) .margin_right(5.0) - .color(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) }), focus_text( move || text.clone(), move || text_indices.clone(), - move || *config.get().get_color(LapceColor::EDITOR_FOCUS), + move || config.get().color(LapceColor::EDITOR_FOCUS), ) .style(|s| s.margin_right(6.0).max_width_full()), focus_text( move || hint.clone(), move || hint_indices.clone(), - move || *config.get().get_color(LapceColor::EDITOR_FOCUS), + move || config.get().color(LapceColor::EDITOR_FOCUS), ) .style(move |s| { - s.color(*config.get().get_color(LapceColor::EDITOR_DIM)) + s.color(config.get().color(LapceColor::EDITOR_DIM)) .min_width(0.0) .flex_grow(1.0) .flex_basis(0.0) @@ -2178,21 +2172,21 @@ fn palette_item( s.min_width(size) .size(size, size) .margin_right(5.0) - .color(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) }), focus_text( move || text.clone(), move || text_indices.clone(), - move || *config.get().get_color(LapceColor::EDITOR_FOCUS), + move || config.get().color(LapceColor::EDITOR_FOCUS), ) .style(|s| s.margin_right(6.0).max_width_full()), focus_text( move || hint.clone(), move || hint_indices.clone(), - move || *config.get().get_color(LapceColor::EDITOR_FOCUS), + move || config.get().color(LapceColor::EDITOR_FOCUS), ) .style(move |s| { - s.color(*config.get().get_color(LapceColor::EDITOR_DIM)) + s.color(config.get().color(LapceColor::EDITOR_DIM)) .min_width(0.0) .flex_grow(1.0) .flex_basis(0.0) @@ -2220,14 +2214,14 @@ fn palette_item( focus_text( move || text.clone(), move || indices.clone(), - move || *config.get().get_color(LapceColor::EDITOR_FOCUS), + move || config.get().color(LapceColor::EDITOR_FOCUS), ) .style(|s| { s.flex_row() .flex_grow(1.0) .align_items(Some(AlignItems::Center)) }), - stack((list( + stack((dyn_stack( move || keys.clone(), |k| k.clone(), move |key| { @@ -2238,9 +2232,7 @@ fn palette_item( .border(1.0) .border_radius(3.0) .border_color( - *config - .get() - .get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }) }, @@ -2263,7 +2255,20 @@ fn palette_item( focus_text( move || text.clone(), move || indices.clone(), - move || *config.get().get_color(LapceColor::EDITOR_FOCUS), + move || config.get().color(LapceColor::EDITOR_FOCUS), + ) + .style(|s| s.align_items(Some(AlignItems::Center)).max_width_full()), + ) + } + #[cfg(windows)] + PaletteItemContent::WslHost { .. } => { + let text = item.filter_text; + let indices = item.indices; + container_box( + focus_text( + move || text.clone(), + move || indices.clone(), + move || config.get().color(LapceColor::EDITOR_FOCUS), ) .style(|s| s.align_items(Some(AlignItems::Center)).max_width_full()), ) @@ -2275,9 +2280,7 @@ fn palette_item( .padding_horiz(10.0) .apply_if(index.get() == i, |style| { style.background( - *config - .get() - .get_color(LapceColor::PALETTE_CURRENT_BACKGROUND), + config.get().color(LapceColor::PALETTE_CURRENT_BACKGROUND), ) }) }) @@ -2288,32 +2291,34 @@ fn palette_input(window_tab_data: Rc) -> impl View { let config = window_tab_data.common.config; let focus = window_tab_data.common.focus; let is_focused = move || focus.get() == Focus::Palette; - container( - container(text_input(editor, is_focused).style(|s| s.width_full())).style( - move |s| { - let config = config.get(); - s.width_full() - .height(25.0) - .items_center() - .border_bottom(1.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) - }, - ), - ) + + let input = text_input(editor, is_focused) + .placeholder(move || window_tab_data.palette.placeholder_text().to_owned()) + .style(|s| s.width_full()); + + container(container(input).style(move |s| { + let config = config.get(); + s.width_full() + .height(25.0) + .items_center() + .border_bottom(1.0) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) + })) .style(|s| s.padding_bottom(5.0)) } struct PaletteItems(im::Vector); -impl VirtualListVector<(usize, PaletteItem)> for PaletteItems { - type ItemIterator = Box>; - +impl VirtualVector<(usize, PaletteItem)> for PaletteItems { fn total_len(&self) -> usize { self.0.len() } - fn slice(&mut self, range: Range) -> Self::ItemIterator { + fn slice( + &mut self, + range: Range, + ) -> impl Iterator { let start = range.start; Box::new( self.0 @@ -2345,9 +2350,9 @@ fn palette_content( stack(( scroll({ let workspace = workspace.clone(); - virtual_list( - VirtualListDirection::Vertical, - VirtualListItemSize::Fixed(Box::new(move || palette_item_height)), + virtual_stack( + VirtualDirection::Vertical, + VirtualItemSize::Fixed(Box::new(move || palette_item_height)), move || PaletteItems(items.get()), move |(i, _item)| { (run_id.get_untracked(), *i, input.get_untracked().input) @@ -2367,7 +2372,7 @@ fn palette_content( cmd_kind .and_then(|kind| keymaps.get(kind.str())) - .and_then(|maps| maps.get(0)) + .and_then(|maps| maps.first()) }; container(palette_item( workspace, @@ -2384,9 +2389,9 @@ fn palette_content( .style(move |s| { s.width_full().cursor(CursorStyle::Pointer).hover(|s| { s.background( - *config + config .get() - .get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + .color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }) @@ -2442,9 +2447,9 @@ fn palette_preview(window_tab_data: Rc) -> impl View { let config = config.get(); s.position(Position::Absolute) .border_top(1.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .size_full() - .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) }), ) .style(move |s| { @@ -2487,9 +2492,9 @@ fn palette(window_tab_data: Rc) -> impl View { .margin_top(5.0) .border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .flex_col() - .background(*config.get_color(LapceColor::PALETTE_BACKGROUND)) + .background(config.color(LapceColor::PALETTE_BACKGROUND)) }), ) .style(move |s| { @@ -2523,15 +2528,15 @@ fn window_message_view( let config = config.get(); let size = config.ui.icon_size() as f32; let color = if let MessageType::ERROR = message.typ { - config.get_color(LapceColor::LAPCE_ERROR) + config.color(LapceColor::LAPCE_ERROR) } else { - config.get_color(LapceColor::LAPCE_WARN) + config.color(LapceColor::LAPCE_WARN) }; s.min_width(size) .size(size, size) .margin_right(10.0) .margin_top(4.0) - .color(*color) + .color(color) }), stack(( text(title.clone()).style(|s| { @@ -2564,8 +2569,8 @@ fn window_message_view( .padding(10.0) .border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) .apply_if(i > 0, |s| s.margin_top(10.0)) }) }; @@ -2575,7 +2580,7 @@ fn window_message_view( container( container( scroll( - list( + dyn_stack( move || messages.get().into_iter().enumerate(), move |_| { id.fetch_add(1, std::sync::atomic::Ordering::Relaxed) @@ -2602,22 +2607,18 @@ fn window_message_view( struct VectorItems(im::Vector); -impl VirtualListVector<(usize, V)> for VectorItems { - type ItemIterator = Box>; - +impl VirtualVector<(usize, V)> for VectorItems { fn total_len(&self) -> usize { self.0.len() } - fn slice(&mut self, range: Range) -> Self::ItemIterator { + fn slice(&mut self, range: Range) -> impl Iterator { let start = range.start; - Box::new( - self.0 - .slice(range) - .into_iter() - .enumerate() - .map(move |(i, item)| (i + start, item)), - ) + self.0 + .slice(range) + .into_iter() + .enumerate() + .map(move |(i, item)| (i + start, item)) } } @@ -2648,7 +2649,7 @@ fn hover(window_tab_data: Rc) -> impl View { let layout_rect = window_tab_data.common.hover.layout_rect; scroll( - list( + dyn_stack( move || hover_data.content.get(), move |_| id.fetch_add(1, std::sync::atomic::Ordering::Relaxed), move |content| match content { @@ -2660,9 +2661,10 @@ fn hover(window_tab_data: Rc) -> impl View { MarkdownContent::Image { .. } => container_box(empty()), MarkdownContent::Separator => { container_box(empty().style(move |s| { - s.width_full().margin_vert(5.0).height(1.0).background( - *config.get().get_color(LapceColor::LAPCE_BORDER), - ) + s.width_full() + .margin_vert(5.0) + .height(1.0) + .background(config.get().color(LapceColor::LAPCE_BORDER)) })) } }, @@ -2686,8 +2688,8 @@ fn hover(window_tab_data: Rc) -> impl View { .max_height(300.0) .border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) } else { s.hide() } @@ -2702,9 +2704,9 @@ fn completion(window_tab_data: Rc) -> impl View { let request_id = move || completion_data.with_untracked(|c| (c.request_id, c.input_id)); scroll( - virtual_list( - VirtualListDirection::Vertical, - VirtualListItemSize::Fixed(Box::new(move || { + virtual_stack( + VirtualDirection::Vertical, + VirtualItemSize::Fixed(Box::new(move || { config.get().editor.line_height() as f64 })), move || completion_data.with(|c| VectorItems(c.filtered_items.clone())), @@ -2738,7 +2740,7 @@ fn completion(window_tab_data: Rc) -> impl View { focus_text( move || item.item.label.clone(), move || item.indices.clone(), - move || *config.get().get_color(LapceColor::EDITOR_FOCUS), + move || config.get().color(LapceColor::EDITOR_FOCUS), ) .style(move |s| { let config = config.get(); @@ -2748,8 +2750,7 @@ fn completion(window_tab_data: Rc) -> impl View { .size_full() .apply_if(active.get() == i, |s| { s.background( - *config - .get_color(LapceColor::COMPLETION_CURRENT), + config.color(LapceColor::COMPLETION_CURRENT), ) }) }), @@ -2791,7 +2792,7 @@ fn completion(window_tab_data: Rc) -> impl View { .max_height(400.0) .margin_left(origin.x as f32) .margin_top(origin.y as f32) - .background(*config.get_color(LapceColor::COMPLETION_BACKGROUND)) + .background(config.color(LapceColor::COMPLETION_BACKGROUND)) .font_family(config.editor.font_family.clone()) .font_size(config.editor.font_size() as f32) .border_radius(6.0) @@ -2807,7 +2808,7 @@ fn code_action(window_tab_data: Rc) -> impl View { move || code_action.with_untracked(|code_action| code_action.request_id); scroll( container( - list( + dyn_stack( move || { code_action.with(|code_action| { code_action.filtered_items.clone().into_iter().enumerate() @@ -2828,8 +2829,7 @@ fn code_action(window_tab_data: Rc) -> impl View { .line_height(1.6) .apply_if(active.get() == i, |s| { s.border_radius(6.0).background( - *config - .get_color(LapceColor::COMPLETION_CURRENT), + config.color(LapceColor::COMPLETION_CURRENT), ) }) }) @@ -2866,7 +2866,7 @@ fn code_action(window_tab_data: Rc) -> impl View { .max_height(400.0) .margin_left(origin.x as f32) .margin_top(origin.y as f32) - .background(*config.get().get_color(LapceColor::COMPLETION_BACKGROUND)) + .background(config.get().color(LapceColor::COMPLETION_BACKGROUND)) .border_radius(6.0) }) } @@ -2887,8 +2887,8 @@ fn rename(window_tab_data: Rc) -> impl View { .font_size(config.editor.font_size() as f32) .border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) }), ) .on_resize(move |rect| { @@ -2902,7 +2902,7 @@ fn rename(window_tab_data: Rc) -> impl View { .apply_if(!active.get(), |s| s.hide()) .margin_left(origin.x as f32) .margin_top(origin.y as f32) - .background(*config.get().get_color(LapceColor::PANEL_BACKGROUND)) + .background(config.get().color(LapceColor::PANEL_BACKGROUND)) .border_radius(6.0) .padding(6.0) }) @@ -2956,14 +2956,14 @@ fn window_tab(window_tab_data: Rc) -> impl View { .style(move |s| { let config = config.get(); s.size_full() - .color(*config.get_color(LapceColor::EDITOR_FOREGROUND)) - .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) .font_size(config.ui.font_size() as f32) .apply_if(!config.ui.font_family.is_empty(), |s| { s.font_family(config.ui.font_family.clone()) }) .class(floem::views::scroll::Handle, |s| { - s.background(*config.get_color(LapceColor::LAPCE_SCROLL_BAR)) + s.background(config.color(LapceColor::LAPCE_SCROLL_BAR)) }) }); @@ -2977,9 +2977,9 @@ fn workspace_title(workspace: &LapceWorkspace) -> Option { let dir = p.file_name().unwrap_or(p.as_os_str()).to_string_lossy(); Some(match &workspace.kind { LapceWorkspaceType::Local => format!("{dir}"), - LapceWorkspaceType::RemoteSSH(ssh) => format!("{dir} [{ssh}]"), + LapceWorkspaceType::RemoteSSH(remote) => format!("{dir} [{remote}]"), #[cfg(windows)] - LapceWorkspaceType::RemoteWSL => format!("{dir} [wsl]"), + LapceWorkspaceType::RemoteWSL(remote) => format!("{dir} [{remote}]"), }) } @@ -3097,9 +3097,7 @@ fn workspace_tab_header(window_data: WindowData) -> impl View { .min_width(0.0) .items_center() .border_right(1.0) - .border_color( - *config.get_color(LapceColor::LAPCE_BORDER), - ) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .apply_if( cfg!(target_os = "macos") && index.get() == 0, |s| s.border_left(1.0), @@ -3111,9 +3109,9 @@ fn workspace_tab_header(window_data: WindowData) -> impl View { s.border_bottom(2.0) }) .border_color( - *config.get().get_color( - LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE, - ), + config + .get() + .color(LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE), ) })) .style(|s| { @@ -3135,15 +3133,15 @@ fn workspace_tab_header(window_data: WindowData) -> impl View { let config = config.get(); s.border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .color( config - .get_color(LapceColor::EDITOR_FOREGROUND) + .color(LapceColor::EDITOR_FOREGROUND) .with_alpha_factor(0.7), ) .background( config - .get_color(LapceColor::PANEL_BACKGROUND) + .color(LapceColor::PANEL_BACKGROUND) .with_alpha_factor(0.7), ) }) @@ -3160,9 +3158,7 @@ fn workspace_tab_header(window_data: WindowData) -> impl View { ) .height_full() .border_color( - *config - .get() - .get_color(LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE), + config.get().color(LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE), ) .apply_if(drag_over_left.get().is_some(), move |s| { let drag_over_left = drag_over_left.get_untracked().unwrap(); @@ -3185,7 +3181,7 @@ fn workspace_tab_header(window_data: WindowData) -> impl View { .width(75.0) .apply_if(!is_macos, |s| s.hide()) }), - list( + dyn_stack( move || { let tabs = tabs.get(); for (i, (index, _)) in tabs.iter().enumerate() { @@ -3256,9 +3252,9 @@ fn workspace_tab_header(window_data: WindowData) -> impl View { s.font_family(config.ui.font_family.clone()) }) .apply_if(tabs.with(|tabs| tabs.len() < 2), |s| s.hide()) - .color(*config.get_color(LapceColor::EDITOR_FOREGROUND)) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) .items_center() }) } diff --git a/lapce-app/src/command.rs b/lapce-app/src/command.rs index 1b94598638..e1b1cd333f 100644 --- a/lapce-app/src/command.rs +++ b/lapce-app/src/command.rs @@ -283,9 +283,10 @@ pub enum LapceWorkbenchCommand { #[strum(message = "Connect to SSH Host")] ConnectSshHost, - #[strum(serialize = "connect_wsl")] - #[strum(message = "Connect to WSL")] - ConnectWsl, + #[cfg(windows)] + #[strum(serialize = "connect_wsl_host")] + #[strum(message = "Connect to WSL Host")] + ConnectWslHost, #[strum(serialize = "disconnect_remote")] #[strum(message = "Disconnect From Remote")] @@ -506,6 +507,10 @@ pub enum LapceWorkbenchCommand { #[strum(serialize = "previous_error")] PreviousError, + #[strum(message = "Diff Files")] + #[strum(serialize = "diff_files")] + DiffFiles, + #[strum(serialize = "quit")] #[strum(message = "Quit Editor")] Quit, @@ -524,6 +529,16 @@ pub enum InternalCommand { OpenFileChanges { path: PathBuf, }, + StartRenamePath { + path: PathBuf, + }, + TestRenamePath { + new_path: PathBuf, + }, + FinishRenamePath { + current_path: PathBuf, + new_path: PathBuf, + }, GoToLocation { location: EditorLocation, }, @@ -655,6 +670,10 @@ pub enum InternalCommand { volt_id: VoltID, }, ResetBlinkCursor, + OpenDiffFiles { + left_path: PathBuf, + right_path: PathBuf, + }, } #[derive(Clone)] diff --git a/lapce-app/src/completion.rs b/lapce-app/src/completion.rs index 96576baaee..f03032dcc9 100644 --- a/lapce-app/src/completion.rs +++ b/lapce-app/src/completion.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, path::PathBuf, rc::Rc, str::FromStr, sync::Arc}; +use std::{borrow::Cow, path::PathBuf, str::FromStr, sync::Arc}; use floem::{ peniko::kurbo::Rect, @@ -13,7 +13,7 @@ use lsp_types::{ use nucleo::Utf32Str; use crate::{ - config::LapceConfig, doc::Document, editor::view_data::EditorViewData, + config::LapceConfig, doc::DocumentExt, editor::view_data::EditorViewData, id::EditorId, snippet::Snippet, }; @@ -310,7 +310,7 @@ impl CompletionData { let config = self.config.get_untracked(); if !config.editor.enable_completion_lens { - clear_completion_lens(doc); + doc.clear_completion_lens(); return; } @@ -332,20 +332,12 @@ impl CompletionData { // Unchanged Some(None) => {} None => { - clear_completion_lens(doc); + doc.clear_completion_lens(); } } } } -/// Clear the current completion lens. Only `update`s if there is a completion lens. -pub fn clear_completion_lens(doc: Rc) { - let has_completion = doc.completion_lens.with_untracked(|lens| lens.is_some()); - if has_completion { - doc.clear_completion_lens(); - } -} - /// Get the text of the completion lens for the given completion item. /// Returns `None` if the completion lens should be hidden. /// Returns `Some(None)` if the completion lens should be shown, but not changed. diff --git a/lapce-app/src/config.rs b/lapce-app/src/config.rs index a3e8c61243..a2cf07e41c 100644 --- a/lapce-app/src/config.rs +++ b/lapce-app/src/config.rs @@ -1,3 +1,4 @@ +use ::core::slice; use std::{ collections::HashMap, path::{Path, PathBuf}, @@ -19,7 +20,7 @@ use self::{ color::LapceColor, color_theme::{ColorThemeConfig, ThemeColor, ThemeColorPreference}, core::CoreConfig, - editor::{EditorConfig, SCALE_OR_SIZE_LIMIT}, + editor::{EditorConfig, WrapStyle, SCALE_OR_SIZE_LIMIT}, icon::LapceIcons, icon_theme::IconThemeConfig, svg::SvgStore, @@ -91,6 +92,9 @@ pub struct LapceConfig { color_theme_list: im::Vector, #[serde(skip)] icon_theme_list: im::Vector, + /// The couple names for the wrap style + #[serde(skip)] + wrap_style_list: im::Vector, } impl LapceConfig { @@ -121,6 +125,13 @@ impl LapceConfig { .collect(); lapce_config.icon_theme_list.sort(); + lapce_config.wrap_style_list = im::vector![ + WrapStyle::None.to_string(), + WrapStyle::EditorWidth.to_string(), + // TODO: WrapStyle::WrapColumn.to_string(), + WrapStyle::WrapWidth.to_string() + ]; + lapce_config.terminal.get_indexed_colors(); lapce_config @@ -171,7 +182,7 @@ impl LapceConfig { } LapceWorkspaceType::RemoteSSH(_) => {} #[cfg(windows)] - LapceWorkspaceType::RemoteWSL => {} + LapceWorkspaceType::RemoteWSL(_) => {} } config @@ -304,16 +315,17 @@ impl LapceConfig { /// # Panics /// If the color was not able to be found in either theme, which may be indicative that /// it is misspelled or needs to be added to the base-theme. - pub fn get_color(&self, name: &str) -> &Color { - self.color + pub fn color(&self, name: &str) -> Color { + *self + .color .ui .get(name) .unwrap_or_else(|| panic!("Key not found: {name}")) } /// Retrieve a color value whose key starts with "style." - pub fn get_style_color(&self, name: &str) -> Option<&Color> { - self.color.syntax.get(name) + pub fn style_color(&self, name: &str) -> Option { + self.color.syntax.get(name).copied() } pub fn completion_color( @@ -339,7 +351,7 @@ impl LapceConfig { _ => "string", }; - self.get_style_color(theme_str).cloned() + self.style_color(theme_str) } fn resolve_colors(&mut self, default_config: Option<&LapceConfig>) { @@ -355,8 +367,8 @@ impl LapceConfig { default_config.map(|c| &c.color.syntax), ); - let fg = self.get_color(LapceColor::EDITOR_FOREGROUND); - let bg = self.get_color(LapceColor::EDITOR_BACKGROUND); + let fg = self.color(LapceColor::EDITOR_FOREGROUND); + let bg = self.color(LapceColor::EDITOR_BACKGROUND); let is_light = fg.r as u32 + fg.g as u32 + fg.b as u32 > bg.r as u32 + bg.g as u32 + bg.b as u32; let high_contrast = self.color_theme.high_contrast.unwrap_or(false); @@ -545,14 +557,15 @@ impl LapceConfig { }) } - pub fn file_svg(&self, path: &Path) -> (String, Option<&Color>) { + pub fn files_svg(&self, paths: &[&Path]) -> (String, Option) { let svg = self .icon_theme - .resolve_path_to_icon(path) + .resolve_path_to_icon(paths) .and_then(|p| self.svg_store.write().get_svg_on_disk(&p)); + if let Some(svg) = svg { let color = if self.icon_theme.use_editor_color.unwrap_or(false) { - Some(self.get_color(LapceColor::LAPCE_ICON_ACTIVE)) + Some(self.color(LapceColor::LAPCE_ICON_ACTIVE)) } else { None }; @@ -560,11 +573,15 @@ impl LapceConfig { } else { ( self.ui_svg(LapceIcons::FILE), - Some(self.get_color(LapceColor::LAPCE_ICON_ACTIVE)), + Some(self.color(LapceColor::LAPCE_ICON_ACTIVE)), ) } } + pub fn file_svg(&self, path: &Path) -> (String, Option) { + self.files_svg(slice::from_ref(&path)) + } + pub fn symbol_svg(&self, kind: &SymbolKind) -> Option { let kind_str = match *kind { SymbolKind::ARRAY => LapceIcons::SYMBOL_KIND_ARRAY, @@ -778,7 +795,7 @@ impl LapceConfig { (LapceColor::TERMINAL_FOREGROUND, 0.66) } }; - (*self.get_color(color)).with_alpha_factor(alpha) + self.color(color).with_alpha_factor(alpha) } /// Get the dropdown information for the specific setting, used for the settings UI. @@ -802,6 +819,18 @@ impl LapceConfig { .unwrap_or(0), items: self.icon_theme_list.clone(), }), + ("editor", "wrap-style") => Some(DropdownInfo { + // TODO: it would be better to have the text not be the default kebab-case when + // displayed in settings, but we would need to map back from the dropdown's value + // or index. + active_index: self + .wrap_style_list + .iter() + .flat_map(|w| WrapStyle::try_from_str(w)) + .position(|w| w == self.editor.wrap_style) + .unwrap_or(0), + items: self.wrap_style_list.clone(), + }), ("ui", "tab-close-button") => Some(DropdownInfo { active_index: self.ui.tab_close_button as usize, items: ui::TabCloseButton::VARIANTS diff --git a/lapce-app/src/config/editor.rs b/lapce-app/src/config/editor.rs index 17559e0157..8d10ed04bb 100644 --- a/lapce-app/src/config/editor.rs +++ b/lapce-app/src/config/editor.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use structdesc::FieldNames; +use crate::doc::RenderWhitespace; + pub const SCALE_OR_SIZE_LIMIT: f64 = 5.0; #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -14,6 +16,45 @@ pub enum ClickMode { DoubleClickAll, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum WrapStyle { + /// No wrapping + None, + /// Wrap at the editor width + #[default] + EditorWidth, + // /// Wrap at the wrap-column + // WrapColumn, + /// Wrap at a specific width + WrapWidth, +} +impl WrapStyle { + pub fn as_str(&self) -> &'static str { + match self { + WrapStyle::None => "none", + WrapStyle::EditorWidth => "editor-width", + // WrapStyle::WrapColumn => "wrap-column", + WrapStyle::WrapWidth => "wrap-width", + } + } + + pub fn try_from_str(s: &str) -> Option { + match s { + "none" => Some(WrapStyle::None), + "editor-width" => Some(WrapStyle::EditorWidth), + // "wrap-column" => Some(WrapStyle::WrapColumn), + "wrap-width" => Some(WrapStyle::WrapWidth), + _ => None, + } + } +} +impl ToString for WrapStyle { + fn to_string(&self) -> String { + self.as_str().to_string() + } +} + #[derive(FieldNames, Debug, Clone, Deserialize, Serialize, Default)] #[serde(rename_all = "kebab-case")] pub struct EditorConfig { @@ -43,6 +84,12 @@ pub struct EditorConfig { desc = "Set the minimum number of visible lines above and below the cursor" )] pub cursor_surrounding_lines: usize, + #[field_names(desc = "The kind of wrapping to perform")] + pub wrap_style: WrapStyle, + // #[field_names(desc = "The number of columns to wrap at")] + // pub wrap_column: usize, + #[field_names(desc = "The number of pixels to wrap at")] + pub wrap_width: usize, #[field_names( desc = "Show code context like functions and classes at the top of editor when scroll" )] @@ -103,6 +150,12 @@ pub struct EditorConfig { desc = "Whether error lens should go to the end of view line, or only to the end of the diagnostic" )] pub error_lens_end_of_line: bool, + #[field_names( + desc = "Whether error lens should extend over multiple lines. If false, it will have newlines stripped." + )] + pub error_lens_multiline: bool, + // TODO: Error lens but put entirely on the next line + // TODO: error lens with indentation matching. #[field_names( desc = "Set error lens font family. If empty, it uses the inlay hint font family." )] @@ -115,6 +168,8 @@ pub struct EditorConfig { desc = "If the editor should display the completion item as phantom text" )] pub enable_completion_lens: bool, + #[field_names(desc = "If the editor should display inline completions")] + pub enable_inline_completion: bool, #[field_names( desc = "Set completion lens font family. If empty, it uses the inlay hint font family." )] @@ -138,7 +193,7 @@ pub struct EditorConfig { #[field_names( desc = "How the editor should render whitespace characters.\nOptions: none, all, boundary, trailing." )] - pub render_whitespace: String, + pub render_whitespace: RenderWhitespace, #[field_names(desc = "Whether the editor show indent guide.")] pub show_indent_guide: bool, #[field_names( diff --git a/lapce-app/src/config/icon_theme.rs b/lapce-app/src/config/icon_theme.rs index a4a4baff0b..c46b10a2cd 100644 --- a/lapce-app/src/config/icon_theme.rs +++ b/lapce-app/src/config/icon_theme.rs @@ -1,8 +1,24 @@ -use std::path::{Path, PathBuf}; +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; +/// Returns the first item yielded from `items` if at least one item is yielded, all yielded items +/// are `Some`, and all yielded items compare equal, else returns `None`. +fn try_all_equal_value>>( + items: I, +) -> Option { + let mut items = items.into_iter(); + let first = items.next().flatten()?; + + items.try_fold(first, |initial_item, item| { + item.and_then(|item| (item == initial_item).then_some(initial_item)) + }) +} + #[derive(Debug, Clone, Deserialize, Serialize, Default)] #[serde(rename_all = "kebab-case")] pub struct IconThemeConfig { @@ -17,23 +33,212 @@ pub struct IconThemeConfig { } impl IconThemeConfig { - pub fn resolve_path_to_icon(&self, path: &Path) -> Option { - if let Some((_, icon)) = self.filename.get_key_value( - path.file_name() - .unwrap_or_default() - .to_str() - .unwrap_or_default(), - ) { - Some(self.path.join(icon)) - } else if let Some((_, icon)) = self.extension.get_key_value( - path.extension() - .unwrap_or_default() - .to_str() - .unwrap_or_default(), - ) { - Some(self.path.join(icon)) - } else { - None + /// If all paths in `paths` have the same file type (as determined by the file name or + /// extension), and there is an icon associated with that file type, returns the path of the + /// icon. + pub fn resolve_path_to_icon(&self, paths: &[&Path]) -> Option { + let file_names = paths + .iter() + .map(|path| path.file_name().and_then(OsStr::to_str)); + let file_name_icon = try_all_equal_value(file_names) + .and_then(|file_name| self.filename.get(file_name)); + + file_name_icon + .or_else(|| { + let extensions = paths + .iter() + .map(|path| path.extension().and_then(OsStr::to_str)); + + try_all_equal_value(extensions) + .and_then(|extension| self.extension.get(extension)) + }) + .map(|icon| self.path.join(icon)) + } +} + +#[cfg(test)] +mod tests { + use crate::config::icon_theme::try_all_equal_value; + + use super::IconThemeConfig; + + #[test] + fn try_all_equal_value_empty_none() { + assert_eq!(Option::::None, try_all_equal_value([])); + } + + #[test] + fn try_all_equal_value_any_none_none() { + assert_eq!(Option::::None, try_all_equal_value([None])); + assert_eq!( + Option::::None, + try_all_equal_value([None, Some(1), Some(1)]) + ); + assert_eq!(Option::::None, try_all_equal_value([Some(0), None])); + assert_eq!( + Option::::None, + try_all_equal_value([Some(3), Some(3), None, Some(3)]) + ); + } + + #[test] + fn try_all_equal_value_any_different_none() { + assert_eq!(Option::::None, try_all_equal_value([Some(1), Some(2)])); + assert_eq!( + Option::::None, + try_all_equal_value([Some(1), Some(10), Some(1)]) + ); + assert_eq!( + Option::::None, + try_all_equal_value([Some(3), Some(3), Some(3), Some(3), Some(2)]) + ); + assert_eq!( + Option::::None, + try_all_equal_value([Some(5), Some(4), Some(4), Some(4), Some(4)]) + ); + assert_eq!( + Option::::None, + try_all_equal_value([Some(3), Some(0), Some(9), Some(20), Some(1)]) + ); + } + + #[test] + fn try_all_equal_value_all_same_some() { + assert_eq!(Option::::Some(1), try_all_equal_value([Some(1)])); + assert_eq!(Option::::Some(-2), try_all_equal_value([Some(-2); 2])); + assert_eq!(Option::::Some(0), try_all_equal_value([Some(0); 3])); + assert_eq!(Option::::Some(30), try_all_equal_value([Some(30); 57])); + } + + fn get_icon_theme_config() -> IconThemeConfig { + IconThemeConfig { + path: "icons".to_owned().into(), + filename: [("Makefile", "makefile.svg"), ("special.rs", "special.svg")] + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .into(), + extension: [("rs", "rust.svg"), ("c", "c.svg"), ("py", "python.svg")] + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .into(), + ..Default::default() } } + + #[test] + fn resolve_path_to_icon_no_paths_none() { + let icon_theme_config = get_icon_theme_config(); + + assert_eq!(None, icon_theme_config.resolve_path_to_icon(&[])); + } + + #[test] + fn resolve_path_to_icon_different_none() { + let icon_theme_config = get_icon_theme_config(); + + assert_eq!( + None, + icon_theme_config + .resolve_path_to_icon(&["foo.rs", "bar.c"].map(AsRef::as_ref)) + ); + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon( + &["/some/path/main.py", "other/path.py", "dir1/./dir2/file.rs"] + .map(AsRef::as_ref) + ) + ); + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon( + &["/root/Makefile", "dir/dir/special.rs", "../../main.rs"] + .map(AsRef::as_ref) + ) + ); + assert_eq!( + None, + icon_theme_config + .resolve_path_to_icon(&["main.c", "foo.txt"].map(AsRef::as_ref)) + ); + } + + #[test] + fn resolve_path_to_icon_no_match_none() { + let icon_theme_config = get_icon_theme_config(); + + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon(&["foo"].map(AsRef::as_ref)) + ); + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon( + &["/some/path/file.txt", "other/path.txt"].map(AsRef::as_ref) + ) + ); + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon( + &["folder/file", "/home/user/file", "../../file"].map(AsRef::as_ref) + ) + ); + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon(&[".."].map(AsRef::as_ref)) + ); + assert_eq!( + None, + icon_theme_config.resolve_path_to_icon(&["."].map(AsRef::as_ref)) + ); + } + + #[test] + fn resolve_path_to_icon_file_name_match_some() { + let icon_theme_config = get_icon_theme_config(); + + assert_eq!( + Some("icons/makefile.svg".to_owned().into()), + icon_theme_config.resolve_path_to_icon(&["Makefile"].map(AsRef::as_ref)) + ); + assert_eq!( + Some("icons/makefile.svg".to_owned().into()), + icon_theme_config.resolve_path_to_icon( + &[ + "baz/Makefile", + "/foo/bar/dir/Makefile", + ".././/././Makefile" + ] + .map(AsRef::as_ref) + ) + ); + assert_eq!( + Some("icons/special.svg".to_owned().into()), + icon_theme_config.resolve_path_to_icon( + &["dir/special.rs", "/dir1/dir2/..//./special.rs"] + .map(AsRef::as_ref) + ) + ); + } + + #[test] + fn resolve_path_to_icon_extension_match_some() { + let icon_theme_config = get_icon_theme_config(); + + assert_eq!( + Some("icons/python.svg".to_owned().into()), + icon_theme_config + .resolve_path_to_icon(&["source.py"].map(AsRef::as_ref)) + ); + assert_eq!( + Some("icons/rust.svg".to_owned().into()), + icon_theme_config.resolve_path_to_icon( + &["/home/user/main.rs", "../../special.rs.rs", "special.rs"] + .map(AsRef::as_ref) + ) + ); + assert_eq!( + Some("icons/c.svg".to_owned().into()), + icon_theme_config.resolve_path_to_icon( + &["/dir1/Makefile.c", "../main.c"].map(AsRef::as_ref) + ) + ); + } } diff --git a/lapce-app/src/config/terminal.rs b/lapce-app/src/config/terminal.rs index 7c386a7a88..0339f459f1 100644 --- a/lapce-app/src/config/terminal.rs +++ b/lapce-app/src/config/terminal.rs @@ -72,4 +72,31 @@ impl TerminalConfig { self.indexed_colors = Arc::new(indexed_colors); } + + pub fn get_default_profile( + &self, + ) -> Option { + let Some(profile) = self.profiles.get( + self.default_profile + .get(&std::env::consts::OS.to_string()) + .unwrap_or(&String::from("default")), + ) else { + return None; + }; + let workdir = if let Some(workdir) = &profile.workdir { + url::Url::parse(&workdir.display().to_string()).ok() + } else { + None + }; + + let profile = profile.clone(); + + Some(lapce_rpc::terminal::TerminalProfile { + name: std::env::consts::OS.to_string(), + command: profile.command, + arguments: profile.arguments, + workdir, + environment: profile.environment, + }) + } } diff --git a/lapce-app/src/debug.rs b/lapce-app/src/debug.rs index cc431683be..71aebe827b 100644 --- a/lapce-app/src/debug.rs +++ b/lapce-app/src/debug.rs @@ -9,7 +9,7 @@ use std::{ use floem::{ ext_event::create_ext_action, reactive::{Memo, RwSignal, Scope}, - views::VirtualListVector, + views::VirtualVector, }; use lapce_rpc::{ dap_types::{ @@ -401,14 +401,15 @@ pub struct DapVariableViewdata { pub level: usize, } -impl VirtualListVector for DapVariable { - type ItemIterator = Box>; - +impl VirtualVector for DapVariable { fn total_len(&self) -> usize { self.children_expanded_count } - fn slice(&mut self, range: std::ops::Range) -> Self::ItemIterator { + fn slice( + &mut self, + range: std::ops::Range, + ) -> impl Iterator { let min = range.start; let max = range.end; let mut i = 0; @@ -416,11 +417,11 @@ impl VirtualListVector for DapVariable { for item in self.children.iter() { i = item.append_view_slice(&mut view_items, min, max, i + 1, 0); if i > max { - return Box::new(view_items.into_iter()); + return view_items.into_iter(); } } - Box::new(view_items.into_iter()) + view_items.into_iter() } } diff --git a/lapce-app/src/doc.rs b/lapce-app/src/doc.rs index 0b16f1cbaa..db60f646e6 100644 --- a/lapce-app/src/doc.rs +++ b/lapce-app/src/doc.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, cell::RefCell, collections::HashMap, path::{Path, PathBuf}, @@ -7,12 +8,14 @@ use std::{ time::Duration, }; -use clipboard::{ClipboardContext, ClipboardProvider}; use floem::{ action::exec_after, - cosmic_text::{Attrs, AttrsList, FamilyOwned, TextLayout}, + cosmic_text::{ + Attrs, AttrsList, FamilyOwned, LineHeightValue, Stretch, TextLayout, Weight, + }, ext_event::create_ext_action, - reactive::{RwSignal, Scope}, + peniko::Color, + reactive::{batch, RwSignal, Scope}, }; use itertools::Itertools; use lapce_core::{ @@ -21,6 +24,7 @@ use lapce_core::{ rope_text::RopeText, Buffer, InvalLines, }, + char_buffer::CharBuffer, command::EditCommand, cursor::Cursor, editor::{EditType, Editor}, @@ -50,7 +54,10 @@ use smallvec::SmallVec; use self::phantom_text::{PhantomText, PhantomTextKind, PhantomTextLine}; use crate::{ config::{color::LapceColor, LapceConfig}, - editor::view_data::{LineExtraStyle, TextLayoutCache, TextLayoutLine}, + editor::{ + view_data::{LineExtraStyle, TextLayoutLine}, + visual_line::TextLayoutCache, + }, find::{Find, FindProgress, FindResult}, history::DocumentHistory, window_tab::CommonData, @@ -59,9 +66,7 @@ use crate::{ pub mod phantom_text; -pub struct SystemClipboard { - ctx: ClipboardContext, -} +pub struct SystemClipboard; impl Default for SystemClipboard { fn default() -> Self { @@ -71,19 +76,17 @@ impl Default for SystemClipboard { impl SystemClipboard { pub fn new() -> Self { - SystemClipboard { - ctx: ClipboardProvider::new().unwrap(), - } + Self } } impl Clipboard for SystemClipboard { fn get_string(&mut self) -> Option { - self.ctx.get_contents().ok() + floem::Clipboard::get_contents().ok() } fn put_string(&mut self, s: impl AsRef) { - let _ = self.ctx.set_contents(s.as_ref().to_string()); + let _ = floem::Clipboard::set_contents(s.as_ref().to_string()); } } @@ -166,601 +169,291 @@ impl std::fmt::Debug for Document { } } -/// A single document that can be viewed by multiple [`EditorData`]'s -/// [`EditorViewData`]s and [`EditorView]s. -#[derive(Clone)] -pub struct Document { - pub scope: Scope, - pub buffer_id: BufferId, - pub content: RwSignal, - pub cache_rev: RwSignal, - /// Whether the buffer's content has been loaded/initialized into the buffer. - pub loaded: RwSignal, - pub buffer: RwSignal, - pub syntax: RwSignal, - /// Semantic highlighting information (which is provided by the LSP) - semantic_styles: RwSignal>>, - /// Inlay hints for the document - pub inlay_hints: RwSignal>>, - /// Current completion lens text, if any. - /// This will be displayed even on views that are not focused. - pub completion_lens: RwSignal>, - /// (line, col) - pub completion_pos: RwSignal<(usize, usize)>, - /// ime preedit information - pub preedit: RwSignal>, - /// (Offset -> (Plugin the code actions are from, Code Actions)) - pub code_actions: - RwSignal>>, - /// Stores information about different versions of the document from source control. - histories: RwSignal>, - pub head_changes: RwSignal>, - line_styles: Rc>, - /// The text layouts for the document. This may be shared with other views. - text_layouts: Rc>, - /// A cache for the sticky headers which maps a line to the lines it should show in the header. - pub sticky_headers: Rc>>>>, - pub find_result: FindResult, - /// The diagnostics for the document - pub diagnostics: DiagnosticData, - common: Rc, +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum RenderWhitespace { + #[default] + None, + All, + Boundary, + Trailing, } -impl Document { - pub fn new( - cx: Scope, - path: PathBuf, - diagnostics: DiagnosticData, - common: Rc, - ) -> Self { - let syntax = Syntax::init(&path); - Self { - scope: cx, - buffer_id: BufferId::next(), - buffer: cx.create_rw_signal(Buffer::new("")), - cache_rev: cx.create_rw_signal(0), - syntax: cx.create_rw_signal(syntax), - line_styles: Rc::new(RefCell::new(HashMap::new())), - semantic_styles: cx.create_rw_signal(None), - inlay_hints: cx.create_rw_signal(None), - diagnostics, - completion_lens: cx.create_rw_signal(None), - completion_pos: cx.create_rw_signal((0, 0)), - content: cx.create_rw_signal(DocContent::File { - path, - read_only: false, - }), - loaded: cx.create_rw_signal(false), - histories: cx.create_rw_signal(im::HashMap::new()), - head_changes: cx.create_rw_signal(im::Vector::new()), - text_layouts: Rc::new(RefCell::new(TextLayoutCache::new())), - sticky_headers: Rc::new(RefCell::new(HashMap::new())), - code_actions: cx.create_rw_signal(im::HashMap::new()), - find_result: FindResult::new(cx), - preedit: cx.create_rw_signal(None), - common, - } +// TODO(floem-editor): Provide a struct version which just has all the fields as a decent default +/// Style information for a specific line. +/// Created by [`Backend::line_style(line)`] +/// This provides a way to query for specific line information. It is not necessarily still valid +/// if there have been edits since it was created. +pub trait LineStyling: Sized { + // TODO: should this return LineHeightValue + /// Default line-height for this line + fn line_height(&self) -> f32 { + let font_size = self.font_size() as f32; + (1.5 * font_size).round().max(font_size) } - pub fn new_local(cx: Scope, common: Rc) -> Self { - Self::new_content(cx, DocContent::Local, common) + /// Default font family for this line + fn font_family(&self) -> Cow<[FamilyOwned]> { + Cow::Borrowed(&[FamilyOwned::SansSerif]) } - pub fn new_content( - cx: Scope, - content: DocContent, - common: Rc, - ) -> Self { - let cx = cx.create_child(); - Self { - scope: cx, - buffer_id: BufferId::next(), - buffer: cx.create_rw_signal(Buffer::new("")), - cache_rev: cx.create_rw_signal(0), - content: cx.create_rw_signal(content), - syntax: cx.create_rw_signal(Syntax::plaintext()), - line_styles: Rc::new(RefCell::new(HashMap::new())), - sticky_headers: Rc::new(RefCell::new(HashMap::new())), - semantic_styles: cx.create_rw_signal(None), - inlay_hints: cx.create_rw_signal(None), - diagnostics: DiagnosticData { - expanded: cx.create_rw_signal(true), - diagnostics: cx.create_rw_signal(im::Vector::new()), - }, - completion_lens: cx.create_rw_signal(None), - completion_pos: cx.create_rw_signal((0, 0)), - loaded: cx.create_rw_signal(true), - histories: cx.create_rw_signal(im::HashMap::new()), - head_changes: cx.create_rw_signal(im::Vector::new()), - text_layouts: Rc::new(RefCell::new(TextLayoutCache::new())), - code_actions: cx.create_rw_signal(im::HashMap::new()), - find_result: FindResult::new(cx), - preedit: cx.create_rw_signal(None), - common, - } + /// Default font size for this line + fn font_size(&self) -> usize { + 16 } - pub fn new_hisotry( - cx: Scope, - content: DocContent, - common: Rc, - ) -> Self { - let syntax = if let DocContent::History(history) = &content { - Syntax::init(&history.path) - } else { - Syntax::plaintext() - }; - let cx = cx.create_child(); - Self { - scope: cx, - buffer_id: BufferId::next(), - buffer: cx.create_rw_signal(Buffer::new("")), - cache_rev: cx.create_rw_signal(0), - content: cx.create_rw_signal(content), - syntax: cx.create_rw_signal(syntax), - line_styles: Rc::new(RefCell::new(HashMap::new())), - sticky_headers: Rc::new(RefCell::new(HashMap::new())), - semantic_styles: cx.create_rw_signal(None), - inlay_hints: cx.create_rw_signal(None), - diagnostics: DiagnosticData { - expanded: cx.create_rw_signal(true), - diagnostics: cx.create_rw_signal(im::Vector::new()), - }, - completion_lens: cx.create_rw_signal(None), - completion_pos: cx.create_rw_signal((0, 0)), - loaded: cx.create_rw_signal(true), - histories: cx.create_rw_signal(im::HashMap::new()), - head_changes: cx.create_rw_signal(im::Vector::new()), - text_layouts: Rc::new(RefCell::new(TextLayoutCache::new())), - code_actions: cx.create_rw_signal(im::HashMap::new()), - find_result: FindResult::new(cx), - preedit: cx.create_rw_signal(None), - common, - } + fn color(&self) -> Color { + Color::BLACK } - pub fn set_syntax(&self, syntax: Syntax) { - self.syntax.set(syntax); - if self.semantic_styles.with_untracked(|s| s.is_none()) { - self.clear_style_cache(); - } - self.clear_sticky_headers_cache(); + fn weight(&self) -> Weight { + Weight::NORMAL } - /// Set the syntax highlighting this document should use. - pub fn set_language(&self, language: LapceLanguage) { - self.syntax.set(Syntax::from_language(language)); + // TODO(minor): better name? + fn italic_style(&self) -> floem::cosmic_text::Style { + floem::cosmic_text::Style::Normal } - pub fn find(&self) -> &Find { - &self.common.find + fn stretch(&self) -> Stretch { + Stretch::Normal } - /// Whether or not the underlying buffer is loaded - pub fn loaded(&self) -> bool { - self.loaded.get_untracked() + fn tab_width(&self) -> usize { + 4 } - //// Initialize the content with some text, this marks the document as loaded. - pub fn init_content(&self, content: Rope) { - self.syntax.with_untracked(|syntax| { - self.buffer.update(|buffer| { - buffer.init_content(content); - buffer.detect_indent(syntax); - }); - }); - self.loaded.set(true); - self.on_update(None); - self.init_diagnostics(); - self.retrieve_head(); + fn render_whitespace(&self) -> RenderWhitespace { + RenderWhitespace::None } - /// Reload the document's content, and is what you should typically use when you want to *set* - /// an existing document's content. - pub fn reload(&self, content: Rope, set_pristine: bool) { - // self.code_actions.clear(); - // self.inlay_hints = None; - let delta = self - .buffer - .try_update(|buffer| buffer.reload(content, set_pristine)) - .unwrap(); - self.apply_deltas(&[delta]); + /// Get the color for specific style names returned by [`Self::line_style`] + /// In Lapce this is used for getting `style.` colors, such as for syntax highlighting. + fn style_color(&self, _style: &str) -> Option { + None } - pub fn handle_file_changed(&self, content: Rope) { - if self.is_pristine() { - self.reload(content, true); - } - } + // TODO: provide functions to do common phantom text operations without actually creating the + // entire phantom text, it would make many pieces of logic cheaper due to not having to + // allocate and construct them. - pub fn do_insert( - &self, - cursor: &mut Cursor, - s: &str, - config: &LapceConfig, - ) -> Vec<(RopeDelta, InvalLines, SyntaxEdit)> { - if self.content.with_untracked(|c| c.read_only()) { - return Vec::new(); - } + // This does not have a default implementation because it *should* provide relevant IME phantom + // text by default! + fn phantom_text(&self) -> PhantomTextLine; - let old_cursor = cursor.mode.clone(); - let deltas = self.syntax.with_untracked(|syntax| { - self.buffer - .try_update(|buffer| { - Editor::insert( - cursor, - buffer, - s, - syntax, - config.editor.auto_closing_matching_pairs, - config.editor.auto_surround, - ) - }) - .unwrap() - }); - // Keep track of the change in the cursor mode for undo/redo - self.buffer.update(|buffer| { - buffer.set_cursor_before(old_cursor); - buffer.set_cursor_after(cursor.mode.clone()); - }); - self.apply_deltas(&deltas); - deltas + fn line_style(&self) -> Arc> { + Arc::new(Vec::new()) } +} - pub fn do_raw_edit( - &self, - edits: &[(impl AsRef, &str)], - edit_type: EditType, - ) -> Option<(RopeDelta, InvalLines, SyntaxEdit)> { - if self.content.with_untracked(|c| c.read_only()) { - return None; - } - let (delta, inval_lines, edits) = self - .buffer - .try_update(|buffer| buffer.edit(edits, edit_type)) - .unwrap(); - self.apply_deltas(&[(delta.clone(), inval_lines.clone(), edits.clone())]); - Some((delta, inval_lines, edits)) +/// The response from a save operation. +/// +/// Currently nothing. +#[derive(Debug)] +pub struct SaveResponse {} + +/// A backend for [`Document`] related operations, such as saving and opening files. +/// This allows swapping out the supplier of the documents. +/// Ex: A direct implementation that saves files to disk. +/// Ex: An implementation that uses a proxy, like `lapce-proxy` to load from local and remote +/// locations. +/// +/// These functions do not take a reference to `&self`, but rather to the `Document` which +/// the backend is accessible from. +/// +/// This requires `Clone` due to some logic on other threads needing references. This does mean +/// that types implementing backend should handle that gracefully. +pub trait Backend: Sized + Clone { + /// The general error type for the backend's operations. + /// This does not require [`std::error::Error`] due to the common `anyhow::Error` not + /// implementing it. + /// + /// A single type is used rather than many different error types at the moment, but this may be + /// changed in the future if it seems beneficial + type Error: std::fmt::Debug; + + /// The type for style information on the line. + type LineStyling: LineStyling; + + /// Get an identifier for the config, used for clearing the cache if it were to change. + fn config_id(doc: &Document) -> u64; + + /// We've initialized the content of the document. The buffer holds the new content. + /// This is ran before `on_update` is called. + /// Note that this is called from within a [`batch`] + fn pre_update_init_content(_doc: &Document) {} + + /// We're initializing the content of the document. The buffer holds the new content. + /// Note that this is called from within a [`batch`] + fn init_content(_doc: &Document) {} + + /// Called when the document is updated, like when there is an edit. + /// Note: this is called from within a [`batch`] + fn on_update(_doc: &Document, _edits: Option<&[SyntaxEdit]>) {} + + // TODO(floem-editor): We may need to pass in the computed `rev` since updating proxy uses it + /// Apply a single edit delta + fn apply_delta( + _doc: &Document, + _rev: u64, + _delta: &RopeDelta, + _inval: &InvalLines, + ) { } - pub fn do_edit( - &self, - cursor: &mut Cursor, - cmd: &EditCommand, - modal: bool, - register: &mut Register, - smart_tab: bool, - ) -> Vec<(RopeDelta, InvalLines, SyntaxEdit)> { - if self.content.with_untracked(|c| c.read_only()) - && !cmd.not_changing_buffer() - { - return Vec::new(); - } - - let mut clipboard = SystemClipboard::new(); - let old_cursor = cursor.mode.clone(); - let deltas = self.syntax.with_untracked(|syntax| { - self.buffer - .try_update(|buffer| { - Editor::do_edit( - cursor, - buffer, - cmd, - syntax, - &mut clipboard, - modal, - register, - smart_tab, - ) - }) - .unwrap() - }); + /// Save a file (potentially asynchronously). + /// The callback will be called with the result of the save operation + fn save( + doc: &Document, + cb: impl FnOnce(Result) + 'static, + ); + + /// How often should the document autosave + /// Returns `None` if autosave is disabled + fn autosave_interval(_doc: &Document) -> Option { + None + } - if !deltas.is_empty() { - self.buffer.update(|buffer| { - buffer.set_cursor_before(old_cursor); - buffer.set_cursor_after(cursor.mode.clone()); - }); - } + fn line_styling(doc: &Document, line: usize) -> Self::LineStyling; - self.apply_deltas(&deltas); - deltas + /// Apply styles onto the text layout line + fn apply_styles( + _doc: &Document, + _line: usize, + _text_layout_line: &mut TextLayoutLine, + ) { } - pub fn apply_deltas(&self, deltas: &[(RopeDelta, InvalLines, SyntaxEdit)]) { - let rev = self.rev() - deltas.len() as u64; - for (i, (delta, inval, _)) in deltas.iter().enumerate() { - self.update_styles(delta); - self.update_inlay_hints(delta); - self.update_diagnostics(delta); - self.update_completion_lens(delta); - self.update_find_result(delta); - if let DocContent::File { path, .. } = self.content.get_untracked() { - self.update_breakpoints(delta, &path, &inval.old_text); - self.common - .proxy - .update(path, delta.clone(), rev + i as u64 + 1); - } - } + fn clear_style_cache(_doc: &Document) {} - // TODO(minor): We could avoid this potential allocation since most apply_delta callers are actually using a Vec - // which we could reuse. - // We use a smallvec because there is unlikely to be more than a couple of deltas - let edits = deltas.iter().map(|(_, _, edits)| edits.clone()).collect(); - self.on_update(Some(edits)); + // TODO: should sticky headers be supplied conditionally from the outside instead? So they can + // set them however they want to whatever they want? + /// Get the sticky headers for a line + fn sticky_headers(_doc: &Document, _line: usize) -> Option> { + None } - pub fn is_pristine(&self) -> bool { - self.buffer.with_untracked(|b| b.is_pristine()) + /// Get the indentation line for a line + fn indent_line( + _doc: &Document, + line: usize, + _line_content: &str, + ) -> usize { + line } - /// Get the buffer's current revision. This is used to track whether the buffer has changed. - pub fn rev(&self) -> u64 { - self.buffer.with_untracked(|b| b.rev()) + /// Get the previous unmatched character `c` from the offset + fn previous_unmatched( + &self, + buffer: &Buffer, + c: char, + offset: usize, + ) -> Option { + WordCursor::new(buffer.text(), offset).previous_unmatched(c) } - fn on_update(&self, edits: Option>) { - self.clear_code_actions(); - self.clear_style_cache(); - self.trigger_syntax_change(edits); - self.clear_sticky_headers_cache(); - self.trigger_head_change(); - self.check_auto_save(); - self.get_semantic_styles(); - self.get_inlay_hints(); - self.find_result.reset(); + fn comment_token(_doc: &Document) -> &str { + "" } - fn check_auto_save(&self) { - let config = self.common.config.get_untracked(); - if config.editor.autosave_interval > 0 { - if !self.content.with_untracked(|c| c.is_file()) { - return; - }; - let rev = self.rev(); - let doc = self.clone(); - exec_after( - Duration::from_millis(config.editor.autosave_interval), - move |_| { - let current_rev = match doc - .buffer - .try_with_untracked(|b| b.as_ref().map(|b| b.rev())) - { - Some(rev) => rev, - None => return, - }; - - if current_rev != rev || doc.is_pristine() { - return; - } + // TODO: configurable + /// Wheter it should automatically close matching pairs like `()`, `[]`, `""`, etc. + fn auto_closing_matching_pairs(_doc: &Document) -> bool { + false + } - doc.save(|| {}); - }, - ); - } + /// Whether it should automatically surround the selection with matching pairs like `()`, `""`, + /// etc. + fn auto_surround(_doc: &Document) -> bool { + false } +} - /// Update the styles after an edit, so the highlights are at the correct positions. - /// This does not do a reparse of the document itself. - fn update_styles(&self, delta: &RopeDelta) { - self.semantic_styles.update(|styles| { - if let Some(styles) = styles.as_mut() { - styles.apply_shape(delta); - } - }); - self.syntax.update(|syntax| { - if let Some(styles) = syntax.styles.as_mut() { - styles.apply_shape(delta); - } - syntax.lens.apply_delta(delta); - }); +#[derive(Clone)] +pub struct DocLineStyling { + line: usize, + // TODO: should we just clone document due to how much of this it grabs.... + config: Arc, + doc: Document, +} +impl DocLineStyling { + /// Iterate over the editor diagnostics on this line + fn iter_diagnostics(&self) -> impl Iterator + '_ { + self.config + .editor + .enable_completion_lens + .then_some(()) + .map(|_| self.doc.backend.diagnostics.diagnostics.get_untracked()) + .into_iter() + .flatten() + .filter(|diag| { + diag.diagnostic.range.end.line as usize == self.line + && diag.diagnostic.severity < Some(DiagnosticSeverity::HINT) + }) } - /// Update the inlay hints so their positions are correct after an edit. - fn update_inlay_hints(&self, delta: &RopeDelta) { - self.inlay_hints.update(|inlay_hints| { - if let Some(hints) = inlay_hints.as_mut() { - hints.apply_shape(delta); + /// Get the max severity of the diagnostics. + /// This is used to determine the color given to the background of the line + fn max_diag_severity(&self) -> Option { + let mut max_severity = None; + for diag in self.iter_diagnostics() { + match (diag.diagnostic.severity, max_severity) { + (Some(severity), Some(max)) => { + if severity < max { + max_severity = Some(severity); + } + } + (Some(severity), None) => { + max_severity = Some(severity); + } + _ => {} } - }); - } - - pub fn trigger_syntax_change(&self, edits: Option>) { - let (rev, text) = - self.buffer.with_untracked(|b| (b.rev(), b.text().clone())); - - self.syntax.update(|syntax| { - syntax.parse(rev, text, edits.as_deref()); - }); - } - - fn clear_style_cache(&self) { - self.line_styles.borrow_mut().clear(); - self.clear_text_cache(); - } - - fn clear_code_actions(&self) { - self.code_actions.update(|c| { - c.clear(); - }); - } + } - pub fn set_preedit( - &self, - text: String, - cursor: Option<(usize, usize)>, - offset: usize, - ) { - self.preedit.set(Some(Preedit { - text, - cursor, - offset, - })); - self.clear_text_cache(); + max_severity } - - pub fn clear_preedit(&self) { - self.preedit.set(None); - self.clear_text_cache(); +} +impl LineStyling for DocLineStyling { + fn line_height(&self) -> f32 { + self.config.editor.line_height() as f32 } - /// Inform any dependents on this document that they should clear any cached text. - pub fn clear_text_cache(&self) { - let cache_rev = self - .cache_rev - .try_update(|cache_rev| { - *cache_rev += 1; - *cache_rev - }) - .unwrap(); - self.text_layouts.borrow_mut().clear(cache_rev); - } + fn font_family(&self) -> Cow<[FamilyOwned]> { + // TODO: cache this font family + let families = + FamilyOwned::parse_list(&self.config.editor.font_family).collect(); - fn clear_sticky_headers_cache(&self) { - self.sticky_headers.borrow_mut().clear(); + Cow::Owned(families) } - /// Get the active style information, either the semantic styles or the - /// tree-sitter syntax styles. - fn styles(&self) -> Option> { - if let Some(semantic_styles) = self.semantic_styles.get_untracked() { - Some(semantic_styles) - } else { - self.syntax.with_untracked(|syntax| syntax.styles.clone()) - } + fn font_size(&self) -> usize { + self.config.editor.font_size() } - /// Get the style information for the particular line from semantic/syntax highlighting. - /// This caches the result if possible. - pub fn line_style(&self, line: usize) -> Arc> { - if self.line_styles.borrow().get(&line).is_none() { - let styles = self.styles(); - - let line_styles = styles - .map(|styles| { - let text = - self.buffer.with_untracked(|buffer| buffer.text().clone()); - line_styles(&text, line, &styles) - }) - .unwrap_or_default(); - self.line_styles - .borrow_mut() - .insert(line, Arc::new(line_styles)); - } - self.line_styles.borrow().get(&line).cloned().unwrap() + fn color(&self) -> Color { + self.config.color(LapceColor::EDITOR_FOREGROUND) } - /// Request semantic styles for the buffer from the LSP through the proxy. - fn get_semantic_styles(&self) { - if !self.loaded() { - return; - } - - let path = - if let DocContent::File { path, .. } = self.content.get_untracked() { - path - } else { - return; - }; - - let (rev, len) = self.buffer.with_untracked(|b| (b.rev(), b.len())); - - let syntactic_styles = - self.syntax.with_untracked(|syntax| syntax.styles.clone()); - - let doc = self.clone(); - let send = create_ext_action(self.scope, move |styles| { - if doc.buffer.with_untracked(|b| b.rev()) == rev { - doc.semantic_styles.set(Some(styles)); - doc.clear_style_cache(); - } - }); - - self.common.proxy.get_semantic_tokens(path, move |result| { - if let Ok(ProxyResponse::GetSemanticTokens { styles }) = result { - rayon::spawn(move || { - let mut styles_span = SpansBuilder::new(len); - for style in styles.styles { - styles_span.add_span( - Interval::new(style.start, style.end), - style.style, - ); - } - - let styles = styles_span.build(); - - let styles = if let Some(syntactic_styles) = syntactic_styles { - syntactic_styles.merge(&styles, |a, b| { - if let Some(b) = b { - return b.clone(); - } - a.clone() - }) - } else { - styles - }; - - send(styles); - }); - } - }); + fn tab_width(&self) -> usize { + self.config.editor.tab_width } - /// Request inlay hints for the buffer from the LSP through the proxy. - fn get_inlay_hints(&self) { - if !self.loaded() { - return; - } - - let path = - if let DocContent::File { path, .. } = self.content.get_untracked() { - path - } else { - return; - }; - - let (buffer, rev, len) = self - .buffer - .with_untracked(|b| (b.clone(), b.rev(), b.len())); - - let doc = self.clone(); - let send = create_ext_action(self.scope, move |hints| { - if doc.buffer.with_untracked(|b| b.rev()) == rev { - doc.inlay_hints.set(Some(hints)); - doc.clear_text_cache(); - } - }); - - self.common.proxy.get_inlay_hints(path, move |result| { - if let Ok(ProxyResponse::GetInlayHints { mut hints }) = result { - // Sort the inlay hints by their position, as the LSP does not guarantee that it will - // provide them in the order that they are in within the file - // as well, Spans does not iterate in the order that they appear - hints.sort_by(|left, right| left.position.cmp(&right.position)); - - let mut hints_span = SpansBuilder::new(len); - for hint in hints { - let offset = buffer.offset_of_position(&hint.position).min(len); - hints_span.add_span( - Interval::new(offset, (offset + 1).min(len)), - hint, - ); - } - let hints = hints_span.build(); - send(hints); - } - }); + fn style_color(&self, style: &str) -> Option { + self.config.style_color(style) } - /// Get the phantom text for a given line - pub fn line_phantom_text(&self, line: usize) -> PhantomTextLine { - let config = self.common.config.get_untracked(); + fn phantom_text(&self) -> PhantomTextLine { + let backend = &self.doc.backend; + let config = &self.config; - let (start_offset, end_offset) = self.buffer.with_untracked(|buffer| { - (buffer.offset_of_line(line), buffer.offset_of_line(line + 1)) + let (start_offset, end_offset) = self.doc.buffer.with_untracked(|buffer| { + ( + buffer.offset_of_line(self.line), + buffer.offset_of_line(self.line + 1), + ) }); - let inlay_hints = self.inlay_hints.get_untracked(); + let inlay_hints = backend.inlay_hints.get_untracked(); // If hints are enabled, and the hints field is filled, then get the hints for this line // and convert them into PhantomText instances let hints = config @@ -776,6 +469,7 @@ impl Document { }) .map(|(interval, inlay_hint)| { let (_, col) = self + .doc .buffer .with_untracked(|b| b.offset_to_line_col(interval.start)); let text = match &inlay_hint.label { @@ -788,10 +482,10 @@ impl Document { kind: PhantomTextKind::InlayHint, col, text, - fg: Some(*config.get_color(LapceColor::INLAY_HINT_FOREGROUND)), + fg: Some(config.color(LapceColor::INLAY_HINT_FOREGROUND)), // font_family: Some(config.editor.inlay_hint_font_family()), font_size: Some(config.editor.inlay_hint_font_size()), - bg: Some(*config.get_color(LapceColor::INLAY_HINT_BACKGROUND)), + bg: Some(config.color(LapceColor::INLAY_HINT_BACKGROUND)), under_line: None, } }); @@ -800,8 +494,6 @@ impl Document { // overall. let mut text: SmallVec<[PhantomText; 6]> = hints.collect(); - // The max severity is used to determine the color given to the background of the line - let mut max_severity = None; // If error lens is enabled, and the diagnostics field is filled, then get the diagnostics // that end on this line which have a severity worse than HINT and convert them into // PhantomText instances @@ -809,28 +501,17 @@ impl Document { .editor .enable_error_lens .then_some(()) - .map(|_| self.diagnostics.diagnostics.get_untracked()) + .map(|_| backend.diagnostics.diagnostics.get_untracked()) .into_iter() .flatten() .filter(|diag| { - diag.diagnostic.range.end.line as usize == line + diag.diagnostic.range.end.line as usize == self.line && diag.diagnostic.severity < Some(DiagnosticSeverity::HINT) }) .map(|diag| { - match (diag.diagnostic.severity, max_severity) { - (Some(severity), Some(max)) => { - if severity < max { - max_severity = Some(severity); - } - } - (Some(severity), None) => { - max_severity = Some(severity); - } - _ => {} - } - - let col = self.buffer.with_untracked(|buffer| { - buffer.offset_of_line(line + 1) - buffer.offset_of_line(line) + let col = self.doc.buffer.with_untracked(|buffer| { + buffer.offset_of_line(self.line + 1) + - buffer.offset_of_line(self.line) }); let fg = { let severity = diag @@ -846,10 +527,14 @@ impl Document { LapceColor::ERROR_LENS_OTHER_FOREGROUND }; - *config.get_color(theme_prop) + config.color(theme_prop) + }; + + let text = if config.editor.error_lens_multiline { + format!(" {}", diag.diagnostic.message) + } else { + format!(" {}", diag.diagnostic.message.lines().join(" ")) }; - let text = - format!(" {}", diag.diagnostic.message.lines().join(" ")); PhantomText { kind: PhantomTextKind::Diagnostic, col, @@ -865,19 +550,20 @@ impl Document { text.append(&mut diag_text); - let (completion_line, completion_col) = self.completion_pos.get_untracked(); + let (completion_line, completion_col) = + backend.completion_pos.get_untracked(); let completion_text = config .editor .enable_completion_lens .then_some(()) - .and(self.completion_lens.get_untracked()) + .and(backend.completion_lens.get_untracked()) // TODO: We're probably missing on various useful completion things to include here! - .filter(|_| line == completion_line) + .filter(|_| self.line == completion_line) .map(|completion| PhantomText { kind: PhantomTextKind::Completion, col: completion_col, text: completion.clone(), - fg: Some(*config.get_color(LapceColor::COMPLETION_LENS_FOREGROUND)), + fg: Some(config.color(LapceColor::COMPLETION_LENS_FOREGROUND)), font_size: Some(config.editor.completion_lens_font_size()), // font_family: Some(config.editor.completion_lens_font_family()), bg: None, @@ -888,27 +574,37 @@ impl Document { text.push(completion_text); } - if let Some(preedit) = self.preedit.get_untracked() { - let (ime_line, col) = self - .buffer - .with_untracked(|b| b.offset_to_line_col(preedit.offset)); - if line == ime_line { - text.push(PhantomText { - kind: PhantomTextKind::Ime, - text: preedit.text, - col, - font_size: None, - fg: None, - bg: None, - under_line: Some( - *self - .common - .config - .get_untracked() - .get_color(LapceColor::EDITOR_FOREGROUND), - ), - }); - } + // TODO: don't display completion lens and inline completion at the same time + // and/or merge them so that they can be shifted between like multiple inline completions + // can + let (inline_completion_line, inline_completion_col) = + backend.inline_completion_pos.get_untracked(); + let inline_completion_text = config + .editor + .enable_inline_completion + .then_some(()) + .and(backend.inline_completion.get_untracked()) + .filter(|_| self.line == inline_completion_line) + .map(|completion| PhantomText { + kind: PhantomTextKind::Completion, + col: inline_completion_col, + text: completion.clone(), + fg: Some(config.color(LapceColor::COMPLETION_LENS_FOREGROUND)), + font_size: Some(config.editor.completion_lens_font_size()), + // font_family: Some(config.editor.completion_lens_font_family()), + bg: None, + under_line: None, + // TODO: italics? + }); + if let Some(inline_completion_text) = inline_completion_text { + text.push(inline_completion_text); + } + + if let Some(preedit) = self.doc.preedit_phantom_text( + Some(config.color(LapceColor::EDITOR_FOREGROUND)), + self.line, + ) { + text.push(preedit) } text.sort_by(|a, b| { @@ -919,81 +615,135 @@ impl Document { } }); - PhantomTextLine { text, max_severity } + PhantomTextLine { text } } - /// Update the diagnostics' positions after an edit so that they appear in the correct place. - fn update_diagnostics(&self, delta: &RopeDelta) { - if self - .diagnostics - .diagnostics - .with_untracked(|d| d.is_empty()) - { - return; - } - self.diagnostics.diagnostics.update(|diagnostics| { - for diagnostic in diagnostics.iter_mut() { - let mut transformer = Transformer::new(delta); - let (start, end) = diagnostic.range; - let (new_start, new_end) = ( - transformer.transform(start, false), - transformer.transform(end, true), - ); + /// Get the style information for the particular line from semantic/syntax highlighting. + /// This caches the result if possible. + fn line_style(&self) -> Arc> { + let line = self.line; + let backend = &self.doc.backend; + if backend.line_styles.borrow().get(&line).is_none() { + let styles = backend.styles(); - let (new_start_pos, new_end_pos) = self.buffer.with_untracked(|b| { - ( - b.offset_to_position(new_start), - b.offset_to_position(new_end), - ) - }); + let line_styles = styles + .map(|styles| { + let text = self + .doc + .buffer + .with_untracked(|buffer| buffer.text().clone()); + line_styles(&text, line, &styles) + }) + .unwrap_or_default(); + backend + .line_styles + .borrow_mut() + .insert(line, Arc::new(line_styles)); + } + backend.line_styles.borrow().get(&line).cloned().unwrap() + } +} - diagnostic.range = (new_start, new_end); +/// (Offset -> (Plugin the code actions are from, Code Actions)) +pub type CodeActions = im::HashMap>; +// TODO(minor): we could try stripping this down to the fields it exactly needs, like proxy +/// Lapce backend for files accessible through proxy (local or remote). +#[derive(Clone)] +pub struct DocBackend { + pub syntax: RwSignal, + /// LSP Semantic highlighting information + semantic_styles: RwSignal>>, + line_styles: Rc>, - diagnostic.diagnostic.range.start = new_start_pos; - diagnostic.diagnostic.range.end = new_end_pos; - } - }); - } + /// Stores information about different versions of the document from source control. + histories: RwSignal>, + pub head_changes: RwSignal>, - /// init diagnostics offset ranges from lsp positions - pub fn init_diagnostics(&self) { - self.clear_text_cache(); - self.clear_code_actions(); - self.diagnostics.diagnostics.update(|diagnostics| { - for diagnostic in diagnostics.iter_mut() { - let (start, end) = self.buffer.with_untracked(|buffer| { - ( - buffer - .offset_of_position(&diagnostic.diagnostic.range.start), - buffer.offset_of_position(&diagnostic.diagnostic.range.end), - ) - }); - diagnostic.range = (start, end); - } - }); - } + /// Inlay hints for the document + pub inlay_hints: RwSignal>>, - /// Get the current completion lens text - pub fn completion_lens(&self) -> Option { - self.completion_lens.get_untracked() - } + /// The diagnostics for the document + pub diagnostics: DiagnosticData, - pub fn set_completion_lens( - &self, - completion_lens: String, - line: usize, - col: usize, - ) { - // TODO: more granular invalidation - self.clear_text_cache(); - self.completion_lens.set(Some(completion_lens)); - self.completion_pos.set((line, col)); + /// Current completion lens text, if any. + /// This will be displayed even on views that are not focused. + pub completion_lens: RwSignal>, + /// (line, col) + pub completion_pos: RwSignal<(usize, usize)>, + + /// Current inline completion text, if any. + /// This will be displayed even on views that are not focused. + pub inline_completion: RwSignal>, + /// (line, col) + pub inline_completion_pos: RwSignal<(usize, usize)>, + + /// (Offset -> (Plugin the code actions are from, Code Actions)) + pub code_actions: RwSignal, + + pub find_result: FindResult, + + common: Rc, +} +impl DocBackend { + pub fn new( + cx: Scope, + syntax: Syntax, + diagnostics: Option, + common: Rc, + ) -> Self { + let diagnostics = diagnostics.unwrap_or_else(|| DiagnosticData { + expanded: cx.create_rw_signal(true), + diagnostics: cx.create_rw_signal(im::Vector::new()), + }); + Self { + syntax: cx.create_rw_signal(syntax), + semantic_styles: cx.create_rw_signal(None), + line_styles: Rc::new(RefCell::new(HashMap::new())), + histories: cx.create_rw_signal(im::HashMap::new()), + head_changes: cx.create_rw_signal(im::Vector::new()), + inlay_hints: cx.create_rw_signal(None), + diagnostics, + completion_lens: cx.create_rw_signal(None), + completion_pos: cx.create_rw_signal((0, 0)), + inline_completion: cx.create_rw_signal(None), + inline_completion_pos: cx.create_rw_signal((0, 0)), + code_actions: cx.create_rw_signal(im::HashMap::new()), + find_result: FindResult::new(cx), + common, + } } - pub fn clear_completion_lens(&self) { - // TODO: more granular invalidation - self.clear_text_cache(); - self.completion_lens.set(None); + /// Update the styles after an edit, so the highlights are at the correct positions. + /// This does not do a reparse of the document itself. + fn update_styles(&self, delta: &RopeDelta) { + batch(|| { + self.semantic_styles.update(|styles| { + if let Some(styles) = styles.as_mut() { + styles.apply_shape(delta); + } + }); + self.syntax.update(|syntax| { + if let Some(styles) = syntax.styles.as_mut() { + styles.apply_shape(delta); + } + syntax.lens.apply_delta(delta); + }); + }); + } + + /// Update the inlay hints so their positions are correct after an edit. + fn update_inlay_hints(&self, delta: &RopeDelta) { + self.inlay_hints.update(|inlay_hints| { + if let Some(hints) = inlay_hints.as_mut() { + hints.apply_shape(delta); + } + }); + } + + fn clear_code_actions(&self) { + self.code_actions.update(|c| { + c.clear(); + }); } fn update_find_result(&self, delta: &RopeDelta) { @@ -1002,137 +752,1304 @@ impl Document { }) } - fn update_breakpoints(&self, delta: &RopeDelta, path: &Path, old_text: &Rope) { - if self - .common - .breakpoints - .with_untracked(|breakpoints| breakpoints.contains_key(path)) - { - self.common.breakpoints.update(|breakpoints| { - if let Some(path_breakpoints) = breakpoints.get_mut(path) { - let mut transformer = Transformer::new(delta); - self.buffer.with_untracked(|buffer| { - *path_breakpoints = path_breakpoints - .clone() - .into_values() - .map(|mut b| { - let offset = old_text.offset_of_line(b.line); - let offset = transformer.transform(offset, false); - let line = buffer.line_of_offset(offset); - b.line = line; - b.offset = offset; - (b.line, b) - }) - .collect(); - }); + /// Get the active style information, either the semantic styles or the + /// tree-sitter syntax styles. + fn styles(&self) -> Option> { + if let Some(semantic_styles) = self.semantic_styles.get_untracked() { + Some(semantic_styles) + } else { + self.syntax.with_untracked(|syntax| syntax.styles.clone()) + } + } +} +impl Backend for DocBackend { + type Error = (); + type LineStyling = DocLineStyling; + + fn config_id(doc: &Document) -> u64 { + doc.backend.common.config.with_untracked(|config| config.id) + } + + fn pre_update_init_content(doc: &Document) { + doc.backend.syntax.with_untracked(|syntax| { + doc.buffer.update(|buffer| { + buffer.detect_indent(syntax); + }); + }); + } + + fn init_content(doc: &Document) { + doc.init_diagnostics(); + doc.retrieve_head(); + } + + fn on_update(doc: &Document, edits: Option<&[SyntaxEdit]>) { + doc.backend.clear_code_actions(); + doc.trigger_syntax_change(edits); + doc.trigger_head_change(); + doc.get_semantic_styles(); + doc.get_inlay_hints(); + doc.backend.find_result.reset(); + } + + fn apply_delta( + doc: &Document, + rev: u64, + delta: &RopeDelta, + inval: &InvalLines, + ) { + doc.backend.update_find_result(delta); + doc.backend.update_styles(delta); + doc.backend.update_inlay_hints(delta); + doc.update_diagnostics(delta); + doc.update_completion_lens(delta); + if let DocContent::File { path, .. } = doc.content.get_untracked() { + doc.update_breakpoints(delta, &path, &inval.old_text); + doc.backend.common.proxy.update(path, delta.clone(), rev); + } + } + + fn save( + doc: &Document, + cb: impl FnOnce(Result) + 'static, + ) { + let content = doc.content.get_untracked(); + if let DocContent::File { path, .. } = content { + let rev = doc.rev(); + let buffer = doc.buffer; + let send = create_ext_action(doc.scope, move |result| { + if let Ok(ProxyResponse::SaveResponse {}) = result { + let current_rev = buffer.with_untracked(|buffer| buffer.rev()); + if current_rev == rev { + buffer.update(|buffer| { + buffer.set_pristine(); + }); + cb(Ok(SaveResponse {})); + } } }); + + doc.backend + .common + .proxy + .save(rev, path, true, move |result| { + send(result); + }) } } - /// Update the completion lens position after an edit so that it appears in the correct place. - pub fn update_completion_lens(&self, delta: &RopeDelta) { - let Some(completion) = self.completion_lens.get_untracked() else { - return; - }; + fn autosave_interval(doc: &Document) -> Option { + let interval = doc + .backend + .common + .config + .with_untracked(|config| config.editor.autosave_interval); - let (line, col) = self.completion_pos.get_untracked(); - let offset = self - .buffer - .with_untracked(|b| b.offset_of_line_col(line, col)); + if interval > 0 { + Some(Duration::from_millis(interval)) + } else { + None + } + } - // If the edit is easily checkable + updateable from, then we alter the lens' text. - // In normal typing, if we didn't do this, then the text would jitter forward and then - // backwards as the completion lens is updated. - // TODO: this could also handle simple deletion, but we don't currently keep track of - // the past copmletion lens string content in the field. - if delta.as_simple_insert().is_some() { - let (iv, new_len) = delta.summary(); - if iv.start() == iv.end() - && iv.start() == offset - && new_len <= completion.len() + fn line_styling(doc: &Document, line: usize) -> Self::LineStyling { + DocLineStyling { + line, + config: doc.backend.common.config.get_untracked(), + doc: doc.clone(), + } + } + + fn apply_styles( + doc: &Document, + line: usize, + text_layout_line: &mut TextLayoutLine, + ) { + let backend = &doc.backend; + let config = backend.common.config.get_untracked(); + + text_layout_line.extra_style.clear(); + let text_layout = &text_layout_line.text; + + let styling = doc.line_styling(line); + let phantom_text = styling.phantom_text(); + + let phantom_styles = phantom_text + .offset_size_iter() + .filter(move |(_, _, _, p)| p.bg.is_some() || p.under_line.is_some()) + .flat_map(move |(col_shift, size, col, phantom)| { + let start = col + col_shift; + let end = start + size; + + extra_styles_for_range( + text_layout, + start, + end, + phantom.bg, + phantom.under_line, + None, + ) + }); + text_layout_line.extra_style.extend(phantom_styles); + + // Add the styling for the diagnostic severity, if applicable + if let Some(max_severity) = styling.max_diag_severity() { + let theme_prop = if max_severity == DiagnosticSeverity::ERROR { + LapceColor::ERROR_LENS_ERROR_BACKGROUND + } else if max_severity == DiagnosticSeverity::WARNING { + LapceColor::ERROR_LENS_WARNING_BACKGROUND + } else { + LapceColor::ERROR_LENS_OTHER_BACKGROUND + }; + + let size = text_layout.size(); + let x1 = if !config.editor.error_lens_end_of_line { + let error_end_x = text_layout.size().width; + Some(error_end_x.max(size.width)) + } else { + None + }; + + // TODO(minor): Should we show the background only on wrapped lines that have the + // diagnostic actually on that line? + // That would make it more obvious where it is from and matches other editors. + text_layout_line.extra_style.push(LineExtraStyle { + x: 0.0, + y: 0.0, + width: x1, + height: text_layout.size().height, + bg_color: Some(config.color(theme_prop)), + under_line: None, + wave_line: None, + }); + } + + backend.diagnostics.diagnostics.with_untracked(|diags| { + doc.buffer.with_untracked(|buffer| { + for diag in diags { + if diag.diagnostic.range.start.line as usize <= line + && line <= diag.diagnostic.range.end.line as usize + { + let start = if diag.diagnostic.range.start.line as usize + == line + { + let (_, col) = buffer.offset_to_line_col(diag.range.0); + col + } else { + let offset = + buffer.first_non_blank_character_on_line(line); + let (_, col) = buffer.offset_to_line_col(offset); + col + }; + let start = phantom_text.col_after(start, true); + + let end = if diag.diagnostic.range.end.line as usize == line + { + let (_, col) = buffer.offset_to_line_col(diag.range.1); + col + } else { + buffer.line_end_col(line, true) + }; + let end = phantom_text.col_after(end, false); + + // let x0 = text_layout.hit_position(start).point.x; + // let x1 = text_layout.hit_position(end).point.x; + let color_name = match diag.diagnostic.severity { + Some(DiagnosticSeverity::ERROR) => { + LapceColor::LAPCE_ERROR + } + _ => LapceColor::LAPCE_WARN, + }; + let color = config.color(color_name); + + let styles = extra_styles_for_range( + text_layout, + start, + end, + None, + None, + Some(color), + ); + + text_layout_line.extra_style.extend(styles); + } + } + }) + }); + } + + fn clear_style_cache(doc: &Document) { + doc.backend.line_styles.borrow_mut().clear(); + } + + fn sticky_headers(doc: &Document, line: usize) -> Option> { + doc.buffer.with_untracked(|buffer| { + let offset = buffer.offset_of_line(line + 1); + doc.backend.syntax.with_untracked(|syntax| { + syntax.sticky_headers(offset).map(|offsets| { + offsets + .iter() + .filter_map(|offset| { + let l = buffer.line_of_offset(*offset); + if l <= line { + Some(l) + } else { + None + } + }) + .dedup() + .sorted() + .collect() + }) + }) + }) + } + + fn indent_line(doc: &Document, line: usize, line_content: &str) -> usize { + if line_content.trim().is_empty() { + let offset = doc.buffer.with_untracked(|b| b.offset_of_line(line)); + if let Some(offset) = doc + .backend + .syntax + .with_untracked(|s| s.parent_offset(offset)) { - // Remove the # of newly inserted characters - // These aren't necessarily the same as the characters literally in the - // text, but the completion will be updated when the completion widget - // receives the update event, and it will fix this if needed. - // TODO: this could be smarter and use the insert's content - self.completion_lens - .set(Some(completion[new_len..].to_string())); + return doc.buffer.with_untracked(|b| b.line_of_offset(offset)); } } - // Shift the position by the rope delta - let mut transformer = Transformer::new(delta); + line + } + + fn previous_unmatched( + &self, + buffer: &Buffer, + c: char, + offset: usize, + ) -> Option { + if self.syntax.with_untracked(|syntax| syntax.layers.is_some()) { + self.syntax.with_untracked(|syntax| { + syntax.find_tag(offset, true, &CharBuffer::new(c)) + }) + } else { + WordCursor::new(buffer.text(), offset).previous_unmatched(c) + } + } + + fn comment_token(doc: &Document) -> &str { + doc.backend + .syntax + .with_untracked(|syntax| syntax.language) + .comment_token() + } + + fn auto_closing_matching_pairs(doc: &Document) -> bool { + doc.backend + .common + .config + .with_untracked(|config| config.editor.auto_closing_matching_pairs) + } + + fn auto_surround(doc: &Document) -> bool { + doc.backend + .common + .config + .with_untracked(|config| config.editor.auto_surround) + } +} + +/// Lapce extension functions for [`Document`]. +/// Some of these are not actually made to be public, but putting them on this trait simplifies +/// giving them the [`Document`] +pub trait DocumentExt { + fn find(&self) -> &Find; + + fn update_find(&self); + + fn syntax(&self) -> RwSignal; + + fn set_syntax(&self, syntax: Syntax); + + /// Set the syntax highlighting this document should use. + fn set_language(&self, language: LapceLanguage); + + fn trigger_syntax_change(&self, edits: Option<&[SyntaxEdit]>); + + fn get_semantic_styles(&self); + + fn head_changes(&self) -> RwSignal>; + + /// Retrieve the `head` version of the buffer + fn retrieve_head(&self); + + fn trigger_head_change(&self); + + /// Get the diagnostics + fn diagnostics(&self) -> &DiagnosticData; + + /// Init diagnostics' offset ranges from lsp positions + fn init_diagnostics(&self); + + /// Update the diagnostics' positions after an edit + fn update_diagnostics(&self, delta: &RopeDelta); + + fn get_inlay_hints(&self); + /// Get the current completion lens text + fn completion_lens(&self) -> Option; + + fn set_completion_lens(&self, completion_lens: String, line: usize, col: usize); + + fn clear_completion_lens(&self); + + /// Update the completion lens position after an edit so that it appears in the correct place. + fn update_completion_lens(&self, delta: &RopeDelta); + + fn set_inline_completion( + &self, + inline_completion: String, + line: usize, + col: usize, + ); + + fn clear_inline_completion(&self); + + /// Update the inline completion position after an edit so that it appears in the correct place. + fn update_inline_completion(&self, delta: &RopeDelta); + + fn code_actions(&self) -> RwSignal; + + fn find_enclosing_brackets(&self, offset: usize) -> Option<(usize, usize)>; + + fn update_breakpoints(&self, delta: &RopeDelta, path: &Path, old_text: &Rope); +} +impl DocumentExt for Document { + fn find(&self) -> &Find { + &self.backend.common.find + } + + fn update_find(&self) { + let find_result = &self.backend.find_result; + let common = &self.backend.common; + let find_rev = common.find.rev.get_untracked(); + if find_result.find_rev.get_untracked() != find_rev { + if common.find.search_string.with_untracked(|search_string| { + search_string + .as_ref() + .map(|s| s.content.is_empty()) + .unwrap_or(true) + }) { + find_result.occurrences.set(Selection::new()); + } + find_result.reset(); + find_result.find_rev.set(find_rev); + } + + if find_result.progress.get_untracked() != FindProgress::Started { + return; + } + + let search = common.find.search_string.get_untracked(); + let search = match search { + Some(search) => search, + None => return, + }; + if search.content.is_empty() { + return; + } + + find_result + .progress + .set(FindProgress::InProgress(Selection::new())); + + let find_result = find_result.clone(); + let send = create_ext_action(self.scope, move |occurrences| { + find_result.occurrences.set(occurrences); + find_result.progress.set(FindProgress::Ready); + }); + + let text = self.buffer.with_untracked(|b| b.text().clone()); + let case_matching = common.find.case_matching.get_untracked(); + let whole_words = common.find.whole_words.get_untracked(); + rayon::spawn(move || { + let mut occurrences = Selection::new(); + Find::find( + &text, + &search, + 0, + text.len(), + case_matching, + whole_words, + true, + &mut occurrences, + ); + send(occurrences); + }); + } + + fn syntax(&self) -> RwSignal { + self.backend.syntax + } + + fn set_syntax(&self, syntax: Syntax) { + batch(|| { + self.backend.syntax.set(syntax); + if self.backend.semantic_styles.with_untracked(|s| s.is_none()) { + self.clear_style_cache(); + } + self.clear_sticky_headers_cache(); + }); + } + + fn set_language(&self, language: LapceLanguage) { + self.backend.syntax.set(Syntax::from_language(language)); + } + + fn trigger_syntax_change(&self, edits: Option<&[SyntaxEdit]>) { + let (rev, text) = + self.buffer.with_untracked(|b| (b.rev(), b.text().clone())); + + self.backend.syntax.update(|syntax| { + syntax.parse(rev, text, edits); + }); + } + + /// Request semantic styles for the buffer from the LSP through the proxy. + fn get_semantic_styles(&self) { + if !self.loaded() { + return; + } + + let path = + if let DocContent::File { path, .. } = self.content.get_untracked() { + path + } else { + return; + }; + + let (rev, len) = self.buffer.with_untracked(|b| (b.rev(), b.len())); + + let syntactic_styles = self + .backend + .syntax + .with_untracked(|syntax| syntax.styles.clone()); + + let doc = self.clone(); + let send = create_ext_action(self.scope, move |styles| { + if doc.buffer.with_untracked(|b| b.rev()) == rev { + doc.backend.semantic_styles.set(Some(styles)); + doc.clear_style_cache(); + } + }); + + self.backend + .common + .proxy + .get_semantic_tokens(path, move |result| { + if let Ok(ProxyResponse::GetSemanticTokens { styles }) = result { + rayon::spawn(move || { + let mut styles_span = SpansBuilder::new(len); + for style in styles.styles { + styles_span.add_span( + Interval::new(style.start, style.end), + style.style, + ); + } + + let styles = styles_span.build(); + + let styles = if let Some(syntactic_styles) = syntactic_styles + { + syntactic_styles.merge(&styles, |a, b| { + if let Some(b) = b { + return b.clone(); + } + a.clone() + }) + } else { + styles + }; + + send(styles); + }); + } + }); + } + + fn head_changes(&self) -> RwSignal> { + self.backend.head_changes + } + + fn retrieve_head(&self) { + if let DocContent::File { path, .. } = self.content.get_untracked() { + let histories = self.backend.histories; + + let send = { + let path = path.clone(); + let doc = self.clone(); + create_ext_action(self.scope, move |result| { + if let Ok(ProxyResponse::BufferHeadResponse { + content, .. + }) = result + { + let hisotry = DocumentHistory::new( + path.clone(), + "head".to_string(), + &content, + ); + histories.update(|histories| { + histories.insert("head".to_string(), hisotry); + }); + + doc.trigger_head_change(); + } + }) + }; + + let path = path.clone(); + let proxy = self.backend.common.proxy.clone(); + std::thread::spawn(move || { + proxy.get_buffer_head(path, move |result| { + send(result); + }); + }); + } + } + + fn trigger_head_change(&self) { + let history = if let Some(text) = + self.backend.histories.with_untracked(|histories| { + histories + .get("head") + .map(|history| history.buffer.text().clone()) + }) { + text + } else { + return; + }; + + let rev = self.rev(); + let left_rope = history; + let (atomic_rev, right_rope) = self + .buffer + .with_untracked(|b| (b.atomic_rev(), b.text().clone())); + + let send = { + let atomic_rev = atomic_rev.clone(); + let head_changes = self.backend.head_changes; + create_ext_action(self.scope, move |changes| { + let changes = if let Some(changes) = changes { + changes + } else { + return; + }; + + if atomic_rev.load(atomic::Ordering::Acquire) != rev { + return; + } + + head_changes.set(changes); + }) + }; + + rayon::spawn(move || { + let changes = + rope_diff(left_rope, right_rope, rev, atomic_rev.clone(), None); + send(changes.map(im::Vector::from)); + }); + } + + fn diagnostics(&self) -> &DiagnosticData { + &self.backend.diagnostics + } + + fn init_diagnostics(&self) { + self.clear_text_cache(); + self.backend.clear_code_actions(); + self.backend.diagnostics.diagnostics.update(|diagnostics| { + for diagnostic in diagnostics.iter_mut() { + let (start, end) = self.buffer.with_untracked(|buffer| { + ( + buffer + .offset_of_position(&diagnostic.diagnostic.range.start), + buffer.offset_of_position(&diagnostic.diagnostic.range.end), + ) + }); + diagnostic.range = (start, end); + } + }); + } + + fn update_diagnostics(&self, delta: &RopeDelta) { + if self + .backend + .diagnostics + .diagnostics + .with_untracked(|d| d.is_empty()) + { + return; + } + self.backend.diagnostics.diagnostics.update(|diagnostics| { + for diagnostic in diagnostics.iter_mut() { + let mut transformer = Transformer::new(delta); + let (start, end) = diagnostic.range; + let (new_start, new_end) = ( + transformer.transform(start, false), + transformer.transform(end, true), + ); + + let (new_start_pos, new_end_pos) = self.buffer.with_untracked(|b| { + ( + b.offset_to_position(new_start), + b.offset_to_position(new_end), + ) + }); + + diagnostic.range = (new_start, new_end); + + diagnostic.diagnostic.range.start = new_start_pos; + diagnostic.diagnostic.range.end = new_end_pos; + } + }); + } + + fn get_inlay_hints(&self) { + if !self.loaded() { + return; + } + + let backend = &self.backend; + + let path = + if let DocContent::File { path, .. } = self.content.get_untracked() { + path + } else { + return; + }; + + let (buffer, rev, len) = self + .buffer + .with_untracked(|b| (b.clone(), b.rev(), b.len())); + + let doc = self.clone(); + let send = create_ext_action(self.scope, move |hints| { + if doc.buffer.with_untracked(|b| b.rev()) == rev { + doc.backend.inlay_hints.set(Some(hints)); + doc.clear_text_cache(); + } + }); + + backend.common.proxy.get_inlay_hints(path, move |result| { + if let Ok(ProxyResponse::GetInlayHints { mut hints }) = result { + // Sort the inlay hints by their position, as the LSP does not guarantee that it will + // provide them in the order that they are in within the file + // as well, Spans does not iterate in the order that they appear + hints.sort_by(|left, right| left.position.cmp(&right.position)); + + let mut hints_span = SpansBuilder::new(len); + for hint in hints { + let offset = buffer.offset_of_position(&hint.position).min(len); + hints_span.add_span( + Interval::new(offset, (offset + 1).min(len)), + hint, + ); + } + let hints = hints_span.build(); + send(hints); + } + }); + } + + fn completion_lens(&self) -> Option { + self.backend.completion_lens.get_untracked() + } + + fn set_completion_lens(&self, completion_lens: String, line: usize, col: usize) { + // TODO: more granular invalidation + self.clear_text_cache(); + self.backend.completion_lens.set(Some(completion_lens)); + self.backend.completion_pos.set((line, col)); + } + + fn clear_completion_lens(&self) { + // TODO: more granular invalidation + if self.backend.completion_lens.with_untracked(Option::is_some) { + self.backend.completion_lens.set(None); + self.clear_text_cache(); + } + } + + /// Update the completion lens position after an edit so that it appears in the correct place. + fn update_completion_lens(&self, delta: &RopeDelta) { + let backend = &self.backend; + let Some(completion) = backend.completion_lens.get_untracked() else { + return; + }; + + let (line, col) = backend.completion_pos.get_untracked(); + let offset = self + .buffer + .with_untracked(|b| b.offset_of_line_col(line, col)); + + // If the edit is easily checkable + updateable from, then we alter the lens' text. + // In normal typing, if we didn't do this, then the text would jitter forward and then + // backwards as the completion lens is updated. + // TODO: this could also handle simple deletion, but we don't currently keep track of + // the past copmletion lens string content in the field. + if delta.as_simple_insert().is_some() { + let (iv, new_len) = delta.summary(); + if iv.start() == iv.end() + && iv.start() == offset + && new_len <= completion.len() + { + // Remove the # of newly inserted characters + // These aren't necessarily the same as the characters literally in the + // text, but the completion will be updated when the completion widget + // receives the update event, and it will fix this if needed. + // TODO: this could be smarter and use the insert's content + backend + .completion_lens + .set(Some(completion[new_len..].to_string())); + } + } + + // Shift the position by the rope delta + let mut transformer = Transformer::new(delta); + + let new_offset = transformer.transform(offset, true); + let new_pos = self + .buffer + .with_untracked(|b| b.offset_to_line_col(new_offset)); + + backend.completion_pos.set(new_pos); + } + + fn set_inline_completion( + &self, + inline_completion: String, + line: usize, + col: usize, + ) { + // TODO: more granular invalidation + batch(|| { + self.clear_text_cache(); + self.backend.inline_completion.set(Some(inline_completion)); + self.backend.inline_completion_pos.set((line, col)); + }); + } + + fn clear_inline_completion(&self) { + if self + .backend + .inline_completion + .with_untracked(Option::is_some) + { + self.backend.inline_completion.set(None); + self.clear_text_cache(); + } + } + + fn update_inline_completion(&self, delta: &RopeDelta) { + let backend = &self.backend; + let Some(completion) = backend.inline_completion.get_untracked() else { + return; + }; + + let (line, col) = backend.completion_pos.get_untracked(); + let offset = self + .buffer + .with_untracked(|b| b.offset_of_line_col(line, col)); + + // If the edit is easily checkable + updateable from, then we alter the text. + // In normal typing, if we didn't do this, then the text would jitter forward and then + // backwards as the completion is updated. + // TODO: this could also handle simple deletion, but we don't currently keep track of + // the past completion string content in the field. + if delta.as_simple_insert().is_some() { + let (iv, new_len) = delta.summary(); + if iv.start() == iv.end() + && iv.start() == offset + && new_len <= completion.len() + { + // Remove the # of newly inserted characters + // These aren't necessarily the same as the characters literally in the + // text, but the completion will be updated when the completion widget + // receives the update event, and it will fix this if needed. + backend + .inline_completion + .set(Some(completion[new_len..].to_string())); + } + } + + // Shift the position by the rope delta + let mut transformer = Transformer::new(delta); + + let new_offset = transformer.transform(offset, true); + let new_pos = self + .buffer + .with_untracked(|b| b.offset_to_line_col(new_offset)); + + backend.inline_completion_pos.set(new_pos); + } + + fn code_actions(&self) -> RwSignal { + self.backend.code_actions + } + + fn find_enclosing_brackets(&self, offset: usize) -> Option<(usize, usize)> { + self.backend + .syntax + .with_untracked(|syntax| { + (!syntax.text.is_empty()).then(|| syntax.find_enclosing_pair(offset)) + }) + // If syntax.text is empty, either the buffer is empty or we don't have syntax support + // for the current language. + // Try a language unaware search for enclosing brackets in case it is the latter. + .unwrap_or_else(|| { + self.buffer.with_untracked(|buffer| { + WordCursor::new(buffer.text(), offset).find_enclosing_pair() + }) + }) + } + + fn update_breakpoints(&self, delta: &RopeDelta, path: &Path, old_text: &Rope) { + if self + .backend + .common + .breakpoints + .with_untracked(|breakpoints| breakpoints.contains_key(path)) + { + self.backend.common.breakpoints.update(|breakpoints| { + if let Some(path_breakpoints) = breakpoints.get_mut(path) { + let mut transformer = Transformer::new(delta); + self.buffer.with_untracked(|buffer| { + *path_breakpoints = path_breakpoints + .clone() + .into_values() + .map(|mut b| { + let offset = old_text.offset_of_line(b.line); + let offset = transformer.transform(offset, false); + let line = buffer.line_of_offset(offset); + b.line = line; + b.offset = offset; + (b.line, b) + }) + .collect(); + }); + } + }); + } + } +} + +// TODO(floem-editor): when we split it out, export a type alias with the default generic +/// A single document that can be viewed by multiple [`EditorData`]'s +/// [`EditorViewData`]s and [`EditorView]s. +#[derive(Clone)] +pub struct Document { + pub scope: Scope, + pub buffer_id: BufferId, + pub content: RwSignal, + pub cache_rev: RwSignal, + /// Whether the buffer's content has been loaded/initialized into the buffer. + pub loaded: RwSignal, + pub buffer: RwSignal, + /// ime preedit information + pub preedit: RwSignal>, + /// The text layouts for the document. This may be shared with other views. + text_layouts: Rc>, + /// A cache for the sticky headers which maps a line to the lines it should show in the header. + pub sticky_headers: Rc>>>>, + pub backend: B, +} +impl Document { + // TODO(floem-editor): These are wrapper shims to avoid needing to call `DocumentBackend::from + // (common)` at every current caller site in Lapce. + // In floem-editor, the `new_backend` should simply be the `new` functions since it wouldn't be + // able to infer what backend instance to use anyway + // (ignoring special functions for backends that impl `Default`) + // An annoyance is that once floem-editor is its own crate, we can't implement fns on the + // `Document` type for ourselves, and a newtype would require even more boilerplate. + // So once we do that, we likely swap these to a couple utility functions. + pub fn new( + cx: Scope, + path: PathBuf, + diagnostics: DiagnosticData, + common: Rc, + ) -> Self { + let syntax = Syntax::init(&path); + Self::new_backend( + cx, + path, + DocBackend::new(cx, syntax, Some(diagnostics), common.clone()), + ) + } + + pub fn new_local(cx: Scope, common: Rc) -> Self { + let syntax = Syntax::plaintext(); + Self::new_local_backend( + cx, + DocBackend::new(cx, syntax, None, common.clone()), + ) + } + + pub fn new_content( + cx: Scope, + content: DocContent, + common: Rc, + ) -> Self { + let syntax = Syntax::plaintext(); + Self::new_content_backend( + cx, + content, + DocBackend::new(cx, syntax, None, common.clone()), + ) + } + + pub fn new_hisotry( + cx: Scope, + content: DocContent, + common: Rc, + ) -> Self { + let syntax = if let DocContent::History(history) = &content { + Syntax::init(&history.path) + } else { + Syntax::plaintext() + }; + Self::new_hisotry_backend( + cx, + content, + DocBackend::new(cx, syntax, None, common.clone()), + ) + } +} +impl Document { + pub fn new_backend(cx: Scope, path: PathBuf, backend: B) -> Self { + Self { + scope: cx, + buffer_id: BufferId::next(), + buffer: cx.create_rw_signal(Buffer::new("")), + cache_rev: cx.create_rw_signal(0), + content: cx.create_rw_signal(DocContent::File { + path, + read_only: false, + }), + loaded: cx.create_rw_signal(false), + text_layouts: Rc::new(RefCell::new(TextLayoutCache::default())), + sticky_headers: Rc::new(RefCell::new(HashMap::new())), + preedit: cx.create_rw_signal(None), + backend, + } + } + + pub fn new_local_backend(cx: Scope, backend: B) -> Self { + Self::new_content_backend(cx, DocContent::Local, backend) + } + + pub fn new_content_backend(cx: Scope, content: DocContent, backend: B) -> Self { + let cx = cx.create_child(); + Self { + scope: cx, + buffer_id: BufferId::next(), + buffer: cx.create_rw_signal(Buffer::new("")), + cache_rev: cx.create_rw_signal(0), + content: cx.create_rw_signal(content), + sticky_headers: Rc::new(RefCell::new(HashMap::new())), + loaded: cx.create_rw_signal(true), + text_layouts: Rc::new(RefCell::new(TextLayoutCache::default())), + preedit: cx.create_rw_signal(None), + backend, + } + } + + pub fn new_hisotry_backend(cx: Scope, content: DocContent, backend: B) -> Self { + let cx = cx.create_child(); + Self { + scope: cx, + buffer_id: BufferId::next(), + buffer: cx.create_rw_signal(Buffer::new("")), + cache_rev: cx.create_rw_signal(0), + content: cx.create_rw_signal(content), + sticky_headers: Rc::new(RefCell::new(HashMap::new())), + loaded: cx.create_rw_signal(true), + text_layouts: Rc::new(RefCell::new(TextLayoutCache::default())), + preedit: cx.create_rw_signal(None), + backend, + } + } + + /// Whether or not the underlying buffer is loaded + pub fn loaded(&self) -> bool { + self.loaded.get_untracked() + } + + //// Initialize the content with some text, this marks the document as loaded. + pub fn init_content(&self, content: Rope) { + batch(|| { + self.buffer.update(|buffer| { + buffer.init_content(content); + }); + B::pre_update_init_content(self); + self.loaded.set(true); + self.on_update(None); + // Call the backend's init from within the batch, this ensures that any effects + // depending on loaded/content/etc don't update until we're all done. + B::init_content(self); + }); + } + + /// Reload the document's content, and is what you should typically use when you want to *set* + /// an existing document's content. + pub fn reload(&self, content: Rope, set_pristine: bool) { + // self.code_actions.clear(); + // self.inlay_hints = None; + let delta = self + .buffer + .try_update(|buffer| buffer.reload(content, set_pristine)) + .unwrap(); + self.apply_deltas(&[delta]); + } + + pub fn handle_file_changed(&self, content: Rope) { + if self.is_pristine() { + self.reload(content, true); + } + } + + pub fn do_insert( + &self, + cursor: &mut Cursor, + s: &str, + ) -> Vec<(RopeDelta, InvalLines, SyntaxEdit)> { + if self.content.with_untracked(|c| c.read_only()) { + return Vec::new(); + } + + let auto_closing_matching_pairs = B::auto_closing_matching_pairs(self); + let auto_surround = B::auto_surround(self); + + let old_cursor = cursor.mode.clone(); + let deltas = self + .buffer + .try_update(|buffer| { + Editor::insert( + cursor, + buffer, + s, + &|buffer, c, offset| { + self.backend.previous_unmatched(buffer, c, offset) + }, + auto_closing_matching_pairs, + auto_surround, + ) + }) + .unwrap(); + // Keep track of the change in the cursor mode for undo/redo + self.buffer.update(|buffer| { + buffer.set_cursor_before(old_cursor); + buffer.set_cursor_after(cursor.mode.clone()); + }); + self.apply_deltas(&deltas); + deltas + } + + pub fn do_raw_edit( + &self, + edits: &[(impl AsRef, &str)], + edit_type: EditType, + ) -> Option<(RopeDelta, InvalLines, SyntaxEdit)> { + if self.content.with_untracked(|c| c.read_only()) { + return None; + } + let (delta, inval_lines, edits) = self + .buffer + .try_update(|buffer| buffer.edit(edits, edit_type)) + .unwrap(); + self.apply_deltas(&[(delta.clone(), inval_lines.clone(), edits.clone())]); + Some((delta, inval_lines, edits)) + } + + pub fn do_edit( + &self, + cursor: &mut Cursor, + cmd: &EditCommand, + modal: bool, + register: &mut Register, + smart_tab: bool, + ) -> Vec<(RopeDelta, InvalLines, SyntaxEdit)> { + if self.content.with_untracked(|c| c.read_only()) + && !cmd.not_changing_buffer() + { + return Vec::new(); + } + + let mut clipboard = SystemClipboard::new(); + let old_cursor = cursor.mode.clone(); + let comment_token = B::comment_token(self); + let deltas = self + .buffer + .try_update(|buffer| { + Editor::do_edit( + cursor, + buffer, + cmd, + comment_token, + &mut clipboard, + modal, + register, + smart_tab, + ) + }) + .unwrap(); + + if !deltas.is_empty() { + self.buffer.update(|buffer| { + buffer.set_cursor_before(old_cursor); + buffer.set_cursor_after(cursor.mode.clone()); + }); + } + + self.apply_deltas(&deltas); + deltas + } + + pub fn apply_deltas(&self, deltas: &[(RopeDelta, InvalLines, SyntaxEdit)]) { + let rev = self.rev() - deltas.len() as u64; + batch(|| { + for (i, (delta, inval, _)) in deltas.iter().enumerate() { + let rev = rev + i as u64 + 1; + B::apply_delta(self, rev, delta, inval); + } + }); + + // TODO(minor): We could avoid this potential allocation since most apply_delta callers are actually using a Vec + // which we could reuse. + // We use a smallvec because there is unlikely to be more than a couple of deltas + let edits: SmallVec<[SyntaxEdit; 3]> = + deltas.iter().map(|(_, _, edits)| edits.clone()).collect(); + self.on_update(Some(&edits)); + } + + pub fn is_pristine(&self) -> bool { + self.buffer.with_untracked(|b| b.is_pristine()) + } + + /// Get the buffer's current revision. This is used to track whether the buffer has changed. + pub fn rev(&self) -> u64 { + self.buffer.with_untracked(|b| b.rev()) + } + + fn on_update(&self, edits: Option<&[SyntaxEdit]>) { + batch(|| { + self.clear_style_cache(); + self.clear_sticky_headers_cache(); + self.check_auto_save(); + B::on_update(self, edits); + }); + } + + fn check_auto_save(&self) { + let Some(autosave_interval) = B::autosave_interval(self) else { + return; + }; + + if !self.content.with_untracked(|c| c.is_file()) { + return; + }; + let rev = self.rev(); + let doc = self.clone(); + exec_after(autosave_interval, move |_| { + let current_rev = match doc + .buffer + .try_with_untracked(|b| b.as_ref().map(|b| b.rev())) + { + Some(rev) => rev, + None => return, + }; + + if current_rev != rev || doc.is_pristine() { + return; + } + + doc.save(|| {}); + }); + } + + fn clear_style_cache(&self) { + self.clear_text_cache(); + B::clear_style_cache(self); + } + + pub fn set_preedit( + &self, + text: String, + cursor: Option<(usize, usize)>, + offset: usize, + ) { + self.preedit.set(Some(Preedit { + text, + cursor, + offset, + })); + self.clear_text_cache(); + } + + pub fn clear_preedit(&self) { + if self.preedit.get_untracked().is_some() { + self.preedit.set(None); + self.clear_text_cache(); + } + } + + /// Get the phantom text for the preedit. + /// This should be included in the [`LineStyling`]'s returned [`PhantomTextLine`] to support IME + pub fn preedit_phantom_text( + &self, + under_line: Option, + line: usize, + ) -> Option { + let preedit = self.preedit.get_untracked()?; - let new_offset = transformer.transform(offset, true); - let new_pos = self + let (ime_line, col) = self .buffer - .with_untracked(|b| b.offset_to_line_col(new_offset)); - - self.completion_pos.set(new_pos); - } - - pub fn update_find(&self) { - let find_rev = self.common.find.rev.get_untracked(); - if self.find_result.find_rev.get_untracked() != find_rev { - if self - .common - .find - .search_string - .with_untracked(|search_string| { - search_string - .as_ref() - .map(|s| s.content.is_empty()) - .unwrap_or(true) - }) - { - self.find_result.occurrences.set(Selection::new()); - } - self.find_result.reset(); - self.find_result.find_rev.set(find_rev); - } + .with_untracked(|b| b.offset_to_line_col(preedit.offset)); - if self.find_result.progress.get_untracked() != FindProgress::Started { - return; + if line != ime_line { + return None; } - let search = self.common.find.search_string.get_untracked(); - let search = match search { - Some(search) => search, - None => return, - }; - if search.content.is_empty() { - return; - } + Some(PhantomText { + kind: PhantomTextKind::Ime, + text: preedit.text, + col, + font_size: None, + fg: None, + bg: None, + under_line, + }) + } - self.find_result - .progress - .set(FindProgress::InProgress(Selection::new())); + /// Inform any dependents on this document that they should clear any cached text. + pub fn clear_text_cache(&self) { + self.cache_rev.try_update(|cache_rev| { + *cache_rev += 1; - let find_result = self.find_result.clone(); - let send = create_ext_action(self.scope, move |occurrences| { - find_result.occurrences.set(occurrences); - find_result.progress.set(FindProgress::Ready); + // Update the text layouts within the callback so that those alerted to cache rev + // will see the now empty layouts. + self.text_layouts.borrow_mut().clear(*cache_rev, None); }); + } - let text = self.buffer.with_untracked(|b| b.text().clone()); - let case_matching = self.common.find.case_matching.get_untracked(); - let whole_words = self.common.find.whole_words.get_untracked(); - rayon::spawn(move || { - let mut occurrences = Selection::new(); - Find::find( - &text, - &search, - 0, - text.len(), - case_matching, - whole_words, - true, - &mut occurrences, - ); - send(occurrences); - }); + fn clear_sticky_headers_cache(&self) { + self.sticky_headers.borrow_mut().clear(); + } + + pub fn line_styling(&self, line: usize) -> B::LineStyling { + B::line_styling(self, line) + } + + /// Get the phantom text for a given line + /// If using other style information, prefer using [`Self::line_styling`] + pub fn line_phantom_text(&self, line: usize) -> PhantomTextLine { + B::line_styling(self, line).phantom_text() } /// Get the sticky headers for a particular line, creating them if necessary. @@ -1140,110 +2057,12 @@ impl Document { if let Some(lines) = self.sticky_headers.borrow().get(&line) { return lines.clone(); } - let lines = self.buffer.with_untracked(|buffer| { - let offset = buffer.offset_of_line(line + 1); - self.syntax.with_untracked(|syntax| { - syntax.sticky_headers(offset).map(|offsets| { - offsets - .iter() - .filter_map(|offset| { - let l = buffer.line_of_offset(*offset); - if l <= line { - Some(l) - } else { - None - } - }) - .dedup() - .sorted() - .collect() - }) - }) - }); + + let lines = B::sticky_headers(self, line); self.sticky_headers.borrow_mut().insert(line, lines.clone()); lines } - /// Retrieve the `head` version of the buffer - pub fn retrieve_head(&self) { - if let DocContent::File { path, .. } = self.content.get_untracked() { - let histories = self.histories; - - let send = { - let path = path.clone(); - let doc = self.clone(); - create_ext_action(self.scope, move |result| { - if let Ok(ProxyResponse::BufferHeadResponse { - content, .. - }) = result - { - let hisotry = DocumentHistory::new( - path.clone(), - "head".to_string(), - &content, - ); - histories.update(|histories| { - histories.insert("head".to_string(), hisotry); - }); - - doc.trigger_head_change(); - } - }) - }; - - let path = path.clone(); - let proxy = self.common.proxy.clone(); - std::thread::spawn(move || { - proxy.get_buffer_head(path, move |result| { - send(result); - }); - }); - } - } - - pub fn trigger_head_change(&self) { - let history = if let Some(text) = - self.histories.with_untracked(|histories| { - histories - .get("head") - .map(|history| history.buffer.text().clone()) - }) { - text - } else { - return; - }; - - let rev = self.rev(); - let left_rope = history; - let (atomic_rev, right_rope) = self - .buffer - .with_untracked(|b| (b.atomic_rev(), b.text().clone())); - - let send = { - let atomic_rev = atomic_rev.clone(); - let head_changes = self.head_changes; - create_ext_action(self.scope, move |changes| { - let changes = if let Some(changes) = changes { - changes - } else { - return; - }; - - if atomic_rev.load(atomic::Ordering::Acquire) != rev { - return; - } - - head_changes.set(changes); - }) - }; - - rayon::spawn(move || { - let changes = - rope_diff(left_rope, right_rope, rev, atomic_rev.clone(), None); - send(changes.map(im::Vector::from)); - }); - } - /// Create rendable whitespace layout by creating a new text layout /// with invisible spaces and special utf8 characters that display /// the different white space characters. @@ -1251,25 +2070,25 @@ impl Document { line_content: &str, text_layout: &TextLayout, phantom: &PhantomTextLine, - config: &LapceConfig, + render_whitespace: RenderWhitespace, ) -> Option> { let mut render_leading = false; let mut render_boundary = false; let mut render_between = false; // TODO: render whitespaces only on highlighted text - match config.editor.render_whitespace.as_str() { - "all" => { + match render_whitespace { + RenderWhitespace::All => { render_leading = true; render_boundary = true; render_between = true; } - "boundary" => { + RenderWhitespace::Boundary => { render_leading = true; render_boundary = true; } - "trailing" => {} // All configs include rendering trailing whitespace - _ => return None, + RenderWhitespace::Trailing => {} // All configs include rendering trailing whitespace + RenderWhitespace::None => return None, } let mut whitespace_buffer = Vec::new(); @@ -1316,11 +2135,13 @@ impl Document { /// Create a new text layout for the given line. /// Typically you should use [`Document::get_text_layout`] instead. fn new_text_layout(&self, line: usize, _font_size: usize) -> TextLayoutLine { - let config = self.common.config.get_untracked(); let line_content_original = self .buffer .with_untracked(|b| b.line_content(line).to_string()); + let styling = B::line_styling(self, line); + let font_size = styling.font_size(); + // Get the line content with newline characters replaced with spaces // and the content without the newline characters let (line_content, line_content_original) = @@ -1341,32 +2162,29 @@ impl Document { ) }; // Combine the phantom text with the line content - let phantom_text = self.line_phantom_text(line); + let phantom_text = styling.phantom_text(); let line_content = phantom_text.combine_with_text(line_content); - let color = config.get_color(LapceColor::EDITOR_FOREGROUND); - let family: Vec = - FamilyOwned::parse_list(&config.editor.font_family).collect(); + let family = styling.font_family(); let attrs = Attrs::new() - .color(*color) + .color(styling.color()) .family(&family) - .font_size(config.editor.font_size() as f32); + .font_size(font_size as f32) + .line_height(LineHeightValue::Px(styling.line_height())); let mut attrs_list = AttrsList::new(attrs); // Apply various styles to the line's text based on our semantic/syntax highlighting - let styles = self.line_style(line); + let styles = styling.line_style(); for line_style in styles.iter() { if let Some(fg_color) = line_style.style.fg_color.as_ref() { - if let Some(fg_color) = config.get_style_color(fg_color) { + if let Some(fg_color) = styling.style_color(fg_color) { let start = phantom_text.col_at(line_style.start); let end = phantom_text.col_at(line_style.end); - attrs_list.add_span(start..end, attrs.color(*fg_color)); + attrs_list.add_span(start..end, attrs.color(fg_color)); } } } - let font_size = config.editor.font_size(); - // Apply phantom text specific styling for (offset, size, col, phantom) in phantom_text.offset_size_iter() { let start = col + offset; @@ -1389,118 +2207,17 @@ impl Document { } let mut text_layout = TextLayout::new(); - text_layout.set_tab_width(config.editor.tab_width); + text_layout.set_tab_width(styling.tab_width()); text_layout.set_text(&line_content, attrs_list); - // Keep track of background styling from phantom text, which is done separately - // from the text layout attributes - let mut extra_style = Vec::new(); - for (offset, size, col, phantom) in phantom_text.offset_size_iter() { - if phantom.bg.is_some() || phantom.under_line.is_some() { - let start = col + offset; - let end = start + size; - let x0 = text_layout.hit_position(start).point.x; - let x1 = text_layout.hit_position(end).point.x; - extra_style.push(LineExtraStyle { - x: x0, - width: Some(x1 - x0), - bg_color: phantom.bg, - under_line: phantom.under_line, - wave_line: None, - }); - } - } - - // Add the styling for the diagnostic severity, if applicable - if let Some(max_severity) = phantom_text.max_severity { - let theme_prop = if max_severity == DiagnosticSeverity::ERROR { - LapceColor::ERROR_LENS_ERROR_BACKGROUND - } else if max_severity == DiagnosticSeverity::WARNING { - LapceColor::ERROR_LENS_WARNING_BACKGROUND - } else { - LapceColor::ERROR_LENS_OTHER_BACKGROUND - }; - - let x1 = (!config.editor.error_lens_end_of_line) - .then(|| text_layout.hit_position(line_content.len()).point.x); - - extra_style.push(LineExtraStyle { - x: 0.0, - width: x1, - bg_color: Some(*config.get_color(theme_prop)), - under_line: None, - wave_line: None, - }); - } - - self.diagnostics.diagnostics.with_untracked(|diags| { - self.buffer.with_untracked(|buffer| { - for diag in diags { - if diag.diagnostic.range.start.line as usize <= line - && line <= diag.diagnostic.range.end.line as usize - { - let start = if diag.diagnostic.range.start.line as usize - == line - { - let (_, col) = buffer.offset_to_line_col(diag.range.0); - col - } else { - let offset = - buffer.first_non_blank_character_on_line(line); - let (_, col) = buffer.offset_to_line_col(offset); - col - }; - let start = phantom_text.col_after(start, true); - - let end = if diag.diagnostic.range.end.line as usize == line - { - let (_, col) = buffer.offset_to_line_col(diag.range.1); - col - } else { - buffer.line_end_col(line, true) - }; - let end = phantom_text.col_after(end, false); - - let x0 = text_layout.hit_position(start).point.x; - let x1 = text_layout.hit_position(end).point.x; - let color_name = match diag.diagnostic.severity { - Some(DiagnosticSeverity::ERROR) => { - LapceColor::LAPCE_ERROR - } - _ => LapceColor::LAPCE_WARN, - }; - let color = *config.get_color(color_name); - extra_style.push(LineExtraStyle { - x: x0, - width: Some(x1 - x0), - bg_color: None, - under_line: None, - wave_line: Some(color), - }); - } - } - }) - }); - let whitespaces = Self::new_whitespace_layout( line_content_original, &text_layout, &phantom_text, - &config, + styling.render_whitespace(), ); - let indent_line = if line_content_original.trim().is_empty() { - let offset = self.buffer.with_untracked(|b| b.offset_of_line(line)); - if let Some(offset) = - self.syntax.with_untracked(|s| s.parent_offset(offset)) - { - self.buffer.with_untracked(|b| b.line_of_offset(offset)) - } else { - line - } - } else { - line - }; + let indent_line = B::indent_line(self, line, line_content_original); let indent = if indent_line != line { self.get_text_layout(indent_line, font_size).indent + 1.0 @@ -1514,12 +2231,16 @@ impl Document { TextLayoutLine { text: text_layout, - extra_style, + extra_style: Vec::new(), whitespaces, indent, } } + pub fn apply_styles(&self, line: usize, text_layout_line: &mut TextLayoutLine) { + B::apply_styles(self, line, text_layout_line) + } + /// Get the text layout for the given line. /// If the text layout is not cached, it will be created and cached. pub fn get_text_layout( @@ -1527,9 +2248,9 @@ impl Document { line: usize, font_size: usize, ) -> Arc { - let config = self.common.config.get_untracked(); + let config_id = B::config_id(self); // Check if the text layout needs to update due to the config being changed - self.text_layouts.borrow_mut().check_attributes(config.id); + self.text_layouts.borrow_mut().check_attributes(config_id); // If we don't have a second layer of the hashmap initialized for this specific font size, // do it now if self.text_layouts.borrow().layouts.get(&font_size).is_none() { @@ -1573,43 +2294,57 @@ impl Document { } pub fn save(&self, after_action: impl Fn() + 'static) { - let content = self.content.get_untracked(); - if let DocContent::File { path, .. } = content { - let rev = self.rev(); - let buffer = self.buffer; - let send = create_ext_action(self.scope, move |result| { - if let Ok(ProxyResponse::SaveResponse {}) = result { - let current_rev = buffer.with_untracked(|buffer| buffer.rev()); - if current_rev == rev { - buffer.update(|buffer| { - buffer.set_pristine(); - }); - after_action(); - } - } - }); - - self.common.proxy.save(rev, path, true, move |result| { - send(result); - }) - } + B::save(self, move |_| after_action()); } +} - /// Returns the offsets of the brackets enclosing the given offset. - /// Uses a language aware algorithm if syntax support is available for the current language, - /// else falls back to a language unaware algorithm. - pub fn find_enclosing_brackets(&self, offset: usize) -> Option<(usize, usize)> { - self.syntax - .with_untracked(|syntax| { - (!syntax.text.is_empty()).then(|| syntax.find_enclosing_pair(offset)) - }) - // If syntax.text is empty, either the buffer is empty or we don't have syntax support - // for the current language. - // Try a language unaware search for enclosing brackets in case it is the latter. - .unwrap_or_else(|| { - self.buffer.with_untracked(|buffer| { - WordCursor::new(buffer.text(), offset).find_enclosing_pair() - }) +fn extra_styles_for_range( + text_layout: &TextLayout, + start: usize, + end: usize, + bg_color: Option, + under_line: Option, + wave_line: Option, +) -> impl Iterator + '_ { + let start_hit = text_layout.hit_position(start); + let end_hit = text_layout.hit_position(end); + + text_layout + .layout_runs() + .enumerate() + .filter_map(move |(current_line, run)| { + if current_line < start_hit.line || current_line > end_hit.line { + return None; + } + + let x = if current_line == start_hit.line { + start_hit.point.x + } else { + run.glyphs.first().map(|g| g.x).unwrap_or(0.0) as f64 + }; + let end_x = if current_line == end_hit.line { + end_hit.point.x + } else { + run.glyphs.last().map(|g| g.x + g.w).unwrap_or(0.0) as f64 + }; + let width = end_x - x; + + if width == 0.0 { + return None; + } + + let y = (run.line_height - run.glyph_ascent - run.glyph_descent) as f64 + / 2.0; + let height = (run.glyph_ascent + run.glyph_descent) as f64; + + Some(LineExtraStyle { + x, + y, + width: Some(width), + height, + bg_color, + under_line, + wave_line, }) - } + }) } diff --git a/lapce-app/src/doc/phantom_text.rs b/lapce-app/src/doc/phantom_text.rs index faafa2a685..7d2aecb981 100644 --- a/lapce-app/src/doc/phantom_text.rs +++ b/lapce-app/src/doc/phantom_text.rs @@ -1,9 +1,9 @@ use floem::peniko::Color; -use lsp_types::DiagnosticSeverity; use smallvec::SmallVec; /// `PhantomText` is for text that is not in the actual document, but should be rendered with it. /// Ex: Inlay hints, IME text, error lens' diagnostics, etc +#[derive(Debug, Clone)] pub struct PhantomText { /// The kind is currently used for sorting the phantom text on a line pub kind: PhantomTextKind, @@ -17,11 +17,11 @@ pub struct PhantomText { pub under_line: Option, } -#[derive(Ord, Eq, PartialEq, PartialOrd)] +#[derive(Debug, Clone, Copy, Ord, Eq, PartialEq, PartialOrd)] pub enum PhantomTextKind { /// Input methods Ime, - /// Completion lens + /// Completion lens / Inline completion Completion, /// Inlay hints supplied by an LSP/PSP (like type annotations) InlayHint, @@ -32,13 +32,10 @@ pub enum PhantomTextKind { /// Information about the phantom text on a specific line. /// This has various utility functions for transforming a coordinate (typically a column) into the /// resulting coordinate after the phantom text is combined with the line's real content. -#[derive(Default)] +#[derive(Debug, Default, Clone)] pub struct PhantomTextLine { /// This uses a smallvec because most lines rarely have more than a couple phantom texts pub text: SmallVec<[PhantomText; 6]>, - /// Maximum diagnostic severity, so that we can color the background as an error if there is a - /// warning and error on the line. (For error lens) - pub max_severity: Option, } impl PhantomTextLine { @@ -54,8 +51,9 @@ impl PhantomTextLine { last } - /// Translate a column position into the text into what it would be after combining - /// If before_cursor is false and the cursor is right at the start then it will stay there + /// Translate a column position into the text into what it would be after combining + /// If `before_cursor` is false and the cursor is right at the start then it will stay there + /// (Think 'is the phantom text before the cursor') pub fn col_after(&self, pre_col: usize, before_cursor: bool) -> usize { let mut last = pre_col; for (col_shift, size, col, _) in self.offset_size_iter() { @@ -67,6 +65,31 @@ impl PhantomTextLine { last } + /// Translate a column position into the text into what it would be after combining + /// If `before_cursor` is false and the cursor is right at the start then it will stay there + /// (Think 'is the phantom text before the cursor') + /// This accepts a `PhantomTextKind` to ignore. Primarily for IME due to it needing to put the + /// cursor in the middle. + pub fn col_after_ignore( + &self, + pre_col: usize, + before_cursor: bool, + skip: impl Fn(&PhantomText) -> bool, + ) -> usize { + let mut last = pre_col; + for (col_shift, size, col, phantom) in self.offset_size_iter() { + if skip(phantom) { + continue; + } + + if pre_col > col || (pre_col == col && before_cursor) { + last = pre_col + col_shift + size; + } + } + + last + } + /// Translate a column position into the position it would be before combining pub fn before_col(&self, col: usize) -> usize { let mut last = col; diff --git a/lapce-app/src/editor.rs b/lapce-app/src/editor.rs index 3b0cca5a9d..4529b7a4e1 100644 --- a/lapce-app/src/editor.rs +++ b/lapce-app/src/editor.rs @@ -11,7 +11,7 @@ use floem::{ menu::{Menu, MenuItem}, peniko::kurbo::{Point, Rect, Vec2}, pointer::{PointerButton, PointerInputEvent, PointerMoveEvent}, - reactive::{use_context, RwSignal, Scope}, + reactive::{batch, use_context, ReadSignal, RwSignal, Scope}, }; use lapce_core::{ buffer::{diff::DiffLines, rope_text::RopeText, InvalLines}, @@ -27,7 +27,7 @@ use lapce_rpc::{buffer::BufferId, plugin::PluginId, proxy::ProxyResponse}; use lapce_xi_rope::{Rope, RopeDelta, Transformer}; use lsp_types::{ CompletionItem, CompletionTextEdit, GotoDefinitionResponse, HoverContents, - Location, MarkedString, MarkupKind, TextEdit, + InlineCompletionTriggerKind, Location, MarkedString, MarkupKind, TextEdit, }; use serde::{Deserialize, Serialize}; @@ -36,13 +36,17 @@ use crate::{ CommandExecuted, CommandKind, InternalCommand, LapceCommand, LapceWorkbenchCommand, }, - completion::{clear_completion_lens, CompletionStatus}, + completion::CompletionStatus, config::LapceConfig, db::LapceDb, - doc::{DocContent, Document}, - editor::location::{EditorLocation, EditorPosition}, + doc::{DocContent, Document, DocumentExt}, + editor::{ + location::{EditorLocation, EditorPosition}, + visual_line::Lines, + }, editor_tab::EditorTabChild, id::{DiffEditorId, EditorId, EditorTabId}, + inline_completion::{InlineCompletionItem, InlineCompletionStatus}, keypress::{condition::Condition, KeyPressFocus}, main_split::{MainSplitData, SplitDirection, SplitMoveDirection}, markdown::{ @@ -54,8 +58,9 @@ use crate::{ }; use self::{ - view::{DiffSection, DiffSectionKind, LineInfo, ScreenLines}, + view::{DiffSection, DiffSectionKind, LineInfo, ScreenLines, ScreenLinesBase}, view_data::{EditorViewData, EditorViewKind}, + visual_line::{TextLayoutProvider, VLine, VLineInfo}, }; pub mod diff; @@ -64,9 +69,9 @@ pub mod location; pub mod movement; pub mod view; pub mod view_data; +pub mod visual_line; const CHAR_WIDTH: f64 = 7.5; -const FONT_SIZE: usize = 12; #[derive(Clone, Debug)] pub enum InlineFindDirection { @@ -98,6 +103,7 @@ impl EditorInfo { None, editor_id, doc, + None, data.common, ); editor_data.go_to_location( @@ -150,6 +156,7 @@ impl EditorInfo { None, editor_id, doc, + None, data.common, ) } @@ -200,10 +207,13 @@ impl EditorData { diff_editor_id: Option<(EditorTabId, DiffEditorId)>, editor_id: EditorId, doc: Rc, + confirmed: Option>, common: Rc, ) -> Self { let cx = cx.create_child(); + let is_local = doc.content.with_untracked(|content| content.is_local()); + let viewport = cx.create_rw_signal(Rect::ZERO); let modal = common.config.with_untracked(|c| c.core.modal); let cursor = Cursor::new( if modal && !is_local { @@ -215,8 +225,13 @@ impl EditorData { None, ); let cursor = cx.create_rw_signal(cursor); - let view = - EditorViewData::new(cx, doc, EditorViewKind::Normal, common.config); + let view = EditorViewData::new( + cx, + doc, + EditorViewKind::Normal, + viewport, + common.config, + ); { let internal_comamnd = common.internal_command; cx.create_effect(move |_| { @@ -224,6 +239,7 @@ impl EditorData { internal_comamnd.send(InternalCommand::ResetBlinkCursor); }); } + let confirmed = confirmed.unwrap_or_else(|| cx.create_rw_signal(false)); Self { scope: cx, editor_tab_id: cx.create_rw_signal(editor_tab_id), @@ -231,10 +247,10 @@ impl EditorData { editor_id, view, cursor, - confirmed: cx.create_rw_signal(false), + confirmed, snippet: cx.create_rw_signal(None), window_origin: cx.create_rw_signal(Point::ZERO), - viewport: cx.create_rw_signal(Rect::ZERO), + viewport, scroll_delta: cx.create_rw_signal(Vec2::ZERO), scroll_to: cx.create_rw_signal(None), last_movement: cx.create_rw_signal(Movement::Left), @@ -254,7 +270,7 @@ impl EditorData { ) -> Self { let cx = cx.create_child(); let doc = Rc::new(Document::new_local(cx, common.clone())); - Self::new(cx, None, None, editor_id, doc, common) + Self::new(cx, None, None, editor_id, doc, None, common) } pub fn editor_info(&self, _data: &WindowTabData) -> EditorInfo { @@ -278,6 +294,7 @@ impl EditorData { editor_tab_id: Option, diff_editor_id: Option<(EditorTabId, DiffEditorId)>, editor_id: EditorId, + confirmed: Option>, ) -> Self { let cx = cx.create_child(); let cursor = cx.create_rw_signal(self.cursor.get_untracked()); @@ -288,20 +305,23 @@ impl EditorData { internal_comamnd.send(InternalCommand::ResetBlinkCursor); }); } + let viewport = cx.create_rw_signal(self.viewport.get_untracked()); + let confirmed = confirmed.unwrap_or_else(|| cx.create_rw_signal(true)); + EditorData { scope: cx, editor_id, editor_tab_id: cx.create_rw_signal(editor_tab_id), diff_editor_id: cx.create_rw_signal(diff_editor_id), - view: self.view.duplicate(cx), + view: self.view.duplicate(cx, viewport), cursor, - viewport: cx.create_rw_signal(self.viewport.get_untracked()), + viewport, scroll_delta: cx.create_rw_signal(Vec2::ZERO), scroll_to: cx.create_rw_signal(Some( self.viewport.get_untracked().origin().to_vec2(), )), window_origin: cx.create_rw_signal(Point::ZERO), - confirmed: cx.create_rw_signal(true), + confirmed, snippet: cx.create_rw_signal(None), last_movement: cx.create_rw_signal(self.last_movement.get_untracked()), inline_find: cx.create_rw_signal(None), @@ -342,7 +362,8 @@ impl EditorData { None }; - let deltas = doc.do_edit(&mut cursor, cmd, modal, &mut register, smart_tab); + let deltas = + batch(|| doc.do_edit(&mut cursor, cmd, modal, &mut register, smart_tab)); if !deltas.is_empty() { if let Some(data) = yank_data { @@ -358,6 +379,17 @@ impl EditorData { } else { self.cancel_completion(); } + + if *cmd == EditCommand::InsertNewLine { + // Cancel so that there's no flickering + self.cancel_inline_completion(); + self.update_inline_completion(InlineCompletionTriggerKind::Automatic); + } else if show_inline_completion(cmd) { + self.update_inline_completion(InlineCompletionTriggerKind::Automatic); + } else { + self.cancel_inline_completion(); + } + self.apply_deltas(&deltas); if let EditCommand::NormalMode = cmd { self.snippet.set(None); @@ -403,6 +435,7 @@ impl EditorData { self.cursor.set(cursor); // self.cancel_signature(); self.cancel_completion(); + self.cancel_inline_completion(); CommandExecuted::Yes } @@ -480,6 +513,9 @@ impl EditorData { .with_untracked(|c| c.active.get_untracked()); match cmd { + FocusCommand::ModalClose => { + self.cancel_completion(); + } FocusCommand::SplitVertical => { if let Some(editor_tab_id) = self.editor_tab_id.get_untracked() { self.common.internal_command.send(InternalCommand::Split { @@ -668,6 +704,7 @@ impl EditorData { } FocusCommand::ListSelect => { self.select_completion(); + self.cancel_inline_completion(); } FocusCommand::JumpToNextSnippetPlaceholder => { self.snippet.update(|snippet| { @@ -702,6 +739,7 @@ impl EditorData { } // self.update_signature(); self.cancel_completion(); + self.cancel_inline_completion(); } }); } @@ -734,6 +772,7 @@ impl EditorData { } // self.update_signature(); self.cancel_completion(); + self.cancel_inline_completion(); } } }); @@ -790,6 +829,21 @@ impl EditorData { self.common.find.replace_focus.set(true); } } + FocusCommand::InlineCompletionSelect => { + self.select_inline_completion(); + } + FocusCommand::InlineCompletionNext => { + self.next_inline_completion(); + } + FocusCommand::InlineCompletionPrevious => { + self.previous_inline_completion(); + } + FocusCommand::InlineCompletionCancel => { + self.cancel_inline_completion(); + } + FocusCommand::InlineCompletionInvoke => { + self.update_inline_completion(InlineCompletionTriggerKind::Invoked); + } _ => {} } @@ -1059,6 +1113,165 @@ impl EditorData { }; } + fn select_inline_completion(&self) { + if self + .common + .inline_completion + .with_untracked(|c| c.status == InlineCompletionStatus::Inactive) + { + return; + } + + let data = self + .common + .inline_completion + .with_untracked(|c| (c.current_item().cloned(), c.start_offset)); + self.cancel_inline_completion(); + + let (Some(item), start_offset) = data else { + return; + }; + + let _ = item.apply(self, start_offset); + } + + fn next_inline_completion(&self) { + if self + .common + .inline_completion + .with_untracked(|c| c.status == InlineCompletionStatus::Inactive) + { + return; + } + + self.common.inline_completion.update(|c| { + c.next(); + }); + } + + fn previous_inline_completion(&self) { + if self + .common + .inline_completion + .with_untracked(|c| c.status == InlineCompletionStatus::Inactive) + { + return; + } + + self.common.inline_completion.update(|c| { + c.previous(); + }); + } + + fn cancel_inline_completion(&self) { + if self + .common + .inline_completion + .with_untracked(|c| c.status == InlineCompletionStatus::Inactive) + { + return; + } + + self.common.inline_completion.update(|c| { + c.cancel(); + }); + + self.view.doc.get_untracked().clear_inline_completion(); + } + + /// Update the current inline completion + fn update_inline_completion(&self, trigger_kind: InlineCompletionTriggerKind) { + if self.get_mode() != Mode::Insert { + self.cancel_inline_completion(); + return; + } + + let doc = self.view.doc.get_untracked(); + let path = match if doc.loaded() { + doc.content.with_untracked(|c| c.path().cloned()) + } else { + None + } { + Some(path) => path, + None => return, + }; + + let offset = self.cursor.with_untracked(|c| c.offset()); + let line = doc + .buffer + .with_untracked(|buffer| buffer.line_of_offset(offset)); + let position = doc + .buffer + .with_untracked(|buffer| buffer.offset_to_position(offset)); + + let inline_completion = self.common.inline_completion; + let doc = self.view.doc.get_untracked(); + + // Update the inline completion's text if it's already active to avoid flickering + let has_relevant = inline_completion.with_untracked(|completion| { + let c_line = doc.buffer.with_untracked(|buffer| { + buffer.line_of_offset(completion.start_offset) + }); + completion.status != InlineCompletionStatus::Inactive + && line == c_line + && completion.path == path + }); + if has_relevant { + let config = self.common.config.get_untracked(); + inline_completion.update(|completion| { + completion.update_inline_completion(&config, &doc, offset); + }); + } + + let path2 = path.clone(); + let send = create_ext_action( + self.scope, + move |items: Vec| { + let items = doc.buffer.with_untracked(|buffer| { + items + .into_iter() + .map(|item| InlineCompletionItem::from_lsp(buffer, item)) + .collect() + }); + inline_completion.update(|c| { + c.set_items(items, offset, path2); + c.update_doc(&doc, offset); + }); + }, + ); + + inline_completion.update(|c| c.status = InlineCompletionStatus::Started); + + self.common.proxy.get_inline_completions( + path, + position, + trigger_kind, + move |res| { + if let Ok(ProxyResponse::GetInlineCompletions { + completions: items, + }) = res + { + let items = match items { + lsp_types::InlineCompletionResponse::Array(items) => items, + // Currently does not have any relevant extra fields + lsp_types::InlineCompletionResponse::List(items) => { + items.items + } + }; + send(items); + } + }, + ); + } + + /// Check if there are inline completions that are being rendered + fn has_inline_completions(&self) -> bool { + self.common.inline_completion.with_untracked(|completion| { + completion.status != InlineCompletionStatus::Inactive + && !completion.items.is_empty() + }) + } + fn select_completion(&self) { let item = self .common @@ -1119,7 +1332,9 @@ impl EditorData { c.cancel(); }); - clear_completion_lens(self.view.doc.get_untracked()); + self.view + .doc + .with_untracked(|doc| doc.clear_completion_lens()); } /// Update the displayed autocompletion box @@ -1168,9 +1383,6 @@ impl EditorData { self.common.completion.update(|completion| { completion.update_input(input.clone()); - let cursor_offset = self.cursor.with_untracked(|c| c.offset()); - completion.update_document_completion(&self.view, cursor_offset); - if !completion.input_items.contains_key("") { let start_pos = doc.buffer.with_untracked(|buffer| { buffer.offset_to_position(start_offset) @@ -1197,6 +1409,12 @@ impl EditorData { ); } }); + let cursor_offset = self.cursor.with_untracked(|c| c.offset()); + self.common + .completion + .get_untracked() + .update_document_completion(&self.view, cursor_offset); + return; } @@ -1324,7 +1542,7 @@ impl EditorData { Ok(()) } - fn completion_apply_snippet( + pub fn completion_apply_snippet( &self, snippet: &str, selection: &Selection, @@ -1412,7 +1630,7 @@ impl EditorData { }); } - fn do_edit( + pub fn do_edit( &self, selection: &Selection, edits: &[(impl AsRef, &str)], @@ -1581,14 +1799,16 @@ impl EditorData { }; let offset = self.cursor.with_untracked(|c| c.offset()); - let exists = doc.code_actions.with_untracked(|c| c.contains_key(&offset)); + let exists = doc + .code_actions() + .with_untracked(|c| c.contains_key(&offset)); if exists { return; } // insert some empty data, so that we won't make the request again - doc.code_actions.update(|c| { + doc.code_actions().update(|c| { c.insert(offset, Arc::new((PluginId(0), Vec::new()))); }); @@ -1599,7 +1819,7 @@ impl EditorData { // Get the diagnostics for the current line, which the LSP might use to inform // what code actions are available (such as fixes for the diagnostics). let diagnostics = doc - .diagnostics + .diagnostics() .diagnostics .get_untracked() .iter() @@ -1616,7 +1836,7 @@ impl EditorData { let send = create_ext_action(self.scope, move |resp| { if doc.rev() == rev { - doc.code_actions.update(|c| { + doc.code_actions().update(|c| { c.insert(offset, Arc::new(resp)); }); } @@ -1641,8 +1861,9 @@ impl EditorData { pub fn show_code_actions(&self, mouse_click: bool) { let offset = self.cursor.with_untracked(|c| c.offset()); let doc = self.view.doc.get_untracked(); - let code_actions = - doc.code_actions.with_untracked(|c| c.get(&offset).cloned()); + let code_actions = doc + .code_actions() + .with_untracked(|c| c.get(&offset).cloned()); if let Some(code_actions) = code_actions { if !code_actions.1.is_empty() { self.common.internal_command.send( @@ -2261,200 +2482,9 @@ impl EditorData { .update(|cursor| cursor.set_offset(0, false, false)); } - pub fn screen_lines(&self) -> ScreenLines { - let viewport = self.viewport.get_untracked(); - let editor_view = self.view.kind; - let config = self.common.config.get_untracked(); - let line_height = config.editor.line_height(); - - let min_line = (viewport.y0 / line_height as f64).floor() as usize; - let max_line = (viewport.y1 / line_height as f64).ceil() as usize; - - let editor_view = editor_view.get_untracked(); - match editor_view { - EditorViewKind::Normal => { - let doc = self.view.doc.get_untracked(); - let last_line = - doc.buffer.with_untracked(|buffer| buffer.last_line()); - let mut lines = Vec::new(); - let mut info = HashMap::new(); - for line in min_line..max_line + 1 { - if line > last_line { - break; - } - lines.push(line); - info.insert( - line, - LineInfo { - y: line * line_height, - }, - ); - } - ScreenLines { - lines, - info, - diff_sections: Vec::new(), - } - } - EditorViewKind::Diff(diff_info) => { - let mut visual_line = 0; - let mut lines = Vec::new(); - let mut info = HashMap::new(); - let mut diff_sections = Vec::new(); - let mut last_change: Option<&DiffLines> = None; - let mut changes = diff_info.changes.iter().peekable(); - let is_right = diff_info.is_right; - while let Some(change) = changes.next() { - match (is_right, change) { - (true, DiffLines::Left(range)) => { - if let Some(DiffLines::Right(_)) = changes.peek() { - } else { - let len = range.len(); - diff_sections.push(DiffSection { - start_line: visual_line, - height: len, - kind: DiffSectionKind::NoCode, - }); - visual_line += len; - } - } - (false, DiffLines::Right(range)) => { - let len = if let Some(DiffLines::Left(r)) = last_change { - range.len() - r.len().min(range.len()) - } else { - range.len() - }; - if len > 0 { - diff_sections.push(DiffSection { - start_line: visual_line, - height: len, - kind: DiffSectionKind::NoCode, - }); - visual_line += len; - } - } - (true, DiffLines::Right(range)) - | (false, DiffLines::Left(range)) => { - let len = range.len(); - - diff_sections.push(DiffSection { - start_line: visual_line, - height: len, - kind: if is_right { - DiffSectionKind::Added - } else { - DiffSectionKind::Removed - }, - }); - - visual_line += len; - - if visual_line < min_line { - if is_right { - if let Some(DiffLines::Left(r)) = last_change { - let len = r.len() - r.len().min(range.len()); - if len > 0 { - diff_sections.push(DiffSection { - start_line: visual_line, - height: len, - kind: DiffSectionKind::NoCode, - }); - visual_line += len; - } - }; - } - last_change = Some(change); - continue; - } - - for l in visual_line - len..visual_line { - if l < min_line { - continue; - } - let actual_line = - l - (visual_line - len) + range.start; - - lines.push(actual_line); - info.insert( - actual_line, - LineInfo { y: l * line_height }, - ); - - if l > max_line { - break; - } - } - - if is_right { - if let Some(DiffLines::Left(r)) = last_change { - let len = r.len() - r.len().min(range.len()); - if len > 0 { - diff_sections.push(DiffSection { - start_line: visual_line, - height: len, - kind: DiffSectionKind::NoCode, - }); - visual_line += len; - } - }; - } - } - (_, DiffLines::Both(bothinfo)) => { - let start = if is_right { - bothinfo.right.start - } else { - bothinfo.left.start - }; - let len = bothinfo.right.len(); - let diff_height = len - - bothinfo - .skip - .as_ref() - .map(|skip| skip.len().saturating_sub(1)) - .unwrap_or(0); - if visual_line + diff_height < min_line { - visual_line += diff_height; - last_change = Some(change); - continue; - } - - let mut actual_line = start; - while actual_line < start + len { - if let Some(skip) = bothinfo.skip.as_ref() { - if skip.start == actual_line - start { - visual_line += 1; - actual_line += skip.len(); - continue; - } - } - - if visual_line >= min_line { - lines.push(actual_line); - info.insert( - actual_line, - LineInfo { - y: visual_line * line_height, - }, - ); - } - visual_line += 1; - actual_line += 1; - - if visual_line - 1 > max_line { - break; - } - } - } - } - last_change = Some(change); - } - ScreenLines { - lines, - info, - diff_sections, - } - } - } + /// Get the line information for lines on the screen. + pub fn screen_lines(&self) -> RwSignal { + self.view.screen_lines } } @@ -2476,6 +2506,7 @@ impl KeyPressFocus for EditorData { } Condition::ListFocus => self.has_completions(), Condition::CompletionFocus => self.has_completions(), + Condition::InlineCompletionVisible => self.has_inline_completions(), Condition::InSnippet => self.snippet.with_untracked(|s| s.is_some()), Condition::EditorFocus => self .view @@ -2595,12 +2626,7 @@ impl KeyPressFocus for EditorData { // normal editor receive char if self.get_mode() == Mode::Insert { let mut cursor = self.cursor.get_untracked(); - let config = self.common.config.get_untracked(); - let deltas = - self.view - .doc - .get_untracked() - .do_insert(&mut cursor, c, &config); + let deltas = self.view.doc.get_untracked().do_insert(&mut cursor, c); self.cursor.set(cursor); if !c @@ -2611,6 +2637,11 @@ impl KeyPressFocus for EditorData { } else { self.cancel_completion(); } + + self.update_inline_completion( + InlineCompletionTriggerKind::Automatic, + ); + self.apply_deltas(&deltas); } else if let Some(direction) = self.inline_find.get_untracked() { self.inline_find(direction.clone(), c); @@ -2634,12 +2665,12 @@ fn show_completion( | EditCommand::DeleteWordBackward | EditCommand::DeleteWordForward | EditCommand::DeleteForwardAndInsert => { - let start = match deltas.get(0).and_then(|delta| delta.0.els.get(0)) { + let start = match deltas.first().and_then(|delta| delta.0.els.first()) { Some(lapce_xi_rope::DeltaElement::Copy(_, start)) => *start, _ => 0, }; - let end = match deltas.get(0).and_then(|delta| delta.0.els.get(1)) { + let end = match deltas.first().and_then(|delta| delta.0.els.get(1)) { Some(lapce_xi_rope::DeltaElement::Copy(end, _)) => *end, _ => 0, }; @@ -2658,6 +2689,325 @@ fn show_completion( show_completion } +fn show_inline_completion(cmd: &EditCommand) -> bool { + matches!( + cmd, + EditCommand::DeleteBackward + | EditCommand::DeleteForward + | EditCommand::DeleteWordBackward + | EditCommand::DeleteWordForward + | EditCommand::DeleteForwardAndInsert + | EditCommand::IndentLine + | EditCommand::InsertMode + ) +} + +// TODO(minor): Should we just put this on view, since it only requires those values? +fn compute_screen_lines( + config: ReadSignal>, + base: RwSignal, + view_kind: ReadSignal, + doc: ReadSignal>, + lines: &Lines, + text_prov: impl TextLayoutProvider + Clone, +) -> ScreenLines { + // TODO: this should probably be a get since we need to depend on line-height + let config = config.get(); + let line_height = config.editor.line_height(); + + let (y0, y1) = base + .with_untracked(|base| (base.active_viewport.y0, base.active_viewport.y1)); + // Get the start and end (visual) lines that are visible in the viewport + let min_vline = VLine((y0 / line_height as f64).floor() as usize); + let max_vline = VLine((y1 / line_height as f64).ceil() as usize); + + let (cache_rev, content, loaded) = + doc.with(|doc| (doc.cache_rev, doc.content, doc.loaded)); + + cache_rev.track(); + // TODO(minor): we don't really need to depend on various subdetails that aren't affecting how + // the screen lines are set up, like the title of a scratch document. + content.track(); + loaded.track(); + + let min_info = once_cell::sync::Lazy::new(|| { + lines + .iter_vlines(text_prov.clone(), false, min_vline) + .next() + }); + // TODO: if you need the max vline you probably need the min vline too and so you could grab + // both in one iter call, which would be more efficient than two iterations + let max_info = once_cell::sync::Lazy::new(|| { + lines + .iter_vlines(text_prov.clone(), false, max_vline) + .next() + }); + + match view_kind.get() { + EditorViewKind::Normal => { + let mut rvlines = Vec::new(); + let mut info = HashMap::new(); + + let Some(min_info) = *min_info else { + return ScreenLines { + lines: Rc::new(rvlines), + info: Rc::new(info), + diff_sections: None, + base, + }; + }; + + // TODO: the original was min_line..max_line + 1, are we iterating too little now? + // the iterator is from min_vline..max_vline + let count = max_vline.get() - min_vline.get(); + let iter = lines + .iter_rvlines_init(text_prov, config.id, min_info.rvline, false) + .take(count); + + for (i, vline_info) in iter.enumerate() { + rvlines.push(vline_info.rvline); + + let y_idx = min_vline.get() + i; + let vline_y = y_idx * line_height; + let line_y = vline_y - vline_info.rvline.line_index * line_height; + + // Add the information to make it cheap to get in the future. + // This y positions are shifted by the baseline y0 + info.insert( + vline_info.rvline, + LineInfo { + y: line_y as f64 - y0, + vline_y: vline_y as f64 - y0, + vline_info, + }, + ); + } + + ScreenLines { + lines: Rc::new(rvlines), + info: Rc::new(info), + diff_sections: None, + base, + } + } + EditorViewKind::Diff(diff_info) => { + // TODO: let lines in diff view be wrapped, possibly screen_lines should be impl'd + // on DiffEditorData + + let mut y_idx = 0; + let mut rvlines = Vec::new(); + let mut info = HashMap::new(); + let mut diff_sections = Vec::new(); + let mut last_change: Option<&DiffLines> = None; + let mut changes = diff_info.changes.iter().peekable(); + let is_right = diff_info.is_right; + + let line_y = |info: VLineInfo<()>, vline_y: usize| -> usize { + vline_y - info.rvline.line_index * line_height + }; + + while let Some(change) = changes.next() { + match (is_right, change) { + (true, DiffLines::Left(range)) => { + if let Some(DiffLines::Right(_)) = changes.peek() { + } else { + let len = range.len(); + diff_sections.push(DiffSection { + y_idx, + height: len, + kind: DiffSectionKind::NoCode, + }); + y_idx += len; + } + } + (false, DiffLines::Right(range)) => { + let len = if let Some(DiffLines::Left(r)) = last_change { + range.len() - r.len().min(range.len()) + } else { + range.len() + }; + if len > 0 { + diff_sections.push(DiffSection { + y_idx, + height: len, + kind: DiffSectionKind::NoCode, + }); + y_idx += len; + } + } + (true, DiffLines::Right(range)) + | (false, DiffLines::Left(range)) => { + // TODO: count vline count in the range instead + let height = range.len(); + + diff_sections.push(DiffSection { + y_idx, + height, + kind: if is_right { + DiffSectionKind::Added + } else { + DiffSectionKind::Removed + }, + }); + + let initial_y_idx = y_idx; + // Mopve forward by the count given. + y_idx += height; + + if y_idx < min_vline.get() { + if is_right { + if let Some(DiffLines::Left(r)) = last_change { + // TODO: count vline count in the other editor since this is skipping an amount dependent on those vlines + let len = r.len() - r.len().min(range.len()); + if len > 0 { + diff_sections.push(DiffSection { + y_idx, + height: len, + kind: DiffSectionKind::NoCode, + }); + y_idx += len; + } + }; + } + last_change = Some(change); + continue; + } + + let Some(min_info) = *min_info else { + // TODO(minor): What is the proper behavior here? + break; + }; + + let Some(max_info) = *max_info else { + // TODO(minor): What is the proper behavior here? + break; + }; + + let start_rvline = + lines.rvline_of_line(&text_prov, range.start); + + // TODO: this wouldn't need to produce vlines if screen lines didn't + // require them. + let iter = lines + .iter_rvlines(&text_prov, false, start_rvline) + .take_while(|vline_info| { + vline_info.rvline.line < range.end + }) + .enumerate(); + for (i, rvline_info) in iter { + let rvline = rvline_info.rvline; + if rvline < min_info.rvline { + continue; + } + + rvlines.push(rvline); + let vline_y = (initial_y_idx + i) * line_height; + info.insert( + rvline, + LineInfo { + y: line_y(rvline_info, vline_y) as f64 - y0, + vline_y: vline_y as f64 - y0, + vline_info: rvline_info, + }, + ); + + if rvline > max_info.rvline { + break; + } + } + + if is_right { + if let Some(DiffLines::Left(r)) = last_change { + // TODO: count vline count in the other editor since this is skipping an amount dependent on those vlines + let len = r.len() - r.len().min(range.len()); + if len > 0 { + diff_sections.push(DiffSection { + y_idx, + height: len, + kind: DiffSectionKind::NoCode, + }); + y_idx += len; + } + }; + } + } + (_, DiffLines::Both(bothinfo)) => { + let start = if is_right { + bothinfo.right.start + } else { + bothinfo.left.start + }; + let len = bothinfo.right.len(); + let diff_height = len + - bothinfo + .skip + .as_ref() + .map(|skip| skip.len().saturating_sub(1)) + .unwrap_or(0); + if y_idx + diff_height < min_vline.get() { + y_idx += diff_height; + last_change = Some(change); + continue; + } + + let start_rvline = lines.rvline_of_line(&text_prov, start); + + let mut iter = lines + .iter_rvlines_init( + &text_prov, + config.id, + start_rvline, + false, + ) + .take_while(|info| info.rvline.line < start + len); + while let Some(rvline_info) = iter.next() { + let line = rvline_info.rvline.line; + + // Skip over the lines + if let Some(skip) = bothinfo.skip.as_ref() { + if Some(skip.start) == line.checked_sub(start) { + y_idx += 1; + // Skip by `skip` count, which is skip - 1 because we will + // go to the next vline on the next iter + let _ = iter.nth(skip.len().saturating_sub(1)); + continue; + } + } + + // Add the vline if it is within view + if y_idx >= min_vline.get() { + rvlines.push(rvline_info.rvline); + let vline_y = y_idx * line_height; + info.insert( + rvline_info.rvline, + LineInfo { + y: line_y(rvline_info, vline_y) as f64 - y0, + vline_y: vline_y as f64 - y0, + vline_info: rvline_info, + }, + ); + } + + y_idx += 1; + + if y_idx - 1 > max_vline.get() { + break; + } + } + } + } + last_change = Some(change); + } + ScreenLines { + lines: Rc::new(rvlines), + info: Rc::new(info), + diff_sections: Some(Rc::new(diff_sections)), + base, + } + } + } +} + fn parse_hover_resp( hover: lsp_types::Hover, config: &LapceConfig, diff --git a/lapce-app/src/editor/diff.rs b/lapce-app/src/editor/diff.rs index 84b2add5c5..2dac5e9426 100644 --- a/lapce-app/src/editor/diff.rs +++ b/lapce-app/src/editor/diff.rs @@ -6,7 +6,7 @@ use floem::{ reactive::{RwSignal, Scope}, style::CursorStyle, view::View, - views::{clip, empty, label, list, stack, svg, Decorators}, + views::{clip, dyn_stack, empty, label, stack, svg, Decorators}, }; use lapce_core::buffer::{ diff::{expand_diff_lines, rope_diff, DiffExpand, DiffLines}, @@ -128,6 +128,7 @@ pub struct DiffEditorData { pub scope: Scope, pub left: Rc, pub right: Rc, + pub confirmed: RwSignal, pub focus_right: RwSignal, } @@ -141,24 +142,21 @@ impl DiffEditorData { common: Rc, ) -> Self { let cx = cx.create_child(); - let left = EditorData::new( - cx, - None, - Some((editor_tab_id, id)), - EditorId::next(), - left_doc, - common.clone(), - ); - let left = Rc::new(left); - let right = EditorData::new( - cx, - None, - Some((editor_tab_id, id)), - EditorId::next(), - right_doc, - common, - ); - let right = Rc::new(right); + let confirmed = cx.create_rw_signal(false); + + let [left, right] = [left_doc, right_doc].map(|doc| { + let editor_data = EditorData::new( + cx, + None, + Some((editor_tab_id, id)), + EditorId::next(), + doc, + Some(confirmed), + common.clone(), + ); + + Rc::new(editor_data) + }); let data = Self { id, @@ -166,6 +164,7 @@ impl DiffEditorData { scope: cx, left, right, + confirmed, focus_right: cx.create_rw_signal(true), }; @@ -194,24 +193,28 @@ impl DiffEditorData { diff_editor_id: EditorId, ) -> Self { let cx = cx.create_child(); + let confirmed = cx.create_rw_signal(true); + + let [left, right] = [&self.left, &self.right].map(|editor_data| { + let editor_data = editor_data.copy( + cx, + None, + Some((editor_tab_id, diff_editor_id)), + EditorId::next(), + Some(confirmed), + ); + + Rc::new(editor_data) + }); let diff_editor = DiffEditorData { scope: cx, id: diff_editor_id, editor_tab_id: cx.create_rw_signal(editor_tab_id), focus_right: cx.create_rw_signal(true), - left: Rc::new(self.left.copy( - cx, - None, - Some((editor_tab_id, diff_editor_id)), - EditorId::next(), - )), - right: Rc::new(self.right.copy( - cx, - None, - Some((editor_tab_id, diff_editor_id)), - EditorId::next(), - )), + left, + right, + confirmed, }; diff_editor.listen_diff_changes(); @@ -384,7 +387,7 @@ pub fn diff_show_more_section_view( wave_box().style(move |s| { s.absolute() .size_pct(100.0, 100.0) - .color(*config.get().get_color(LapceColor::PANEL_BACKGROUND)) + .color(config.get().color(LapceColor::PANEL_BACKGROUND)) }), label(move || format!("{} Hidden Lines", section.lines)), label(|| "|".to_string()).style(|s| s.margin_left(10.0)), @@ -393,7 +396,7 @@ pub fn diff_show_more_section_view( let config = config.get(); let size = config.ui.icon_size() as f32; s.size(size, size) - .color(*config.get_color(LapceColor::EDITOR_FOREGROUND)) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) }), label(|| "Expand All".to_string()).style(|s| s.margin_left(6.0)), )) @@ -433,7 +436,7 @@ pub fn diff_show_more_section_view( let config = config.get(); let size = config.ui.icon_size() as f32; s.size(size, size) - .color(*config.get_color(LapceColor::EDITOR_FOREGROUND)) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) }, ), label(|| "Expand Up".to_string()).style(|s| s.margin_left(6.0)), @@ -474,7 +477,7 @@ pub fn diff_show_more_section_view( let config = config.get(); let size = config.ui.icon_size() as f32; s.size(size, size) - .color(*config.get_color(LapceColor::EDITOR_FOREGROUND)) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) }, ), label(|| "Expand Down".to_string()).style(|s| s.margin_left(6.0)), @@ -529,7 +532,7 @@ pub fn diff_show_more_section_view( s.height(config.get().editor.line_height() as f32 + 1.0) }), clip( - list(each_fn, key_fn, view_fn) + dyn_stack(each_fn, key_fn, view_fn) .style(|s| s.flex_col().size_pct(100.0, 100.0)), ) .style(|s| s.size_pct(100.0, 100.0)), diff --git a/lapce-app/src/editor/gutter.rs b/lapce-app/src/editor/gutter.rs index 2515777beb..9775f5de0c 100644 --- a/lapce-app/src/editor/gutter.rs +++ b/lapce-app/src/editor/gutter.rs @@ -12,10 +12,10 @@ use lapce_core::{buffer::rope_text::RopeText, mode::Mode}; use crate::{ config::{color::LapceColor, LapceConfig}, - doc::Document, + doc::DocumentExt, }; -use super::{view::changes_colors, EditorData}; +use super::{view::changes_colors_screen, view_data::EditorViewData, EditorData}; pub struct EditorGutterView { id: Id, @@ -39,7 +39,7 @@ impl EditorGutterView { fn paint_head_changes( &self, cx: &mut PaintCx, - doc: Rc, + view: &EditorViewData, viewport: Rect, is_normal: bool, config: &LapceConfig, @@ -48,20 +48,19 @@ impl EditorGutterView { return; } - let changes = doc.head_changes.get_untracked(); + let changes = view + .doc + .with_untracked(|doc| doc.head_changes().get_untracked()); let line_height = config.editor.line_height() as f64; - let min_line = (viewport.y0 / line_height).floor() as usize; - let max_line = (viewport.y1 / line_height).ceil() as usize; - - let changes = changes_colors(changes, min_line, max_line, config); + let changes = changes_colors_screen(view, changes); for (y, height, removed, color) in changes { let height = if removed { 10.0 } else { height as f64 * line_height }; - let mut y = y as f64 * line_height - viewport.y0; + let mut y = y - viewport.y0; if removed { y -= 5.0; } @@ -101,12 +100,12 @@ impl EditorGutterView { .inflate(25.0, 0.0); cx.fill( &sticky_area_rect, - config.get_color(LapceColor::LAPCE_DROPDOWN_SHADOW), + config.color(LapceColor::LAPCE_DROPDOWN_SHADOW), 3.0, ); cx.fill( &sticky_area_rect, - config.get_color(LapceColor::EDITOR_STICKY_HEADER_BACKGROUND), + config.color(LapceColor::EDITOR_STICKY_HEADER_BACKGROUND), 0.0, ); } @@ -162,57 +161,56 @@ impl View for EditorGutterView { FamilyOwned::parse_list(&config.editor.font_family).collect(); let attrs = Attrs::new() .family(&family) - .color(*config.get_color(LapceColor::EDITOR_DIM)) + .color(config.color(LapceColor::EDITOR_DIM)) .font_size(config.editor.font_size() as f32); let attrs_list = AttrsList::new(attrs); - let current_line_attrs_list = AttrsList::new( - attrs.color(*config.get_color(LapceColor::EDITOR_FOREGROUND)), - ); + let current_line_attrs_list = + AttrsList::new(attrs.color(config.color(LapceColor::EDITOR_FOREGROUND))); let show_relative = config.core.modal && config.editor.modal_mode_relative_line_numbers && mode != Mode::Insert && kind_is_normal; - for line in &screen_lines.lines { - let line = *line; - if line > last_line { - break; - } + screen_lines.with_untracked(|screen_lines| { + for (line, y) in screen_lines.iter_lines_y() { + // If it ends up outside the bounds of the file, stop trying to display line numbers + if line > last_line { + break; + } - let text = if show_relative { - if line == current_line { - line + 1 + let text = if show_relative { + if line == current_line { + line + 1 + } else { + line.abs_diff(current_line) + } } else { - line.abs_diff(current_line) + line + 1 } - } else { - line + 1 - } - .to_string(); + .to_string(); - let info = screen_lines.info.get(&line).unwrap(); - let mut text_layout = TextLayout::new(); - if line == current_line { - text_layout.set_text(&text, current_line_attrs_list.clone()); - } else { - text_layout.set_text(&text, attrs_list.clone()); + let mut text_layout = TextLayout::new(); + if line == current_line { + text_layout.set_text(&text, current_line_attrs_list.clone()); + } else { + text_layout.set_text(&text, attrs_list.clone()); + } + let size = text_layout.size(); + let height = size.height; + + cx.draw_text( + &text_layout, + Point::new( + (self.width - (size.width)).max(0.0), + y + (line_height - height) / 2.0 - viewport.y0, + ), + ); } - let size = text_layout.size(); - let height = size.height; - let y = info.y; - - cx.draw_text( - &text_layout, - Point::new( - (self.width - (size.width)).max(0.0), - y as f64 + (line_height - height) / 2.0 - viewport.y0, - ), - ); - } + }); self.paint_head_changes( cx, - self.editor.view.doc.get_untracked(), + &self.editor.view, viewport, kind_is_normal, &config, diff --git a/lapce-app/src/editor/movement.rs b/lapce-app/src/editor/movement.rs index b607756860..2ec76f9fee 100644 --- a/lapce-app/src/editor/movement.rs +++ b/lapce-app/src/editor/movement.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; use lapce_core::{ buffer::rope_text::RopeText, command::MultiSelectionCommand, - cursor::{ColPosition, Cursor, CursorMode}, + cursor::{ColPosition, Cursor, CursorAffinity, CursorMode}, editor::Editor, mode::{Mode, MotionMode, VisualMode}, movement::{LinePosition, Movement}, @@ -16,7 +16,10 @@ use lapce_core::{ use crate::doc::Document; -use super::view_data::EditorViewData; +use super::{ + view_data::EditorViewData, + visual_line::{RVLine, VLineInfo}, +}; /// Move a selection region by a given movement. /// Much of the time, this will just be a matter of moving the cursor, but @@ -24,6 +27,7 @@ use super::view_data::EditorViewData; fn move_region( view: &EditorViewData, region: &SelRegion, + affinity: &mut CursorAffinity, count: usize, modify: bool, movement: &Movement, @@ -57,6 +61,7 @@ fn move_region( view, region.end, region.horiz.as_ref(), + affinity, count, movement, mode, @@ -71,6 +76,7 @@ fn move_region( pub fn move_selection( view: &EditorViewData, selection: &Selection, + affinity: &mut CursorAffinity, count: usize, modify: bool, movement: &Movement, @@ -78,16 +84,19 @@ pub fn move_selection( ) -> Selection { let mut new_selection = Selection::new(); for region in selection.regions() { - new_selection - .add_region(move_region(view, region, count, modify, movement, mode)); + new_selection.add_region(move_region( + view, region, affinity, count, modify, movement, mode, + )); } new_selection } +// TODO: It would probably fit the overall logic better if affinity was immutable and it just returned the new affinity! pub fn move_offset( view: &EditorViewData, offset: usize, horiz: Option<&ColPosition>, + affinity: &mut CursorAffinity, count: usize, movement: &Movement, mode: Mode, @@ -99,6 +108,7 @@ pub fn move_offset( let new_offset = move_left( view.rope_text(), offset, + affinity, mode, count, config.editor.atomic_soft_tab_width(), @@ -108,8 +118,9 @@ pub fn move_offset( } Movement::Right => { let new_offset = move_right( - view.rope_text(), + view, offset, + affinity, mode, count, config.editor.atomic_soft_tab_width(), @@ -118,44 +129,45 @@ pub fn move_offset( (new_offset, None) } Movement::Up => { - let font_size = config.editor.font_size(); let (new_offset, horiz) = - move_up(view, font_size, offset, horiz.cloned(), mode, count); + move_up(view, offset, affinity, horiz.cloned(), mode, count); (new_offset, Some(horiz)) } Movement::Down => { - let font_size = config.editor.font_size(); let (new_offset, horiz) = - move_down(view, font_size, offset, horiz.cloned(), mode, count); + move_down(view, offset, affinity, horiz.cloned(), mode, count); (new_offset, Some(horiz)) } - Movement::DocumentStart => (0, Some(ColPosition::Start)), + Movement::DocumentStart => { + // Put it before any inlay hints at the very start + *affinity = CursorAffinity::Backward; + (0, Some(ColPosition::Start)) + } Movement::DocumentEnd => { - let (new_offset, horiz) = document_end(view.rope_text(), mode); + let (new_offset, horiz) = document_end(view.rope_text(), affinity, mode); (new_offset, Some(horiz)) } Movement::FirstNonBlank => { - let (new_offset, horiz) = first_non_blank(view.rope_text(), offset); + let (new_offset, horiz) = first_non_blank(view, affinity, offset); (new_offset, Some(horiz)) } Movement::StartOfLine => { - let (new_offset, horiz) = start_of_line(view.rope_text(), offset); + let (new_offset, horiz) = start_of_line(view, affinity, offset); (new_offset, Some(horiz)) } Movement::EndOfLine => { - let (new_offset, horiz) = end_of_line(view.rope_text(), offset, mode); + let (new_offset, horiz) = end_of_line(view, affinity, offset, mode); (new_offset, Some(horiz)) } Movement::Line(position) => { - let font_size = config.editor.font_size(); let (new_offset, horiz) = - to_line(view, font_size, offset, horiz.cloned(), mode, position); + to_line(view, offset, horiz.cloned(), mode, position); (new_offset, Some(horiz)) } @@ -215,6 +227,7 @@ pub fn move_offset( fn move_left( rope_text: impl RopeText, offset: usize, + affinity: &mut CursorAffinity, mode: Mode, count: usize, soft_tab_width: Option, @@ -232,18 +245,22 @@ fn move_left( } } + *affinity = CursorAffinity::Forward; + new_offset } /// Move the offset to the right by `count` amount. /// If `soft_tab_width` is `Some` (and greater than 1) then the offset will snap to the soft tab. fn move_right( - rope_text: impl RopeText, + view: &EditorViewData, offset: usize, + affinity: &mut CursorAffinity, mode: Mode, count: usize, soft_tab_width: Option, ) -> usize { + let rope_text = view.rope_text(); let mut new_offset = rope_text.move_right(offset, mode, count); if let Some(soft_tab_width) = soft_tab_width { @@ -257,89 +274,230 @@ fn move_right( } } + let (rvline, col) = view.rvline_col_of_offset(offset, *affinity); + let info = view.rvline_info(rvline); + + *affinity = if col == info.last_col(&view.text_prov(), false) { + CursorAffinity::Backward + } else { + CursorAffinity::Forward + }; + new_offset } -// These could be abstracted away from `EditorViewData` by having some trait that has a 'get Rope' and 'get point information about text' functions. Would be useful for testing. -/// Move the offset up by `count` amount. +fn find_prev_rvline( + view: &EditorViewData, + start: RVLine, + count: usize, +) -> Option { + if count == 0 { + return Some(start); + } + + // We can't just directly subtract count because of multi-line phantom text. + // As just subtracting count wouldn't properly skip over the phantom lines. + // So we have to search backwards for the previous line that has real content. + let mut info = None; + let mut found_count = 0; + for prev_info in view.iter_rvlines(true, start).skip(1) { + if prev_info.is_empty() { + // We skip any phantom text lines in our consideration + continue; + } + + // Otherwise we found a real line. + found_count += 1; + + if found_count == count { + // If we've completed all the count instances then we're done + info = Some(prev_info); + break; + } + // Otherwise we continue on to find the previous line with content before that. + } + + info.map(|info| info.rvline) +} + +/// Move the offset up by `count` amount. +/// `count` may be zero, because moving up in a selection just jumps to the start of the selection. fn move_up( view: &EditorViewData, - font_size: usize, offset: usize, + affinity: &mut CursorAffinity, horiz: Option, mode: Mode, count: usize, ) -> (usize, ColPosition) { - let rope_text = view.rope_text(); + let rvline = view.rvline_of_offset(offset, *affinity); + if rvline.line == 0 && rvline.line_index == 0 { + // Zeroth line + let horiz = horiz.unwrap_or_else(|| { + ColPosition::Col(view.line_point_of_offset(offset, *affinity).x) + }); - let line = rope_text.line_of_offset(offset); + *affinity = CursorAffinity::Backward; - if line == 0 { - let line = rope_text.line_of_offset(offset); - let new_offset = rope_text.offset_of_line(line); + return (0, horiz); + } + + let Some(rvline) = find_prev_rvline(view, rvline, count) else { + // Zeroth line let horiz = horiz.unwrap_or_else(|| { - ColPosition::Col(view.line_point_of_offset(offset, font_size).x) + ColPosition::Col(view.line_point_of_offset(offset, *affinity).x) }); - return (new_offset, horiz); - } - let visual_line = view.visual_line(line).saturating_sub(count); - let line = view.actual_line(visual_line, false); + *affinity = CursorAffinity::Backward; + + return (0, horiz); + }; let horiz = horiz.unwrap_or_else(|| { - ColPosition::Col(view.line_point_of_offset(offset, font_size).x) + ColPosition::Col(view.line_point_of_offset(offset, *affinity).x) }); - let col = view.line_horiz_col(line, font_size, &horiz, mode != Mode::Normal); - let new_offset = rope_text.offset_of_line_col(line, col); + let col = view.rvline_horiz_col(rvline, &horiz, mode != Mode::Normal); + + // TODO: this should maybe be doing `new_offset == info.interval.start`? + *affinity = if col == 0 { + CursorAffinity::Forward + } else { + CursorAffinity::Backward + }; + + let new_offset = view.offset_of_line_col(rvline.line, col); (new_offset, horiz) } -/// Move the offset down by `count` amount. -fn move_down( +/// Move down for when the cursor is on the last visual line. +fn move_down_last_rvline( view: &EditorViewData, - font_size: usize, offset: usize, + affinity: &mut CursorAffinity, horiz: Option, mode: Mode, - count: usize, ) -> (usize, ColPosition) { let rope_text = view.rope_text(); let last_line = rope_text.last_line(); - let line = rope_text.line_of_offset(offset); - if line == last_line { - let new_offset = rope_text.offset_line_end(offset, mode != Mode::Normal); - let horiz = horiz.unwrap_or_else(|| { - ColPosition::Col(view.line_point_of_offset(offset, font_size).x) - }); - return (new_offset, horiz); + let new_offset = rope_text.line_end_offset(last_line, mode != Mode::Normal); + + // We should appear after any phantom text at the very end of the line. + *affinity = CursorAffinity::Forward; + + let horiz = horiz.unwrap_or_else(|| { + ColPosition::Col(view.line_point_of_offset(offset, *affinity).x) + }); + + (new_offset, horiz) +} + +fn find_next_rvline_info( + view: &EditorViewData, + offset: usize, + start: RVLine, + count: usize, +) -> Option> { + // We can't just directly add count because of multi-line phantom text. + // These lines are 'not there' and also don't have any position that can be moved into + // (unlike phantom text that is mixed with real text) + // So we have to search forward for the next line that has real content. + // The typical iteration count for this is 1, and even after that it is usually only a handful. + let mut found_count = 0; + for next_info in view.iter_rvlines(false, start) { + if count == 0 { + return Some(next_info); + } + + if next_info.is_empty() { + // We skip any phantom text lines in our consideration + // TODO: Would this skip over an empty line? + continue; + } + + if next_info.interval.start <= offset { + // If we're on or before our current visual line then we skip it + continue; + } + + // Otherwise we found a real line. + found_count += 1; + + if found_count == count { + // If we've completed all the count instances then we're done + return Some(next_info); + } + // Otherwise we continue on to find the next line with content after that. } - let visual_line = view.visual_line(line); - let line = view.actual_line(visual_line + count, true); - let line = line.min(last_line); + None +} + +/// Move the offset down by `count` amount. +/// `count` may be zero, because moving down in a selection just jumps to the end of the selection. +fn move_down( + view: &EditorViewData, + offset: usize, + affinity: &mut CursorAffinity, + horiz: Option, + mode: Mode, + count: usize, +) -> (usize, ColPosition) { + let rvline = view.rvline_of_offset(offset, *affinity); + + let Some(info) = find_next_rvline_info(view, offset, rvline, count) else { + // There was no next entry, this typically means that we would go past the end if we went + // further + return move_down_last_rvline(view, offset, affinity, horiz, mode); + }; + // TODO(minor): is this the right affinity? let horiz = horiz.unwrap_or_else(|| { - ColPosition::Col(view.line_point_of_offset(offset, font_size).x) + ColPosition::Col(view.line_point_of_offset(offset, *affinity).x) }); - let col = view.line_horiz_col(line, font_size, &horiz, mode != Mode::Normal); - let new_offset = rope_text.offset_of_line_col(line, col); + + let col = view.rvline_horiz_col(info.rvline, &horiz, mode != Mode::Normal); + + let new_offset = view.offset_of_line_col(info.rvline.line, col); + + *affinity = if new_offset == info.interval.start { + // The column was zero so we shift it to be at the line itself. + // This lets us move down to an empty - for example - next line and appear at the + // start of that line without coinciding with the offset at the end of the previous line. + CursorAffinity::Forward + } else { + CursorAffinity::Backward + }; (new_offset, horiz) } -fn document_end(rope_text: impl RopeText, mode: Mode) -> (usize, ColPosition) { +fn document_end( + rope_text: impl RopeText, + affinity: &mut CursorAffinity, + mode: Mode, +) -> (usize, ColPosition) { let last_offset = rope_text.offset_line_end(rope_text.len(), mode != Mode::Normal); + // Put it past any inlay hints directly at the end + *affinity = CursorAffinity::Forward; + (last_offset, ColPosition::End) } -fn first_non_blank(rope_text: impl RopeText, offset: usize) -> (usize, ColPosition) { - let line = rope_text.line_of_offset(offset); - let non_blank_offset = rope_text.first_non_blank_character_on_line(line); - let start_line_offset = rope_text.offset_of_line(line); +fn first_non_blank( + view: &EditorViewData, + affinity: &mut CursorAffinity, + offset: usize, +) -> (usize, ColPosition) { + let info = view.rvline_info_of_offset(offset, *affinity); + let non_blank_offset = info.first_non_blank_character(&view.text_prov()); + let start_line_offset = info.interval.start; + // TODO: is this always the correct affinity? It might be desirable for the very first character on a wrapped line? + *affinity = CursorAffinity::Forward; + if offset > non_blank_offset { // Jump to the first non-whitespace character if we're strictly after it (non_blank_offset, ColPosition::FirstNonBlank) @@ -354,26 +512,41 @@ fn first_non_blank(rope_text: impl RopeText, offset: usize) -> (usize, ColPositi } } -fn start_of_line(rope_text: impl RopeText, offset: usize) -> (usize, ColPosition) { - let line = rope_text.line_of_offset(offset); - let new_offset = rope_text.offset_of_line(line); +fn start_of_line( + view: &EditorViewData, + affinity: &mut CursorAffinity, + offset: usize, +) -> (usize, ColPosition) { + let rvline = view.rvline_of_offset(offset, *affinity); + let new_offset = view.offset_of_rvline(rvline); + // TODO(minor): if the line has zero characters, it should probably be forward affinity but + // other cases might be better as backwards? + *affinity = CursorAffinity::Forward; (new_offset, ColPosition::Start) } fn end_of_line( - rope_text: impl RopeText, + view: &EditorViewData, + affinity: &mut CursorAffinity, offset: usize, mode: Mode, ) -> (usize, ColPosition) { - let new_offset = rope_text.offset_line_end(offset, mode != Mode::Normal); + let info = view.rvline_info_of_offset(offset, *affinity); + let new_col = info.last_col(&view.text_prov(), mode != Mode::Normal); + *affinity = if new_col == 0 { + CursorAffinity::Forward + } else { + CursorAffinity::Backward + }; + + let new_offset = view.offset_of_line_col(info.rvline.line, new_col); (new_offset, ColPosition::End) } fn to_line( view: &EditorViewData, - font_size: usize, offset: usize, horiz: Option, mode: Mode, @@ -381,24 +554,25 @@ fn to_line( ) -> (usize, ColPosition) { let rope_text = view.rope_text(); + // TODO(minor): Should this use rvline? let line = match position { LinePosition::Line(line) => (line - 1).min(rope_text.last_line()), LinePosition::First => 0, LinePosition::Last => rope_text.last_line(), }; + // TODO(minor): is this the best affinity? let horiz = horiz.unwrap_or_else(|| { - ColPosition::Col(view.line_point_of_offset(offset, font_size).x) + ColPosition::Col( + view.line_point_of_offset(offset, CursorAffinity::Backward) + .x, + ) }); - let col = view.line_horiz_col(line, font_size, &horiz, mode != Mode::Normal); + let col = view.line_horiz_col(line, &horiz, mode != Mode::Normal); let new_offset = rope_text.offset_of_line_col(line, col); (new_offset, horiz) } -// TODO: passing a view with the document is kinda ehhh, because this would be used when you call -// .update on the `RwSignal`, which presumably delinks it from the RwSignal the view has. -// A solution might be to change the caller to only pass in the editor view and have this do the update, but only when it is needed. - /// Move the current cursor. /// This will signal-update the document for some motion modes. pub fn move_cursor( @@ -420,6 +594,7 @@ pub fn move_cursor( view, offset, cursor.horiz.as_ref(), + &mut cursor.affinity, count, movement, Mode::Normal, @@ -429,6 +604,7 @@ pub fn move_cursor( view, new_offset, None, + &mut cursor.affinity, 1, &Movement::Right, Mode::Insert, @@ -473,6 +649,7 @@ pub fn move_cursor( view, end, cursor.horiz.as_ref(), + &mut cursor.affinity, count, movement, Mode::Visual(VisualMode::Normal), @@ -488,6 +665,7 @@ pub fn move_cursor( let selection = move_selection( view, selection, + &mut cursor.affinity, count, modify, movement, @@ -522,6 +700,7 @@ pub fn do_multi_selection( view, offset, cursor.horiz.as_ref(), + &mut cursor.affinity, 1, &Movement::Up, Mode::Insert, @@ -540,6 +719,7 @@ pub fn do_multi_selection( view, offset, cursor.horiz.as_ref(), + &mut cursor.affinity, 1, &Movement::Down, Mode::Insert, @@ -747,4 +927,5 @@ pub fn do_motion_mode( } } -// TODO: Write tests for the various functions. +// TODO: Write tests for the various functions. We'll need a more easily swappable API than +// `EditorViewData` for that. diff --git a/lapce-app/src/editor/view.rs b/lapce-app/src/editor/view.rs index 955b3ea159..6f71638fbd 100644 --- a/lapce-app/src/editor/view.rs +++ b/lapce-app/src/editor/view.rs @@ -1,4 +1,6 @@ -use std::{cmp, collections::HashMap, path::PathBuf, rc::Rc, sync::Arc}; +use std::{ + cmp, collections::HashMap, ops::RangeInclusive, path::PathBuf, rc::Rc, sync::Arc, +}; use floem::{ action::{set_ime_allowed, set_ime_cursor_area}, @@ -12,17 +14,21 @@ use floem::{ Color, }, reactive::{ - create_effect, create_memo, create_rw_signal, Memo, ReadSignal, RwSignal, + batch, create_effect, create_memo, create_rw_signal, Memo, ReadSignal, + RwSignal, Scope, }, style::{CursorStyle, Style}, taffy::prelude::Node, view::{View, ViewData}, - views::{clip, container, empty, label, list, scroll, stack, svg, Decorators}, + views::{ + clip, container, dyn_stack, empty, label, scroll, stack, svg, Decorators, + }, EventPropagation, Renderer, }; +use itertools::Itertools; use lapce_core::{ buffer::{diff::DiffLines, rope_text::RopeText}, - cursor::{ColPosition, CursorMode}, + cursor::{ColPosition, CursorAffinity, CursorMode}, mode::{Mode, VisualMode}, }; use lapce_rpc::dap_types::{DapId, SourceBreakpoint}; @@ -31,43 +37,305 @@ use lapce_xi_rope::find::CaseMatching; use super::{ gutter::editor_gutter_view, view_data::{EditorViewData, LineExtraStyle}, - EditorData, CHAR_WIDTH, FONT_SIZE, + visual_line::{RVLine, VLine, VLineInfo}, + EditorData, CHAR_WIDTH, }; use crate::{ app::clickable_icon, command::InternalCommand, config::{color::LapceColor, icon::LapceIcons, LapceConfig}, debug::LapceBreakpoint, - doc::{DocContent, Document}, + doc::{phantom_text::PhantomTextKind, DocContent, Document, DocumentExt}, keypress::KeyPressFocus, text_input::text_input, window_tab::{Focus, WindowTabData}, workspace::LapceWorkspace, }; +#[derive(Clone, Copy, PartialEq, Eq)] pub enum DiffSectionKind { NoCode, Added, Removed, } +#[derive(Clone, PartialEq)] pub struct DiffSection { - pub start_line: usize, + /// The y index that the diff section is at. + /// This is multiplied by the line height to get the y position. + /// So this can roughly be considered as the `VLine of the start of this diff section, but it + /// isn't necessarily convertable to a `VLine` due to jumping over empty code sections. + pub y_idx: usize, pub height: usize, pub kind: DiffSectionKind, } +#[derive(Clone, PartialEq)] pub struct ScreenLines { - pub lines: Vec, - pub info: HashMap, - pub diff_sections: Vec, + pub lines: Rc>, + /// Guaranteed to have an entry for each `VLine` in `lines` + /// You should likely use accessor functions rather than this directly. + pub info: Rc>, + pub diff_sections: Option>>, + /// The base y position that all the y positions inside `info` are relative to. + /// This exists so that if a text layout is created outside of the view, we don't have to + /// completely recompute the screen lines (or do somewhat intricate things to update them) + /// we simply have to update the `base_y`. + pub base: RwSignal, } +impl ScreenLines { + pub fn new(cx: Scope, viewport: Rect) -> ScreenLines { + ScreenLines { + lines: Default::default(), + info: Default::default(), + diff_sections: Default::default(), + base: cx.create_rw_signal(ScreenLinesBase { + active_viewport: viewport, + }), + } + } + + pub fn clear(&mut self, viewport: Rect) { + self.lines = Default::default(); + self.info = Default::default(); + self.diff_sections = Default::default(); + self.base.set(ScreenLinesBase { + active_viewport: viewport, + }); + } + + /// Get the line info for the given rvline. + pub fn info(&self, rvline: RVLine) -> Option { + let info = self.info.get(&rvline)?; + let base = self.base.get(); + + Some(info.clone().with_base(base)) + } + + pub fn vline_info(&self, rvline: RVLine) -> Option> { + self.info.get(&rvline).map(|info| info.vline_info) + } + + pub fn rvline_range(&self) -> Option<(RVLine, RVLine)> { + self.lines.first().copied().zip(self.lines.last().copied()) + } + + /// Iterate over the line info, copying them with the full y positions. + pub fn iter_line_info(&self) -> impl Iterator + '_ { + self.lines.iter().map(|rvline| self.info(*rvline).unwrap()) + } + + /// Iterate over the line info within the range, copying them with the full y positions. + /// If the values are out of range, it is clamped to the valid lines within. + pub fn iter_line_info_r( + &self, + r: RangeInclusive, + ) -> impl Iterator + '_ { + // We search for the start/end indices due to not having a good way to iterate over + // successive rvlines without the view. + // This should be good enough due to lines being small. + let start_idx = self.lines.binary_search(r.start()).ok().or_else(|| { + if self.lines.first().map(|l| r.start() < l).unwrap_or(false) { + Some(0) + } else { + // The start is past the start of our lines + None + } + }); + + let end_idx = self.lines.binary_search(r.end()).ok().or_else(|| { + if self.lines.last().map(|l| r.end() > l).unwrap_or(false) { + Some(self.lines.len()) + } else { + // The end is before the end of our lines but not available + None + } + }); + if let (Some(start_idx), Some(end_idx)) = (start_idx, end_idx) { + self.lines.get(start_idx..=end_idx) + } else { + // Hacky method to get an empty iterator of the same type + self.lines.get(0..0) + } + .into_iter() + .flatten() + .copied() + .map(|rvline| self.info(rvline).unwrap()) + } + + pub fn iter_vline_info(&self) -> impl Iterator> + '_ { + self.lines + .iter() + .map(|vline| &self.info[vline].vline_info) + .copied() + } + + pub fn iter_vline_info_r( + &self, + r: RangeInclusive, + ) -> impl Iterator> + '_ { + // TODO(minor): this should probably skip tracking? + self.iter_line_info_r(r).map(|x| x.vline_info) + } + + /// Iter the real lines underlying the visual lines on the screen + pub fn iter_lines(&self) -> impl Iterator + '_ { + // We can just assume that the lines stored are contiguous and thus just get the first + // buffer line and then the last buffer line. + let start_vline = self.lines.first().copied().unwrap_or_default(); + let end_vline = self.lines.last().copied().unwrap_or_default(); + + let start_line = self.info(start_vline).unwrap().vline_info.rvline.line; + let end_line = self.info(end_vline).unwrap().vline_info.rvline.line; + + start_line..=end_line + } + + /// Iterate over the real lines underlying the visual lines on the screen with the y position + /// of their layout. + /// (line, y) + pub fn iter_lines_y(&self) -> impl Iterator + '_ { + let mut last_line = None; + self.lines.iter().filter_map(move |vline| { + let info = self.info(*vline).unwrap(); + + let line = info.vline_info.rvline.line; + + if last_line == Some(line) { + // We've already considered this line. + return None; + } + + last_line = Some(line); + + Some((line, info.y)) + }) + } + + /// Get the earliest line info for a given line. + pub fn info_for_line(&self, line: usize) -> Option { + self.info(self.first_rvline_for_line(line)?) + } + + /// Get the earliest rvline for the given line + pub fn first_rvline_for_line(&self, line: usize) -> Option { + self.lines + .iter() + .find(|rvline| rvline.line == line) + .copied() + } + + /// Get the latest rvline for the given line + pub fn last_rvline_for_line(&self, line: usize) -> Option { + self.lines + .iter() + .rfind(|rvline| rvline.line == line) + .copied() + } + + /// Ran on [`LayoutEvent::CreatedLayout`] to update [`ScreenLinesBase`] & + /// the viewport if necessary. + /// + /// Returns `true` if [`ScreenLines`] needs to be completely updated in response + pub(crate) fn on_created_layout( + &self, + view: &EditorViewData, + line: usize, + ) -> bool { + let config = view.config.get_untracked(); + let base = self.base.get_untracked(); + let vp = view.viewport.get_untracked(); + + let is_before = self + .iter_vline_info() + .next() + .map(|l| line < l.rvline.line) + .unwrap_or(false); + + // If the line is created before the current screenlines, we can simply shift the + // base and viewport forward by the number of extra wrapped lines, + // without needing to recompute the screen lines. + if is_before { + let line_height = config.editor.line_height(); + + // We could use `try_get_text_layout` here, but I believe this guards against a rare + // crash (though it is hard to verify) wherein the config id has changed and so the + // layouts get cleared. + // However, the original trigger of the layout event was when a layout was created + // and it expects it to still exist. So we create it just in case, though we of course + // don't trigger another layout event. + let layout = view.get_text_layout_trigger(line, false); + + // One line was already accounted for by treating it as an unwrapped line. + let new_lines = layout.line_count() - 1; + + let new_y0 = base.active_viewport.y0 + (new_lines * line_height) as f64; + let new_y1 = new_y0 + vp.height(); + let new_viewport = Rect::new(vp.x0, new_y0, vp.x1, new_y1); + + batch(|| { + self.base.set(ScreenLinesBase { + active_viewport: new_viewport, + }); + view.viewport.set(new_viewport); + }); + + // Ensure that it is created even after the base/viewport signals have been updated. + // (We need the `get_text_layout` to still have the layout) + // But we have to trigger an event still if it is created because it *would* alter the + // screenlines. + // TODO: this has some risk for infinite looping if we're unlucky. + let _layout = view.get_text_layout_trigger(line, true); + + return false; + } + + let is_after = self + .iter_vline_info() + .last() + .map(|l| line > l.rvline.line) + .unwrap_or(false); + + // If the line created was after the current view, we don't need to update the screenlines + // at all, since the new line is not visible and has no effect on y positions + if is_after { + return false; + } + + // If the line is created within the current screenlines, we need to update the + // screenlines to account for the new line. + // That is handled by the caller. + true + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ScreenLinesBase { + /// The current/previous viewport. + /// Used for determining whether there were any changes, and the `y0` serves as the + /// base for positioning the lines. + pub active_viewport: Rect, +} + +#[derive(Debug, Clone, PartialEq)] pub struct LineInfo { // font_size: usize, // line_height: f64, // x: f64, - pub y: usize, + /// The starting y position of the overall line that this vline + /// is a part of. + pub y: f64, + /// The y position of the visual line + pub vline_y: f64, + pub vline_info: VLineInfo<()>, +} +impl LineInfo { + pub fn with_base(mut self, base: ScreenLinesBase) -> Self { + self.y += base.active_viewport.y0; + self.vline_y += base.active_viewport.y0; + self + } } struct StickyHeaderInfo { @@ -108,7 +376,7 @@ pub fn editor_view( let hide_cursor = editor.common.window_common.hide_cursor; create_effect(move |_| { hide_cursor.track(); - let occurrences = doc.with(|doc| doc.find_result.occurrences); + let occurrences = doc.with(|doc| doc.backend.find_result.occurrences); occurrences.track(); id.request_paint(); }); @@ -125,6 +393,7 @@ pub fn editor_view( let config = editor.common.config; let sticky_header_height_signal = editor.sticky_header_height; + let editor2 = editor.clone(); create_effect(move |last_rev| { let config = config.get(); if !config.editor.sticky_header { @@ -144,6 +413,7 @@ pub fn editor_view( } let sticky_header_info = get_sticky_header_info( + &editor2.view, doc, viewport, sticky_header_height_signal, @@ -177,8 +447,9 @@ pub fn editor_view( ime_allowed.set(true); set_ime_allowed(true); } - let offset = cursor.with(|c| c.offset()); - let (_, point_below) = editor_view.points_of_offset(offset); + let (offset, affinity) = cursor.with(|c| (c.offset(), c.affinity)); + let (_, point_below) = + editor_view.points_of_offset(offset, affinity); let window_origin = editor_window_origin.get(); let viewport = editor_viewport.get(); let pos = window_origin @@ -240,12 +511,15 @@ impl EditorView { screen_lines: &ScreenLines, config: &LapceConfig, ) { - for section in &screen_lines.diff_sections { + let Some(diff_sections) = &screen_lines.diff_sections else { + return; + }; + for section in diff_sections.iter() { match section.kind { DiffSectionKind::NoCode => self.paint_diff_no_code( cx, viewport, - section.start_line, + section.y_idx, section.height, config, ), @@ -259,11 +533,10 @@ impl EditorView { )) .with_origin(Point::new( viewport.x0, - (section.start_line * config.editor.line_height()) - as f64, + (section.y_idx * config.editor.line_height()) as f64, )), config - .get_color(LapceColor::SOURCE_CONTROL_ADDED) + .color(LapceColor::SOURCE_CONTROL_ADDED) .with_alpha_factor(0.2), 0.0, ); @@ -278,11 +551,10 @@ impl EditorView { )) .with_origin(Point::new( viewport.x0, - (section.start_line * config.editor.line_height()) - as f64, + (section.y_idx * config.editor.line_height()) as f64, )), config - .get_color(LapceColor::SOURCE_CONTROL_REMOVED) + .color(LapceColor::SOURCE_CONTROL_REMOVED) .with_alpha_factor(0.2), 0.0, ); @@ -333,7 +605,7 @@ impl EditorView { let p1 = Point::new(x as f64 - height, y + height); cx.stroke( &Line::new(p0, p1), - *config.get_color(LapceColor::EDITOR_DIM), + config.color(LapceColor::EDITOR_DIM), 1.0, ); } @@ -349,47 +621,76 @@ impl EditorView { screen_lines: &ScreenLines, start_offset: usize, end_offset: usize, + affinity: CursorAffinity, is_block_cursor: bool, ) { let view = &self.editor.view; - let viewport = self.viewport.get_untracked(); - let (start_line, start_col) = view.offset_to_line_col(start_offset); - let (end_line, end_col) = view.offset_to_line_col(end_offset); + // TODO: selections should have separate start/end affinity + let (start_rvline, start_col) = + view.rvline_col_of_offset(start_offset, affinity); + let (end_rvline, end_col) = view.rvline_col_of_offset(end_offset, affinity); - for line in start_line..=end_line { - if let Some(info) = screen_lines.info.get(&line) { - let phantom_text = view.line_phantom_text(line); - let right_col = if line == end_line { - end_col - } else { - view.line_end_col(line, true) - }; - let right_col = phantom_text.col_after(right_col, false); + for LineInfo { + vline_y, + vline_info: info, + .. + } in screen_lines.iter_line_info_r(start_rvline..=end_rvline) + { + let rvline = info.rvline; + let line = rvline.line; - let x0 = if line == start_line { - let left_col = - phantom_text.col_after(start_col, is_block_cursor); - view.line_point_of_line_col(line, left_col, FONT_SIZE).x - } else { - viewport.x0 - }; - let x1 = view.line_point_of_line_col(line, right_col, FONT_SIZE).x; - let x1 = if line != end_line { - x1 + CHAR_WIDTH - } else { - x1 - }; + let phantom_text = view.line_phantom_text(line); + let left_col = if rvline == start_rvline { + start_col + } else { + view.first_col(info) + }; + let right_col = if rvline == end_rvline { + end_col + } else { + view.last_col(info, true) + }; + let left_col = phantom_text.col_after(left_col, is_block_cursor); + let right_col = phantom_text.col_after(right_col, false); - let rect = Rect::from_origin_size( - (x0, info.y as f64), - (x1 - x0, line_height), - ); - cx.fill(&rect, color, 0.0); + // Skip over empty selections + if !info.is_empty() && left_col == right_col { + continue; } + + // TODO: What affinity should these use? + let x0 = view + .line_point_of_line_col(line, left_col, CursorAffinity::Forward) + .x; + let x1 = view + .line_point_of_line_col(line, right_col, CursorAffinity::Backward) + .x; + // TODO(minor): Should this be line != end_line? + let x1 = if rvline != end_rvline { + x1 + CHAR_WIDTH + } else { + x1 + }; + + let (x0, width) = if info.is_empty() { + let text_layout = view.get_text_layout(line); + let width = text_layout + .get_layout_x(rvline.line_index) + .map(|(_, x1)| x1) + .unwrap_or(0.0) + .into(); + (0.0, width) + } else { + (x0, x1 - x0) + }; + + let rect = Rect::from_origin_size((x0, vline_y), (width, line_height)); + cx.fill(&rect, color, 0.0); } } + #[allow(clippy::too_many_arguments)] fn paint_linewise_selection( &self, cx: &mut PaintCx, @@ -398,28 +699,47 @@ impl EditorView { screen_lines: &ScreenLines, start_offset: usize, end_offset: usize, + affinity: CursorAffinity, ) { let view = &self.editor.view; let viewport = self.viewport.get_untracked(); - let (start_line, _) = view.offset_to_line_col(start_offset); - let (end_line, _) = view.offset_to_line_col(end_offset); + let (start_rvline, _) = view.rvline_col_of_offset(start_offset, affinity); + let (end_rvline, _) = view.rvline_col_of_offset(end_offset, affinity); + // Linewise selection is by *line* so we move to the start/end rvlines of the line + let start_rvline = screen_lines + .first_rvline_for_line(start_rvline.line) + .unwrap_or(start_rvline); + let end_rvline = screen_lines + .last_rvline_for_line(end_rvline.line) + .unwrap_or(end_rvline); + + for LineInfo { + vline_info: info, + vline_y, + .. + } in screen_lines.iter_line_info_r(start_rvline..=end_rvline) + { + let rvline = info.rvline; + let line = rvline.line; - for line in start_line..=end_line { - if let Some(info) = screen_lines.info.get(&line) { - let phantom_text = view.line_phantom_text(line); - let right_col = view.line_end_col(line, true); - let right_col = phantom_text.col_after(right_col, false); + let phantom_text = view.line_phantom_text(line); + + // The left column is always 0 for linewise selections. + let right_col = view.last_col(info, true); + let right_col = phantom_text.col_after(right_col, false); - let x1 = view.line_point_of_line_col(line, right_col, FONT_SIZE).x - + CHAR_WIDTH; + // TODO: what affinity to use? + let x1 = view + .line_point_of_line_col(line, right_col, CursorAffinity::Backward) + .x + + CHAR_WIDTH; - let rect = Rect::from_origin_size( - (viewport.x0, info.y as f64), - (x1 - viewport.x0, line_height), - ); - cx.fill(&rect, color, 0.0); - } + let rect = Rect::from_origin_size( + (viewport.x0, vline_y), + (x1 - viewport.x0, line_height), + ); + cx.fill(&rect, color, 0.0); } } @@ -432,28 +752,26 @@ impl EditorView { screen_lines: &ScreenLines, start_offset: usize, end_offset: usize, + affinity: CursorAffinity, horiz: Option, ) { let view = &self.editor.view; - let (start_line, start_col) = view.offset_to_line_col(start_offset); - let (end_line, end_col) = view.offset_to_line_col(end_offset); + let (start_rvline, start_col) = + view.rvline_col_of_offset(start_offset, affinity); + let (end_rvline, end_col) = view.rvline_col_of_offset(end_offset, affinity); let left_col = start_col.min(end_col); let right_col = start_col.max(end_col) + 1; - let lines = (start_line..=end_line) - .filter_map(|line| { - let max_col = view.line_end_col(line, true); - (max_col > left_col).then_some((line, max_col)) - }) - .filter_map(|(line, max_col)| { - screen_lines - .info - .get(&line) - .map(|info| (line, max_col, info)) + let lines = screen_lines + .iter_line_info_r(start_rvline..=end_rvline) + .filter_map(|line_info| { + let max_col = view.last_col(line_info.vline_info, true); + (max_col > left_col).then_some((line_info, max_col)) }); - for (line, max_col, info) in lines { + for (line_info, max_col) in lines { + let line = line_info.vline_info.rvline.line; let right_col = if let Some(ColPosition::End) = horiz { max_col } else { @@ -463,11 +781,18 @@ impl EditorView { let left_col = phantom_text.col_after(left_col, true); let right_col = phantom_text.col_after(right_col, false); - let x0 = view.line_point_of_line_col(line, left_col, FONT_SIZE).x; - let x1 = view.line_point_of_line_col(line, right_col, FONT_SIZE).x; - - let rect = - Rect::from_origin_size((x0, info.y as f64), (x1 - x0, line_height)); + // TODO: what affinity to use? + let x0 = view + .line_point_of_line_col(line, left_col, CursorAffinity::Forward) + .x; + let x1 = view + .line_point_of_line_col(line, right_col, CursorAffinity::Backward) + .x; + + let rect = Rect::from_origin_size( + (x0, line_info.vline_y), + (x1 - x0, line_height), + ); cx.fill(&rect, color, 0.0); } } @@ -490,9 +815,9 @@ impl EditorView { let is_active = self.is_active.get_untracked() && !find_focus.get_untracked(); - let current_line_color = *config.get_color(LapceColor::EDITOR_CURRENT_LINE); - let selection_color = *config.get_color(LapceColor::EDITOR_SELECTION); - let caret_color = *config.get_color(LapceColor::EDITOR_CARET); + let current_line_color = config.color(LapceColor::EDITOR_CURRENT_LINE); + let selection_color = config.color(LapceColor::EDITOR_SELECTION); + let caret_color = config.color(LapceColor::EDITOR_CARET); let breakline = self.debug_breakline.get_untracked().and_then( |(breakline, breakline_path)| { @@ -509,16 +834,17 @@ impl EditorView { }, ); + // TODO: check if this is correct if let Some(breakline) = breakline { - if let Some(info) = screen_lines.info.get(&breakline) { + if let Some(info) = screen_lines.info_for_line(breakline) { let rect = Rect::from_origin_size( - (viewport.x0, info.y as f64), + (viewport.x0, info.vline_y), (viewport.width(), line_height), ); cx.fill( &rect, - config.get_color(LapceColor::EDITOR_DEBUG_BREAK_LINE), + config.color(LapceColor::EDITOR_DEBUG_BREAK_LINE), 0.0, ); } @@ -530,17 +856,18 @@ impl EditorView { CursorMode::Visual { .. } => false, }; + // Highlight the current line if !is_local && highlight_current_line { for (_, end) in cursor.regions_iter() { - let line = view.line_of_offset(end); + // TODO: unsure if this is correct for wrapping lines + let rvline = view.rvline_of_offset(end, cursor.affinity); if let Some(info) = screen_lines - .info - .get(&line) - .filter(|_| Some(line) != breakline) + .info(rvline) + .filter(|_| Some(rvline.line) != breakline) { let rect = Rect::from_origin_size( - (viewport.x0, info.y as f64), + (viewport.x0, info.vline_y), (viewport.width(), line_height), ); @@ -567,6 +894,7 @@ impl EditorView { screen_lines, start_offset, end_offset, + cursor.affinity, true, ); } @@ -582,6 +910,7 @@ impl EditorView { screen_lines, start.min(end), start.max(end), + cursor.affinity, ); } CursorMode::Visual { @@ -596,6 +925,7 @@ impl EditorView { screen_lines, start.min(end), start.max(end), + cursor.affinity, cursor.horiz, ); } @@ -610,6 +940,7 @@ impl EditorView { screen_lines, start.min(end), start.max(end), + cursor.affinity, false, ); } @@ -622,12 +953,12 @@ impl EditorView { CursorMode::Normal(_) | CursorMode::Visual { .. } => true, CursorMode::Insert(_) => false, }; - let LineRegion { x, width, line } = - cursor_caret(view, end, is_block); + let LineRegion { x, width, rvline } = + cursor_caret(view, end, is_block, cursor.affinity); - if let Some(info) = screen_lines.info.get(&line) { + if let Some(info) = screen_lines.info(rvline) { let rect = Rect::from_origin_size( - (x, info.y as f64), + (x, info.vline_y), (width, line_height), ); cx.fill(&rect, caret_color, 0.0); @@ -668,25 +999,23 @@ impl EditorView { cx: &mut PaintCx, extra_styles: &[LineExtraStyle], y: f64, - height: f64, - line_height: f64, viewport: Rect, ) { for style in extra_styles { + let height = style.height; if let Some(bg) = style.bg_color { let width = style.width.unwrap_or_else(|| viewport.width()); + let base = if style.width.is_none() { + viewport.x0 + } else { + 0.0 + }; + let x = style.x + base; + let y = y + style.y; cx.fill( - &Rect::ZERO.with_size(Size::new(width, height)).with_origin( - Point::new( - style.x - + if style.width.is_none() { - viewport.x0 - } else { - 0.0 - }, - y + (line_height - height) / 2.0, - ), - ), + &Rect::ZERO + .with_size(Size::new(width, height)) + .with_origin(Point::new(x, y)), bg, 0.0, ); @@ -694,13 +1023,13 @@ impl EditorView { if let Some(color) = style.under_line { let width = style.width.unwrap_or_else(|| viewport.width()); - let x = style.x - + if style.width.is_none() { - viewport.x0 - } else { - 0.0 - }; - let y = y + height + (line_height - height) / 2.0; + let base = if style.width.is_none() { + viewport.x0 + } else { + 0.0 + }; + let x = style.x + base; + let y = y + style.y + height; cx.stroke( &Line::new(Point::new(x, y), Point::new(x + width, y)), color, @@ -710,12 +1039,8 @@ impl EditorView { if let Some(color) = style.wave_line { let width = style.width.unwrap_or_else(|| viewport.width()); - self.paint_wave_line( - cx, - width, - Point::new(style.x, y + (line_height - height) / 2.0 + height), - color, - ); + let y = y + style.y + height; + self.paint_wave_line(cx, width, Point::new(style.x, y), color); } } } @@ -731,8 +1056,8 @@ impl EditorView { let config = config.get_untracked(); let line_height = config.editor.line_height() as f64; - let font_size = config.editor.font_size(); + // TODO: cache indent text layout let indent_unit = view.indent_unit(); let family: Vec = FamilyOwned::parse_list(&config.editor.font_family).collect(); @@ -745,33 +1070,16 @@ impl EditorView { indent_text.set_text(&format!("{indent_unit}a"), attrs_list); let indent_text_width = indent_text.hit_position(indent_unit.len()).point.x; - let last_line = view.last_line(); + for (line, y) in screen_lines.iter_lines_y() { + let text_layout = view.get_text_layout(line); - for line in &screen_lines.lines { - let line = *line; - if line > last_line { - break; - } - - let info = screen_lines.info.get(&line).unwrap(); - let text_layout = view.get_text_layout(line, font_size); - let height = text_layout.text.size().height; - let y = info.y; - - self.paint_extra_style( - cx, - &text_layout.extra_style, - y as f64, - height, - line_height, - viewport, - ); + self.paint_extra_style(cx, &text_layout.extra_style, y, viewport); if let Some(whitespaces) = &text_layout.whitespaces { let family: Vec = FamilyOwned::parse_list(&config.editor.font_family).collect(); let attrs = Attrs::new() - .color(*config.get_color(LapceColor::EDITOR_VISIBLE_WHITESPACE)) + .color(config.color(LapceColor::EDITOR_VISIBLE_WHITESPACE)) .family(&family) .font_size(config.editor.font_size() as f32); let attrs_list = AttrsList::new(attrs); @@ -783,22 +1091,10 @@ impl EditorView { for (c, (x0, _x1)) in whitespaces.iter() { match *c { '\t' => { - cx.draw_text( - &tab_text, - Point::new( - *x0, - y as f64 + (line_height - height) / 2.0, - ), - ); + cx.draw_text(&tab_text, Point::new(*x0, y)); } ' ' => { - cx.draw_text( - &space_text, - Point::new( - *x0, - y as f64 + (line_height - height) / 2.0, - ), - ); + cx.draw_text(&space_text, Point::new(*x0, y)); } _ => {} } @@ -809,21 +1105,15 @@ impl EditorView { let mut x = 0.0; while x + 1.0 < text_layout.indent { cx.stroke( - &Line::new( - Point::new(x, y as f64), - Point::new(x, y as f64 + line_height), - ), - config.get_color(LapceColor::EDITOR_INDENT_GUIDE), + &Line::new(Point::new(x, y), Point::new(x, y + line_height)), + config.color(LapceColor::EDITOR_INDENT_GUIDE), 1.0, ); x += indent_text_width; } } - cx.draw_text( - &text_layout.text, - Point::new(0.0, y as f64 + (line_height - height) / 2.0), - ); + cx.draw_text(&text_layout.text, Point::new(0.0, y)); } } @@ -836,8 +1126,10 @@ impl EditorView { return; } - let min_line = *screen_lines.lines.first().unwrap(); - let max_line = *screen_lines.lines.last().unwrap(); + let min_vline = *screen_lines.lines.first().unwrap(); + let max_vline = *screen_lines.lines.last().unwrap(); + let min_line = screen_lines.info(min_vline).unwrap().vline_info.rvline.line; + let max_line = screen_lines.info(max_vline).unwrap().vline_info.rvline.line; let view = self.editor.view.clone(); let config = self.editor.common.config; @@ -850,63 +1142,82 @@ impl EditorView { let start = view.offset_of_line(min_line); let end = view.offset_of_line(max_line + 1); + // TODO: The selection rect creation logic for find is quite similar to the version + // within insert cursor. It would be good to deduplicate it. let mut rects = Vec::new(); for region in occurrences.with_untracked(|selection| { selection.regions_in_range(start, end).to_vec() }) { let start = region.min(); let end = region.max(); - let (start_line, start_col) = view.offset_to_line_col(start); - let (end_line, end_col) = view.offset_to_line_col(end); - for line in &screen_lines.lines { - let line = *line; - if line < start_line { + + // TODO(minor): the proper affinity here should probably be tracked by selregion + let (start_rvline, start_col) = + view.rvline_col_of_offset(start, CursorAffinity::Forward); + let (end_rvline, end_col) = + view.rvline_col_of_offset(end, CursorAffinity::Backward); + + for line_info in screen_lines.iter_line_info() { + let rvline_info = line_info.vline_info; + let rvline = rvline_info.rvline; + let line = rvline.line; + + if rvline < start_rvline { continue; } - if line > end_line { + if rvline > end_rvline { break; } - let info = screen_lines.info.get(&line).unwrap(); + let phantom_text = view.line_phantom_text(line); - let left_col = match line { - _ if line == start_line => start_col, - _ => 0, - }; - let (right_col, _line_end) = match line { - _ if line == end_line => { - let max_col = view.line_end_col(line, true); - (end_col.min(max_col), false) - } - _ => (view.line_end_col(line, true), true), + let left_col = if rvline == start_rvline { start_col } else { 0 }; + let (right_col, _vline_end) = if rvline == end_rvline { + let max_col = view.last_col(rvline_info, true); + (end_col.min(max_col), false) + } else { + (view.last_col(rvline_info, true), true) }; - // Shift it by the inlay hints - let phantom_text = view.line_phantom_text(line); + // Shift it by the phantom text let left_col = phantom_text.col_after(left_col, false); let right_col = phantom_text.col_after(right_col, false); - let x0 = view.line_point_of_line_col(line, left_col, FONT_SIZE).x; - let x1 = view.line_point_of_line_col(line, right_col, FONT_SIZE).x; + // TODO(minor): sel region should have the affinity of the start/end + let x0 = view + .line_point_of_line_col(line, left_col, CursorAffinity::Forward) + .x; + let x1 = view + .line_point_of_line_col( + line, + right_col, + CursorAffinity::Backward, + ) + .x; - if start != end { + if !rvline_info.is_empty() && start != end && left_col != right_col { rects.push( Size::new(x1 - x0, line_height) .to_rect() - .with_origin(Point::new(x0, info.y as f64)), + .with_origin(Point::new(x0, line_info.vline_y)), ); } } } - let color = config.get_color(LapceColor::EDITOR_FOREGROUND); + let color = config.color(LapceColor::EDITOR_FOREGROUND); for rect in rects { cx.stroke(&rect, color, 1.0); } } - fn paint_sticky_headers(&self, cx: &mut PaintCx, viewport: Rect) { + fn paint_sticky_headers( + &self, + cx: &mut PaintCx, + viewport: Rect, + screen_lines: &ScreenLines, + ) { let config = self.editor.common.config.get_untracked(); if !config.editor.sticky_header { return; @@ -915,8 +1226,12 @@ impl EditorView { return; } - let line_height = config.editor.line_height() as f64; - let start_line = (viewport.y0 / line_height).floor() as usize; + let line_height = config.editor.line_height(); + let Some(start_vline) = screen_lines.lines.first() else { + return; + }; + let start_info = screen_lines.vline_info(*start_vline).unwrap(); + let start_line = start_info.rvline.line; let total_sticky_lines = self.sticky_header_info.sticky_lines.len(); @@ -943,7 +1258,19 @@ impl EditorView { }; // Clear background - let area_height = total_sticky_lines as f64 * line_height - scroll_offset; + + let area_height = self + .sticky_header_info + .sticky_lines + .iter() + .copied() + .map(|line| { + let layout = self.editor.view.get_text_layout(line); + layout.line_count() * line_height + }) + .sum::() as f64 + - scroll_offset; + let sticky_area_rect = Size::new(viewport.x1, area_height) .to_rect() .with_origin(Point::new(0.0, viewport.y0)) @@ -951,16 +1278,17 @@ impl EditorView { cx.fill( &sticky_area_rect, - config.get_color(LapceColor::LAPCE_DROPDOWN_SHADOW), + config.color(LapceColor::LAPCE_DROPDOWN_SHADOW), 3.0, ); cx.fill( &sticky_area_rect, - config.get_color(LapceColor::EDITOR_STICKY_HEADER_BACKGROUND), + config.color(LapceColor::EDITOR_STICKY_HEADER_BACKGROUND), 0.0, ); // Paint lines + let mut y_accum = 0.0; for (i, line) in self .sticky_header_info .sticky_lines @@ -974,27 +1302,24 @@ impl EditorView { 0.0 }; + let text_layout = self.editor.view.get_text_layout(line); + + let text_height = (text_layout.line_count() * line_height) as f64; + let height = text_height - y_diff; + cx.save(); - let line_area_rect = Size::new(viewport.width(), line_height - y_diff) + let line_area_rect = Size::new(viewport.width(), height) .to_rect() - .with_origin(Point::new( - viewport.x0, - viewport.y0 + line_height * i as f64, - )); + .with_origin(Point::new(viewport.x0, viewport.y0 + y_accum)); cx.clip(&line_area_rect); - let text_layout = self - .editor - .view - .get_text_layout(line, config.editor.font_size()); - let y = viewport.y0 - + line_height * i as f64 - + (line_height - text_layout.text.size().height) / 2.0 - - y_diff; + let y = viewport.y0 - y_diff + y_accum; cx.draw_text(&text_layout.text, Point::new(viewport.x0, y)); + y_accum += text_height; + cx.restore(); } } @@ -1018,7 +1343,7 @@ impl EditorView { viewport.y0, )) .inflate(0.0, 10.0), - config.get_color(LapceColor::LAPCE_SCROLL_BAR), + config.color(LapceColor::LAPCE_SCROLL_BAR), 0.0, ); @@ -1028,7 +1353,7 @@ impl EditorView { let doc = self.editor.view.doc.get_untracked(); let total_len = doc.buffer.with_untracked(|buffer| buffer.last_line()); - let changes = doc.head_changes.get_untracked(); + let changes = doc.head_changes().get_untracked(); let total_height = viewport.height(); let total_width = viewport.width(); let line_height = config.editor.line_height(); @@ -1038,9 +1363,9 @@ impl EditorView { (total_len * line_height) as f64 }; - let colors = changes_colors(changes, 0, total_len, &config); + let colors = changes_colors_all(&self.editor.view, changes); for (y, height, _, color) in colors { - let y = (y * line_height) as f64 / content_height * total_height; + let y = y / content_height * total_height; let height = ((height * line_height) as f64 / content_height * total_height) .max(3.0); @@ -1062,11 +1387,12 @@ impl EditorView { view: &EditorViewData, line: usize, col: usize, - before_cursor: bool, + affinity: CursorAffinity, ) -> f64 { + let before_cursor = affinity == CursorAffinity::Backward; let phantom_text = view.line_phantom_text(line); let col = phantom_text.col_after(col, before_cursor); - view.line_point_of_line_col(line, col, FONT_SIZE).x + view.line_point_of_line_col(line, col, affinity).x } /// Paint a highlight around the characters at the given positions. @@ -1074,55 +1400,72 @@ impl EditorView { &self, cx: &mut PaintCx, screen_lines: &ScreenLines, - highlight_line_cols: impl Iterator, + highlight_line_cols: impl Iterator, ) { let view = &self.editor.view; let config = self.editor.common.config.get_untracked(); let line_height = config.editor.line_height() as f64; - for (line, col) in highlight_line_cols { + for (rvline, col) in highlight_line_cols { // Is the given line on screen? - if let Some(line_info) = screen_lines.info.get(&line) { - let x0 = Self::calculate_col_x(view, line, col, true); - let x1 = Self::calculate_col_x(view, line, col + 1, false); + if let Some(line_info) = screen_lines.info(rvline) { + let x0 = Self::calculate_col_x( + view, + rvline.line, + col, + CursorAffinity::Backward, + ); + let x1 = Self::calculate_col_x( + view, + rvline.line, + col + 1, + CursorAffinity::Forward, + ); - let y0 = line_info.y as f64; + let y0 = line_info.vline_y; let y1 = y0 + line_height; let rect = Rect::new(x0, y0, x1, y1); - cx.stroke( - &rect, - config.get_color(LapceColor::EDITOR_FOREGROUND), - 1.0, - ); + cx.stroke(&rect, config.color(LapceColor::EDITOR_FOREGROUND), 1.0); } } } - /// Paint scope lines between `(start_line, start_col)` and `(end_line, end_col)`. + /// Paint scope lines between `(start_rvline, start_line, start_col)` and + /// `(end_rvline, end_line end_col)`. fn paint_scope_lines( &self, cx: &mut PaintCx, viewport: Rect, screen_lines: &ScreenLines, - (start_line, start_col): (usize, usize), - (end_line, end_col): (usize, usize), + (start, start_col): (RVLine, usize), + (end, end_col): (RVLine, usize), ) { let view = &self.editor.view; let doc = view.doc.get_untracked(); let config = self.editor.common.config.get_untracked(); let line_height = config.editor.line_height() as f64; - let brush = config.get_color(LapceColor::EDITOR_FOREGROUND); - - if start_line == end_line { - if let Some(line_info) = screen_lines.info.get(&start_line) { - let x0 = - Self::calculate_col_x(view, start_line, start_col + 1, false); - let x1 = Self::calculate_col_x(view, end_line, end_col, true); + let brush = config.color(LapceColor::EDITOR_FOREGROUND); + + if start == end { + if let Some(line_info) = screen_lines.info(start) { + // TODO: Due to line wrapping the y positions of these two spots could be different, do we need to change it? + let x0 = Self::calculate_col_x( + view, + start.line, + start_col + 1, + CursorAffinity::Forward, + ); + let x1 = Self::calculate_col_x( + view, + end.line, + end_col, + CursorAffinity::Backward, + ); if x0 < x1 { - let y = line_info.y as f64 + line_height; + let y = line_info.vline_y + line_height; let p0 = Point::new(x0, y); let p1 = Point::new(x1, y); @@ -1134,13 +1477,11 @@ impl EditorView { } else { // Are start_line and end_line on screen? let start_line_y = screen_lines - .info - .get(&start_line) - .map(|line_info| line_info.y as f64 + line_height); + .info(start) + .map(|line_info| line_info.vline_y + line_height); let end_line_y = screen_lines - .info - .get(&end_line) - .map(|line_info| line_info.y as f64 + line_height); + .info(end) + .map(|line_info| line_info.vline_y + line_height); // We only need to draw anything if start_line is on or before the visible section and // end_line is on or after the visible section. @@ -1148,33 +1489,48 @@ impl EditorView { screen_lines .lines .first() - .is_some_and(|&first_line| first_line > start_line) + .is_some_and(|&first_vline| first_vline > start) .then(|| viewport.min_y()) }); let y1 = end_line_y.or_else(|| { screen_lines .lines .last() - .is_some_and(|&last_line| last_line < end_line) + .is_some_and(|&last_line| last_line < end) .then(|| viewport.max_y()) }); if let [Some(y0), Some(y1)] = [y0, y1] { - let start_x = - Self::calculate_col_x(view, start_line, start_col + 1, false); - let end_x = Self::calculate_col_x(view, end_line, end_col, true); + let start_x = Self::calculate_col_x( + view, + start.line, + start_col + 1, + CursorAffinity::Forward, + ); + let end_x = Self::calculate_col_x( + view, + end.line, + end_col, + CursorAffinity::Backward, + ); + // TODO(minor): is this correct with line wrapping? // The vertical line should be drawn to the left of any non-whitespace characters // in the enclosed section. let min_text_x = doc.buffer.with_untracked(|buffer| { - ((start_line + 1)..=end_line) + ((start.line + 1)..=end.line) .filter(|&line| !buffer.is_line_whitespace(line)) .map(|line| { let non_blank_offset = buffer.first_non_blank_character_on_line(line); let (_, col) = view.offset_to_line_col(non_blank_offset); - Self::calculate_col_x(view, line, col, true) + Self::calculate_col_x( + view, + line, + col, + CursorAffinity::Backward, + ) }) .min_by(f64::total_cmp) }); @@ -1237,7 +1593,11 @@ impl EditorView { .map(|(start, end)| [start, end]); let bracket_line_cols = bracket_offsets.map(|bracket_offsets| { - bracket_offsets.map(|offset| view.offset_to_line_col(offset)) + bracket_offsets.map(|offset| { + let (rvline, col) = + view.rvline_col_of_offset(offset, CursorAffinity::Forward); + (rvline, col) + }) }); if config.editor.highlight_matching_brackets { @@ -1299,19 +1659,9 @@ impl View for EditorView { let config = self.editor.common.config.get_untracked(); let line_height = config.editor.line_height() as f64; - let font_size = config.editor.font_size(); - let screen_lines = self.editor.screen_lines(); - for line in screen_lines.lines { - self.editor.view.get_text_layout(line, font_size); - } - - let doc = self.editor.view.doc.get_untracked(); - let width = self.editor.view.text_layouts.borrow().max_width + 20.0; - let height = line_height - * (self.editor.view.visual_line( - doc.buffer.with_untracked(|buffer| buffer.last_line()), - ) + 1) as f64; + let width = self.editor.view.max_line_width() + 20.0; + let height = line_height * self.editor.view.last_vline().get() as f64; let style = Style::new().width(width).height(height).to_taffy_style(); cx.set_style(inner_node, style); @@ -1334,32 +1684,56 @@ impl View for EditorView { fn paint(&mut self, cx: &mut PaintCx) { let viewport = self.viewport.get_untracked(); let config = self.editor.common.config.get_untracked(); - let screen_lines = self.editor.screen_lines(); - let doc = self.editor.view.doc.get_untracked(); let is_local = doc.content.with_untracked(|content| content.is_local()); + // We repeatedly get the screen lines because we don't currently carefully manage the + // paint functions to avoid potentially needing to recompute them, which could *maybe* + // make them invalid. + // TODO: One way to get around the above issue would be to more careful, since we + // technically don't need to stop it from *recomputing* just stop any possible changes, but + // avoiding recomputation seems easiest/clearest. + // I expect that most/all of the paint functions could restrict themselves to only what is + // within the active screen lines without issue. + let screen_lines = self.editor.screen_lines().get_untracked(); self.paint_cursor(cx, is_local, &screen_lines); + let screen_lines = self.editor.screen_lines().get_untracked(); self.paint_diff_sections(cx, viewport, &screen_lines, &config); + let screen_lines = self.editor.screen_lines().get_untracked(); self.paint_find(cx, &screen_lines); + let screen_lines = self.editor.screen_lines().get_untracked(); self.paint_bracket_highlights_scope_lines(cx, viewport, &screen_lines); + let screen_lines = self.editor.screen_lines().get_untracked(); self.paint_text(cx, viewport, &screen_lines); - self.paint_sticky_headers(cx, viewport); + let screen_lines = self.editor.screen_lines().get_untracked(); + self.paint_sticky_headers(cx, viewport, &screen_lines); self.paint_scroll_bar(cx, viewport, is_local, config); } } fn get_sticky_header_info( + view: &EditorViewData, doc: Rc, viewport: RwSignal, sticky_header_height_signal: RwSignal, config: &LapceConfig, ) -> StickyHeaderInfo { let viewport = viewport.get(); + // TODO(minor): should this be a `get` + let screen_lines = view.screen_lines.get(); let line_height = config.editor.line_height() as f64; - let start_line = (viewport.y0 / line_height).floor() as usize; + // let start_line = (viewport.y0 / line_height).floor() as usize; + let Some(start) = screen_lines.lines.first() else { + return StickyHeaderInfo { + sticky_lines: Vec::new(), + last_sticky_should_scroll: false, + y_diff: 0.0, + }; + }; + let start_info = screen_lines.info(*start).unwrap(); + let start_line = start_info.vline_info.rvline.line; - let y_diff = viewport.y0 - start_line as f64 * line_height; + let y_diff = viewport.y0 - start_info.vline_y; let mut last_sticky_should_scroll = false; let mut sticky_lines = Vec::new(); @@ -1423,16 +1797,22 @@ fn get_sticky_header_info( 0.0 }; - let mut sticky_header_height = 0.0; - for (i, _line) in sticky_lines.iter().enumerate() { - let y_diff = if i == total_sticky_lines - 1 { - scroll_offset - } else { - 0.0 - }; + let sticky_header_height = sticky_lines + .iter() + .enumerate() + .map(|(i, line)| { + // TODO(question): won't y_diff always be scroll_offset here? so we should just sub on + // the outside + let y_diff = if i == total_sticky_lines - 1 { + scroll_offset + } else { + 0.0 + }; - sticky_header_height += line_height - y_diff; - } + let layout = view.get_text_layout(*line); + layout.line_count() as f64 * line_height - y_diff + }) + .sum(); sticky_header_height_signal.set(sticky_header_height); StickyHeaderInfo { @@ -1446,55 +1826,88 @@ fn get_sticky_header_info( pub struct LineRegion { pub x: f64, pub width: f64, - pub line: usize, + pub rvline: RVLine, } +/// Get the render information for a caret cursor at the given `offset`. pub fn cursor_caret( view: &EditorViewData, offset: usize, block: bool, + affinity: CursorAffinity, ) -> LineRegion { - let (line, col) = view.offset_to_line_col(offset); - let after_last_char = col == view.line_end_col(line, true); - let phantom_text = view.line_phantom_text(line); - // The left edge of the block cursor goes after any inlay hint unless the cursor is after - // the final character on the line. - let col = phantom_text.col_after(col, block && !after_last_char); - let doc = view.doc.get_untracked(); - - let col = col - + doc - .preedit - .with_untracked(|preedit| { - preedit.as_ref().and_then(|preedit| { - preedit.cursor.as_ref().and_then(|&(start, _)| { - let preedit_line = doc - .buffer - .with_untracked(|b| b.line_of_offset(preedit.offset)); + let info = view.rvline_info_of_offset(offset, affinity); + let (_, col) = view.offset_to_line_col(offset); + let after_last_char = col == view.line_end_col(info.rvline.line, true); - (preedit_line == line).then_some(start) - }) - }) + let doc = view.doc.get_untracked(); + let preedit_start = doc + .preedit + .with_untracked(|preedit| { + preedit.as_ref().and_then(|preedit| { + let preedit_line = doc + .buffer + .with_untracked(|b| b.line_of_offset(preedit.offset)); + preedit.cursor.map(|x| (preedit_line, x)) }) - .unwrap_or(0); + }) + .filter(|(preedit_line, _)| *preedit_line == info.rvline.line) + .map(|(_, (start, _))| start); + + let phantom_text = view.line_phantom_text(info.rvline.line); + + let (_, col) = view.offset_to_line_col(offset); + let ime_kind = preedit_start.map(|_| PhantomTextKind::Ime); + // The cursor should be after phantom text if the affinity is forward, or it is a block cursor. + // - if we have a relevant preedit we skip over IMEs + // - we skip over completion lens, as the text should be after the cursor + let col = phantom_text.col_after_ignore( + col, + affinity == CursorAffinity::Forward || (block && !after_last_char), + |p| p.kind == PhantomTextKind::Completion || Some(p.kind) == ime_kind, + ); + // We shift forward by the IME's start. This is due to the cursor potentially being in the + // middle of IME phantom text while editing it. + let col = col + preedit_start.unwrap_or(0); + + let point = view.line_point_of_line_col(info.rvline.line, col, affinity); + + let rvline = if preedit_start.is_some() { + // If there's an IME edit, then we need to use the point's y to get the actual y position + // that the IME cursor is at. Since it could be in the middle of the IME phantom text + // TODO(minor): is there a cleaner way of doing this? It also won't work nicely with + // varying line heights. + let y = point.y; + let config = view.config.get_untracked(); + let line_height = config.editor.line_height() as f64; - if block { - let x0 = view.line_point_of_line_col(line, col, FONT_SIZE).x; + let line_index = (y / line_height).floor() as usize; + RVLine::new(info.rvline.line, line_index) + } else { + info.rvline + }; + let x0 = point.x; + if block { let width = if after_last_char { CHAR_WIDTH } else { - let x1 = view.line_point_of_line_col(line, col + 1, FONT_SIZE).x; + let x1 = view + .line_point_of_line_col(info.rvline.line, col + 1, affinity) + .x; x1 - x0 }; - LineRegion { x: x0, width, line } + LineRegion { + x: x0, + width, + rvline, + } } else { - let x = view.line_point_of_line_col(line, col, FONT_SIZE).x; LineRegion { - x: x - 1.0, + x: x0 - 1.0, width: 2.0, - line, + rvline, } } } @@ -1542,7 +1955,7 @@ pub fn editor_container_view( // .box_shadow_blur(5.0) // .border_bottom(1.0) // .border_color( - // *config.get_color(LapceColor::LAPCE_BORDER), + // config.get_color(LapceColor::LAPCE_BORDER), // ) .apply_if( !config.editor.sticky_header @@ -1599,38 +2012,44 @@ fn editor_gutter( editor: RwSignal>, is_active: impl Fn(bool) -> bool + 'static + Copy, ) -> impl View { + let screen_lines = editor.with_untracked(|x| x.screen_lines()); let breakpoints = window_tab_data.terminal.debug.breakpoints; let daps = window_tab_data.terminal.debug.daps; let padding_left = 25.0; let padding_right = 30.0; - let (doc, cursor, viewport, scroll_delta, config) = editor.with_untracked(|e| { - ( - e.view.doc, - e.cursor, - e.viewport, - e.scroll_delta, - e.common.config, - ) - }); + let (view, cursor, viewport, scroll_delta, config) = + editor.with_untracked(|e| { + ( + e.view.clone(), + e.cursor, + e.viewport, + e.scroll_delta, + e.common.config, + ) + }); + let doc = view.doc; let num_display_lines = create_memo(move |_| { - let viewport = viewport.get(); - let line_height = config.get().editor.line_height() as f64; - (viewport.height() / line_height).ceil() as usize + 1 + let screen_lines = screen_lines.get(); + screen_lines.lines.len() + // let viewport = viewport.get(); + // let line_height = config.get().editor.line_height() as f64; + // (viewport.height() / line_height).ceil() as usize + 1 }); - let code_action_line = create_memo(move |_| { + let code_action_vline = create_memo(move |_| { if is_active(true) { let doc = doc.get(); - let offset = cursor.with(|cursor| cursor.offset()); + let (offset, affinity) = + cursor.with(|cursor| (cursor.offset(), cursor.affinity)); let has_code_actions = doc - .code_actions + .code_actions() .with(|c| c.get(&offset).map(|c| !c.1.is_empty()).unwrap_or(false)); if has_code_actions { - let line = doc.buffer.with(|b| b.line_of_offset(offset)); - Some(line) + let vline = view.vline_of_offset(offset, affinity); + Some(vline) } else { None } @@ -1650,16 +2069,18 @@ fn editor_gutter( let config = config.get(); let size = config.ui.icon_size() as f32 + 2.0; s.size(size, size) - .color(*config.get_color(LapceColor::DEBUG_BREAKPOINT_HOVER)) + .color(config.color(LapceColor::DEBUG_BREAKPOINT_HOVER)) .apply_if(!hovered.get(), |s| s.hide()) }, ), ) .on_click_stop(move |_| { - let line = (viewport.get_untracked().y0 - / config.get_untracked().editor.line_height() as f64) - .floor() as usize - + i; + let screen_lines = screen_lines.get_untracked(); + let line = screen_lines.lines.get(i).map(|r| r.line).unwrap_or(0); + // let line = (viewport.get_untracked().y0 + // / config.get_untracked().editor.line_height() as f64) + // .floor() as usize + // + i; let editor = editor.get_untracked(); let doc = editor.view.doc.get_untracked(); let offset = doc.buffer.with_untracked(|b| b.offset_of_line(line)); @@ -1749,7 +2170,7 @@ fn editor_gutter( .style(|s| s.height_pct(100.0)), clip( stack(( - list( + dyn_stack( move || { let num = num_display_lines.get(); 0..num @@ -1764,7 +2185,7 @@ fn editor_gutter( as f32, ) }), - list( + dyn_stack( move || { let editor = editor.get(); let doc = editor.view.doc.get(); @@ -1781,6 +2202,10 @@ fn editor_gutter( move |(line, b)| (*line, b.active), move |(line, breakpoint)| { let active = breakpoint.active; + let line_y = screen_lines + .with_untracked(|s| s.info_for_line(line)) + .map(|l| l.y) + .unwrap_or_default(); container( svg(move || { config.get().ui_svg(LapceIcons::DEBUG_BREAKPOINT) @@ -1793,7 +2218,7 @@ fn editor_gutter( } else { LapceColor::EDITOR_DIM }; - let color = *config.get_color(color); + let color = config.color(color); s.size(size, size).color(color) }), ) @@ -1804,10 +2229,7 @@ fn editor_gutter( .height(config.editor.line_height() as f32) .justify_center() .items_center() - .margin_top( - (line * config.editor.line_height()) as f32 - - viewport.get().y0 as f32, - ) + .margin_top(line_y as f32 - viewport.get().y0 as f32) }) }, ) @@ -1818,7 +2240,7 @@ fn editor_gutter( .style(move |s| { s.absolute() .size_pct(100.0, 100.0) - .background(*config.get().get_color(LapceColor::EDITOR_BACKGROUND)) + .background(config.get().color(LapceColor::EDITOR_BACKGROUND)) }), clip( stack(( @@ -1838,7 +2260,7 @@ fn editor_gutter( let config = config.get(); let size = config.ui.icon_size() as f32; s.size(size, size) - .color(*config.get_color(LapceColor::LAPCE_WARN)) + .color(config.color(LapceColor::LAPCE_WARN)) }, ), ) @@ -1849,13 +2271,13 @@ fn editor_gutter( let config = config.get(); let viewport = viewport.get(); let gutter_width = gutter_width.get(); - let code_action_line = code_action_line.get(); + let code_action_vline = code_action_vline.get(); let size = config.ui.icon_size() as f32; let margin_left = gutter_width as f32 + (padding_right - size) / 2.0 - 4.0; let line_height = config.editor.line_height(); - let margin_top = if let Some(line) = code_action_line { - (line * line_height) as f32 - viewport.y0 as f32 + let margin_top = if let Some(vline) = code_action_vline { + (vline.get() * line_height) as f32 - viewport.y0 as f32 + (line_height as f32 - size) / 2.0 - 4.0 } else { @@ -1866,17 +2288,18 @@ fn editor_gutter( .border_radius(6.0) .margin_left(margin_left) .margin_top(margin_top) - .apply_if(code_action_line.is_none(), |s| s.hide()) + .apply_if(code_action_vline.is_none(), |s| s.hide()) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config - .get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) .active(|s| { - s.background(*config.get_color( - LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND, - )) + s.background( + config.color( + LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND, + ), + ) }) }), )) @@ -1912,7 +2335,7 @@ fn editor_breadcrumbs( stack(( { let workspace = workspace.clone(); - list( + dyn_stack( move || { let full_path = doc_path.get().unwrap_or_default(); let mut path = full_path; @@ -1950,9 +2373,11 @@ fn editor_breadcrumbs( let size = config.ui.icon_size() as f32; s.apply_if(i == 0, |s| s.hide()) .size(size, size) - .color(*config.get_color( - LapceColor::LAPCE_ICON_ACTIVE, - )) + .color( + config.color( + LapceColor::LAPCE_ICON_ACTIVE, + ), + ) }), label(move || section.clone()), )) @@ -1989,7 +2414,7 @@ fn editor_breadcrumbs( s.absolute() .size_pct(100.0, 100.0) .border_bottom(1.0) - .border_color(*config.get().get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) .items_center() }), ) @@ -2039,7 +2464,8 @@ fn editor_content( } else { 0.0 }; - s.padding_bottom(padding_bottom) + s.absolute() + .padding_bottom(padding_bottom) .cursor(CursorStyle::Text) .min_size_pct(100.0, 100.0) }, @@ -2079,12 +2505,15 @@ fn editor_content( let offset = cursor.offset(); editor.view.doc.track(); editor.view.kind.track(); - let LineRegion { x, width, line } = - cursor_caret(&editor.view, offset, !cursor.is_insert()); + + let LineRegion { x, width, rvline } = + cursor_caret(&editor.view, offset, !cursor.is_insert(), cursor.affinity); let config = config.get_untracked(); let line_height = config.editor.line_height(); + // TODO: is there a good way to avoid the calculation of the vline here? + let vline = editor.view.vline_of_rvline(rvline); let rect = Rect::from_origin_size( - (x, (editor.view.visual_line(line) * line_height) as f64), + (x, (vline.get() * line_height) as f64), (width, line_height as f64), ) .inflate(10.0, 0.0); @@ -2186,8 +2615,8 @@ fn search_editor_view( .items_center() .border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) }) } @@ -2226,8 +2655,8 @@ fn replace_editor_view( .items_center() .border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) }) } @@ -2253,7 +2682,7 @@ fn find_view( let editor = editor.get_untracked(); let cursor = editor.cursor; let offset = cursor.with(|cursor| cursor.offset()); - let occurrences = editor.view.doc.get().find_result.occurrences; + let occurrences = editor.view.doc.get().backend.find_result.occurrences; occurrences.with(|occurrences| { for (i, region) in occurrences.regions().iter().enumerate() { if offset <= region.max() { @@ -2385,10 +2814,10 @@ fn find_view( .style(move |s| { let config = config.get(); s.margin_right(50.0) - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) .border_radius(6.0) .border(1.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .padding_vert(4.0) .cursor(CursorStyle::Default) .flex_col() @@ -2413,61 +2842,150 @@ fn find_view( }) } -pub fn changes_colors( - changes: im::Vector, - min_line: usize, - max_line: usize, - config: &LapceConfig, -) -> Vec<(usize, usize, bool, Color)> { - let mut line = 0; +/// Iterator over (len, color, modified) for each change in the diff +fn changes_color_iter<'a>( + changes: &'a im::Vector, + config: &'a LapceConfig, +) -> impl Iterator, bool)> + 'a { let mut last_change = None; - let mut colors = Vec::new(); - for change in changes.iter() { + changes.iter().map(move |change| { let len = match change { DiffLines::Left(_range) => 0, DiffLines::Both(info) => info.right.len(), DiffLines::Right(range) => range.len(), }; - line += len; - if line < min_line { - last_change = Some(change); - continue; - } - let mut modified = false; let color = match change { DiffLines::Left(_range) => { - Some(config.get_color(LapceColor::SOURCE_CONTROL_REMOVED)) + Some(config.color(LapceColor::SOURCE_CONTROL_REMOVED)) } DiffLines::Right(_range) => { if let Some(DiffLines::Left(_)) = last_change.as_ref() { modified = true; } if modified { - Some(config.get_color(LapceColor::SOURCE_CONTROL_MODIFIED)) + Some(config.color(LapceColor::SOURCE_CONTROL_MODIFIED)) } else { - Some(config.get_color(LapceColor::SOURCE_CONTROL_ADDED)) + Some(config.color(LapceColor::SOURCE_CONTROL_ADDED)) } } _ => None, }; - if let Some(color) = color.cloned() { - let y = line - len; - let height = len; - let removed = len == 0; + last_change = Some(change.clone()); + + (len, color, modified) + }) +} + +// TODO: both of the changes color functions could easily return iterators + +/// Get the position and coloring information for over the entire current [`ScreenLines`] +/// Returns `(y, height_idx, removed, color)` +pub fn changes_colors_screen( + view: &EditorViewData, + changes: im::Vector, +) -> Vec<(f64, usize, bool, Color)> { + let screen_lines = view.screen_lines.get_untracked(); + let config = view.config.get_untracked(); + + let Some((min, max)) = screen_lines.rvline_range() else { + return Vec::new(); + }; + + let mut line = 0; + let mut colors = Vec::new(); + + for (len, color, modified) in changes_color_iter(&changes, &config) { + let pre_line = line; + + line += len; + if line < min.line { + continue; + } + if let Some(color) = color { if modified { colors.pop(); } + let Some(info) = screen_lines.info_for_line(pre_line) else { + continue; + }; + + let y = info.vline_y; + let height = { + // Accumulate the number of line indices each potentially wrapped line spans + let rvline = info.vline_info.rvline; + let end_line = rvline.line + len; + + view.iter_rvlines_over(false, rvline, end_line).count() + }; + let removed = len == 0; + colors.push((y, height, removed, color)); } - if line > max_line { + if line > max.line { break; } - last_change = Some(change); } + + colors +} + +// TODO: limit the visual line that changes are considered past to some reasonable number +// TODO(minor): This could be a `changes_colors_range` with some minor changes, but it isn't needed +/// Get the position and coloring information for over the entire current [`ScreenLines`] +/// Returns `(y, height_idx, removed, color)` +pub fn changes_colors_all( + view: &EditorViewData, + changes: im::Vector, +) -> Vec<(f64, usize, bool, Color)> { + let config = view.config.get_untracked(); + let line_height = config.editor.line_height(); + + let mut line = 0; + let mut colors = Vec::new(); + + let mut vline_iter = view.iter_vlines(false, VLine(0)).peekable(); + + for (len, color, modified) in changes_color_iter(&changes, &config) { + let pre_line = line; + + line += len; + + // Skip over all vlines that are before the current line + vline_iter + .by_ref() + .peeking_take_while(|info| info.rvline.line < pre_line) + .count(); + + if let Some(color) = color { + if modified { + colors.pop(); + } + + // Find the info with a line == pre_line + let Some(info) = vline_iter.peek() else { + continue; + }; + + let y = info.vline.get() * line_height; + let end_line = info.rvline.line + len; + let height = vline_iter + .by_ref() + .peeking_take_while(|info| info.rvline.line < end_line) + .count(); + let removed = len == 0; + + colors.push((y as f64, height, removed, color)); + } + + if vline_iter.peek().is_none() { + break; + } + } + colors } diff --git a/lapce-app/src/editor/view_data.rs b/lapce-app/src/editor/view_data.rs index e30513bb91..b2b4ed98fb 100644 --- a/lapce-app/src/editor/view_data.rs +++ b/lapce-app/src/editor/view_data.rs @@ -1,18 +1,23 @@ -use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; +use std::{ + cell::{Cell, RefCell}, + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + rc::Rc, + sync::Arc, +}; use floem::{ - cosmic_text::TextLayout, - peniko::{kurbo::Point, Color}, - reactive::{ReadSignal, RwSignal, Scope}, - views::VirtualListVector, + cosmic_text::{LayoutLine, TextLayout, Wrap}, + peniko::{ + kurbo::{Point, Rect}, + Color, + }, + reactive::{batch, untrack, ReadSignal, RwSignal, Scope}, }; use lapce_core::{ - buffer::{ - diff::DiffLines, - rope_text::{RopeText, RopeTextVal}, - }, + buffer::rope_text::{RopeText, RopeTextVal}, char_buffer::CharBuffer, - cursor::ColPosition, + cursor::{ColPosition, CursorAffinity}, mode::Mode, soft_tab::{snap_to_soft_tab_line_col, SnapDirection}, word::WordCursor, @@ -20,17 +25,27 @@ use lapce_core::{ use lapce_xi_rope::Rope; use crate::{ - config::LapceConfig, - doc::{phantom_text::PhantomTextLine, Document}, + config::{editor::WrapStyle, LapceConfig}, + doc::{phantom_text::PhantomTextLine, Document, DocumentExt}, find::{Find, FindResult}, }; -use super::{diff::DiffInfo, FONT_SIZE}; +use super::{ + compute_screen_lines, + diff::DiffInfo, + view::ScreenLines, + visual_line::{ + hit_position_aff, FontSizeCacheId, LayoutEvent, LineFontSizeProvider, Lines, + RVLine, ResolvedWrap, TextLayoutProvider, VLine, VLineInfo, + }, +}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct LineExtraStyle { pub x: f64, + pub y: f64, pub width: Option, + pub height: f64, pub bg_color: Option, pub under_line: Option, pub wave_line: Option, @@ -45,50 +60,130 @@ pub struct TextLayoutLine { pub whitespaces: Option>, pub indent: f64, } +impl TextLayoutLine { + /// The number of line breaks in the text layout. Always at least `1`. + pub fn line_count(&self) -> usize { + self.relevant_layouts().count().max(1) + } -/// Keeps track of the text layouts so that we can efficiently reuse them. -#[derive(Clone, Default)] -pub struct TextLayoutCache { - /// The id of the last config, which lets us know when the config changes so we can update - /// the cache. - config_id: u64, - cache_rev: u64, - /// (Font Size -> (Line Number -> Text Layout)) - /// Different font-sizes are cached separately, which is useful for features like code lens - /// where the text becomes small but you may wish to revert quickly. - pub layouts: HashMap>>, - pub max_width: f64, -} + /// Iterate over all the layouts that are nonempty. + /// Note that this may be empty if the line is completely empty, like the last line + fn relevant_layouts(&self) -> impl Iterator + '_ { + // Even though we only have one hard line (and thus only one `lines` entry) typically, for + // normal buffer lines, we can have more than one due to multiline phantom text. So we have + // to sum over all of the entries line counts. + self.text + .lines + .iter() + .flat_map(|l| l.layout_opt().as_deref()) + .flat_map(|ls| ls.iter()) + .filter(|l| !l.glyphs.is_empty()) + } -impl TextLayoutCache { - pub fn new() -> Self { - Self { - config_id: 0, - cache_rev: 0, - layouts: HashMap::new(), - max_width: 0.0, + /// Iterator over the (start, end) columns of the relevant layouts. + pub fn layout_cols<'a>( + &'a self, + text_prov: impl TextLayoutProvider + 'a, + line: usize, + ) -> impl Iterator + 'a { + let mut prefix = None; + // Include an entry if there is nothing + if self.text.lines.len() == 1 { + let line_start = self.text.lines[0].start_index(); + if let Some(layouts) = self.text.lines[0].layout_opt().as_deref() { + // Do we need to require !layouts.is_empty()? + if !layouts.is_empty() && layouts.iter().all(|l| l.glyphs.is_empty()) + { + // We assume the implicit glyph start is zero + prefix = Some((line_start, line_start)); + } + } } + + let line_v = line; + let iter = self + .text + .lines + .iter() + .filter_map(|line| line.layout_opt().as_deref().map(|ls| (line, ls))) + .flat_map(|(line, ls)| ls.iter().map(move |l| (line, l))) + .filter(|(_, l)| !l.glyphs.is_empty()) + .map(move |(tl_line, l)| { + let line_start = tl_line.start_index(); + + let start = line_start + l.glyphs[0].start; + let end = line_start + l.glyphs.last().unwrap().end; + + let text = text_prov.rope_text(); + // We can't just use the original end, because the *true* last glyph on the line + // may be a space, but it isn't included in the layout! Though this only happens + // for single spaces, for some reason. + let pre_end = text_prov.before_phantom_col(line_v, end); + let line_offset = text.offset_of_line(line); + + // TODO(minor): We don't really need the entire line, just the two characters after + let line_end = text.line_end_col(line, true); + + let end = if pre_end <= line_end { + let after = text + .slice_to_cow(line_offset + pre_end..line_offset + line_end); + if after.starts_with(' ') && !after.starts_with(" ") { + end + 1 + } else { + end + } + } else { + end + }; + + (start, end) + }); + + prefix.into_iter().chain(iter) } - pub fn clear(&mut self, cache_rev: u64) { - self.layouts.clear(); - self.cache_rev = cache_rev; - self.max_width = 0.0; + /// Iterator over the start columns of the relevant layouts + pub fn start_layout_cols<'a>( + &'a self, + text_prov: impl TextLayoutProvider + 'a, + line: usize, + ) -> impl Iterator + 'a { + self.layout_cols(text_prov, line).map(|(start, _)| start) } - pub fn check_attributes(&mut self, config_id: u64) { - if self.config_id != config_id { - self.clear(self.cache_rev + 1); - self.config_id = config_id; + /// Get the top y position of the given line index + pub fn get_layout_y(&self, nth: usize) -> Option { + if nth == 0 { + return Some(0.0); + } + + let mut line_y = 0.0; + for (i, layout) in self.relevant_layouts().enumerate() { + // This logic matches how layout run iter computes the line_y + let line_height = layout.line_ascent + layout.line_descent; + if i == nth { + let offset = (line_height + - (layout.glyph_ascent + layout.glyph_descent)) + / 2.0; + + return Some((line_y - offset - layout.glyph_descent) as f64); + } + + line_y += line_height; } + + None } -} -pub struct DocLine { - pub rev: u64, - pub style_rev: u64, - pub line: usize, - pub text: Arc, + /// Get the (start x, end x) positions of the given line index + pub fn get_layout_x(&self, nth: usize) -> Option<(f32, f32)> { + let layout = self.relevant_layouts().nth(nth)?; + + let start = layout.glyphs.first().map(|g| g.x).unwrap_or(0.0); + let end = layout.glyphs.last().map(|g| g.x + g.w).unwrap_or(0.0); + + Some((start, end)) + } } #[derive(Clone)] @@ -103,7 +198,6 @@ impl EditorViewKind { } } -// TODO(minor): Should this go in another file? It doesn't really need to be with the drawing code for editor views /// Data specific to the rendering of a view. /// This has various helper methods that may dispatch to the held [`Document`] signal, but are /// untracked by default. If you need your signal to depend on the document, @@ -114,31 +208,18 @@ pub struct EditorViewData { /// Equivalent to the `EditorData::doc` that contains this view. pub doc: RwSignal>, pub kind: RwSignal, - /// The text layouts for the document. This may be shared with other views. - pub text_layouts: Rc>, + /// Equivalent to the `EditorData::viewport that contains this view data. + pub viewport: RwSignal, + /// Lines holds various `RefCell`/`Cell`s so it can be accessed immutably, while still in + /// reality modifying it. + lines: Rc, - pub config: ReadSignal>, -} + cx: Cell, + effects_cx: Cell, -impl VirtualListVector for EditorViewData { - type ItemIterator = std::vec::IntoIter; - - fn total_len(&self) -> usize { - self.num_lines() - } + pub config: ReadSignal>, - fn slice(&mut self, range: std::ops::Range) -> Self::ItemIterator { - let lines = range - .into_iter() - .map(|line| DocLine { - rev: self.rev(), - style_rev: self.cache_rev(), - line, - text: self.get_text_layout(line, FONT_SIZE), - }) - .collect::>(); - lines.into_iter() - } + pub screen_lines: RwSignal, } impl EditorViewData { @@ -146,14 +227,35 @@ impl EditorViewData { cx: Scope, doc: Rc, kind: EditorViewKind, + viewport: RwSignal, config: ReadSignal>, ) -> EditorViewData { - EditorViewData { - doc: cx.create_rw_signal(doc), - kind: cx.create_rw_signal(kind), - text_layouts: Rc::new(RefCell::new(TextLayoutCache::new())), + let font_sizes = RefCell::new(Arc::new(ViewDataFontSizes { config, - } + loaded: doc.loaded.read_only(), + })); + let lines = Rc::new(Lines::new(cx, font_sizes)); + let doc = cx.create_rw_signal(doc); + let kind = cx.create_rw_signal(kind); + + let screen_lines = + cx.create_rw_signal(ScreenLines::new(cx, viewport.get_untracked())); + + let view = EditorViewData { + doc, + kind, + viewport, + lines, + cx: Cell::new(cx), + effects_cx: Cell::new(cx.create_child()), + config, + screen_lines, + }; + + // Watch the relevant parts for recalculating the screen lines + create_view_effects(view.effects_cx.get(), &view); + + view } // Note: There is no document / buffer function as that would require cloning it @@ -177,13 +279,22 @@ impl EditorViewData { RopeTextVal::new(self.text()) } + /// Get the text layout provider of this view. + /// Typically should not be needed outside of view's code. + pub(crate) fn text_prov(&self) -> ViewDataTextLayoutProv { + ViewDataTextLayoutProv { + text: self.text(), + doc: self.doc, + } + } + /// Return the [`Document`]'s [`Find`] instance. Find uses signals, and so can be updated. pub fn find(&self) -> Find { self.doc.with_untracked(|doc| doc.find().clone()) } pub fn find_result(&self) -> FindResult { - self.doc.get_untracked().find_result.clone() + self.doc.get_untracked().backend.find_result.clone() } pub fn update_find(&self) { @@ -196,36 +307,42 @@ impl EditorViewData { self.doc.with_untracked(|doc| doc.rev()) } - // TODO: Should the editor view handle the style information? - // It does not need to, since we can just get that information from the document, - // but it might be nicer? It would let us make the document more strictly about the - // data and the view more about the rendering. - // but you could also consider it as the document managing the syntax it should apply - // Though. There is the question of whether we'd want to allow different syntax highlighting in - // different views. However, we probably wouldn't. - pub fn cache_rev(&self) -> u64 { self.doc.get_untracked().cache_rev.get_untracked() } /// The document for the given view was swapped out. pub fn update_doc(&self, doc: Rc) { - self.doc.set(doc); - self.text_layouts.borrow_mut().clear(0); + batch(|| { + // Get rid of all the effects + self.effects_cx.get().dispose(); + + *self.lines.font_sizes.borrow_mut() = Arc::new(ViewDataFontSizes { + config: self.config, + loaded: doc.loaded.read_only(), + }); + self.lines.clear(0, None); + self.doc.set(doc); + self.screen_lines.update(|screen_lines| { + screen_lines.clear(self.viewport.get_untracked()) + }); + // Recreate the effects + self.effects_cx.set(self.cx.get().create_child()); + create_view_effects(self.effects_cx.get(), self); + }); } /// Duplicate as a new view which refers to the same document. - pub fn duplicate(&self, cx: Scope) -> Self { - // TODO: This is correct right now, as it has the views share the same text layout cache. - // However, once we have line wrapping or other view-specific rendering changes, this should check for whether they're different. - // This will likely require more information to be passed into duplicate, - // like whether the wrap width will be the editor's width, and so it is unlikely to be exactly the same as the current view. - EditorViewData { - doc: cx.create_rw_signal(self.doc.get_untracked()), - text_layouts: Rc::new(RefCell::new(TextLayoutCache::new())), - kind: cx.create_rw_signal(self.kind.get_untracked()), - config: self.config, - } + pub fn duplicate(&self, cx: Scope, viewport: RwSignal) -> Self { + let kind = self.kind.get_untracked(); + + EditorViewData::new( + cx, + self.doc.get_untracked(), + kind, + viewport, + self.config, + ) } pub fn line_phantom_text(&self, line: usize) -> PhantomTextLine { @@ -234,65 +351,35 @@ impl EditorViewData { /// Get the text layout for the given line. /// If the text layout is not cached, it will be created and cached. - pub fn get_text_layout( + /// This triggers a layout event. + pub fn get_text_layout(&self, line: usize) -> Arc { + self.get_text_layout_trigger(line, true) + } + + /// Get the text layout for the given line, deciding whether or not it should trigger a layout + /// event. + /// If the text layout is not cached, it will be created and cached. + pub fn get_text_layout_trigger( &self, line: usize, - font_size: usize, + trigger: bool, ) -> Arc { - { - let mut text_layouts = self.text_layouts.borrow_mut(); - let cache_rev = self.doc.get_untracked().cache_rev.get_untracked(); - if cache_rev != text_layouts.cache_rev { - text_layouts.clear(cache_rev); - } - } - - // TODO: Should we just move the config cache check into `check_cache`? let config = self.config.get_untracked(); - // Check if the text layout needs to update due to the config being changed - self.text_layouts.borrow_mut().check_attributes(config.id); - // If we don't have a second layer of the hashmap initialized for this specific font size, - // do it now - if self.text_layouts.borrow().layouts.get(&font_size).is_none() { - let mut cache = self.text_layouts.borrow_mut(); - cache.layouts.insert(font_size, HashMap::new()); - } + let config_id: u64 = config.id; - // Get whether there's an entry for this specific font size and line - let cache_exists = self - .text_layouts - .borrow() - .layouts - .get(&font_size) - .unwrap() - .get(&line) - .is_some(); - // If there isn't an entry then we actually have to create it - if !cache_exists { - let text_layout = self - .doc - .with_untracked(|doc| doc.get_text_layout(line, font_size)); - let mut cache = self.text_layouts.borrow_mut(); - let width = text_layout.text.size().width; - if width > cache.max_width { - cache.max_width = width; - } - cache - .layouts - .get_mut(&font_size) - .unwrap() - .insert(line, text_layout); - } + let text_prov = self.text_prov(); + + self.lines + .get_init_text_layout(config_id, &text_prov, line, trigger) + } + + /// Try to get a text layout for the given line, without creating it if it doesn't exist. + /// Note that it may still clear the cache if the config id has changed. + pub fn try_get_text_layout(&self, line: usize) -> Option> { + let config = self.config.get_untracked(); + let config_id = config.id; - // Just get the entry, assuming it has been created because we initialize it above. - self.text_layouts - .borrow() - .layouts - .get(&font_size) - .unwrap() - .get(&line) - .cloned() - .unwrap() + self.lines.try_get_text_layout(config_id, line) } pub fn indent_unit(&self) -> &'static str { @@ -300,23 +387,85 @@ impl EditorViewData { .with_untracked(|doc| doc.buffer.with_untracked(|b| b.indent_unit())) } + /// Iterate over the visual lines in the view, starting at the given line. + pub fn iter_vlines( + &self, + backwards: bool, + start: VLine, + ) -> impl Iterator { + self.lines.iter_vlines(self.text_prov(), backwards, start) + } + + /// Iterate over the visual lines in the view, starting at the given line and ending at the + /// given line. `start_line..end_line` + pub fn iter_vlines_over( + &self, + backwards: bool, + start: VLine, + end: VLine, + ) -> impl Iterator { + self.lines + .iter_vlines_over(self.text_prov(), backwards, start, end) + } + + /// Iterator over *relative* [`VLineInfo`]s, starting at the buffer line, `start_line`. + /// The `visual_line`s provided by this will start at 0 from your `start_line`. + /// This is preferable over `iter_lines` if you do not need to absolute visual line value. + pub fn iter_rvlines( + &self, + backwards: bool, + start: RVLine, + ) -> impl Iterator> { + self.lines.iter_rvlines(self.text_prov(), backwards, start) + } + + /// Iterator over *relative* [`VLineInfo`]s, starting at the buffer line, `start_line` and + /// ending at `end_line`. + /// `start_line..end_line` + /// This is preferable over `iter_lines` if you do not need to absolute visual line value. + pub fn iter_rvlines_over( + &self, + backwards: bool, + start: RVLine, + end_line: usize, + ) -> impl Iterator> { + self.lines + .iter_rvlines_over(self.text_prov(), backwards, start, end_line) + } + // ==== Position Information ==== - /// The number of visual lines in the document. + pub fn first_rvline_info(&self) -> VLineInfo<()> { + self.rvline_info(RVLine::default()) + } + + /// The number of lines in the document. pub fn num_lines(&self) -> usize { self.doc .with_untracked(|doc| doc.buffer.with_untracked(|b| b.num_lines())) } - /// The last allowed line in the document. + /// The last allowed buffer line in the document. pub fn last_line(&self) -> usize { self.doc .with_untracked(|doc| doc.buffer.with_untracked(|b| b.last_line())) } + pub fn last_vline(&self) -> VLine { + self.lines.last_vline(self.text_prov()) + } + + pub fn last_rvline(&self) -> RVLine { + self.lines.last_rvline(self.text_prov()) + } + + pub fn last_rvline_info(&self) -> VLineInfo<()> { + self.rvline_info(self.last_rvline()) + } + // ==== Line/Column Positioning ==== - /// Convert an offset into the buffer into a line and column. + /// Convert an offset into the buffer into a line and idx. pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) { self.doc.with_untracked(|doc| { doc.buffer.with_untracked(|b| b.offset_to_line_col(offset)) @@ -336,6 +485,7 @@ impl EditorViewData { }) } + /// Get the buffer line of an offset pub fn line_of_offset(&self, offset: usize) -> usize { self.doc.with_untracked(|doc| { doc.buffer.with_untracked(|b| b.line_of_offset(offset)) @@ -362,269 +512,166 @@ impl EditorViewData { }) } + /// `affinity` decides whether an offset at a soft line break is considered to be on the + /// previous line or the next line. + /// If `affinity` is `CursorAffinity::Forward` and is at the very end of the wrapped line, then + /// the offset is considered to be on the next line. + pub fn vline_of_offset(&self, offset: usize, affinity: CursorAffinity) -> VLine { + self.lines + .vline_of_offset(&self.text_prov(), offset, affinity) + } + + pub fn vline_of_line(&self, line: usize) -> VLine { + self.lines.vline_of_line(&self.text_prov(), line) + } + + pub fn vline_of_rvline(&self, rvline: RVLine) -> VLine { + self.lines.vline_of_rvline(&self.text_prov(), rvline) + } + + /// Get the nearest offset to the start of the visual line. + pub fn offset_of_vline(&self, vline: VLine) -> usize { + self.lines.offset_of_vline(&self.text_prov(), vline) + } + + /// Get the visual line and column of the given offset. + /// The column is before phantom text is applied. + pub fn vline_col_of_offset( + &self, + offset: usize, + affinity: CursorAffinity, + ) -> (VLine, usize) { + self.lines + .vline_col_of_offset(&self.text_prov(), offset, affinity) + } + + pub fn rvline_of_offset( + &self, + offset: usize, + affinity: CursorAffinity, + ) -> RVLine { + self.lines + .rvline_of_offset(&self.text_prov(), offset, affinity) + } + + pub fn rvline_col_of_offset( + &self, + offset: usize, + affinity: CursorAffinity, + ) -> (RVLine, usize) { + self.lines + .rvline_col_of_offset(&self.text_prov(), offset, affinity) + } + + pub fn offset_of_rvline(&self, rvline: RVLine) -> usize { + self.lines.offset_of_rvline(&self.text_prov(), rvline) + } + + pub fn vline_info(&self, vline: VLine) -> VLineInfo { + let vline = vline.min(self.last_vline()); + self.iter_vlines(false, vline).next().unwrap() + } + + pub fn screen_rvline_info_of_offset( + &self, + offset: usize, + affinity: CursorAffinity, + ) -> Option> { + let rvline = self.rvline_of_offset(offset, affinity); + self.screen_lines.with_untracked(|screen_lines| { + screen_lines + .iter_vline_info() + .find(|vline_info| vline_info.rvline == rvline) + }) + } + + pub fn rvline_info(&self, rvline: RVLine) -> VLineInfo<()> { + let rvline = rvline.min(self.last_rvline()); + self.iter_rvlines(false, rvline).next().unwrap() + } + + pub fn rvline_info_of_offset( + &self, + offset: usize, + affinity: CursorAffinity, + ) -> VLineInfo<()> { + let rvline = self.rvline_of_offset(offset, affinity); + self.rvline_info(rvline) + } + + /// Get the first column of the overall line of the visual line + pub fn first_col(&self, info: VLineInfo) -> usize { + info.first_col(&self.text_prov()) + } + + /// Get the last column in the overall line of the visual line + pub fn last_col( + &self, + info: VLineInfo, + caret: bool, + ) -> usize { + info.last_col(&self.text_prov(), caret) + } + // ==== Points of locations ==== + pub fn max_line_width(&self) -> f64 { + self.lines.max_width() + } + /// Returns the point into the text layout of the line at the given offset. /// `x` being the leading edge of the character, and `y` being the baseline. - pub fn line_point_of_offset(&self, offset: usize, font_size: usize) -> Point { + pub fn line_point_of_offset( + &self, + offset: usize, + affinity: CursorAffinity, + ) -> Point { let (line, col) = self.offset_to_line_col(offset); - self.line_point_of_line_col(line, col, font_size) + self.line_point_of_line_col(line, col, affinity) } - /// Returns the point into the text layout of the line at the given line and column. - /// `x` being the leading edge of the character, and `y` being the baseline. + /// Returns the point into the text layout of the line at the given line and col. + /// `x` being the leading edge of the character, and `y` being the baseline. pub fn line_point_of_line_col( &self, line: usize, col: usize, - font_size: usize, + affinity: CursorAffinity, ) -> Point { - let text_layout = self.get_text_layout(line, font_size); - text_layout.text.hit_position(col).point + let text_layout = self.get_text_layout(line); + hit_position_aff( + &text_layout.text, + col, + affinity == CursorAffinity::Backward, + ) + .point } /// Get the (point above, point below) of a particular offset within the editor. - pub fn points_of_offset(&self, offset: usize) -> (Point, Point) { - let (line, col) = self.offset_to_line_col(offset); - self.points_of_line_col(line, col) - } - - /// Get the (point above, point below) of a particular (line, col) within the editor. - pub fn points_of_line_col(&self, line: usize, col: usize) -> (Point, Point) { + pub fn points_of_offset( + &self, + offset: usize, + affinity: CursorAffinity, + ) -> (Point, Point) { let config = self.config.get_untracked(); - let (line_height, font_size) = - (config.editor.line_height(), config.editor.font_size()); + let line_height = config.editor.line_height() as f64; - let y = self.visual_line(line) * line_height; + let info = self.screen_lines.with_untracked(|sl| { + sl.iter_line_info() + .find(|info| info.vline_info.interval.contains(offset)) + }); + let Some(info) = info else { + // TODO: We could do a smarter method where we get the approximate y position + // because, for example, this spot could be folded away, and so it would be better to + // supply the *nearest* position on the screen. + return (Point::new(0.0, 0.0), Point::new(0.0, 0.0)); + }; - let line = line.min(self.last_line()); + let y = info.vline_y; - let phantom_text = self.line_phantom_text(line); - let col = phantom_text.col_after(col, false); - - let mut x_shift = 0.0; - if font_size < config.editor.font_size() { - let mut col = 0usize; - self.doc.get_untracked().buffer.with_untracked(|buffer| { - let line_content = buffer.line_content(line); - for ch in line_content.chars() { - if ch == ' ' || ch == '\t' { - col += 1; - } else { - break; - } - } - }); - - if col > 0 { - let normal_text_layout = - self.get_text_layout(line, config.editor.font_size()); - let small_text_layout = self.get_text_layout(line, font_size); - x_shift = normal_text_layout.text.hit_position(col).point.x - - small_text_layout.text.hit_position(col).point.x; - } - } - - let x = self.line_point_of_line_col(line, col, font_size).x + x_shift; - ( - Point::new(x, y as f64), - Point::new(x, (y + line_height) as f64), - ) - } + let x = self.line_point_of_offset(offset, affinity).x; - pub fn actual_line(&self, visual_line: usize, bottom_affinity: bool) -> usize { - self.kind.with_untracked(|kind| match kind { - EditorViewKind::Normal => visual_line, - EditorViewKind::Diff(diff) => { - let is_right = diff.is_right; - let mut actual_line: usize = 0; - let mut current_visual_line = 0; - let mut last_change: Option<&DiffLines> = None; - let mut changes = diff.changes.iter().peekable(); - while let Some(change) = changes.next() { - match (is_right, change) { - (true, DiffLines::Left(range)) => { - if let Some(DiffLines::Right(_)) = changes.peek() { - } else { - current_visual_line += range.len(); - if current_visual_line >= visual_line { - return if bottom_affinity { - actual_line - } else { - actual_line.saturating_sub(1) - }; - } - } - } - (false, DiffLines::Right(range)) => { - let len = if let Some(DiffLines::Left(r)) = last_change { - range.len() - r.len().min(range.len()) - } else { - range.len() - }; - if len > 0 { - current_visual_line += len; - if current_visual_line >= visual_line { - return actual_line; - } - } - } - (true, DiffLines::Right(range)) - | (false, DiffLines::Left(range)) => { - let len = range.len(); - if current_visual_line + len > visual_line { - return range.start - + (visual_line - current_visual_line); - } - current_visual_line += len; - actual_line += len; - if is_right { - if let Some(DiffLines::Left(r)) = last_change { - let len = r.len() - r.len().min(range.len()); - if len > 0 { - current_visual_line += len; - if current_visual_line > visual_line { - return if bottom_affinity { - actual_line - } else { - actual_line - range.len() - }; - } - } - } - } - } - (_, DiffLines::Both(info)) => { - let len = info.right.len(); - let start = if is_right { - info.right.start - } else { - info.left.start - }; - - if let Some(skip) = info.skip.as_ref() { - if current_visual_line + skip.start == visual_line { - return if bottom_affinity { - actual_line + skip.end - } else { - (actual_line + skip.start).saturating_sub(1) - }; - } else if current_visual_line + skip.start + 1 - > visual_line - { - return actual_line + visual_line - - current_visual_line; - } else if current_visual_line + len - skip.len() + 1 - >= visual_line - { - return actual_line - + skip.end - + (visual_line - - current_visual_line - - skip.start - - 1); - } - actual_line += len; - current_visual_line += len - skip.len() + 1; - } else { - if current_visual_line + len > visual_line { - return start - + (visual_line - current_visual_line); - } - current_visual_line += len; - actual_line += len; - } - } - } - last_change = Some(change); - } - actual_line - } - }) - } - - pub fn visual_line(&self, line: usize) -> usize { - self.kind.with_untracked(|kind| match kind { - EditorViewKind::Normal => line, - EditorViewKind::Diff(diff) => { - let is_right = diff.is_right; - let mut last_change: Option<&DiffLines> = None; - let mut visual_line = 0; - let mut changes = diff.changes.iter().peekable(); - while let Some(change) = changes.next() { - match (is_right, change) { - (true, DiffLines::Left(range)) => { - if let Some(DiffLines::Right(_)) = changes.peek() { - } else { - visual_line += range.len(); - } - } - (false, DiffLines::Right(range)) => { - let len = if let Some(DiffLines::Left(r)) = last_change { - range.len() - r.len().min(range.len()) - } else { - range.len() - }; - if len > 0 { - visual_line += len; - } - } - (true, DiffLines::Right(range)) - | (false, DiffLines::Left(range)) => { - if line < range.end { - return visual_line + line - range.start; - } - visual_line += range.len(); - if is_right { - if let Some(DiffLines::Left(r)) = last_change { - let len = r.len() - r.len().min(range.len()); - if len > 0 { - visual_line += len; - } - } - } - } - (_, DiffLines::Both(info)) => { - let end = if is_right { - info.right.end - } else { - info.left.end - }; - if line >= end { - visual_line += info.right.len() - - info - .skip - .as_ref() - .map(|skip| skip.len().saturating_sub(1)) - .unwrap_or(0); - last_change = Some(change); - continue; - } - - let start = if is_right { - info.right.start - } else { - info.left.start - }; - if let Some(skip) = info.skip.as_ref() { - if start + skip.start > line { - return visual_line + line - start; - } else if start + skip.end > line { - return visual_line + skip.start; - } else { - return visual_line - + (line - start - skip.len() + 1); - } - } else { - return visual_line + line - start; - } - } - } - last_change = Some(change); - } - visual_line - } - }) + (Point::new(x, y), Point::new(x, y + line_height)) } /// Get the offset of a particular point within the editor. @@ -647,13 +694,39 @@ impl EditorViewData { ) -> ((usize, usize), bool) { let config = self.config.get_untracked(); - let visual_line = - (point.y / config.editor.line_height() as f64).floor() as usize; - let line = self.actual_line(visual_line, true); - let line = line.min(self.last_line()); - let font_size = config.editor.font_size(); - let text_layout = self.get_text_layout(line, font_size); - let hit_point = text_layout.text.hit_point(Point::new(point.x, 0.0)); + let line_height = config.editor.line_height() as f64; + let info = if point.y <= 0.0 { + Some(self.first_rvline_info()) + } else { + self.screen_lines + .with_untracked(|sl| { + sl.iter_line_info().find(|info| { + info.vline_y <= point.y + && info.vline_y + line_height >= point.y + }) + }) + .map(|info| info.vline_info) + }; + let info = info.unwrap_or_else(|| { + for (y_idx, info) in + self.iter_rvlines(false, RVLine::default()).enumerate() + { + let vline_y = y_idx as f64 * line_height; + if vline_y <= point.y && vline_y + line_height >= point.y { + return info; + } + } + + self.last_rvline_info() + }); + + let rvline = info.rvline; + let line = rvline.line; + let text_layout = self.get_text_layout(line); + + let y = text_layout.get_layout_y(rvline.line_index).unwrap_or(0.0); + + let hit_point = text_layout.text.hit_point(Point::new(point.x, y)); // We have to unapply the phantom text shifting in order to get back to the column in // the actual buffer let phantom_text = self.line_phantom_text(line); @@ -663,6 +736,16 @@ impl EditorViewData { let max_col = self.line_end_col(line, mode != Mode::Normal); let mut col = col.min(max_col); + // TODO: we need to handle affinity. Clicking at end of a wrapped line should give it a + // backwards affinity, while being at the start of the next line should be a forwards aff + + // TODO: this is a hack to get around text layouts not including spaces at the end of + // wrapped lines, but we want to be able to click on them + if !hit_point.is_inside { + // TODO(minor): this is probably wrong in some manners + col = info.last_col(&self.text_prov(), true); + } + if config.editor.atomic_soft_tabs && config.editor.tab_width > 1 { col = snap_to_soft_tab_line_col( &self.text(), @@ -676,16 +759,18 @@ impl EditorViewData { ((line, col), hit_point.is_inside) } + // TODO: colposition probably has issues with wrapping? pub fn line_horiz_col( &self, line: usize, - font_size: usize, horiz: &ColPosition, caret: bool, ) -> usize { match *horiz { ColPosition::Col(x) => { - let text_layout = self.get_text_layout(line, font_size); + // TODO: won't this be incorrect with phantom text? Shouldn't this just use + // line_col_of_point and get the col from that? + let text_layout = self.get_text_layout(line); let hit_point = text_layout.text.hit_point(Point::new(x, 0.0)); let n = hit_point.index; @@ -699,7 +784,35 @@ impl EditorViewData { } } - /// Advance to the right in the manner of the given mode. + /// Advance to the right in the manner of the given mode. + /// Get the column from a horizontal at a specific line index (in a text layout) + pub fn rvline_horiz_col( + &self, + RVLine { line, line_index }: RVLine, + horiz: &ColPosition, + caret: bool, + ) -> usize { + match *horiz { + ColPosition::Col(x) => { + let text_layout = self.get_text_layout(line); + // TODO: It would be better to have an alternate hit point function that takes a + // line index.. + let y_pos = text_layout + .relevant_layouts() + .take(line_index) + .map(|l| (l.line_ascent + l.line_descent) as f64) + .sum(); + let hit_point = text_layout.text.hit_point(Point::new(x, y_pos)); + let n = hit_point.index; + + n.min(self.line_end_col(line, caret)) + } + // Otherwise it is the same as the other function + _ => self.line_horiz_col(line, horiz, caret), + } + } + + /// Advance to the right in the manner of the given mode. /// This is not the same as the [`Movement::Right`] command. pub fn move_right(&self, offset: usize, mode: Mode, count: usize) -> usize { self.doc.with_untracked(|doc| { @@ -724,7 +837,7 @@ impl EditorViewData { // This needs the doc's syntax, but it isn't cheap to clone // so this has to be a method on view for now. self.doc.with_untracked(|doc| { - doc.syntax.with_untracked(|syntax| { + doc.syntax().with_untracked(|syntax| { if syntax.layers.is_some() { syntax .find_tag(offset, previous, &CharBuffer::from(ch)) @@ -750,7 +863,7 @@ impl EditorViewData { // This needs the doc's syntax, but it isn't cheap to clone // so this has to be a method on view for now. self.doc.with_untracked(|doc| { - doc.syntax.with_untracked(|syntax| { + doc.syntax().with_untracked(|syntax| { if syntax.layers.is_some() { syntax.find_matching_pair(offset).unwrap_or(offset) } else { @@ -763,3 +876,210 @@ impl EditorViewData { }) } } + +#[derive(Clone)] +pub(crate) struct ViewDataTextLayoutProv { + text: Rope, + doc: RwSignal>, +} +impl TextLayoutProvider for ViewDataTextLayoutProv { + fn text(&self) -> &Rope { + &self.text + } + + fn new_text_layout( + &self, + line: usize, + font_size: usize, + wrap: ResolvedWrap, + ) -> Arc { + let mut text_layout = self + .doc + .with_untracked(|doc| doc.get_text_layout(line, font_size)); + // TODO: This will potentially duplicate the text layout even without wrapping, but without wrapping we can just use the same styles. + // TODO: It does also pretty similar for other wrapping modes. So it would be good to have smarter deduplication, like if both of your splits for the same doc have a column wrap of 100 then they can share all of the text layout lines. + let text_layout_r = Arc::make_mut(&mut text_layout); + match wrap { + ResolvedWrap::None => { + // We do not have to set the wrap mode if we do not set the width + } + ResolvedWrap::Column(_col) => todo!(), + ResolvedWrap::Width(px) => { + // TODO: we could do some more invasive text layout structure to avoid multiple arcs since this definitely clones.. + text_layout_r.text.set_wrap(Wrap::Word); + text_layout_r.text.set_size(px, f32::MAX); + } + } + + self.doc.with_untracked(|doc| { + doc.apply_styles(line, text_layout_r); + }); + + text_layout + } + + fn before_phantom_col(&self, line: usize, col: usize) -> usize { + // TODO: this only needs to shift the col so it does not need the full string allocated + // phantom text! + self.doc + .with_untracked(|doc| doc.line_phantom_text(line)) + .before_col(col) + } + + fn has_multiline_phantom(&self) -> bool { + // TODO: We could be more specific here. Use the config for error lens &etc to determine + // whether we can ever have multiline phantom text in this view. + // Perhaps should track whether any multiline phantom text gets added, to get the fast path + // more often. + true + } +} + +struct ViewDataFontSizes { + config: ReadSignal>, + loaded: ReadSignal, +} +impl LineFontSizeProvider for ViewDataFontSizes { + fn font_size(&self, _line: usize) -> usize { + // TODO: code lens + self.config + .with_untracked(|config| config.editor.font_size()) + } + + fn cache_id(&self) -> FontSizeCacheId { + let mut hasher = DefaultHasher::new(); + + // TODO: is this actually good enough for comparing cache state? + // Potentially we should just have it return an arbitrary type that impl's Eq + self.config.with_untracked(|config| { + // TODO: we could do only pieces relevant to the lines + config.id.hash(&mut hasher); + }); + + self.loaded.with_untracked(|loaded| { + loaded.hash(&mut hasher); + }); + + hasher.finish() + } +} + +/// Minimum width that we'll allow the view to be wrapped at. +const MIN_WRAPPED_WIDTH: f32 = 100.0; + +/// Create various reactive effects to update the screen lines whenever relevant parts of the view, +/// doc, text layouts, viewport, etc. change. +/// This tries to be smart to a degree. +fn create_view_effects(cx: Scope, view: &EditorViewData) { + // Cloning is fun. + let view = view.clone(); + let view2 = view.clone(); + let view3 = view.clone(); + let view4 = view.clone(); + + let update_screen_lines = |view: &EditorViewData| { + // This function should not depend on the viewport signal directly. + + // This is wrapped in an update to make any updates-while-updating very obvious + // which they wouldn't be if we computed and then `set`. + view.screen_lines.update(|screen_lines| { + let new_screen_lines = compute_screen_lines( + view.config, + screen_lines.base, + view.kind.read_only(), + view.doc.read_only(), + &view.lines, + view.text_prov(), + ); + + *screen_lines = new_screen_lines; + }); + }; + + // Listen for cache revision changes (essentially edits to the file or requiring + // changes on text layouts, like if diagnostics load in) + cx.create_effect(move |_| { + // We can't put this with the other effects because we only want to update screen lines if + // the cache rev actually changed + let cache_rev = view.doc.with(|doc| doc.cache_rev).get(); + view.lines.check_cache_rev(cache_rev); + }); + + // Listen for layout events, currently only when a layout is created, and update screen + // lines based on that + view3.lines.layout_event.listen_with(cx, move |val| { + let view = &view2; + // TODO: Move this logic onto screen lines somehow, perhaps just an auxilary + // function, to avoid getting confused about what is relevant where. + + match val { + LayoutEvent::CreatedLayout { line, .. } => { + let sl = view.screen_lines.get_untracked(); + + // Intelligently update screen lines, avoiding recalculation if possible + let should_update = sl.on_created_layout(view, line); + + if should_update { + untrack(|| { + update_screen_lines(view); + }); + } + } + } + }); + + // TODO: should we have some debouncing for editor width? Ideally we'll be fast enough to not + // even need it, though we might not want to use a bunch of cpu whilst resizing anyway. + + let viewport_changed_trigger = cx.create_trigger(); + + // Watch for changes to the viewport so that we can alter the wrapping + // As well as updating the screen lines base + cx.create_effect(move |_| { + let view = &view3; + + let viewport = view.viewport.get(); + let config = view.config.get(); + let wrap_style = if let EditorViewKind::Diff(_) = view.kind.get() { + // TODO: let diff have wrapped text + WrapStyle::None + } else { + config.editor.wrap_style + }; + let res_wrap = match wrap_style { + WrapStyle::None => ResolvedWrap::None, + // TODO: ensure that these values have some minimums that is not too small + WrapStyle::EditorWidth => { + ResolvedWrap::Width((viewport.width() as f32).max(MIN_WRAPPED_WIDTH)) + } + // WrapStyle::WrapColumn => ResolvedWrap::Column(config.editor.wrap_column), + WrapStyle::WrapWidth => ResolvedWrap::Width( + (config.editor.wrap_width as f32).max(MIN_WRAPPED_WIDTH), + ), + }; + + view.lines.set_wrap(res_wrap); + + // Update the base + let base = view.screen_lines.with_untracked(|sl| sl.base); + + // TODO: should this be a with or with_untracked? + if viewport != base.with_untracked(|base| base.active_viewport) { + batch(|| { + base.update(|base| { + base.active_viewport = viewport; + }); + // TODO: Can I get rid of this and just call update screen lines with an + // untrack around it? + viewport_changed_trigger.notify(); + }); + } + }); + // Watch for when the viewport as changed in a relevant manner + // and for anything that `update_screen_lines` tracks. + cx.create_effect(move |_| { + viewport_changed_trigger.track(); + + update_screen_lines(&view4); + }); +} diff --git a/lapce-app/src/editor/visual_line.rs b/lapce-app/src/editor/visual_line.rs new file mode 100644 index 0000000000..1067f5b659 --- /dev/null +++ b/lapce-app/src/editor/visual_line.rs @@ -0,0 +1,3494 @@ +//! Visual Line implementation +//! +//! Files are easily broken up into buffer lines by just spliiting on `\n` or `\r\n`. +//! However, editors require features like wrapping and multiline phantom text. These break the +//! nice one-to-one correspondence between buffer lines and visual lines. +//! +//! When rendering with those, we have to display based on visual lines rather than the +//! underlying buffer lines. As well, it is expected for interaction - like movement and clicking - +//! to work in a similar intuitive manner as it would be if there was no wrapping or phantom text. +//! Ex: Moving down a line should move to the next visual line, not the next buffer line by +//! default. +//! (Sometimes! Some vim defaults are to move to the next buffer line, or there might be other +//! differences) +//! +//! There's two types of ways of talking about Visual Lines: +//! - [`VLine`]: Variables are often written with `vline` in the name +//! - [`RVLine`]: Variables are often written with `rvline` in the name +//! +//! [`VLine`] is an absolute visual line within the file. This is useful for some positioning tasks +//! but is more expensive to calculate due to the nontriviality of the `buffer line <-> visual line` +//! conversion when the file has any wrapping or multiline phantom text. +//! +//! Typically, code should prefer to use [`RVLine`]. This simply stores the underlying +//! buffer line, and a line index. This is not enough for absolute positioning within the display, +//! but it is enough for most other things (like movement). This is easier to calculate since it +//! only needs to find the right (potentially wrapped or multiline) layout for the easy-to-work +//! with buffer line. +//! +//! [`VLine`] is a single `usize` internally which can be multiplied by the line-height to get the +//! absolute position. This means that it is not stable across text layouts being changed. +//! An [`RVLine`] holds the buffer line and the 'line index' within the layout. The line index +//! would be `0` for the first line, `1` if it is on the second wrapped line, etc. This is more +//! stable across text layouts being changed, as it is only relative to a specific line. +//! +//! ----- +//! +//! [`Lines`] is the main structure. It is responsible for holding the text layouts, as well as +//! providing the functions to convert between (r)vlines and buffer lines. +//! +//! ---- +//! +//! Many of [`Lines`] functions are passed a [`TextLayoutProvider`]. +//! This serves the dual-purpose of giving us the text of the underlying file, as well as +//! for constructing the text layouts that we use for rendering. +//! Having a trait that is passed in simplifies the logic, since the caller is the one who tracks +//! the text in whatever manner they chose. + +// TODO: This file is getting long. Possibly it should be broken out into multiple files. +// Especially as it will only grow with more utility functions. + +// TODO(minor): We use a lot of `impl TextLayoutProvider`. +// This has the desired benefit of inlining the functions, so that the compiler can optimize the +// logic better than a naive for-loop or whatnot. +// However it does have the issue that it overuses generics, and we sometimes end up instantiating +// multiple versions of the same function. `T: TextLayoutProvider`, `&T`... +// - It would be better to standardize on one way of doing that, probably `&impl TextLayoutProvider` + +use std::{ + cell::{Cell, RefCell}, + cmp::Ordering, + collections::HashMap, + rc::Rc, + sync::Arc, +}; + +use floem::{cosmic_text::LayoutGlyph, reactive::Scope}; +use lapce_core::{ + buffer::rope_text::{RopeText, RopeTextRef}, + cursor::CursorAffinity, + word::WordCursor, +}; +use lapce_xi_rope::{Interval, Rope}; + +use crate::listener::Listener; + +use super::view_data::TextLayoutLine; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ResolvedWrap { + None, + Column(usize), + Width(f32), +} +impl ResolvedWrap { + pub fn is_different_kind(self, other: ResolvedWrap) -> bool { + !matches!( + (self, other), + (ResolvedWrap::None, ResolvedWrap::None) + | (ResolvedWrap::Column(_), ResolvedWrap::Column(_)) + | (ResolvedWrap::Width(_), ResolvedWrap::Width(_)) + ) + } +} + +/// A line within the editor view. +/// This gives the absolute position of the visual line. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct VLine(pub usize); +impl VLine { + pub fn get(&self) -> usize { + self.0 + } +} + +/// A visual line relative to some other line within the editor view. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RVLine { + /// The buffer line this is for + pub line: usize, + /// The index of the actual visual line's layout + pub line_index: usize, +} +impl RVLine { + pub fn new(line: usize, line_index: usize) -> RVLine { + RVLine { line, line_index } + } + + /// Is this the first visual line for the buffer line? + pub fn is_first(&self) -> bool { + self.line_index == 0 + } +} + +/// (Font Size -> (Buffer Line Number -> Text Layout)) +pub type Layouts = HashMap>>; + +#[derive(Default)] +pub struct TextLayoutCache { + /// The id of the last config so that we can clear when the config changes + config_id: u64, + /// The most recent cache revision of the document. + cache_rev: u64, + /// (Font Size -> (Buffer Line Number -> Text Layout)) + /// Different font-sizes are cached separately, which is useful for features like code lens + /// where the font-size can rapidly change. + /// It would also be useful for a prospective minimap feature. + pub layouts: Layouts, + /// The maximum width seen so far, used to determine if we need to show horizontal scrollbar + pub max_width: f64, +} +impl TextLayoutCache { + pub fn clear(&mut self, cache_rev: u64, config_id: Option) { + self.layouts.clear(); + if let Some(config_id) = config_id { + self.config_id = config_id; + } + self.cache_rev = cache_rev; + self.max_width = 0.0; + } + + /// Clear the layouts without changing the document cache revision. + /// Ex: Wrapping width changed, which does not change what the document holds. + pub fn clear_unchanged(&mut self) { + self.layouts.clear(); + self.max_width = 0.0; + } + + pub fn get( + &self, + font_size: usize, + line: usize, + ) -> Option<&Arc> { + self.layouts.get(&font_size).and_then(|c| c.get(&line)) + } + + pub fn get_mut( + &mut self, + font_size: usize, + line: usize, + ) -> Option<&mut Arc> { + self.layouts + .get_mut(&font_size) + .and_then(|c| c.get_mut(&line)) + } + + /// Get the (start, end) columns of the (line, line_index) + pub fn get_layout_col( + &self, + text_prov: &impl TextLayoutProvider, + font_size: usize, + line: usize, + line_index: usize, + ) -> Option<(usize, usize)> { + self.get(font_size, line) + .and_then(|l| l.layout_cols(text_prov, line).nth(line_index)) + } + + /// Check whether the config id has changed, clearing the cache if it has. + pub fn check_attributes(&mut self, config_id: u64) { + if self.config_id != config_id { + self.clear(self.cache_rev + 1, Some(config_id)); + } + } +} + +// TODO(minor): Should we rename this? It does more than just providing the text layout. It provides the text, text layouts, phantom text, and whether it has multiline phantom text. It is more of an outside state. +/// The [`TextLayoutProvider`] serves two primary roles: +/// - Providing the [`Rope`] text of the underlying file +/// - Constructing the text layout for a given line +/// +/// Note: `text` does not necessarily include every piece of text. The obvious example is phantom +/// text, which is not in the underlying buffer. +/// +/// Using this trait rather than passing around something like [`Document`] allows the backend to +/// be swapped out if needed. This would be useful if we ever wanted to reuse it across different +/// views that did not naturally fit into our 'document' model. As well as when we want to extract +/// the editor view code int a separate crate for Floem. +pub trait TextLayoutProvider { + fn text(&self) -> &Rope; + + /// Shorthand for getting a rope text version of `text`. + /// This MUST hold the same rope that `text` would return. + fn rope_text(&self) -> RopeTextRef { + RopeTextRef::new(self.text()) + } + + // TODO(minor): Do we really need to pass font size to this? The outer-api is providing line + // font size provider already, so it should be able to just use that. + fn new_text_layout( + &self, + line: usize, + font_size: usize, + wrap: ResolvedWrap, + ) -> Arc; + + /// Translate a column position into the postiion it would be before combining with the phantom + /// text + fn before_phantom_col(&self, line: usize, col: usize) -> usize; + + /// Whether the text has *any* multiline phantom text. + /// This is used to determine whether we can use the fast route where the lines are linear, + /// which also requires no wrapping. + /// This should be a conservative estimate, so if you aren't bothering to check all of your + /// phantom text then just return true. + fn has_multiline_phantom(&self) -> bool; +} +impl TextLayoutProvider for &T { + fn text(&self) -> &Rope { + (**self).text() + } + + fn new_text_layout( + &self, + line: usize, + font_size: usize, + wrap: ResolvedWrap, + ) -> Arc { + (**self).new_text_layout(line, font_size, wrap) + } + + fn before_phantom_col(&self, line: usize, col: usize) -> usize { + (**self).before_phantom_col(line, col) + } + + fn has_multiline_phantom(&self) -> bool { + (**self).has_multiline_phantom() + } +} + +pub type FontSizeCacheId = u64; +pub trait LineFontSizeProvider { + /// Get the 'general' font size for a specific buffer line. + /// This is typically the editor font size. + /// There might be alternate font-sizes within the line, like for phantom text, but those are + /// not considered here. + fn font_size(&self, line: usize) -> usize; + + /// An identifier used to mark when the font size info has changed. + /// This lets us update information. + fn cache_id(&self) -> FontSizeCacheId; +} + +/// Layout events. This is primarily needed for logic which tracks visual lines intelligently, like +/// `ScreenLines` in Lapce. +/// This is currently limited to only a `CreatedLayout` event, as changed to the cache rev would +/// capture the idea of all the layouts being cleared. In the future it could be expanded to more +/// events, especially if cache rev gets more specific than clearing everything. +#[derive(Debug, Clone, PartialEq)] +pub enum LayoutEvent { + CreatedLayout { font_size: usize, line: usize }, +} + +/// The main structure for tracking visual line information. +pub struct Lines { + /// This is inside out from the usual way of writing Arc-RefCells due to sometimes wanting to + /// swap out font sizes, while also grabbing an `Arc` to hold. + /// An `Arc>` has the issue that with a `dyn` it can't know they're the same size + /// if you were to assign. So this allows us to swap out the `Arc`, though it does mean that + /// the other holders of the `Arc` don't get the new version. That is fine currently. + pub font_sizes: RefCell>, + text_layouts: Rc>, + wrap: Cell, + font_size_cache_id: Cell, + last_vline: Rc>>, + pub layout_event: Listener, +} +impl Lines { + pub fn new( + cx: Scope, + font_sizes: RefCell>, + ) -> Lines { + let id = font_sizes.borrow().cache_id(); + Lines { + font_sizes, + text_layouts: Rc::new(RefCell::new(TextLayoutCache::default())), + wrap: Cell::new(ResolvedWrap::None), + font_size_cache_id: Cell::new(id), + last_vline: Rc::new(Cell::new(None)), + layout_event: Listener::new_empty(cx), + } + } + + /// The current wrapping style + pub fn wrap(&self) -> ResolvedWrap { + self.wrap.get() + } + + /// Set the wrapping style + /// Does nothing if the wrapping style is the same as the current one. + /// Will trigger a clear of the text layouts if the wrapping style is different. + pub fn set_wrap(&self, wrap: ResolvedWrap) { + if wrap == self.wrap.get() { + return; + } + + // TODO(perf): We could improve this by only clearing the lines that would actually change + // Ex: Single vline lines don't need to be cleared if the wrapping changes from + // some width to None, or from some width to some larger width. + self.clear_unchanged(); + + self.wrap.set(wrap); + } + + /// The max width of the text layouts displayed + pub fn max_width(&self) -> f64 { + self.text_layouts.borrow().max_width + } + + /// Check if the lines can be modelled as a purely linear file. + /// If `true` this makes various operations simpler because there is a one-to-one + /// correspondence between visual lines and buffer lines. + /// However, if there is wrapping or any multiline phantom text, then we can't rely on that. + /// + /// TODO:? + /// We could be smarter about various pieces. + /// - If there was no lines that exceeded the wrap width then we could do the fast path + /// - Would require tracking that but might not be too hard to do it whenever we create a + /// text layout + /// - `is_linear` could be up to some line, which allows us to make at least the earliest parts + /// before any wrapping were faster. However, early lines are faster to calculate anyways. + pub fn is_linear(&self, text_prov: impl TextLayoutProvider) -> bool { + self.wrap.get() == ResolvedWrap::None && !text_prov.has_multiline_phantom() + } + + /// Get the font size that [`Self::font_sizes`] provides + pub fn font_size(&self, line: usize) -> usize { + self.font_sizes.borrow().font_size(line) + } + + /// Get the last visual line of the file. + /// Cached. + pub fn last_vline(&self, text_prov: impl TextLayoutProvider) -> VLine { + let current_id = self.font_sizes.borrow().cache_id(); + if current_id != self.font_size_cache_id.get() { + self.last_vline.set(None); + self.font_size_cache_id.set(current_id); + } + + if let Some(last_vline) = self.last_vline.get() { + last_vline + } else { + // For most files this should easily be fast enough. + // Though it could still be improved. + let rope_text = text_prov.rope_text(); + let hard_line_count = rope_text.num_lines(); + + let line_count = if self.is_linear(text_prov) { + hard_line_count + } else { + let mut soft_line_count = 0; + + let layouts = self.text_layouts.borrow(); + for i in 0..hard_line_count { + let font_size = self.font_size(i); + if let Some(text_layout) = layouts.get(font_size, i) { + let line_count = text_layout.line_count(); + soft_line_count += line_count; + } else { + soft_line_count += 1; + } + } + + soft_line_count + }; + + let last_vline = line_count.saturating_sub(1); + self.last_vline.set(Some(VLine(last_vline))); + VLine(last_vline) + } + } + + /// Clear the cache for the last vline + pub fn clear_last_vline(&self) { + self.last_vline.set(None); + } + + /// The last relative visual line. + /// Cheap, so not cached + pub fn last_rvline(&self, text_prov: impl TextLayoutProvider) -> RVLine { + let rope_text = text_prov.rope_text(); + let last_line = rope_text.last_line(); + let layouts = self.text_layouts.borrow(); + let font_size = self.font_size(last_line); + + if let Some(layout) = layouts.get(font_size, last_line) { + let line_count = layout.line_count(); + + RVLine::new(last_line, line_count - 1) + } else { + RVLine::new(last_line, 0) + } + } + + /// 'len' version of [`Lines::last_vline`] + /// Cached. + pub fn num_vlines(&self, text_prov: impl TextLayoutProvider) -> usize { + self.last_vline(text_prov).get() + 1 + } + + /// Get the text layout for the given buffer line number. + /// This will create the text layout if it doesn't exist. + /// + /// `trigger` (default to true) decides whether the creation of the text layout should trigger + /// the [`LayoutEvent::CreatedLayout`] event. + /// + /// This will check the `config_id`, which decides whether it should clear out the text layout + /// cache. + pub fn get_init_text_layout( + &self, + config_id: u64, + text_prov: impl TextLayoutProvider, + line: usize, + trigger: bool, + ) -> Arc { + self.check_config_id(config_id); + + let font_size = self.font_size(line); + get_init_text_layout( + &self.text_layouts, + trigger.then_some(self.layout_event), + text_prov, + line, + font_size, + self.wrap.get(), + &self.last_vline, + ) + } + + /// Try to get the text layout for the given line number. + /// + /// This will check the `config_id`, which decides whether it should clear out the text layout + /// cache. + pub fn try_get_text_layout( + &self, + config_id: u64, + line: usize, + ) -> Option> { + self.check_config_id(config_id); + + let font_size = self.font_size(line); + + self.text_layouts + .borrow() + .layouts + .get(&font_size) + .and_then(|f| f.get(&line)) + .cloned() + } + + /// Initialize the text layout of every line in the real line interval. + /// + /// `trigger` (default to true) decides whether the creation of the text layout should trigger + /// the [`LayoutEvent::CreatedLayout`] event. + pub fn init_line_interval( + &self, + config_id: u64, + text_prov: &impl TextLayoutProvider, + lines: impl Iterator, + trigger: bool, + ) { + for line in lines { + self.get_init_text_layout(config_id, text_prov, line, trigger); + } + } + + /// Initialize the text layout of every line in the file. + /// This should typically not be used. + /// + /// `trigger` (default to true) decides whether the creation of the text layout should trigger + /// the [`LayoutEvent::CreatedLayout`] event. + pub fn init_all( + &self, + config_id: u64, + text_prov: &impl TextLayoutProvider, + trigger: bool, + ) { + let text = text_prov.text(); + let last_line = text.line_of_offset(text.len()); + self.init_line_interval(config_id, text_prov, 0..=last_line, trigger); + } + + /// Iterator over [`VLineInfo`]s, starting at `start_line`. + pub fn iter_vlines( + &self, + text_prov: impl TextLayoutProvider, + backwards: bool, + start: VLine, + ) -> impl Iterator { + VisualLines::new(self, text_prov, backwards, start) + } + + /// Iterator over [`VLineInfo`]s, starting at `start_line` and ending at `end_line`. + /// `start_line..end_line` + pub fn iter_vlines_over( + &self, + text_prov: impl TextLayoutProvider, + backwards: bool, + start: VLine, + end: VLine, + ) -> impl Iterator { + self.iter_vlines(text_prov, backwards, start) + .take_while(move |info| info.vline < end) + } + + /// Iterator over *relative* [`VLineInfo`]s, starting at the rvline, `start_line`. + /// This is preferable over `iter_vlines` if you do not need to absolute visual line value and + /// can provide the buffer line. + pub fn iter_rvlines( + &self, + text_prov: impl TextLayoutProvider, + backwards: bool, + start: RVLine, + ) -> impl Iterator> { + VisualLinesRelative::new(self, text_prov, backwards, start) + } + + /// Iterator over *relative* [`VLineInfo`]s, starting at the rvline `start_line` and + /// ending at the buffer line `end_line`. + /// `start_line..end_line` + /// This is preferable over `iter_vlines` if you do not need the absolute visual line value and + /// you can provide the buffer line. + pub fn iter_rvlines_over( + &self, + text_prov: impl TextLayoutProvider, + backwards: bool, + start: RVLine, + end_line: usize, + ) -> impl Iterator> { + self.iter_rvlines(text_prov, backwards, start) + .take_while(move |info| info.rvline.line < end_line) + } + + // TODO(minor): Get rid of the clone bound. + /// Initialize the text layouts as you iterate over them. + pub fn iter_vlines_init( + &self, + text_prov: impl TextLayoutProvider + Clone, + config_id: u64, + start: VLine, + trigger: bool, + ) -> impl Iterator { + self.check_config_id(config_id); + + if start <= self.last_vline(&text_prov) { + // We initialize the text layout for the line that start line is for + let (_, rvline) = find_vline_init_info(self, &text_prov, start).unwrap(); + self.get_init_text_layout(config_id, &text_prov, rvline.line, trigger); + // If the start line was past the last vline then we don't need to initialize anything + // since it won't get anything. + } + + let text_layouts = self.text_layouts.clone(); + let font_sizes = self.font_sizes.clone(); + let wrap = self.wrap.get(); + let last_vline = self.last_vline.clone(); + let layout_event = trigger.then_some(self.layout_event); + self.iter_vlines(text_prov.clone(), false, start) + .map(move |v| { + if v.is_first() { + // For every (first) vline we initialize the next buffer line's text layout + // This ensures it is ready for when re reach it. + let next_line = v.rvline.line + 1; + let font_size = font_sizes.borrow().font_size(next_line); + // `init_iter_vlines` is the reason `get_init_text_layout` is split out. + // Being split out lets us avoid attaching lifetimes to the iterator, since it + // only uses Rc/Arcs it is given. + // This is useful since `Lines` would be in a + // `Rc>` which would make iterators with lifetimes referring to + // `Lines` a pain. + get_init_text_layout( + &text_layouts, + layout_event, + &text_prov, + next_line, + font_size, + wrap, + &last_vline, + ); + } + v + }) + } + + /// Iterator over [`VLineInfo`]s, starting at `start_line` and ending at `end_line`. + /// `start_line..end_line` + /// Initializes the text layouts as you iterate over them. + /// + /// `trigger` (default to true) decides whether the creation of the text layout should trigger + /// the [`LayoutEvent::CreatedLayout`] event. + pub fn iter_vlines_init_over( + &self, + text_prov: impl TextLayoutProvider + Clone, + config_id: u64, + start: VLine, + end: VLine, + trigger: bool, + ) -> impl Iterator { + self.iter_vlines_init(text_prov, config_id, start, trigger) + .take_while(move |info| info.vline < end) + } + + /// Iterator over *relative* [`VLineInfo`]s, starting at the rvline, `start_line` and + /// ending at the buffer line `end_line`. + /// `start_line..end_line` + /// + /// `trigger` (default to true) decides whether the creation of the text layout should trigger + /// the [`LayoutEvent::CreatedLayout`] event. + pub fn iter_rvlines_init( + &self, + text_prov: impl TextLayoutProvider + Clone, + config_id: u64, + start: RVLine, + trigger: bool, + ) -> impl Iterator> { + self.check_config_id(config_id); + + if start.line <= text_prov.rope_text().last_line() { + // Initialize the text layout for the line that start line is for + self.get_init_text_layout(config_id, &text_prov, start.line, trigger); + } + + let text_layouts = self.text_layouts.clone(); + let font_sizes = self.font_sizes.clone(); + let wrap = self.wrap.get(); + let last_vline = self.last_vline.clone(); + let layout_event = trigger.then_some(self.layout_event); + self.iter_rvlines(text_prov.clone(), false, start) + .map(move |v| { + if v.is_first() { + // For every (first) vline we initialize the next buffer line's text layout + // This ensures it is ready for when re reach it. + let next_line = v.rvline.line + 1; + let font_size = font_sizes.borrow().font_size(next_line); + // `init_iter_lines` is the reason `get_init_text_layout` is split out. + // Being split out lets us avoid attaching lifetimes to the iterator, since it + // only uses Rc/Arcs that it. This is useful since `Lines` would be in a + // `Rc>` which would make iterators with lifetimes referring to + // `Lines` a pain. + get_init_text_layout( + &text_layouts, + layout_event, + &text_prov, + next_line, + font_size, + wrap, + &last_vline, + ); + } + v + }) + } + + /// Get the visual line of the offset. + /// + /// `affinity` decides whether an offset at a soft line break is considered to be on the + /// previous line or the next line. + /// If `affinity` is `CursorAffinity::Forward` and is at the very end of the wrapped line, then + /// the offset is considered to be on the next vline. + pub fn vline_of_offset( + &self, + text_prov: &impl TextLayoutProvider, + offset: usize, + affinity: CursorAffinity, + ) -> VLine { + let text = text_prov.text(); + + let offset = offset.min(text.len()); + + if self.is_linear(text_prov) { + let buffer_line = text.line_of_offset(offset); + return VLine(buffer_line); + } + + let Some((vline, _line_index)) = + find_vline_of_offset(self, text_prov, offset, affinity) + else { + // We assume it is out of bounds + return self.last_vline(text_prov); + }; + + vline + } + + /// Get the visual line and column of the given offset. + /// + /// The column is before phantom text is applied and is into the overall line, not the + /// individual visual line. + pub fn vline_col_of_offset( + &self, + text_prov: &impl TextLayoutProvider, + offset: usize, + affinity: CursorAffinity, + ) -> (VLine, usize) { + let vline = self.vline_of_offset(text_prov, offset, affinity); + let last_col = self + .iter_vlines(text_prov, false, vline) + .next() + .map(|info| info.last_col(text_prov, true)) + .unwrap_or(0); + + let line = text_prov.text().line_of_offset(offset); + let line_offset = text_prov.text().offset_of_line(line); + + let col = offset - line_offset; + let col = col.min(last_col); + + (vline, col) + } + + /// Get the nearest offset to the start of the visual line + pub fn offset_of_vline( + &self, + text_prov: &impl TextLayoutProvider, + vline: VLine, + ) -> usize { + find_vline_init_info(self, text_prov, vline) + .map(|x| x.0) + .unwrap_or_else(|| text_prov.text().len()) + } + + /// Get the first visual line of the buffer line. + pub fn vline_of_line( + &self, + text_prov: &impl TextLayoutProvider, + line: usize, + ) -> VLine { + if self.is_linear(text_prov) { + return VLine(line); + } + + find_vline_of_line(self, text_prov, line) + .unwrap_or_else(|| self.last_vline(text_prov)) + } + + /// Find the matching visual line for the given relative visual line. + pub fn vline_of_rvline( + &self, + text_prov: &impl TextLayoutProvider, + rvline: RVLine, + ) -> VLine { + if self.is_linear(text_prov) { + debug_assert_eq!(rvline.line_index, 0, "Got a nonzero line index despite being linear, old RVLine was used."); + return VLine(rvline.line); + } + + let vline = self.vline_of_line(text_prov, rvline.line); + + // TODO(minor): There may be edge cases with this, like when you have a bunch of multiline + // phantom text at the same offset + VLine(vline.get() + rvline.line_index) + } + + /// Get the relative visual line of the offset. + /// + /// `affinity` decides whether an offset at a soft line break is considered to be on the + /// previous line or the next line. + /// If `affinity` is `CursorAffinity::Forward` and is at the very end of the wrapped line, then + /// the offset is considered to be on the next rvline. + pub fn rvline_of_offset( + &self, + text_prov: &impl TextLayoutProvider, + offset: usize, + affinity: CursorAffinity, + ) -> RVLine { + let text = text_prov.text(); + + let offset = offset.min(text.len()); + + if self.is_linear(text_prov) { + let buffer_line = text.line_of_offset(offset); + return RVLine::new(buffer_line, 0); + } + + find_rvline_of_offset(self, text_prov, offset, affinity) + .unwrap_or_else(|| self.last_rvline(text_prov)) + } + + /// Get the relative visual line and column of the given offset + /// + /// The column is before phantom text is applied and is into the overall line, not the + /// individual visual line. + pub fn rvline_col_of_offset( + &self, + text_prov: &impl TextLayoutProvider, + offset: usize, + affinity: CursorAffinity, + ) -> (RVLine, usize) { + let rvline = self.rvline_of_offset(text_prov, offset, affinity); + let info = self.iter_rvlines(text_prov, false, rvline).next().unwrap(); + let line_offset = text_prov.text().offset_of_line(rvline.line); + + let col = offset - line_offset; + let col = col.min(info.last_col(text_prov, true)); + + (rvline, col) + } + + /// Get the offset of a relative visual line + pub fn offset_of_rvline( + &self, + text_prov: &impl TextLayoutProvider, + RVLine { line, line_index }: RVLine, + ) -> usize { + let rope_text = text_prov.rope_text(); + let font_size = self.font_size(line); + let layouts = self.text_layouts.borrow(); + + let base_offset = rope_text.offset_of_line(line); + + // We could remove the debug asserts and allow invalid line indices. However I think it is + // desirable to avoid those since they are probably indicative of bugs. + if let Some(text_layout) = layouts.get(font_size, line) { + debug_assert!( + line_index < text_layout.line_count(), + "Line index was out of bounds. This likely indicates keeping an rvline past when it was valid." + ); + + let line_index = line_index.min(text_layout.line_count() - 1); + + let col = text_layout + .start_layout_cols(text_prov, line) + .nth(line_index) + .unwrap_or(0); + let col = text_prov.before_phantom_col(line, col); + + base_offset + col + } else { + // There was no text layout for this line, so we treat it like if line index is zero + // even if it is not. + + debug_assert_eq!(line_index, 0, "Line index was zero. This likely indicates keeping an rvline past when it was valid."); + + base_offset + } + } + + /// Get the relative visual line of the buffer line + pub fn rvline_of_line( + &self, + text_prov: &impl TextLayoutProvider, + line: usize, + ) -> RVLine { + if self.is_linear(text_prov) { + return RVLine::new(line, 0); + } + + let offset = text_prov.rope_text().offset_of_line(line); + + find_rvline_of_offset(self, text_prov, offset, CursorAffinity::Backward) + .unwrap_or_else(|| self.last_rvline(text_prov)) + } + + /// Check whether the config id has changed, clearing the cache if it has. + pub fn check_config_id(&self, config_id: u64) { + // Check if the text layout needs to update due to the config being changed + if config_id != self.text_layouts.borrow().config_id { + let cache_rev = self.text_layouts.borrow().cache_rev + 1; + self.clear(cache_rev, Some(config_id)); + } + } + + /// Check whether the text layout cache revision is different. + /// Clears the layouts and updates the cache rev if it was different. + pub fn check_cache_rev(&self, cache_rev: u64) { + if cache_rev != self.text_layouts.borrow().cache_rev { + self.clear(cache_rev, None); + } + } + + /// Clear the text layouts with a given cache revision + pub fn clear(&self, cache_rev: u64, config_id: Option) { + self.text_layouts.borrow_mut().clear(cache_rev, config_id); + self.last_vline.set(None); + } + + /// Clear the layouts and vline without changing the cache rev or config id. + pub fn clear_unchanged(&self) { + self.text_layouts.borrow_mut().clear_unchanged(); + self.last_vline.set(None); + } +} + +/// This is a separate function as a hacky solution to lifetimes. +/// While it being on `Lines` makes the most sense, it being separate lets us only have +/// `text_layouts` and `wrap` from the original to then initialize a text layout. This simplifies +/// lifetime issues in some functions, since they can just have an `Arc`/`Rc`. +/// +/// Note: This does not clear the cache or check via config id. That should be done outside this +/// as `Lines` does require knowing when the cache is invalidated. +fn get_init_text_layout( + text_layouts: &RefCell, + layout_event: Option>, + text_prov: impl TextLayoutProvider, + line: usize, + font_size: usize, + wrap: ResolvedWrap, + last_vline: &Cell>, +) -> Arc { + // If we don't have a second layer of the hashmap initialized for this specific font size, + // do it now + if text_layouts.borrow().layouts.get(&font_size).is_none() { + let mut cache = text_layouts.borrow_mut(); + cache.layouts.insert(font_size, HashMap::new()); + } + + // Get whether there's an entry for this specific font size and line + let cache_exists = text_layouts + .borrow() + .layouts + .get(&font_size) + .unwrap() + .get(&line) + .is_some(); + // If there isn't an entry then we actually have to create it + if !cache_exists { + let text_layout = text_prov.new_text_layout(line, font_size, wrap); + + // Update last vline + if let Some(vline) = last_vline.get() { + let last_line = text_prov.rope_text().last_line(); + if line <= last_line { + // We can get rid of the old line count and add our new count. + // This lets us typically avoid having to calculate the last visual line. + let vline = vline.get(); + let new_vline = vline + (text_layout.line_count() - 1); + + last_vline.set(Some(VLine(new_vline))); + } + // If the line is past the end of the file, then we don't need to update the last + // visual line. It is garbage. + } + // Otherwise last vline was already None. + + { + // Add the text layout to the cache. + let mut cache = text_layouts.borrow_mut(); + let width = text_layout.text.size().width; + if width > cache.max_width { + cache.max_width = width; + } + cache + .layouts + .get_mut(&font_size) + .unwrap() + .insert(line, text_layout); + } + + if let Some(layout_event) = layout_event { + layout_event.send(LayoutEvent::CreatedLayout { font_size, line }); + } + } + + // Just get the entry, assuming it has been created because we initialize it above. + text_layouts + .borrow() + .layouts + .get(&font_size) + .unwrap() + .get(&line) + .cloned() + .unwrap() +} + +/// Returns (visual line, line_index) +fn find_vline_of_offset( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + offset: usize, + affinity: CursorAffinity, +) -> Option<(VLine, usize)> { + let layouts = lines.text_layouts.borrow(); + + let rope_text = text_prov.rope_text(); + + let buffer_line = rope_text.line_of_offset(offset); + let line_start_offset = rope_text.offset_of_line(buffer_line); + let vline = find_vline_of_line(lines, text_prov, buffer_line)?; + + let font_size = lines.font_size(buffer_line); + let Some(text_layout) = layouts.get(font_size, buffer_line) else { + // No text layout for this line, so the vline we found is definitely correct. + // As well, there is no previous soft line to consider + return Some((vline, 0)); + }; + + let col = offset - line_start_offset; + + let (vline, line_index) = + find_start_line_index(text_prov, text_layout, buffer_line, col) + .map(|line_index| (VLine(vline.get() + line_index), line_index))?; + + // If the most recent line break was due to a soft line break, + if line_index > 0 { + if let CursorAffinity::Backward = affinity { + // TODO: This can definitely be smarter. We're doing a vline search, and then this is + // practically doing another! + let line_end = lines.offset_of_vline(text_prov, vline); + // then if we're right at that soft line break, a backwards affinity + // means that we are on the previous visual line. + if line_end == offset && vline.get() != 0 { + return Some((VLine(vline.get() - 1), line_index - 1)); + } + } + } + + Some((vline, line_index)) +} + +fn find_rvline_of_offset( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + offset: usize, + affinity: CursorAffinity, +) -> Option { + let layouts = lines.text_layouts.borrow(); + + let rope_text = text_prov.rope_text(); + + let buffer_line = rope_text.line_of_offset(offset); + let line_start_offset = rope_text.offset_of_line(buffer_line); + + let font_size = lines.font_size(buffer_line); + let Some(text_layout) = layouts.get(font_size, buffer_line) else { + // There is no text layout for this line so the line index is always zero. + return Some(RVLine::new(buffer_line, 0)); + }; + + let col = offset - line_start_offset; + + let rv = find_start_line_index(text_prov, text_layout, buffer_line, col) + .map(|line_index| RVLine::new(buffer_line, line_index))?; + + // If the most recent line break was due to a soft line break, + if rv.line_index > 0 { + if let CursorAffinity::Backward = affinity { + let line_end = lines.offset_of_rvline(text_prov, rv); + // then if we're right at that soft line break, a backwards affinity + // means that we are on the previous visual line. + if line_end == offset { + if rv.line_index > 0 { + return Some(RVLine::new(rv.line, rv.line_index - 1)); + } else if rv.line == 0 { + // There is no previous line, we do nothing. + } else { + // We have to get rvline info for that rvline, so we can get the last line index + // This should aways have at least one rvline in it. + let font_sizes = lines.font_sizes.borrow(); + let (prev, _) = + prev_rvline(&layouts, text_prov, &**font_sizes, rv)?; + return Some(prev); + } + } + } + } + + Some(rv) +} + +// TODO: a lot of these just take lines, so should possibly just be put on it. + +/// Find the line index which contains the column. +fn find_start_line_index( + text_prov: &impl TextLayoutProvider, + text_layout: &TextLayoutLine, + line: usize, + col: usize, +) -> Option { + let mut starts = text_layout + .layout_cols(text_prov, line) + .enumerate() + .peekable(); + + while let Some((i, (layout_start, _))) = starts.next() { + // TODO: we should just apply after_col to col to do this transformation once + let layout_start = text_prov.before_phantom_col(line, layout_start); + if layout_start >= col { + return Some(i); + } + + let next_start = starts.peek().map(|(_, (next_start, _))| { + text_prov.before_phantom_col(line, *next_start) + }); + + if let Some(next_start) = next_start { + if next_start > col { + // The next layout starts *past* our column, so we're on the previous line. + return Some(i); + } + } else { + // There was no next glyph, which implies that we are either on this line or not at all + return Some(i); + } + } + + None +} + +/// Get the first visual line of a buffer line. +fn find_vline_of_line( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + line: usize, +) -> Option { + let rope = text_prov.rope_text(); + + let last_line = rope.last_line(); + + if line > last_line / 2 { + // Often the last vline will already be cached, which lets us half the search time. + // The compiler may or may not be smart enough to combine the last vline calculation with + // our calculation of the vline of the line we're looking for, but it might not. + // If it doesn't, we could write a custom version easily. + let last_vline = lines.last_vline(text_prov); + let last_rvline = lines.last_rvline(text_prov); + let last_start_vline = VLine(last_vline.get() - last_rvline.line_index); + find_vline_of_line_backwards(lines, (last_start_vline, last_line), line) + } else { + find_vline_of_line_forwards(lines, (VLine(0), 0), line) + } +} + +/// Get the first visual line of a buffer line. +/// This searches backwards from `pivot`, so it should be *after* the given line. +/// This requires that the `pivot` is the first line index of the line it is for. +fn find_vline_of_line_backwards( + lines: &Lines, + (start, s_line): (VLine, usize), + line: usize, +) -> Option { + if line > s_line { + return None; + } else if line == s_line { + return Some(start); + } else if line == 0 { + return Some(VLine(0)); + } + + let layouts = lines.text_layouts.borrow(); + + let mut cur_vline = start.get(); + + for cur_line in line..s_line { + let font_size = lines.font_size(cur_line); + + let Some(text_layout) = layouts.get(font_size, cur_line) else { + // no text layout, so its just a normal line + cur_vline -= 1; + continue; + }; + + let line_count = text_layout.line_count(); + + cur_vline -= line_count; + } + + Some(VLine(cur_vline)) +} + +fn find_vline_of_line_forwards( + lines: &Lines, + (start, s_line): (VLine, usize), + line: usize, +) -> Option { + match line.cmp(&s_line) { + Ordering::Equal => return Some(start), + Ordering::Less => return None, + Ordering::Greater => (), + } + + let layouts = lines.text_layouts.borrow(); + + let mut cur_vline = start.get(); + + for cur_line in s_line..line { + let font_size = lines.font_size(cur_line); + + let Some(text_layout) = layouts.get(font_size, cur_line) else { + // no text layout, so its just a normal line + cur_vline += 1; + continue; + }; + + let line_count = text_layout.line_count(); + cur_vline += line_count; + } + + Some(VLine(cur_vline)) +} + +/// Find the (start offset, buffer line, layout line index) of a given visual line. +/// +/// start offset is into the file, rather than the text layouts string, so it does not include +/// phantom text. +/// +/// Returns `None` if the visual line is out of bounds. +fn find_vline_init_info( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + vline: VLine, +) -> Option<(usize, RVLine)> { + let rope_text = text_prov.rope_text(); + + if vline.get() == 0 { + return Some((0, RVLine::new(0, 0))); + } + + if lines.is_linear(text_prov) { + // If lines is linear then we can trivially convert the visual line to a buffer line + let line = vline.get(); + if line > rope_text.last_line() { + return None; + } + + return Some((rope_text.offset_of_line(line), RVLine::new(line, 0))); + } + + let last_vline = lines.last_vline(text_prov); + + if vline > last_vline { + return None; + } + + if vline.get() < last_vline.get() / 2 { + let last_rvline = lines.last_rvline(text_prov); + find_vline_init_info_rv_backward( + lines, + text_prov, + (last_vline, last_rvline), + vline, + ) + } else { + find_vline_init_info_forward(lines, text_prov, (VLine(0), 0), vline) + } +} + +// TODO(minor): should we package (VLine, buffer line) into a struct since we use it for these +// pseudo relative calculations often? +/// Find the `(start offset, rvline)` of a given [`VLine`] +/// +/// start offset is into the file, rather than text layout's string, so it does not include +/// phantom text. +/// +/// Returns `None` if the visual line is out of bounds, or if the start is past our target. +fn find_vline_init_info_forward( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + (start, start_line): (VLine, usize), + vline: VLine, +) -> Option<(usize, RVLine)> { + if start > vline { + return None; + } + + let rope_text = text_prov.rope_text(); + + let mut cur_line = start_line; + let mut cur_vline = start.get(); + + let layouts = lines.text_layouts.borrow(); + while cur_vline < vline.get() { + let font_size = lines.font_size(cur_line); + let line_count = if let Some(text_layout) = layouts.get(font_size, cur_line) + { + let line_count = text_layout.line_count(); + + // We can then check if the visual line is in this intervening range. + if cur_vline + line_count > vline.get() { + // We found the line that contains the visual line. + // We can now find the offset of the visual line within the line. + let line_index = vline.get() - cur_vline; + // TODO: is it fine to unwrap here? + let col = text_layout + .start_layout_cols(text_prov, cur_line) + .nth(line_index) + .unwrap_or(0); + let col = text_prov.before_phantom_col(cur_line, col); + + let base_offset = rope_text.offset_of_line(cur_line); + return Some((base_offset + col, RVLine::new(cur_line, line_index))); + } + + // The visual line is not in this line, so we have to keep looking. + line_count + } else { + // There was no text layout so we only have to consider the line breaks in this line. + // Which, since we don't handle phantom text, is just one. + + 1 + }; + + cur_line += 1; + cur_vline += line_count; + } + + // We've reached the visual line we're looking for, we can return the offset. + // This also handles the case where the vline is past the end of the text. + if cur_vline == vline.get() { + if cur_line > rope_text.last_line() { + return None; + } + + // We use cur_line because if our target vline is out of bounds + // then the result should be len + Some((rope_text.offset_of_line(cur_line), RVLine::new(cur_line, 0))) + } else { + // We've gone past the visual line we're looking for, so it is out of bounds. + None + } +} + +/// Find the `(start offset, rvline)` of a given [`VLine`] +/// +/// `start offset` is into the file, rather than the text layout's content, so it does not +/// include phantom text. +/// +/// Returns `None` if the visual line is out of bounds or if the start is before our target. +/// This iterates backwards. +fn find_vline_init_info_rv_backward( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + (start, start_rvline): (VLine, RVLine), + vline: VLine, +) -> Option<(usize, RVLine)> { + if start < vline { + // The start was before the target. + return None; + } + + // This would the vline at the very start of the buffer line + let shifted_start = VLine(start.get() - start_rvline.line_index); + match shifted_start.cmp(&vline) { + // The shifted start was equivalent to the vline, which makes it easy to compute + Ordering::Equal => { + let offset = text_prov.rope_text().offset_of_line(start_rvline.line); + Some((offset, RVLine::new(start_rvline.line, 0))) + } + // The new start is before the vline, that means the vline is on the same line. + Ordering::Less => { + let line_index = vline.get() - shifted_start.get(); + let layouts = lines.text_layouts.borrow(); + let font_size = lines.font_size(start_rvline.line); + if let Some(text_layout) = layouts.get(font_size, start_rvline.line) { + vline_init_info_b( + text_prov, + text_layout, + RVLine::new(start_rvline.line, line_index), + ) + } else { + // There was no text layout so we only have to consider the line breaks in this line. + + let base_offset = + text_prov.rope_text().offset_of_line(start_rvline.line); + Some((base_offset, RVLine::new(start_rvline.line, 0))) + } + } + Ordering::Greater => find_vline_init_info_backward( + lines, + text_prov, + (shifted_start, start_rvline.line), + vline, + ), + } +} + +fn find_vline_init_info_backward( + lines: &Lines, + text_prov: &impl TextLayoutProvider, + (mut start, mut start_line): (VLine, usize), + vline: VLine, +) -> Option<(usize, RVLine)> { + loop { + let (prev_vline, prev_line) = prev_line_start(lines, start, start_line)?; + + match prev_vline.cmp(&vline) { + // We found the target, and it was at the start + Ordering::Equal => { + let offset = text_prov.rope_text().offset_of_line(prev_line); + return Some((offset, RVLine::new(prev_line, 0))); + } + // The target is on this line, so we can just search for it + Ordering::Less => { + let font_size = lines.font_size(prev_line); + let layouts = lines.text_layouts.borrow(); + if let Some(text_layout) = layouts.get(font_size, prev_line) { + return vline_init_info_b( + text_prov, + text_layout, + RVLine::new(prev_line, vline.get() - prev_vline.get()), + ); + } else { + // There was no text layout so we only have to consider the line breaks in this line. + // Which, since we don't handle phantom text, is just one. + + let base_offset = + text_prov.rope_text().offset_of_line(prev_line); + return Some((base_offset, RVLine::new(prev_line, 0))); + } + } + // The target is before this line, so we have to keep searching + Ordering::Greater => { + start = prev_vline; + start_line = prev_line; + } + } + } +} + +/// Get the previous (line, start visual line) from a (line, start visual line). +fn prev_line_start( + lines: &Lines, + vline: VLine, + line: usize, +) -> Option<(VLine, usize)> { + if line == 0 { + return None; + } + + let layouts = lines.text_layouts.borrow(); + + let prev_line = line - 1; + let font_size = lines.font_size(line); + if let Some(layout) = layouts.get(font_size, prev_line) { + let line_count = layout.line_count(); + let prev_vline = vline.get() - line_count; + Some((VLine(prev_vline), prev_line)) + } else { + // There's no layout for the previous line which makes this easy + Some((VLine(vline.get() - 1), prev_line)) + } +} + +fn vline_init_info_b( + text_prov: &impl TextLayoutProvider, + text_layout: &TextLayoutLine, + rv: RVLine, +) -> Option<(usize, RVLine)> { + let rope_text = text_prov.rope_text(); + let col = text_layout + .start_layout_cols(text_prov, rv.line) + .nth(rv.line_index) + .unwrap_or(0); + let col = text_prov.before_phantom_col(rv.line, col); + + let base_offset = rope_text.offset_of_line(rv.line); + + Some((base_offset + col, rv)) +} + +/// Information about the visual line and how it relates to the underlying buffer line. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub struct VLineInfo { + /// Start offset to end offset in the buffer that this visual line covers. + /// Note that this is obviously not including phantom text. + pub interval: Interval, + /// The total number of lines in this buffer line. Always at least 1. + pub line_count: usize, + pub rvline: RVLine, + /// The actual visual line this is for. + /// For relative visual line iteration, this is empty. + pub vline: L, +} +impl VLineInfo { + fn new>( + iv: I, + rvline: RVLine, + line_count: usize, + vline: L, + ) -> Self { + Self { + interval: iv.into(), + line_count, + rvline, + vline, + } + } + + pub fn to_blank(&self) -> VLineInfo<()> { + VLineInfo::new(self.interval, self.rvline, self.line_count, ()) + } + + /// Check whether the interval is empty. + /// Note that there could still be phantom text on this line. + pub fn is_empty(&self) -> bool { + self.interval.is_empty() + } + + pub fn is_first(&self) -> bool { + self.rvline.is_first() + } + + // TODO: is this correct for phantom lines? + // TODO: can't we just use the line count field now? + /// Is this the last visual line for the relevant buffer line? + pub fn is_last(&self, text_prov: &impl TextLayoutProvider) -> bool { + let rope_text = text_prov.rope_text(); + let line_end = rope_text.line_end_offset(self.rvline.line, false); + let vline_end = self.line_end_offset(text_prov, false); + + line_end == vline_end + } + + /// Get the first column of the overall line of the visual line + pub fn first_col(&self, text_prov: &impl TextLayoutProvider) -> usize { + let line_start = self.interval.start; + let start_offset = text_prov.text().offset_of_line(self.rvline.line); + line_start - start_offset + } + + /// Get the last column in the overall line of this visual line + /// The caret decides whether it is after the last character, or before it. + /// ```rust,ignore + /// // line content = "conf = Config::default();\n" + /// // wrapped breakup = ["conf = ", "Config::default();\n"] + /// + /// // when vline_info is for "conf = " + /// assert_eq!(vline_info.last_col(text_prov, false), 6) // "conf =| " + /// assert_eq!(vline_info.last_col(text_prov, true), 7) // "conf = |" + /// // when vline_info is for "Config::default();\n" + /// // Notice that the column is in the overall line, not the wrapped line. + /// assert_eq!(vline_info.last_col(text_prov, false), 24) // "Config::default()|;" + /// assert_eq!(vline_info.last_col(text_prov, true), 25) // "Config::default();|" + /// ``` + pub fn last_col( + &self, + text_prov: &impl TextLayoutProvider, + caret: bool, + ) -> usize { + let vline_end = self.interval.end; + let start_offset = text_prov.text().offset_of_line(self.rvline.line); + // If these subtractions crash, then it is likely due to a bad vline being kept around + // somewhere + if !caret && !self.interval.is_empty() { + let vline_pre_end = + text_prov.rope_text().prev_grapheme_offset(vline_end, 1, 0); + vline_pre_end - start_offset + } else { + vline_end - start_offset + } + } + + // TODO: we could generalize `RopeText::line_end_offset` to any interval, and then just use it here instead of basically reimplementing it. + pub fn line_end_offset( + &self, + text_prov: &impl TextLayoutProvider, + caret: bool, + ) -> usize { + let text = text_prov.text(); + let rope_text = RopeTextRef::new(text); + + let mut offset = self.interval.end; + let mut line_content: &str = &text.slice_to_cow(self.interval); + if line_content.ends_with("\r\n") { + offset -= 2; + line_content = &line_content[..line_content.len() - 2]; + } else if line_content.ends_with('\n') { + offset -= 1; + line_content = &line_content[..line_content.len() - 1]; + } + if !caret && !line_content.is_empty() { + offset = rope_text.prev_grapheme_offset(offset, 1, 0); + } + offset + } + + /// Returns the offset of the first non-blank character in the line. + pub fn first_non_blank_character( + &self, + text_prov: &impl TextLayoutProvider, + ) -> usize { + WordCursor::new(text_prov.text(), self.interval.start).next_non_blank_char() + } +} + +/// Iterator of the visual lines in a [`Lines`]. +/// This only considers wrapped and phantom text lines that have been rendered into a text layout. +/// +/// In principle, we could consider the newlines in phantom text for lines that have not been +/// rendered. However, that is more expensive to compute and is probably not actually *useful*. +struct VisualLines { + v: VisualLinesRelative, + vline: VLine, +} +impl VisualLines { + pub fn new( + lines: &Lines, + text_prov: T, + backwards: bool, + start: VLine, + ) -> VisualLines { + // TODO(minor): If we aren't using offset here then don't calculate it. + let Some((_offset, rvline)) = find_vline_init_info(lines, &text_prov, start) + else { + return VisualLines::empty(lines, text_prov, backwards); + }; + + VisualLines { + v: VisualLinesRelative::new(lines, text_prov, backwards, rvline), + vline: start, + } + } + + pub fn empty(lines: &Lines, text_prov: T, backwards: bool) -> VisualLines { + VisualLines { + v: VisualLinesRelative::empty(lines, text_prov, backwards), + vline: VLine(0), + } + } +} +impl Iterator for VisualLines { + type Item = VLineInfo; + + fn next(&mut self) -> Option { + let was_first_iter = self.v.is_first_iter; + let info = self.v.next()?; + + if !was_first_iter { + if self.v.backwards { + // This saturation isn't really needed, but just in case. + debug_assert!( + self.vline.get() != 0, + "Expected VLine to always be nonzero if we were going backwards" + ); + self.vline = VLine(self.vline.get().saturating_sub(1)); + } else { + self.vline = VLine(self.vline.get() + 1); + } + } + + Some(VLineInfo { + interval: info.interval, + line_count: info.line_count, + rvline: info.rvline, + vline: self.vline, + }) + } +} + +/// Iterator of the visual lines in a [`Lines`] relative to some starting buffer line. +/// This only considers wrapped and phantom text lines that have been rendered into a text layout. +struct VisualLinesRelative { + font_sizes: Arc, + text_layouts: Rc>, + text_prov: T, + + is_done: bool, + + rvline: RVLine, + /// Our current offset into the rope. + offset: usize, + + /// Which direction we should move in. + backwards: bool, + /// Whether there is a one-to-one mapping between buffer lines and visual lines. + linear: bool, + + is_first_iter: bool, +} +impl VisualLinesRelative { + pub fn new( + lines: &Lines, + text_prov: T, + backwards: bool, + start: RVLine, + ) -> VisualLinesRelative { + // Empty iterator if we're past the end of the possible lines + if start > lines.last_rvline(&text_prov) { + return VisualLinesRelative::empty(lines, text_prov, backwards); + } + + let layouts = lines.text_layouts.borrow(); + let font_size = lines.font_size(start.line); + let offset = rvline_offset(&layouts, &text_prov, font_size, start); + + let linear = lines.is_linear(&text_prov); + + VisualLinesRelative { + font_sizes: lines.font_sizes.borrow().clone(), + text_layouts: lines.text_layouts.clone(), + text_prov, + is_done: false, + rvline: start, + offset, + backwards, + linear, + is_first_iter: true, + } + } + + pub fn empty( + lines: &Lines, + text_prov: T, + backwards: bool, + ) -> VisualLinesRelative { + VisualLinesRelative { + font_sizes: lines.font_sizes.borrow().clone(), + text_layouts: lines.text_layouts.clone(), + text_prov, + is_done: true, + rvline: RVLine::new(0, 0), + offset: 0, + backwards, + linear: true, + is_first_iter: true, + } + } +} +impl Iterator for VisualLinesRelative { + type Item = VLineInfo<()>; + + fn next(&mut self) -> Option { + if self.is_done { + return None; + } + + let layouts = self.text_layouts.borrow(); + if self.is_first_iter { + // This skips the next line call on the first line. + self.is_first_iter = false; + } else { + let v = shift_rvline( + &layouts, + &self.text_prov, + &*self.font_sizes, + self.rvline, + self.backwards, + self.linear, + ); + let Some((new_rel_vline, offset)) = v else { + self.is_done = true; + return None; + }; + + self.rvline = new_rel_vline; + self.offset = offset; + + if self.rvline.line > self.text_prov.rope_text().last_line() { + self.is_done = true; + return None; + } + } + + let line = self.rvline.line; + let line_index = self.rvline.line_index; + let vline = self.rvline; + + let start = self.offset; + + let font_size = self.font_sizes.font_size(line); + let end = end_of_rvline(&layouts, &self.text_prov, font_size, self.rvline); + + let line_count = if let Some(text_layout) = layouts.get(font_size, line) { + text_layout.line_count() + } else { + 1 + }; + debug_assert!(start <= end, "line: {line}, line_index: {line_index}, line_count: {line_count}, vline: {vline:?}, start: {start}, end: {end}, backwards: {} text_len: {}", self.backwards, self.text_prov.text().len()); + let info = VLineInfo::new(start..end, self.rvline, line_count, ()); + + Some(info) + } +} + +// TODO: This might skip spaces at the end of lines, which we probably don't want? +/// Get the end offset of the visual line from the file's line and the line index. +fn end_of_rvline( + layouts: &TextLayoutCache, + text_prov: &impl TextLayoutProvider, + font_size: usize, + RVLine { line, line_index }: RVLine, +) -> usize { + if line > text_prov.rope_text().last_line() { + return text_prov.text().len(); + } + + if let Some((_, end_col)) = + layouts.get_layout_col(text_prov, font_size, line, line_index) + { + let end_col = text_prov.before_phantom_col(line, end_col); + let base_offset = text_prov.text().offset_of_line(line); + + base_offset + end_col + } else { + let rope_text = text_prov.rope_text(); + + rope_text.line_end_offset(line, true) + } +} + +/// Shift a relative visual line forward or backwards based on the `backwards` parameter. +fn shift_rvline( + layouts: &TextLayoutCache, + text_prov: &impl TextLayoutProvider, + font_sizes: &dyn LineFontSizeProvider, + vline: RVLine, + backwards: bool, + linear: bool, +) -> Option<(RVLine, usize)> { + if linear { + let rope_text = text_prov.rope_text(); + debug_assert_eq!( + vline.line_index, 0, + "Line index should be zero if we're linearly working with lines" + ); + if backwards { + if vline.line == 0 { + return None; + } + + let prev_line = vline.line - 1; + let offset = rope_text.offset_of_line(prev_line); + Some((RVLine::new(prev_line, 0), offset)) + } else { + let next_line = vline.line + 1; + + if next_line > rope_text.last_line() { + return None; + } + + let offset = rope_text.offset_of_line(next_line); + Some((RVLine::new(next_line, 0), offset)) + } + } else if backwards { + prev_rvline(layouts, text_prov, font_sizes, vline) + } else { + let font_size = font_sizes.font_size(vline.line); + Some(next_rvline(layouts, text_prov, font_size, vline)) + } +} + +fn rvline_offset( + layouts: &TextLayoutCache, + text_prov: &impl TextLayoutProvider, + font_size: usize, + RVLine { line, line_index }: RVLine, +) -> usize { + let rope_text = text_prov.rope_text(); + if let Some((line_col, _)) = + layouts.get_layout_col(text_prov, font_size, line, line_index) + { + let line_offset = rope_text.offset_of_line(line); + let line_col = text_prov.before_phantom_col(line, line_col); + + line_offset + line_col + } else { + // There was no text layout line so this is a normal line. + debug_assert_eq!(line_index, 0); + + rope_text.offset_of_line(line) + } +} + +/// Move to the next visual line, giving the new information. +/// Returns `(new rel vline, offset)` +fn next_rvline( + layouts: &TextLayoutCache, + text_prov: &impl TextLayoutProvider, + font_size: usize, + RVLine { line, line_index }: RVLine, +) -> (RVLine, usize) { + let rope_text = text_prov.rope_text(); + if let Some(layout_line) = layouts.get(font_size, line) { + if let Some((line_col, _)) = + layout_line.layout_cols(text_prov, line).nth(line_index + 1) + { + let line_offset = rope_text.offset_of_line(line); + let line_col = text_prov.before_phantom_col(line, line_col); + let offset = line_offset + line_col; + + (RVLine::new(line, line_index + 1), offset) + } else { + // There was no next layout/vline on this buffer line. + // So we can simply move to the start of the next buffer line. + + (RVLine::new(line + 1, 0), rope_text.offset_of_line(line + 1)) + } + } else { + // There was no text layout line, so this is a normal line. + debug_assert_eq!(line_index, 0); + + (RVLine::new(line + 1, 0), rope_text.offset_of_line(line + 1)) + } +} + +/// Move to the previous visual line, giving the new information. +/// Returns `(new line, new line_index, offset)` +/// Returns `None` if the line and line index are zero and thus there is no previous visual line. +fn prev_rvline( + layouts: &TextLayoutCache, + text_prov: &impl TextLayoutProvider, + font_sizes: &dyn LineFontSizeProvider, + RVLine { line, line_index }: RVLine, +) -> Option<(RVLine, usize)> { + let rope_text = text_prov.rope_text(); + if line_index == 0 { + // Line index was zero so we must be moving back a buffer line + if line == 0 { + return None; + } + + let prev_line = line - 1; + let font_size = font_sizes.font_size(prev_line); + if let Some(layout_line) = layouts.get(font_size, prev_line) { + let line_offset = rope_text.offset_of_line(prev_line); + let (i, line_col) = layout_line + .start_layout_cols(text_prov, prev_line) + .enumerate() + .last() + .unwrap_or((0, 0)); + let line_col = text_prov.before_phantom_col(prev_line, line_col); + let offset = line_offset + line_col; + + Some((RVLine::new(prev_line, i), offset)) + } else { + // There was no text layout line, so the previous line is a normal line. + let prev_line_offset = rope_text.offset_of_line(prev_line); + Some((RVLine::new(prev_line, 0), prev_line_offset)) + } + } else { + // We're still on the same buffer line, so we can just move to the previous layout/vline. + + let prev_line_index = line_index - 1; + let font_size = font_sizes.font_size(line); + if let Some(layout_line) = layouts.get(font_size, line) { + if let Some((line_col, _)) = layout_line + .layout_cols(text_prov, line) + .nth(prev_line_index) + { + let line_offset = rope_text.offset_of_line(line); + let line_col = text_prov.before_phantom_col(line, line_col); + let offset = line_offset + line_col; + + Some((RVLine::new(line, prev_line_index), offset)) + } else { + // There was no previous layout/vline on this buffer line. + // So we can simply move to the end of the previous buffer line. + + let prev_line_offset = rope_text.offset_of_line(line - 1); + Some((RVLine::new(line - 1, 0), prev_line_offset)) + } + } else { + debug_assert!( + false, + "line_index was nonzero but there was no text layout line" + ); + // Despite that this shouldn't happen we default to just giving the start of this + // normal line + let line_offset = rope_text.offset_of_line(line); + Some((RVLine::new(line, 0), line_offset)) + } + } +} + +// FIXME: Put this in our cosmic-text fork. + +/// Hit position but decides wether it should go to the next line based on the `before` bool. +/// (Hit position should be equivalent to `before=false`). +/// This is needed when we have an idx at the end of, for example, a wrapped line which could be on +/// the first or second line. +pub fn hit_position_aff( + this: &floem::cosmic_text::TextLayout, + idx: usize, + before: bool, +) -> floem::cosmic_text::HitPosition { + use floem::{cosmic_text::HitPosition, kurbo::Point}; + let mut last_line = 0; + let mut last_end: usize = 0; + let mut offset = 0; + let mut last_glyph: Option<&LayoutGlyph> = None; + let mut last_line_width = 0.0; + let mut last_glyph_width = 0.0; + let mut last_position = HitPosition { + line: 0, + point: Point::ZERO, + glyph_ascent: 0.0, + glyph_descent: 0.0, + }; + for (line, run) in this.layout_runs().enumerate() { + if run.line_i > last_line { + last_line = run.line_i; + offset += last_end + 1; + } + + // Handles wrapped lines, like: + // ```rust + // let config_path = | + // dirs::config_dir(); + // ``` + // The glyphs won't contain the space at the end of the first part, and the position right + // after the space is the same column as at `|dirs`, which is what before is letting us + // distinguish. + // So essentially, if the next run has a glyph that is at the same idx as the end of the + // previous run, *and* it is at `idx` itself, then we know to position it on the previous. + if let Some(last_glyph) = last_glyph { + if let Some(first_glyph) = run.glyphs.first() { + let end = last_glyph.end + offset + 1; + if before && end == idx && end == first_glyph.start + offset { + last_position.point.x = (last_line_width + last_glyph.w) as f64; + return last_position; + } + } + } + + for glyph in run.glyphs { + if glyph.start + offset > idx { + last_position.point.x += last_glyph_width as f64; + return last_position; + } + last_end = glyph.end; + last_glyph_width = glyph.w; + last_position = HitPosition { + line, + point: Point::new(glyph.x as f64, run.line_y as f64), + glyph_ascent: run.glyph_ascent as f64, + glyph_descent: run.glyph_descent as f64, + }; + if (glyph.start + offset..glyph.end + offset).contains(&idx) { + return last_position; + } + } + + last_glyph = run.glyphs.last(); + last_line_width = run.line_w; + } + + if idx > 0 { + last_position.point.x += last_glyph_width as f64; + return last_position; + } + + HitPosition { + line: 0, + point: Point::ZERO, + glyph_ascent: 0.0, + glyph_descent: 0.0, + } +} + +#[cfg(test)] +mod tests { + use std::{borrow::Cow, cell::RefCell, sync::Arc}; + + use floem::{ + cosmic_text::{Attrs, AttrsList, FamilyOwned, TextLayout, Wrap}, + reactive::Scope, + }; + use im::HashMap; + use lapce_core::{ + buffer::rope_text::{RopeText, RopeTextRef}, + cursor::CursorAffinity, + }; + use lapce_xi_rope::Rope; + use smallvec::smallvec; + + use crate::{ + doc::phantom_text::{PhantomText, PhantomTextKind, PhantomTextLine}, + editor::{ + view_data::TextLayoutLine, + visual_line::{ + find_vline_of_line_backwards, find_vline_of_line_forwards, RVLine, + }, + }, + }; + + use super::{ + find_vline_init_info_forward, find_vline_init_info_rv_backward, + FontSizeCacheId, LineFontSizeProvider, Lines, ResolvedWrap, + TextLayoutProvider, VLine, + }; + + /// For most of the logic we standardize on a specific font size. + const FONT_SIZE: usize = 12; + + struct TestTextLayoutProvider<'a> { + text: &'a Rope, + phantom: HashMap, + font_family: Vec, + #[allow(dead_code)] + wrap: Wrap, + } + impl<'a> TestTextLayoutProvider<'a> { + fn new( + text: &'a Rope, + ph: HashMap, + wrap: Wrap, + ) -> Self { + Self { + text, + phantom: ph, + // we use a specific font to make width calculations consistent between platforms. + // TODO(minor): Is there a more common font that we can use? + font_family: FamilyOwned::parse_list("Cascadia Code").collect(), + wrap, + } + } + } + impl<'a> TextLayoutProvider for TestTextLayoutProvider<'a> { + fn text(&self) -> &Rope { + self.text + } + + // An implementation relatively close to the actual new text layout impl but simplified. + // TODO(minor): It would be nice to just use the same impl as view's + fn new_text_layout( + &self, + line: usize, + font_size: usize, + wrap: ResolvedWrap, + ) -> Arc { + let rope_text = RopeTextRef::new(self.text); + let line_content_original = rope_text.line_content(line); + + // Get the line content with newline characters replaced with spaces + // and the content without the newline characters + let (line_content, _line_content_original) = + if let Some(s) = line_content_original.strip_suffix("\r\n") { + ( + format!("{s} "), + &line_content_original[..line_content_original.len() - 2], + ) + } else if let Some(s) = line_content_original.strip_suffix('\n') { + ( + format!("{s} ",), + &line_content_original[..line_content_original.len() - 1], + ) + } else { + ( + line_content_original.to_string(), + &line_content_original[..], + ) + }; + + let phantom_text = self.phantom.get(&line).cloned().unwrap_or_default(); + let line_content = phantom_text.combine_with_text(line_content); + + // let color + + let attrs = Attrs::new() + .family(&self.font_family) + .font_size(font_size as f32); + let mut attrs_list = AttrsList::new(attrs); + + // We don't do line styles, since they aren't relevant + + // Apply phantom text specific styling + for (offset, size, col, phantom) in phantom_text.offset_size_iter() { + let start = col + offset; + let end = start + size; + + let mut attrs = attrs; + if let Some(fg) = phantom.fg { + attrs = attrs.color(fg); + } + if let Some(phantom_font_size) = phantom.font_size { + attrs = attrs.font_size(phantom_font_size.min(font_size) as f32); + } + attrs_list.add_span(start..end, attrs); + // if let Some(font_family) = phantom.font_family.clone() { + // layout_builder = layout_builder.range_attribute( + // start..end, + // TextAttribute::FontFamily(font_family), + // ); + // } + } + + let mut text_layout = TextLayout::new(); + text_layout.set_wrap(Wrap::Word); + match wrap { + // We do not have to set the wrap mode if we do not set the width + ResolvedWrap::None => {} + ResolvedWrap::Column(_col) => todo!(), + ResolvedWrap::Width(px) => { + text_layout.set_size(px, f32::MAX); + } + } + text_layout.set_text(&line_content, attrs_list); + + // skip phantom text background styling because it doesn't shift positions + // skip severity styling + // skip diagnostic background styling + + Arc::new(TextLayoutLine { + extra_style: Vec::new(), + text: text_layout, + whitespaces: None, + indent: 0.0, + }) + } + + fn before_phantom_col(&self, line: usize, col: usize) -> usize { + self.phantom + .get(&line) + .map(|x| x.before_col(col)) + .unwrap_or(col) + } + + fn has_multiline_phantom(&self) -> bool { + // Conservatively, yes. + true + } + } + + struct TestFontSize { + font_size: usize, + } + impl LineFontSizeProvider for TestFontSize { + fn font_size(&self, _line: usize) -> usize { + self.font_size + } + + fn cache_id(&self) -> FontSizeCacheId { + 0 + } + } + + fn make_lines( + text: &Rope, + width: f32, + init: bool, + ) -> (TestTextLayoutProvider<'_>, Lines) { + make_lines_ph(text, width, init, HashMap::new()) + } + + fn make_lines_ph( + text: &Rope, + width: f32, + init: bool, + ph: HashMap, + ) -> (TestTextLayoutProvider<'_>, Lines) { + let wrap = Wrap::Word; + let r_wrap = ResolvedWrap::Width(width); + let font_sizes = TestFontSize { + font_size: FONT_SIZE, + }; + let text = TestTextLayoutProvider::new(text, ph, wrap); + let cx = Scope::new(); + let lines = Lines::new(cx, RefCell::new(Arc::new(font_sizes))); + lines.set_wrap(r_wrap); + + if init { + let config_id = 0; + lines.init_all(config_id, &text, true); + } + + (text, lines) + } + + fn render_breaks<'a>( + text: &'a Rope, + lines: &mut Lines, + font_size: usize, + ) -> Vec> { + // TODO: line_content on ropetextref would have the lifetime reference rope_text + // rather than the held &'a Rope. + // I think this would require an alternate trait for those functions to avoid incorrect lifetimes. Annoying but workable. + let rope_text = RopeTextRef::new(text); + let mut result = Vec::new(); + let layouts = lines.text_layouts.borrow(); + + for line in 0..rope_text.num_lines() { + if let Some(text_layout) = layouts.get(font_size, line) { + let lines = &text_layout.text.lines; + for line in lines { + let layouts = line.layout_opt().as_deref().unwrap(); + for layout in layouts { + // Spacing + if layout.glyphs.is_empty() { + continue; + } + let start_idx = layout.glyphs[0].start; + let end_idx = layout.glyphs.last().unwrap().end; + // Hacky solution to include the ending space/newline since those get trimmed off + let line_content = line + .text() + .get(start_idx..=end_idx) + .unwrap_or(&line.text()[start_idx..end_idx]); + result.push(Cow::Owned(line_content.to_string())); + } + } + } else { + let line_content = rope_text.line_content(line); + + let line_content = match line_content { + Cow::Borrowed(x) => { + if let Some(x) = x.strip_suffix('\n') { + // Cow::Borrowed(x) + Cow::Owned(x.to_string()) + } else { + // Cow::Borrowed(x) + Cow::Owned(x.to_string()) + } + } + Cow::Owned(x) => { + if let Some(x) = x.strip_suffix('\n') { + Cow::Owned(x.to_string()) + } else { + Cow::Owned(x) + } + } + }; + result.push(line_content); + } + } + result + } + + /// Utility fn to quickly create simple phantom text + fn mph(kind: PhantomTextKind, col: usize, text: &str) -> PhantomText { + PhantomText { + kind, + col, + text: text.to_string(), + font_size: None, + fg: None, + bg: None, + under_line: None, + } + } + + fn ffvline_info( + lines: &Lines, + text_prov: impl TextLayoutProvider, + vline: VLine, + ) -> Option<(usize, RVLine)> { + find_vline_init_info_forward(lines, &text_prov, (VLine(0), 0), vline) + } + + fn fbvline_info( + lines: &Lines, + text_prov: impl TextLayoutProvider, + vline: VLine, + ) -> Option<(usize, RVLine)> { + let last_vline = lines.last_vline(&text_prov); + let last_rvline = lines.last_rvline(&text_prov); + find_vline_init_info_rv_backward( + lines, + &text_prov, + (last_vline, last_rvline), + vline, + ) + } + + #[test] + fn find_vline_init_info_empty() { + // Test empty buffer + let text = Rope::from(""); + let (text_prov, lines) = make_lines(&text, 50.0, false); + + assert_eq!( + ffvline_info(&lines, &text_prov, VLine(0)), + Some((0, RVLine::new(0, 0))) + ); + assert_eq!( + fbvline_info(&lines, &text_prov, VLine(0)), + Some((0, RVLine::new(0, 0))) + ); + assert_eq!(ffvline_info(&lines, &text_prov, VLine(1)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(1)), None); + + // Test empty buffer with phantom text and no wrapping + let text = Rope::from(""); + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph( + PhantomTextKind::Completion, + 0, + "hello world abc" + )], + }, + ); + let (text_prov, lines) = make_lines_ph(&text, 20.0, false, ph); + + assert_eq!( + ffvline_info(&lines, &text_prov, VLine(0)), + Some((0, RVLine::new(0, 0))) + ); + assert_eq!( + fbvline_info(&lines, &text_prov, VLine(0)), + Some((0, RVLine::new(0, 0))) + ); + assert_eq!(ffvline_info(&lines, &text_prov, VLine(1)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(1)), None); + + // Test empty buffer with phantom text and wrapping + lines.init_all(0, &text_prov, true); + + assert_eq!( + ffvline_info(&lines, &text_prov, VLine(0)), + Some((0, RVLine::new(0, 0))) + ); + assert_eq!( + fbvline_info(&lines, &text_prov, VLine(0)), + Some((0, RVLine::new(0, 0))) + ); + assert_eq!( + ffvline_info(&lines, &text_prov, VLine(1)), + Some((0, RVLine::new(0, 1))) + ); + assert_eq!( + fbvline_info(&lines, &text_prov, VLine(1)), + Some((0, RVLine::new(0, 1))) + ); + assert_eq!( + ffvline_info(&lines, &text_prov, VLine(2)), + Some((0, RVLine::new(0, 2))) + ); + assert_eq!( + fbvline_info(&lines, &text_prov, VLine(2)), + Some((0, RVLine::new(0, 2))) + ); + // Going outside bounds only ends up with None + assert_eq!(ffvline_info(&lines, &text_prov, VLine(3)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(3)), None); + // The affinity would shift from the front/end of the phantom line + // TODO: test affinity of logic behind clicking past the last vline? + } + + #[test] + fn find_vline_init_info_unwrapping() { + // Multiple lines with too large width for there to be any wrapping. + let text = Rope::from("hello\nworld toast and jam\nthe end\nhi"); + let rope_text = RopeTextRef::new(&text); + let (text_prov, mut lines) = make_lines(&text, 500.0, false); + + // Assert that with no text layouts (aka no wrapping and no phantom text) the function + // works + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["hello", "world toast and jam", "the end", "hi"] + ); + + lines.init_all(0, &text_prov, true); + + // Assert that even with text layouts, if it has no wrapping applied (because the width is large in this case) and no phantom text then it produces the same offsets as before. + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["hello ", "world toast and jam ", "the end ", "hi"] + ); + } + + #[test] + fn find_vline_init_info_phantom_unwrapping() { + let text = Rope::from("hello\nworld toast and jam\nthe end\nhi"); + let rope_text = RopeTextRef::new(&text); + + // Multiple lines with too large width for there to be any wrapping and phantom text + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 0, "greet world")], + }, + ); + + let (text_prov, lines) = make_lines_ph(&text, 500.0, false, ph); + + // With no text layouts, phantom text isn't initialized so it has no affect. + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + lines.init_all(0, &text_prov, true); + + // With text layouts, the phantom text is applied. + // But with a single line of phantom text, it doesn't affect the offsets. + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + // Multiple lines with too large width and a phantom text that takes up multiple lines. + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![ + mph(PhantomTextKind::Completion, 0, "greet\nworld"), + ], + }, + ); + + let (text_prov, mut lines) = make_lines_ph(&text, 500.0, false, ph); + + // With no text layouts, phantom text isn't initialized so it has no affect. + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + lines.init_all(0, &text_prov, true); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + [ + "greet", + "worldhello ", + "world toast and jam ", + "the end ", + "hi" + ] + ); + + // With text layouts, the phantom text is applied. + // With a phantom text that takes up multiple lines, it does not affect the offsets + // but it does affect the valid visual lines. + let info = ffvline_info(&lines, &text_prov, VLine(0)); + assert_eq!(info, Some((0, RVLine::new(0, 0)))); + let info = fbvline_info(&lines, &text_prov, VLine(0)); + assert_eq!(info, Some((0, RVLine::new(0, 0)))); + let info = ffvline_info(&lines, &text_prov, VLine(1)); + assert_eq!(info, Some((0, RVLine::new(0, 1)))); + let info = fbvline_info(&lines, &text_prov, VLine(1)); + assert_eq!(info, Some((0, RVLine::new(0, 1)))); + + for line in 2..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line - 1); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!( + info, + (line_offset, RVLine::new(line - 1, 0)), + "vline {}", + line + ); + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!( + info, + (line_offset, RVLine::new(line - 1, 0)), + "vline {}", + line + ); + } + + // Then there's one extra vline due to the phantom text wrapping + let line_offset = rope_text.offset_of_line(rope_text.last_line()); + + let info = + ffvline_info(&lines, &text_prov, VLine(rope_text.last_line() + 1)); + assert_eq!( + info, + Some((line_offset, RVLine::new(rope_text.last_line(), 0))), + "line {}", + rope_text.last_line() + 1, + ); + let info = + fbvline_info(&lines, &text_prov, VLine(rope_text.last_line() + 1)); + assert_eq!( + info, + Some((line_offset, RVLine::new(rope_text.last_line(), 0))), + "line {}", + rope_text.last_line() + 1, + ); + + // Multiple lines with too large width and a phantom text that takes up multiple lines. + // But the phantom text is not at the start of the first line. + let mut ph = HashMap::new(); + ph.insert( + 2, // "the end" + PhantomTextLine { + text: smallvec![ + mph(PhantomTextKind::Completion, 3, "greet\nworld"), + ], + }, + ); + + let (text_prov, mut lines) = make_lines_ph(&text, 500.0, false, ph); + + // With no text layouts, phantom text isn't initialized so it has no affect. + for line in 0..rope_text.num_lines() { + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + + let line_offset = rope_text.offset_of_line(line); + + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + lines.init_all(0, &text_prov, true); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + [ + "hello ", + "world toast and jam ", + "thegreet", + "world end ", + "hi" + ] + ); + + // With text layouts, the phantom text is applied. + // With a phantom text that takes up multiple lines, it does not affect the offsets + // but it does affect the valid visual lines. + for line in 0..3 { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line); + } + + // ' end' + let info = ffvline_info(&lines, &text_prov, VLine(3)); + assert_eq!(info, Some((29, RVLine::new(2, 1)))); + let info = fbvline_info(&lines, &text_prov, VLine(3)); + assert_eq!(info, Some((29, RVLine::new(2, 1)))); + + let info = ffvline_info(&lines, &text_prov, VLine(4)); + assert_eq!(info, Some((34, RVLine::new(3, 0)))); + let info = fbvline_info(&lines, &text_prov, VLine(4)); + assert_eq!(info, Some((34, RVLine::new(3, 0)))); + } + + #[test] + fn find_vline_init_info_basic_wrapping() { + // Tests with more mixes of text layout lines and uninitialized lines + + // Multiple lines with a small enough width for there to be a bunch of wrapping + let text = Rope::from("hello\nworld toast and jam\nthe end\nhi"); + let rope_text = RopeTextRef::new(&text); + let (text_prov, mut lines) = make_lines(&text, 30.0, false); + + // Assert that with no text layouts (aka no wrapping and no phantom text) the function + // works + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "line {}", line); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!(info, (line_offset, RVLine::new(line, 0)), "line {}", line); + } + + assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["hello", "world toast and jam", "the end", "hi"] + ); + + lines.init_all(0, &text_prov, true); + + { + let layouts = lines.text_layouts.borrow(); + + assert!(layouts.get(FONT_SIZE, 0).is_some()); + assert!(layouts.get(FONT_SIZE, 1).is_some()); + assert!(layouts.get(FONT_SIZE, 2).is_some()); + assert!(layouts.get(FONT_SIZE, 3).is_some()); + assert!(layouts.get(FONT_SIZE, 4).is_none()); + } + + // start offset, start buffer line, layout line index) + let line_data = [ + (0, 0, 0), + (6, 1, 0), + (12, 1, 1), + (18, 1, 2), + (22, 1, 3), + (26, 2, 0), + (30, 2, 1), + (34, 3, 0), + ]; + assert_eq!(lines.last_vline(&text_prov), VLine(7)); + assert_eq!(lines.last_rvline(&text_prov), RVLine::new(3, 0)); + #[allow(clippy::needless_range_loop)] + for line in 0..8 { + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!( + (info.0, info.1.line, info.1.line_index), + line_data[line], + "vline {}", + line + ); + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!( + (info.0, info.1.line, info.1.line_index), + line_data[line], + "vline {}", + line + ); + } + + // Directly out of bounds + assert_eq!(ffvline_info(&lines, &text_prov, VLine(9)), None,); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(9)), None,); + + assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["hello ", "world ", "toast ", "and ", "jam ", "the ", "end ", "hi"] + ); + + let vline_line_data = [0, 1, 5, 7]; + + let rope = text_prov.rope_text(); + let last_start_vline = VLine( + lines.last_vline(&text_prov).get() + - lines.last_rvline(&text_prov).line_index, + ); + #[allow(clippy::needless_range_loop)] + for line in 0..4 { + let vline = VLine(vline_line_data[line]); + assert_eq!( + find_vline_of_line_forwards(&lines, Default::default(), line), + Some(vline) + ); + assert_eq!( + find_vline_of_line_backwards( + &lines, + (last_start_vline, rope.last_line()), + line + ), + Some(vline), + "line: {line}" + ); + } + + let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); + let (text_prov, mut lines) = make_lines(&text, 2., true); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + [ + "aaaa ", "bb ", "bb ", "cc ", "cc ", "dddd ", "eeee ", "ff ", "ff ", + "gggg" + ] + ); + + // (start offset, start buffer line, layout line index) + let line_data = [ + (0, 0, 0), + (5, 1, 0), + (8, 1, 1), + (11, 1, 2), + (14, 2, 0), + (17, 2, 1), + (22, 2, 2), + (27, 2, 3), + (30, 3, 0), + (33, 3, 1), + ]; + #[allow(clippy::needless_range_loop)] + for vline in 0..10 { + let info = ffvline_info(&lines, &text_prov, VLine(vline)).unwrap(); + assert_eq!( + (info.0, info.1.line, info.1.line_index), + line_data[vline], + "vline {}", + vline + ); + let info = fbvline_info(&lines, &text_prov, VLine(vline)).unwrap(); + assert_eq!( + (info.0, info.1.line, info.1.line_index), + line_data[vline], + "vline {}", + vline + ); + } + + let vline_line_data = [0, 1, 4, 8]; + + let rope = text_prov.rope_text(); + let last_start_vline = VLine( + lines.last_vline(&text_prov).get() + - lines.last_rvline(&text_prov).line_index, + ); + #[allow(clippy::needless_range_loop)] + for line in 0..4 { + let vline = VLine(vline_line_data[line]); + assert_eq!( + find_vline_of_line_forwards(&lines, Default::default(), line), + Some(vline) + ); + assert_eq!( + find_vline_of_line_backwards( + &lines, + (last_start_vline, rope.last_line()), + line + ), + Some(vline), + "line: {line}" + ); + } + + // TODO: tests that have less line wrapping + } + + #[test] + fn find_vline_init_info_basic_wrapping_phantom() { + // Single line Phantom text at the very start + let text = Rope::from("hello\nworld toast and jam\nthe end\nhi"); + let rope_text = RopeTextRef::new(&text); + + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 0, "greet world")], + }, + ); + + let (text_prov, mut lines) = make_lines_ph(&text, 30.0, false, ph); + + // Assert that with no text layouts there is no change in behavior from having no phantom + // text + for line in 0..rope_text.num_lines() { + let line_offset = rope_text.offset_of_line(line); + + let info = ffvline_info(&lines, &text_prov, VLine(line)); + assert_eq!( + info, + Some((line_offset, RVLine::new(line, 0))), + "line {}", + line + ); + + let info = fbvline_info(&lines, &text_prov, VLine(line)); + assert_eq!( + info, + Some((line_offset, RVLine::new(line, 0))), + "line {}", + line + ); + } + + assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["hello", "world toast and jam", "the end", "hi"] + ); + + lines.init_all(0, &text_prov, true); + + { + let layouts = lines.text_layouts.borrow(); + + assert!(layouts.get(FONT_SIZE, 0).is_some()); + assert!(layouts.get(FONT_SIZE, 1).is_some()); + assert!(layouts.get(FONT_SIZE, 2).is_some()); + assert!(layouts.get(FONT_SIZE, 3).is_some()); + assert!(layouts.get(FONT_SIZE, 4).is_none()); + } + + // start offset, start buffer line, layout line index) + let line_data = [ + (0, 0, 0), + (0, 0, 1), + (6, 1, 0), + (12, 1, 1), + (18, 1, 2), + (22, 1, 3), + (26, 2, 0), + (30, 2, 1), + (34, 3, 0), + ]; + + #[allow(clippy::needless_range_loop)] + for line in 0..9 { + let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!( + (info.0, info.1.line, info.1.line_index), + line_data[line], + "vline {}", + line + ); + + let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap(); + assert_eq!( + (info.0, info.1.line, info.1.line_index), + line_data[line], + "vline {}", + line + ); + } + + // Directly out of bounds + assert_eq!(ffvline_info(&lines, &text_prov, VLine(9)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(9)), None); + + assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None); + assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None); + + // TODO: Currently the way we join phantom text and how cosmic wraps lines, + // the phantom text will be joined with whatever the word next to it is - if there is no + // spaces. It might be desirable to always separate them to let it wrap independently. + // An easy way to do this is to always include a space, and then manually cut the glyph + // margin in the text layout. + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + [ + "greet ", + "worldhello ", + "world ", + "toast ", + "and ", + "jam ", + "the ", + "end ", + "hi" + ] + ); + + // TODO: multiline phantom text in the middle + // TODO: test at the end + } + + #[test] + fn num_vlines() { + let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); + let (text_prov, lines) = make_lines(&text, 2., true); + assert_eq!(lines.num_vlines(&text_prov), 10); + + // With phantom text + let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 0, "greet\nworld")], + }, + ); + + let (text_prov, lines) = make_lines_ph(&text, 2., true, ph); + + // Only one increase because the second line of the phantom text is directly attached to + // the word at the start of the next line. + assert_eq!(lines.num_vlines(&text_prov), 11); + } + + #[test] + fn offset_to_line() { + let text = "a b c d ".into(); + let (text_prov, lines) = make_lines(&text, 1., true); + assert_eq!(lines.num_vlines(&text_prov), 4); + + let vlines = [0, 0, 1, 1, 2, 2, 3, 3]; + for (i, v) in vlines.iter().enumerate() { + assert_eq!( + lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward), + VLine(*v), + "offset: {i}" + ); + } + + assert_eq!(lines.offset_of_vline(&text_prov, VLine(0)), 0); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(1)), 2); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(2)), 4); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(3)), 6); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(10)), 8); + + for offset in 0..text.len() { + let line = + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Forward); + let line_offset = lines.offset_of_vline(&text_prov, line); + assert!( + line_offset <= offset, + "{} <= {} L{:?} O{}", + line_offset, + offset, + line, + offset + ); + } + + let text = "blah\n\n\nhi\na b c d e".into(); + let (text_prov, lines) = make_lines(&text, 12.0 * 3.0, true); + let vlines = [0, 0, 0, 0, 0]; + for (i, v) in vlines.iter().enumerate() { + assert_eq!( + lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward), + VLine(*v), + "offset: {i}" + ); + } + assert_eq!( + lines + .vline_of_offset(&text_prov, 4, CursorAffinity::Backward) + .get(), + 0 + ); + // Test that cursor affinity has no effect for hard line breaks + assert_eq!( + lines + .vline_of_offset(&text_prov, 5, CursorAffinity::Forward) + .get(), + 1 + ); + assert_eq!( + lines + .vline_of_offset(&text_prov, 5, CursorAffinity::Backward) + .get(), + 1 + ); + // starts at 'd'. Tests that cursor affinity works for soft line breaks + assert_eq!( + lines + .vline_of_offset(&text_prov, 16, CursorAffinity::Forward) + .get(), + 5 + ); + assert_eq!( + lines + .vline_of_offset(&text_prov, 16, CursorAffinity::Backward) + .get(), + 4 + ); + + assert_eq!( + lines.vline_of_offset(&text_prov, 20, CursorAffinity::Forward), + lines.last_vline(&text_prov) + ); + + let text = "a\nb\nc\n".into(); + let (text_prov, lines) = make_lines(&text, 1., true); + assert_eq!(lines.num_vlines(&text_prov), 4); + + // let vlines = [(0, 0), (0, 0), (1, 1), (1, 1), (2, 2), (2, 2), (3, 3)]; + let vlines = [0, 0, 1, 1, 2, 2, 3, 3]; + for (i, v) in vlines.iter().enumerate() { + assert_eq!( + lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward), + VLine(*v), + "offset: {i}" + ); + assert_eq!( + lines.vline_of_offset(&text_prov, i, CursorAffinity::Backward), + VLine(*v), + "offset: {i}" + ); + } + + let text = Rope::from( + "asdf\nposition: Some(EditorPosition::Offset(self.offset))\nasdf\nasdf", + ); + let (text_prov, mut lines) = make_lines(&text, 1., true); + println!("Breaks: {:?}", render_breaks(&text, &mut lines, FONT_SIZE)); + + let rvline = lines.rvline_of_offset(&text_prov, 3, CursorAffinity::Backward); + assert_eq!(rvline, RVLine::new(0, 0)); + let rvline_info = lines + .iter_rvlines(&text_prov, false, rvline) + .next() + .unwrap(); + assert_eq!(rvline_info.rvline, rvline); + let offset = lines.offset_of_rvline(&text_prov, rvline); + assert_eq!(offset, 0); + assert_eq!( + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Backward), + VLine(0) + ); + assert_eq!(lines.vline_of_rvline(&text_prov, rvline), VLine(0)); + + let rvline = lines.rvline_of_offset(&text_prov, 7, CursorAffinity::Backward); + assert_eq!(rvline, RVLine::new(1, 0)); + let rvline_info = lines + .iter_rvlines(&text_prov, false, rvline) + .next() + .unwrap(); + assert_eq!(rvline_info.rvline, rvline); + let offset = lines.offset_of_rvline(&text_prov, rvline); + assert_eq!(offset, 5); + assert_eq!( + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Backward), + VLine(1) + ); + assert_eq!(lines.vline_of_rvline(&text_prov, rvline), VLine(1)); + + let rvline = + lines.rvline_of_offset(&text_prov, 17, CursorAffinity::Backward); + assert_eq!(rvline, RVLine::new(1, 1)); + let rvline_info = lines + .iter_rvlines(&text_prov, false, rvline) + .next() + .unwrap(); + assert_eq!(rvline_info.rvline, rvline); + let offset = lines.offset_of_rvline(&text_prov, rvline); + assert_eq!(offset, 15); + assert_eq!( + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Backward), + VLine(1) + ); + assert_eq!( + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Forward), + VLine(2) + ); + assert_eq!(lines.vline_of_rvline(&text_prov, rvline), VLine(2)); + } + + #[test] + fn offset_to_line_phantom() { + let text = "a b c d ".into(); + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 1, "hi")], + }, + ); + + let (text_prov, mut lines) = make_lines_ph(&text, 1., true, ph); + + // The 'hi' is joined with the 'a' so it's not wrapped to a separate line + assert_eq!(lines.num_vlines(&text_prov), 4); + + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["ahi ", "b ", "c ", "d "] + ); + + let vlines = [0, 0, 1, 1, 2, 2, 3, 3]; + // Unchanged. The phantom text has no effect in the position. It doesn't shift a line with + // the affinity due to its position and it isn't multiline. + for (i, v) in vlines.iter().enumerate() { + assert_eq!( + lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward), + VLine(*v), + "offset: {i}" + ); + } + + assert_eq!(lines.offset_of_vline(&text_prov, VLine(0)), 0); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(1)), 2); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(2)), 4); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(3)), 6); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(10)), 8); + + for offset in 0..text.len() { + let line = + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Forward); + let line_offset = lines.offset_of_vline(&text_prov, line); + assert!( + line_offset <= offset, + "{} <= {} L{:?} O{}", + line_offset, + offset, + line, + offset + ); + } + + // Same as above but with a slightly shifted to make the affinity change the resulting vline + let mut ph = HashMap::new(); + ph.insert( + 0, + PhantomTextLine { + text: smallvec![mph(PhantomTextKind::Completion, 2, "hi")], + }, + ); + + let (text_prov, mut lines) = make_lines_ph(&text, 1., true, ph); + + // The 'hi' is joined with the 'a' so it's not wrapped to a separate line + assert_eq!(lines.num_vlines(&text_prov), 4); + + // TODO: Should this really be forward rendered? + assert_eq!( + render_breaks(&text, &mut lines, FONT_SIZE), + ["a ", "hib ", "c ", "d "] + ); + + for (i, v) in vlines.iter().enumerate() { + assert_eq!( + lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward), + VLine(*v), + "offset: {i}" + ); + } + assert_eq!( + lines.vline_of_offset(&text_prov, 2, CursorAffinity::Backward), + VLine(0) + ); + + assert_eq!(lines.offset_of_vline(&text_prov, VLine(0)), 0); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(1)), 2); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(2)), 4); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(3)), 6); + assert_eq!(lines.offset_of_vline(&text_prov, VLine(10)), 8); + + for offset in 0..text.len() { + let line = + lines.vline_of_offset(&text_prov, offset, CursorAffinity::Forward); + let line_offset = lines.offset_of_vline(&text_prov, line); + assert!( + line_offset <= offset, + "{} <= {} L{:?} O{}", + line_offset, + offset, + line, + offset + ); + } + } + + #[test] + fn iter_lines() { + let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); + let (text_prov, lines) = make_lines(&text, 2., true); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(0)) + .take(2) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec!["aaaa", "bb "]); + + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(1)) + .take(2) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec!["bb ", "bb "]); + + let v = lines.get_init_text_layout(0, &text_prov, 2, true); + let v = v.layout_cols(&text_prov, 2).collect::>(); + assert_eq!(v, [(0, 3), (3, 8), (8, 13), (13, 15)]); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(3)) + .take(3) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec!["cc", "cc ", "dddd "]); + + let mut r: Vec<_> = lines.iter_vlines(&text_prov, false, VLine(0)).collect(); + r.reverse(); + let r1: Vec<_> = lines + .iter_vlines(&text_prov, true, lines.last_vline(&text_prov)) + .collect(); + assert_eq!(r, r1); + + let rel1: Vec<_> = lines + .iter_rvlines(&text_prov, false, RVLine::new(0, 0)) + .map(|i| i.rvline) + .collect(); + r.reverse(); // revert back + assert!(r.iter().map(|i| i.rvline).eq(rel1)); + + // Empty initialized + let text: Rope = "".into(); + let (text_prov, lines) = make_lines(&text, 2., true); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(0)) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec![""]); + // Empty initialized - Out of bounds + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(1)) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, Vec::<&str>::new()); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(2)) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, Vec::<&str>::new()); + + let mut r: Vec<_> = lines.iter_vlines(&text_prov, false, VLine(0)).collect(); + r.reverse(); + let r1: Vec<_> = lines + .iter_vlines(&text_prov, true, lines.last_vline(&text_prov)) + .collect(); + assert_eq!(r, r1); + + let rel1: Vec<_> = lines + .iter_rvlines(&text_prov, false, RVLine::new(0, 0)) + .map(|i| i.rvline) + .collect(); + r.reverse(); // revert back + assert!(r.iter().map(|i| i.rvline).eq(rel1)); + + // Empty uninitialized + let text: Rope = "".into(); + let (text_prov, lines) = make_lines(&text, 2., false); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(0)) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec![""]); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(1)) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, Vec::<&str>::new()); + let r: Vec<_> = lines + .iter_vlines(&text_prov, false, VLine(2)) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, Vec::<&str>::new()); + + let mut r: Vec<_> = lines.iter_vlines(&text_prov, false, VLine(0)).collect(); + r.reverse(); + let r1: Vec<_> = lines + .iter_vlines(&text_prov, true, lines.last_vline(&text_prov)) + .collect(); + assert_eq!(r, r1); + + let rel1: Vec<_> = lines + .iter_rvlines(&text_prov, false, RVLine::new(0, 0)) + .map(|i| i.rvline) + .collect(); + r.reverse(); // revert back + assert!(r.iter().map(|i| i.rvline).eq(rel1)); + + // TODO: clean up the above tests with some helper function. Very noisy at the moment. + // TODO: phantom text iter lines tests? + } + + // TODO(minor): Deduplicate the test code between this and iter_lines + // We're just testing whether it has equivalent behavior to iter lines (when lines are + // initialized) + #[test] + fn init_iter_vlines() { + let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); + let (text_prov, lines) = make_lines(&text, 2., false); + let r: Vec<_> = lines + .iter_vlines_init(&text_prov, 0, VLine(0), true) + .take(2) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec!["aaaa", "bb "]); + + let r: Vec<_> = lines + .iter_vlines_init(&text_prov, 0, VLine(1), true) + .take(2) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec!["bb ", "bb "]); + + let r: Vec<_> = lines + .iter_vlines_init(&text_prov, 0, VLine(3), true) + .take(3) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec!["cc", "cc ", "dddd "]); + + // Empty initialized + let text: Rope = "".into(); + let (text_prov, lines) = make_lines(&text, 2., false); + let r: Vec<_> = lines + .iter_vlines_init(&text_prov, 0, VLine(0), true) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, vec![""]); + let r: Vec<_> = lines + .iter_vlines_init(&text_prov, 0, VLine(1), true) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, Vec::<&str>::new()); + let r: Vec<_> = lines + .iter_vlines_init(&text_prov, 0, VLine(2), true) + .map(|l| text.slice_to_cow(l.interval)) + .collect(); + assert_eq!(r, Vec::<&str>::new()); + } + + #[test] + fn line_numbers() { + let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); + let (text_prov, lines) = make_lines(&text, 12.0 * 2.0, true); + let get_nums = |start_vline: usize| { + lines + .iter_vlines(&text_prov, false, VLine(start_vline)) + .map(|l| { + ( + l.rvline.line, + l.vline.get(), + l.is_first(), + text.slice_to_cow(l.interval), + ) + }) + .collect::>() + }; + // (line, vline, is_first, text) + let x = vec![ + (0, 0, true, "aaaa".into()), + (1, 1, true, "bb ".into()), + (1, 2, false, "bb ".into()), + (1, 3, false, "cc\n".into()), // TODO: why does this have \n but the first line doesn't?? + (2, 4, true, "cc ".into()), + (2, 5, false, "dddd ".into()), + (2, 6, false, "eeee ".into()), + (2, 7, false, "ff\n".into()), + (3, 8, true, "ff ".into()), + (3, 9, false, "gggg".into()), + ]; + + // This ensures that there's no inconsistencies between starting at a specific index + // vs starting at zero and iterating to that index. + for i in 0..x.len() { + let nums = get_nums(i); + println!("i: {i}, #nums: {}, #&x[i..]: {}", nums.len(), x[i..].len()); + assert_eq!(nums, &x[i..], "failed at #{i}"); + } + + // TODO: test this without any wrapping + } + + #[test] + fn last_col() { + let text: Rope = Rope::from("conf = Config::default();"); + let (text_prov, lines) = make_lines(&text, 24.0 * 2.0, true); + + let mut iter = lines.iter_rvlines(&text_prov, false, RVLine::default()); + + // "conf = " + let v = iter.next().unwrap(); + assert_eq!(v.last_col(&text_prov, false), 6); + assert_eq!(v.last_col(&text_prov, true), 7); + + // "Config::default();" + let v = iter.next().unwrap(); + assert_eq!(v.last_col(&text_prov, false), 24); + assert_eq!(v.last_col(&text_prov, true), 25); + } +} diff --git a/lapce-app/src/editor_tab.rs b/lapce-app/src/editor_tab.rs index bb1b55138b..2c17c50fe0 100644 --- a/lapce-app/src/editor_tab.rs +++ b/lapce-app/src/editor_tab.rs @@ -238,7 +238,7 @@ impl EditorTabChild { let (svg, color) = config.file_svg(&path); ( svg, - color.cloned(), + color, path.file_name() .unwrap_or_default() .to_string_lossy() @@ -249,7 +249,7 @@ impl EditorTabChild { } None => ( config.ui_svg(LapceIcons::FILE), - Some(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)), + Some(config.color(LapceColor::LAPCE_ICON_ACTIVE)), "local".to_string(), create_rw_signal(true), true, @@ -267,41 +267,67 @@ impl EditorTabChild { let config = config.get(); let diff_editor_data = diff_editors .with(|diff_editors| diff_editors.get(&diff_editor_id).cloned()); - let confirmed = diff_editor_data.as_ref().map(|d| d.right.confirmed); - let path = if let Some(diff_editor_data) = diff_editor_data { - let (content, is_pristine) = - diff_editor_data.right.view.doc.with(|doc| { - (doc.content.get(), doc.buffer.with(|b| b.is_pristine())) - }); - match content { - DocContent::File { path, .. } => Some((path, is_pristine)), - DocContent::Local => None, - DocContent::History(_) => None, - DocContent::Scratch { name, .. } => { - Some((PathBuf::from(name), is_pristine)) - } - } - } else { - None - }; - let (icon, color, path, is_pristine) = match path { - Some((path, is_pritine)) => { + let confirmed = diff_editor_data.as_ref().map(|d| d.confirmed); + + let info = diff_editor_data + .map(|diff_editor_data| { + [diff_editor_data.left, diff_editor_data.right].map(|data| { + let (content, is_pristine) = data.view.doc.with(|doc| { + ( + doc.content.get(), + doc.buffer.with(|b| b.is_pristine()), + ) + }); + match content { + DocContent::File { path, .. } => { + Some((path, is_pristine)) + } + DocContent::Local => None, + DocContent::History(_) => None, + DocContent::Scratch { name, .. } => { + Some((PathBuf::from(name), is_pristine)) + } + } + }) + }) + .unwrap_or([None, None]); + + let (icon, color, path, is_pristine) = match info { + [Some((path, is_pristine)), None] + | [None, Some((path, is_pristine))] => { let (svg, color) = config.file_svg(&path); ( svg, - color.cloned(), + color, format!( "{} (Diff)", path.file_name() .unwrap_or_default() .to_string_lossy() ), - is_pritine, + is_pristine, ) } - None => ( + [Some((left_path, left_is_pristine)), Some((right_path, right_is_pristine))] => + { + let (svg, color) = + config.files_svg(&[&left_path, &right_path]); + let [left_file_name, right_file_name] = + [&left_path, &right_path].map(|path| { + path.file_name() + .unwrap_or_default() + .to_string_lossy() + }); + ( + svg, + color, + format!("{left_file_name} - {right_file_name} (Diff)"), + left_is_pristine && right_is_pristine, + ) + } + [None, None] => ( config.ui_svg(LapceIcons::FILE), - Some(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)), + Some(config.color(LapceColor::LAPCE_ICON_ACTIVE)), "local".to_string(), true, ), @@ -318,7 +344,7 @@ impl EditorTabChild { let config = config.get(); EditorTabChildViewInfo { icon: config.ui_svg(LapceIcons::SETTINGS), - color: Some(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)), + color: Some(config.color(LapceColor::LAPCE_ICON_ACTIVE)), path: "Settings".to_string(), confirmed: None, is_pristine: true, @@ -328,7 +354,7 @@ impl EditorTabChild { let config = config.get(); EditorTabChildViewInfo { icon: config.ui_svg(LapceIcons::SYMBOL_COLOR), - color: Some(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)), + color: Some(config.color(LapceColor::LAPCE_ICON_ACTIVE)), path: "Theme Colors".to_string(), confirmed: None, is_pristine: true, @@ -338,7 +364,7 @@ impl EditorTabChild { let config = config.get(); EditorTabChildViewInfo { icon: config.ui_svg(LapceIcons::KEYBOARD), - color: Some(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)), + color: Some(config.color(LapceColor::LAPCE_ICON_ACTIVE)), path: "Keyboard Shortcuts".to_string(), confirmed: None, is_pristine: true, @@ -361,7 +387,7 @@ impl EditorTabChild { .unwrap_or_else(|| id.name.clone()); EditorTabChildViewInfo { icon: config.ui_svg(LapceIcons::EXTENSIONS), - color: Some(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)), + color: Some(config.color(LapceColor::LAPCE_ICON_ACTIVE)), path: display_name, confirmed: None, is_pristine: true, @@ -429,11 +455,8 @@ impl EditorTabData { } EditorTabChild::DiffEditor(diff_editor_id) => { if let Some(diff_editor) = diff_editors.get(diff_editor_id) { - let left_confirmed = - diff_editor.left.confirmed.get_untracked(); - let right_confirmed = - diff_editor.right.confirmed.get_untracked(); - if !left_confirmed && !right_confirmed { + let confirmed = diff_editor.confirmed.get_untracked(); + if !confirmed { return Some((i, child.clone())); } } @@ -444,24 +467,6 @@ impl EditorTabData { None } - pub fn get_unconfirmed_editor( - &self, - editors: &im::HashMap>, - ) -> Option<(usize, RwSignal)> { - for (i, child) in self.children.iter().enumerate() { - if let (_, _, EditorTabChild::Editor(editor_id)) = child { - if let Some(editor) = editors.get(editor_id) { - let e = editor.get_untracked(); - let confirmed = e.confirmed.get_untracked(); - if !confirmed { - return Some((i, *editor)); - } - } - } - } - None - } - pub fn tab_info(&self, data: &WindowTabData) -> EditorTabInfo { let info = EditorTabInfo { active: self.active, diff --git a/lapce-app/src/file_explorer/data.rs b/lapce-app/src/file_explorer/data.rs index 9f43fad7b4..0484a699aa 100644 --- a/lapce-app/src/file_explorer/data.rs +++ b/lapce-app/src/file_explorer/data.rs @@ -1,22 +1,120 @@ use std::{ + borrow::Cow, collections::HashMap, + ffi::OsStr, path::{Path, PathBuf}, rc::Rc, }; use floem::{ + action::show_context_menu, ext_event::create_ext_action, + keyboard::ModifiersState, + menu::{Menu, MenuItem}, reactive::{RwSignal, Scope}, + EventPropagation, +}; +use lapce_core::{ + command::{EditCommand, FocusCommand}, + mode::Mode, +}; +use lapce_rpc::{ + file::{FileNodeItem, RenameState}, + proxy::ProxyResponse, }; -use lapce_rpc::{file::FileNodeItem, proxy::ProxyResponse}; -use crate::{command::InternalCommand, window_tab::CommonData}; +use crate::{ + command::{CommandExecuted, CommandKind, InternalCommand, LapceCommand}, + editor::EditorData, + id::EditorId, + keypress::{condition::Condition, KeyPressFocus}, + window_tab::CommonData, +}; + +enum RenamedPath { + NotRenaming, + NameUnchanged, + Renamed { + current_path: PathBuf, + new_path: PathBuf, + }, +} #[derive(Clone)] pub struct FileExplorerData { pub id: RwSignal, pub root: RwSignal, + pub rename_state: RwSignal, + pub rename_editor_data: EditorData, pub common: Rc, + left_diff_path: RwSignal>, +} + +impl KeyPressFocus for FileExplorerData { + fn get_mode(&self) -> Mode { + Mode::Insert + } + + fn check_condition(&self, condition: Condition) -> bool { + self.rename_state + .with_untracked(RenameState::is_accepting_input) + && condition == Condition::ModalFocus + } + + fn run_command( + &self, + command: &LapceCommand, + count: Option, + mods: ModifiersState, + ) -> CommandExecuted { + if self + .rename_state + .with_untracked(RenameState::is_accepting_input) + { + match command.kind { + CommandKind::Focus(FocusCommand::ModalClose) => { + self.cancel_rename(); + CommandExecuted::Yes + } + CommandKind::Edit(EditCommand::InsertNewLine) => { + self.finish_rename(); + CommandExecuted::Yes + } + CommandKind::Edit(_) => { + let command_executed = + self.rename_editor_data.run_command(command, count, mods); + + if let RenamedPath::Renamed { new_path, .. } = + self.renamed_path() + { + self.common + .internal_command + .send(InternalCommand::TestRenamePath { new_path }); + } + + command_executed + } + _ => self.rename_editor_data.run_command(command, count, mods), + } + } else { + CommandExecuted::No + } + } + + fn receive_char(&self, c: &str) { + if self + .rename_state + .with_untracked(RenameState::is_accepting_input) + { + self.rename_editor_data.receive_char(c); + + if let RenamedPath::Renamed { new_path, .. } = self.renamed_path() { + self.common + .internal_command + .send(InternalCommand::TestRenamePath { new_path }); + } + } + } } impl FileExplorerData { @@ -30,10 +128,16 @@ impl FileExplorerData { children: HashMap::new(), children_open_count: 0, }); + let rename_state = cx.create_rw_signal(RenameState::NotRenaming); + let rename_editor_data = + EditorData::new_local(cx, EditorId::next(), common.clone()); let data = Self { id: cx.create_rw_signal(0), root, + rename_state, + rename_editor_data, common, + left_diff_path: cx.create_rw_signal(None), }; if data.common.workspace.path.is_some() { // only fill in the child files if there is open folder @@ -123,12 +227,74 @@ impl FileExplorerData { }); } + /// Returns `true` if `path` exists in the file explorer tree and is a directory, `false` + /// otherwise. + fn is_dir(&self, path: &Path) -> bool { + self.root.with_untracked(|root| { + root.get_file_node(path).is_some_and(|node| node.is_dir) + }) + } + + /// If there is an in progress rename and the user has entered a path that differs from the + /// current path, gets the current and new paths of the renamed node. + fn renamed_path(&self) -> RenamedPath { + self.rename_state.with_untracked(|rename_state| { + if let Some(current_path) = rename_state.path() { + let current_file_name = current_path.file_name().unwrap_or_default(); + // `new_relative_path` is the new path relative to the parent directory, unless the + // user has entered an absolute path. + let new_relative_path = self.rename_editor_data.view.text(); + + let new_relative_path: Cow = + match new_relative_path.slice_to_cow(..) { + Cow::Borrowed(path) => Cow::Borrowed(path.as_ref()), + Cow::Owned(path) => Cow::Owned(path.into()), + }; + + if new_relative_path == current_file_name { + RenamedPath::NameUnchanged + } else { + let new_path = current_path + .parent() + .unwrap_or("".as_ref()) + .join(new_relative_path); + + RenamedPath::Renamed { + current_path: current_path.to_owned(), + new_path, + } + } + } else { + RenamedPath::NotRenaming + } + }) + } + + /// If a rename is in progress and the user has entered a path that differs from the current + /// path, sends a request to perform the rename. + pub fn finish_rename(&self) { + match self.renamed_path() { + RenamedPath::NotRenaming => (), + RenamedPath::NameUnchanged => self.cancel_rename(), + RenamedPath::Renamed { + current_path, + new_path, + } => self.common.internal_command.send( + InternalCommand::FinishRenamePath { + current_path, + new_path, + }, + ), + } + } + + /// Closes the rename text box without renaming the item. + pub fn cancel_rename(&self) { + self.rename_state.set(RenameState::NotRenaming); + } + pub fn click(&self, path: &Path) { - let is_dir = self - .root - .with_untracked(|root| root.get_file_node(path).map(|n| n.is_dir)) - .unwrap_or(false); - if is_dir { + if self.is_dir(path) { self.toggle_expand(path); } else { self.common @@ -139,35 +305,67 @@ impl FileExplorerData { } } - pub fn double_click(&self, path: &Path) -> bool { - let is_dir = self - .root - .with_untracked(|root| root.get_file_node(path).map(|n| n.is_dir)) - .unwrap_or(false); - if is_dir { - false + pub fn double_click(&self, path: &Path) -> EventPropagation { + if self.is_dir(path) { + EventPropagation::Continue } else { self.common .internal_command .send(InternalCommand::MakeConfirmed); - true + EventPropagation::Stop } } - pub fn middle_click(&self, path: &Path) -> bool { - let is_dir = self - .root - .with_untracked(|root| root.get_file_node(path).map(|n| n.is_dir)) - .unwrap_or(false); - if is_dir { - false + pub fn secondary_click(&self, path: &Path) { + let menu = { + let common = self.common.clone(); + let path = path.to_owned(); + let left_path = path.clone(); + let left_diff_path = self.left_diff_path; + + Menu::new("") + .entry(MenuItem::new("Rename...").action(move || { + common + .internal_command + .send(InternalCommand::StartRenamePath { + path: path.clone(), + }); + })) + .entry( + MenuItem::new("Select for Compare") + .action(move || left_diff_path.set(Some(left_path.clone()))), + ) + }; + + let menu = if let Some(left_path) = self.left_diff_path.get_untracked() { + let common = self.common.clone(); + let right_path = path.to_owned(); + + menu.entry(MenuItem::new("Compare with Selected").action(move || { + common + .internal_command + .send(InternalCommand::OpenDiffFiles { + left_path: left_path.clone(), + right_path: right_path.clone(), + }) + })) + } else { + menu + }; + + show_context_menu(menu, None); + } + + pub fn middle_click(&self, path: &Path) -> EventPropagation { + if self.is_dir(path) { + EventPropagation::Continue } else { self.common .internal_command .send(InternalCommand::OpenFileInNewTab { path: path.to_path_buf(), }); - true + EventPropagation::Stop } } } diff --git a/lapce-app/src/file_explorer/node.rs b/lapce-app/src/file_explorer/node.rs index 4b15878fe6..01bba66e19 100644 --- a/lapce-app/src/file_explorer/node.rs +++ b/lapce-app/src/file_explorer/node.rs @@ -1,28 +1,49 @@ -use floem::views::VirtualListVector; - +use floem::views::VirtualVector; use lapce_rpc::file::{FileNodeItem, FileNodeViewData}; -pub struct FileNodeVirtualList(pub FileNodeItem); +use lapce_rpc::file::RenameState; + +pub struct FileNodeVirtualList { + file_node_item: FileNodeItem, + rename_state: RenameState, +} -impl VirtualListVector for FileNodeVirtualList { - type ItemIterator = Box>; +impl FileNodeVirtualList { + pub fn new(file_node_item: FileNodeItem, rename_state: RenameState) -> Self { + Self { + file_node_item, + rename_state, + } + } +} +impl VirtualVector for FileNodeVirtualList { fn total_len(&self) -> usize { - self.0.children_open_count + self.file_node_item.children_open_count } - fn slice(&mut self, range: std::ops::Range) -> Self::ItemIterator { + fn slice( + &mut self, + range: std::ops::Range, + ) -> impl Iterator { let min = range.start; let max = range.end; let mut i = 0; let mut view_items = Vec::new(); - for item in self.0.sorted_children() { - i = item.append_view_slice(&mut view_items, min, max, i + 1, 0); + for item in self.file_node_item.sorted_children() { + i = item.append_view_slice( + &mut view_items, + &self.rename_state, + min, + max, + i + 1, + 0, + ); if i > max { - return Box::new(view_items.into_iter()); + return view_items.into_iter(); } } - Box::new(view_items.into_iter()) + view_items.into_iter() } } diff --git a/lapce-app/src/file_explorer/view.rs b/lapce-app/src/file_explorer/view.rs index 28733a7c8c..737661dd24 100644 --- a/lapce-app/src/file_explorer/view.rs +++ b/lapce-app/src/file_explorer/view.rs @@ -1,18 +1,21 @@ -use std::rc::Rc; +use std::{path::Path, rc::Rc}; use floem::{ cosmic_text::Style as FontStyle, event::{Event, EventListener}, peniko::Color, reactive::{create_rw_signal, RwSignal}, - style::{CursorStyle, Style}, + style::{AlignItems, CursorStyle, Position, Style}, view::View, views::{ - container, label, list, scroll, stack, svg, virtual_list, Decorators, - VirtualListDirection, VirtualListItemSize, + container, container_box, dyn_stack, label, scroll, stack, svg, + virtual_stack, Decorators, VirtualDirection, VirtualItemSize, }, EventPropagation, }; +use lapce_core::selection::Selection; +use lapce_rpc::file::{FileNodeViewData, IsRenaming, RenameState}; +use lapce_xi_rope::Rope; use super::{data::FileExplorerData, node::FileNodeVirtualList}; use crate::{ @@ -20,11 +23,45 @@ use crate::{ command::InternalCommand, config::{color::LapceColor, icon::LapceIcons}, editor_tab::{EditorTabChild, EditorTabData}, - panel::{position::PanelPosition, view::panel_header}, + panel::{kind::PanelKind, position::PanelPosition, view::panel_header}, plugin::PluginData, - window_tab::WindowTabData, + text_input::text_input, + window_tab::{Focus, WindowTabData}, }; +/// Blends `foreground` with `background`. +/// +/// Uses the alpha channel from `foreground` - if `foreground` is opaque, `foreground` will be +/// returned unchanged. +/// +/// The result is always opaque regardless of the transparency of the inputs. +fn blend_colors(background: Color, foreground: Color) -> Color { + let Color { + r: background_r, + g: background_g, + b: background_b, + .. + } = background; + let Color { + r: foreground_r, + g: foreground_g, + b: foreground_b, + a, + } = foreground; + let a: u16 = a.into(); + + let [r, g, b] = [ + [background_r, foreground_r], + [background_g, foreground_g], + [background_b, foreground_b], + ] + .map(|x| x.map(u16::from)) + .map(|[b, f]| (a * f + (255 - a) * b) / 255) + .map(|x| x as u8); + + Color { r, g, b, a: 255 } +} + pub fn file_explorer_panel( window_tab_data: Rc, position: PanelPosition, @@ -54,27 +91,166 @@ pub fn file_explorer_panel( }) } +fn initialize_rename_editor(data: &FileExplorerData, path: &Path) { + let file_name = path.file_name().unwrap_or_default().to_string_lossy(); + // Start with the part of the file or directory name before the extension + // selected. + let selection_end = { + let without_leading_dot = file_name.strip_prefix('.').unwrap_or(&file_name); + let idx = without_leading_dot + .find('.') + .unwrap_or(without_leading_dot.len()); + + idx + file_name.len() - without_leading_dot.len() + }; + + let doc = data.rename_editor_data.view.doc.get_untracked(); + doc.reload(Rope::from(&file_name), true); + data.rename_editor_data + .cursor + .update(|cursor| cursor.set_insert(Selection::region(0, selection_end))); + + data.rename_state + .update(|rename_state| rename_state.set_editor_needs_reset(false)); +} + +fn file_node_text_view( + data: FileExplorerData, + node: FileNodeViewData, + path: &Path, +) -> impl View { + let ui_line_height = data.common.ui_line_height; + + let view = if let IsRenaming::Renaming { err } = node.is_renaming { + let rename_editor_data = data.rename_editor_data.clone(); + let text_input_file_explorer_data = data.clone(); + let focus = data.common.focus; + let config = data.common.config; + + if data + .rename_state + .with_untracked(RenameState::editor_needs_reset) + { + initialize_rename_editor(&data, path); + } + + let text_input_view = text_input(rename_editor_data.clone(), move || { + focus.with_untracked(|focus| { + focus == &Focus::Panel(PanelKind::FileExplorer) + }) + }) + .on_event_stop(EventListener::FocusLost, move |_| { + data.finish_rename(); + data.rename_state + .set(lapce_rpc::file::RenameState::NotRenaming); + }) + .on_event(EventListener::KeyDown, move |event| { + if let Event::KeyDown(event) = event { + let keypress = rename_editor_data.common.keypress.get_untracked(); + if keypress.key_down(event, &text_input_file_explorer_data) { + EventPropagation::Stop + } else { + EventPropagation::Continue + } + } else { + EventPropagation::Continue + } + }) + .style(move |s| { + s.width_full() + .height(ui_line_height.get()) + .padding(0.0) + .margin(0.0) + .border_radius(6.0) + .border(1.0) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) + }); + + let text_input_id = text_input_view.id(); + text_input_id.request_focus(); + + if let Some(err) = err { + container_box( + stack(( + text_input_view, + label(move || err.clone()).style(move |s| { + let config = config.get(); + + let editor_background_color = + config.color(LapceColor::PANEL_CURRENT_BACKGROUND); + let error_background_color = + config.color(LapceColor::ERROR_LENS_ERROR_BACKGROUND); + + let background_color = blend_colors( + editor_background_color, + error_background_color, + ); + + s.position(Position::Absolute) + .inset_top(ui_line_height.get()) + .width_full() + .color( + config + .color(LapceColor::ERROR_LENS_ERROR_FOREGROUND), + ) + .background(background_color) + .z_index(100) + }), + )) + .style(|s| s.flex_grow(1.0)), + ) + } else { + container_box(text_input_view) + } + } else { + container_box( + label(move || { + node.path + .file_name() + .map(|f| f.to_string_lossy().to_string()) + .unwrap_or_default() + }) + .style(move |s| s.flex_grow(1.0).height(ui_line_height.get())), + ) + }; + + view.style(|s| s.flex_grow(1.0).padding(0.0).margin(0.0)) +} + fn new_file_node_view(data: FileExplorerData) -> impl View { let root = data.root; let ui_line_height = data.common.ui_line_height; let config = data.common.config; - virtual_list( - VirtualListDirection::Vertical, - VirtualListItemSize::Fixed(Box::new(move || ui_line_height.get())), - move || FileNodeVirtualList(root.get()), - move |node| (node.path.clone(), node.is_dir, node.open, node.level), + virtual_stack( + VirtualDirection::Vertical, + VirtualItemSize::Fixed(Box::new(move || ui_line_height.get())), + move || FileNodeVirtualList::new(root.get(), data.rename_state.get()), + move |node| { + ( + node.path.clone(), + node.is_dir, + node.open, + node.is_renaming.clone(), + node.level, + ) + }, move |node| { let level = node.level; let data = data.clone(); + let click_data = data.clone(); let double_click_data = data.clone(); + let secondary_click_data = data.clone(); let aux_click_data = data.clone(); let path = node.path.clone(); let click_path = node.path.clone(); let double_click_path = node.path.clone(); + let secondary_click_path = node.path.clone(); let aux_click_path = path.clone(); let open = node.open; let is_dir = node.is_dir; - stack(( + let is_renaming = node.is_renaming.clone(); + + let view = stack(( svg(move || { let config = config.get(); let svg_str = match open { @@ -88,11 +264,14 @@ fn new_file_node_view(data: FileExplorerData) -> impl View { let size = config.ui.icon_size() as f32; let color = if is_dir { - *config.get_color(LapceColor::LAPCE_ICON_ACTIVE) + config.color(LapceColor::LAPCE_ICON_ACTIVE) } else { Color::TRANSPARENT }; - s.size(size, size).margin_left(10.0).color(color) + s.size(size, size) + .flex_shrink(0.0) + .margin_left(10.0) + .color(color) }), { let path = path.clone(); @@ -114,61 +293,59 @@ fn new_file_node_view(data: FileExplorerData) -> impl View { let size = config.ui.icon_size() as f32; s.size(size, size) + .flex_shrink(0.0) .margin_horiz(6.0) .apply_if(is_dir, |s| { - s.color( - *config.get_color(LapceColor::LAPCE_ICON_ACTIVE), - ) + s.color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) }) .apply_if(!is_dir, |s| { s.apply_opt( - config.file_svg(&path_for_style).1.cloned(), + config.file_svg(&path_for_style).1, Style::color, ) }) }) }, - label(move || { - node.path - .file_name() - .map(|f| f.to_string_lossy().to_string()) - .unwrap_or_default() - }), + file_node_text_view(data, node, &path), )) .style(move |s| { - s.items_center() - .padding_right(10.0) + s.padding_right(5.0) .padding_left((level * 10) as f32) - .min_width_pct(100.0) + .align_items(AlignItems::Center) .hover(|s| { s.background( - *config - .get() - .get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.get().color(LapceColor::PANEL_HOVERED_BACKGROUND), ) .cursor(CursorStyle::Pointer) }) - }) - .on_click_stop(move |_| { - data.click(&click_path); - }) - .on_double_click(move |_| { - if double_click_data.double_click(&double_click_path) { - EventPropagation::Stop - } else { - EventPropagation::Continue - } - }) - .on_event_stop(EventListener::PointerDown, move |event| { - if let Event::PointerDown(pointer_event) = event { - if pointer_event.button.is_auxiliary() { - aux_click_data.middle_click(&aux_click_path); - } - } - }) + }); + + if let IsRenaming::NotRenaming = is_renaming { + view.on_click_stop(move |_| { + click_data.click(&click_path); + }) + .on_double_click(move |_| { + double_click_data.double_click(&double_click_path) + }) + .on_secondary_click_stop(move |_| { + secondary_click_data.secondary_click(&secondary_click_path); + }) + .on_event_stop( + EventListener::PointerDown, + move |event| { + if let Event::PointerDown(pointer_event) = event { + if pointer_event.button.is_auxiliary() { + aux_click_data.middle_click(&aux_click_path); + } + } + }, + ) + } else { + view + } }, ) - .style(|s| s.flex_col().min_width_pct(100.0)) + .style(|s| s.flex_col().align_items(AlignItems::Stretch).width_full()) } fn open_editors_view(window_tab_data: Rc) -> impl View { @@ -247,14 +424,12 @@ fn open_editors_view(window_tab_data: Rc) -> impl View { == child_index.get(), |s| { s.background( - *config.get_color(LapceColor::PANEL_CURRENT_BACKGROUND), + config.color(LapceColor::PANEL_CURRENT_BACKGROUND), ) }, ) .hover(|s| { - s.background( - *config.get_color(LapceColor::PANEL_HOVERED_BACKGROUND), - ) + s.background(config.color(LapceColor::PANEL_HOVERED_BACKGROUND)) }) }) .on_event_cont(EventListener::PointerDown, move |_| { @@ -266,7 +441,7 @@ fn open_editors_view(window_tab_data: Rc) -> impl View { }; scroll( - list( + dyn_stack( move || editor_tabs.get().into_iter().enumerate(), move |(index, (editor_tab_id, _))| (*index, *editor_tab_id), move |(index, (_, editor_tab))| { @@ -274,7 +449,7 @@ fn open_editors_view(window_tab_data: Rc) -> impl View { stack(( label(move || format!("Group {}", index + 1)) .style(|s| s.margin_left(10.0)), - list( + dyn_stack( move || editor_tab.get().children, move |(_, _, child)| child.id(), move |(child_index, _, child)| { diff --git a/lapce-app/src/global_search.rs b/lapce-app/src/global_search.rs index 3ee5c9e29e..d35d6d861d 100644 --- a/lapce-app/src/global_search.rs +++ b/lapce-app/src/global_search.rs @@ -4,7 +4,7 @@ use floem::{ ext_event::create_ext_action, keyboard::ModifiersState, reactive::{Memo, RwSignal, Scope}, - views::VirtualListVector, + views::VirtualVector, }; use indexmap::IndexMap; use lapce_core::{mode::Mode, selection::Selection}; @@ -80,16 +80,9 @@ impl KeyPressFocus for GlobalSearchData { } } -impl VirtualListVector<(PathBuf, SearchMatchData)> for GlobalSearchData { - type ItemIterator = Box>; - +impl VirtualVector<(PathBuf, SearchMatchData)> for GlobalSearchData { fn total_len(&self) -> usize { - 0 - } - - fn total_size(&self) -> Option { - let line_height = self.common.ui_line_height.get(); - let count: usize = self.search_result.with(|result| { + self.search_result.with(|result| { result .iter() .map(|(_, data)| { @@ -100,12 +93,14 @@ impl VirtualListVector<(PathBuf, SearchMatchData)> for GlobalSearchData { } }) .sum() - }); - Some(line_height * count as f64) + }) } - fn slice(&mut self, _range: Range) -> Self::ItemIterator { - Box::new(self.search_result.get().into_iter()) + fn slice( + &mut self, + _range: Range, + ) -> impl Iterator { + self.search_result.get().into_iter() } } diff --git a/lapce-app/src/inline_completion.rs b/lapce-app/src/inline_completion.rs new file mode 100644 index 0000000000..b04a43db7a --- /dev/null +++ b/lapce-app/src/inline_completion.rs @@ -0,0 +1,293 @@ +use std::{borrow::Cow, ops::Range, path::PathBuf, str::FromStr}; + +use floem::reactive::{batch, RwSignal, Scope}; +use lapce_core::{ + buffer::{ + rope_text::{RopeText, RopeTextRef}, + Buffer, + }, + selection::Selection, +}; + +use lsp_types::InsertTextFormat; + +use crate::{ + config::LapceConfig, + doc::{Document, DocumentExt}, + editor::EditorData, + snippet::Snippet, +}; + +// TODO: we could integrate completion lens with this, so it is considered at the same time + +/// Redefinition of lsp types inline completion item with offset range +#[derive(Debug, Clone)] +pub struct InlineCompletionItem { + /// The text to replace the range with. + pub insert_text: String, + /// Text used to decide if this inline completion should be shown. + pub filter_text: Option, + /// The range (of offsets) to replace + pub range: Option>, + pub command: Option, + pub insert_text_format: Option, +} +impl InlineCompletionItem { + pub fn from_lsp(buffer: &Buffer, item: lsp_types::InlineCompletionItem) -> Self { + let range = item.range.map(|r| { + let start = buffer.offset_of_position(&r.start); + let end = buffer.offset_of_position(&r.end); + start..end + }); + Self { + insert_text: item.insert_text, + filter_text: item.filter_text, + range, + command: item.command, + insert_text_format: item.insert_text_format, + } + } + + pub fn apply( + &self, + editor: &EditorData, + start_offset: usize, + ) -> anyhow::Result<()> { + let text_format = self + .insert_text_format + .unwrap_or(InsertTextFormat::PLAIN_TEXT); + + let selection = if let Some(range) = &self.range { + Selection::region(range.start, range.end) + } else { + Selection::caret(start_offset) + }; + + match text_format { + InsertTextFormat::PLAIN_TEXT => editor.do_edit( + &selection, + &[(selection.clone(), self.insert_text.as_str())], + ), + InsertTextFormat::SNIPPET => { + editor.completion_apply_snippet( + &self.insert_text, + &selection, + Vec::new(), + start_offset, + )?; + } + _ => { + // We don't know how to support this text format + } + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InlineCompletionStatus { + /// The inline completion is not active. + Inactive, + /// The inline completion is active and is waiting for the server to respond. + Started, + /// The inline completion is active and has received a response from the server. + Active, +} + +#[derive(Clone)] +pub struct InlineCompletionData { + pub status: InlineCompletionStatus, + /// The active inline completion index in the list of completions. + pub active: RwSignal, + pub items: im::Vector, + pub start_offset: usize, + pub path: PathBuf, +} +impl InlineCompletionData { + pub fn new(cx: Scope) -> Self { + Self { + status: InlineCompletionStatus::Inactive, + active: cx.create_rw_signal(0), + items: im::vector![], + start_offset: 0, + path: PathBuf::new(), + } + } + + pub fn current_item(&self) -> Option<&InlineCompletionItem> { + let active = self.active.get_untracked(); + self.items.get(active) + } + + pub fn next(&mut self) { + if !self.items.is_empty() { + let next_index = (self.active.get_untracked() + 1) % self.items.len(); + self.active.set(next_index); + } + } + + pub fn previous(&mut self) { + if !self.items.is_empty() { + let prev_index = if self.active.get_untracked() == 0 { + self.items.len() - 1 + } else { + self.active.get_untracked() - 1 + }; + self.active.set(prev_index); + } + } + + pub fn cancel(&mut self) { + if self.status == InlineCompletionStatus::Inactive { + return; + } + + self.items.clear(); + self.status = InlineCompletionStatus::Inactive; + } + + /// Set the items for the inline completion. + /// Sets `active` to `0` and `status` to `InlineCompletionStatus::Active`. + pub fn set_items( + &mut self, + items: im::Vector, + start_offset: usize, + path: PathBuf, + ) { + batch(|| { + self.items = items; + self.active.set(0); + self.status = InlineCompletionStatus::Active; + self.start_offset = start_offset; + self.path = path; + }); + } + + pub fn update_doc(&self, doc: &Document, offset: usize) { + if self.status != InlineCompletionStatus::Active { + doc.clear_inline_completion(); + return; + } + + if self.items.is_empty() { + doc.clear_inline_completion(); + return; + } + + let active = self.active.get_untracked(); + let active = if active >= self.items.len() { + self.active.set(0); + 0 + } else { + active + }; + + let item = &self.items[active]; + let text = item.insert_text.clone(); + + // TODO: is range really meant to be used for this? + let offset = item.range.as_ref().map(|r| r.start).unwrap_or(offset); + let (line, col) = doc + .buffer + .with_untracked(|buffer| buffer.offset_to_line_col(offset)); + doc.set_inline_completion(text, line, col); + } + + pub fn update_inline_completion( + &self, + config: &LapceConfig, + doc: &Document, + cursor_offset: usize, + ) { + if !config.editor.enable_inline_completion { + doc.clear_inline_completion(); + return; + } + + let text = doc.buffer.with_untracked(|buffer| buffer.text().clone()); + let text = RopeTextRef::new(&text); + let Some(item) = self.current_item() else { + // TODO(minor): should we cancel completion + return; + }; + + let completion = doc.backend.inline_completion.with_untracked(|cur| { + let cur = cur.as_deref(); + inline_completion_text(text, self.start_offset, cursor_offset, item, cur) + }); + + match completion { + ICompletionRes::Hide => { + doc.clear_inline_completion(); + } + ICompletionRes::Unchanged => {} + ICompletionRes::Set(new, shift) => { + let offset = self.start_offset + shift; + let (line, col) = text.offset_to_line_col(offset); + doc.set_inline_completion(new, line, col); + } + } + } +} + +enum ICompletionRes { + Hide, + Unchanged, + Set(String, usize), +} + +/// Get the text of the inline completion item +fn inline_completion_text( + rope_text: impl RopeText, + start_offset: usize, + cursor_offset: usize, + item: &InlineCompletionItem, + current_completion: Option<&str>, +) -> ICompletionRes { + let text_format = item + .insert_text_format + .unwrap_or(InsertTextFormat::PLAIN_TEXT); + + // TODO: is this check correct? I mostly copied it from completion lens + let cursor_prev_offset = rope_text.prev_code_boundary(cursor_offset); + if let Some(range) = &item.range { + let edit_start = range.start; + + // If the start of the edit isn't where the cursor currently is, and is not at the start of + // the inline completion, then we ignore it. + if cursor_prev_offset != edit_start && start_offset != edit_start { + return ICompletionRes::Hide; + } + } + + let text = match text_format { + InsertTextFormat::PLAIN_TEXT => Cow::Borrowed(&item.insert_text), + InsertTextFormat::SNIPPET => { + let Ok(snippet) = Snippet::from_str(&item.insert_text) else { + return ICompletionRes::Hide; + }; + let text = snippet.text(); + + Cow::Owned(text) + } + _ => { + // We don't know how to support this text format + return ICompletionRes::Hide; + } + }; + + let range = start_offset..rope_text.offset_line_end(start_offset, true); + let prefix = rope_text.slice_to_cow(range); + // We strip the prefix of the current input from the label. + // So that, for example `p` with a completion of `println` will show `rintln`. + let Some(text) = text.strip_prefix(prefix.as_ref()) else { + return ICompletionRes::Hide; + }; + + if Some(text) == current_completion { + ICompletionRes::Unchanged + } else { + ICompletionRes::Set(text.to_string(), prefix.len()) + } +} diff --git a/lapce-app/src/keymap.rs b/lapce-app/src/keymap.rs index 4a5976a1c2..c1decddae0 100644 --- a/lapce-app/src/keymap.rs +++ b/lapce-app/src/keymap.rs @@ -9,8 +9,8 @@ use floem::{ style::CursorStyle, view::View, views::{ - container, label, list, scroll, stack, text, virtual_list, Decorators, - VirtualListDirection, VirtualListItemSize, + container, dyn_stack, label, scroll, stack, text, virtual_stack, Decorators, + VirtualDirection, VirtualItemSize, }, }; use lapce_core::mode::Modes; @@ -115,13 +115,11 @@ pub fn keymap_view(common: Rc) -> impl View { .flex_basis(0.0) .flex_grow(1.0) .border_right(1.0) - .border_color( - *config.get().get_color(LapceColor::LAPCE_BORDER), - ) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) }), { let keymap = keymap.clone(); - list( + dyn_stack( move || { keymap .as_ref() @@ -144,9 +142,7 @@ pub fn keymap_view(common: Rc) -> impl View { .border(1.0) .border_radius(3.0) .border_color( - *config - .get() - .get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }) }, @@ -158,7 +154,7 @@ pub fn keymap_view(common: Rc) -> impl View { .height_pct(100.0) .border_right(1.0) .border_color( - *config.get().get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }) }, @@ -184,7 +180,7 @@ pub fn keymap_view(common: Rc) -> impl View { .collect::>() }) .unwrap_or_default(); - list( + dyn_stack( move || modes.clone(), |m| m.clone(), move |mode| { @@ -195,9 +191,7 @@ pub fn keymap_view(common: Rc) -> impl View { .border(1.0) .border_radius(3.0) .border_color( - *config - .get() - .get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }) }, @@ -209,7 +203,7 @@ pub fn keymap_view(common: Rc) -> impl View { .height_pct(100.0) .border_right(1.0) .border_color( - *config.get().get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) .apply_if(!modal.get(), |s| s.hide()) }) @@ -260,12 +254,10 @@ pub fn keymap_view(common: Rc) -> impl View { .height(ui_line_height() as f32) .width_pct(100.0) .apply_if(i % 2 > 0, |s| { - s.background( - *config.get_color(LapceColor::EDITOR_CURRENT_LINE), - ) + s.background(config.color(LapceColor::EDITOR_CURRENT_LINE)) }) .border_bottom(1.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) }) }; @@ -278,9 +270,7 @@ pub fn keymap_view(common: Rc) -> impl View { s.width_pct(100.0) .border_radius(6.0) .border(1.0) - .border_color( - *config.get().get_color(LapceColor::LAPCE_BORDER), - ) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) }), ) .style(|s| s.padding_bottom(10.0).width_pct(100.0)), @@ -295,7 +285,7 @@ pub fn keymap_view(common: Rc) -> impl View { .flex_basis(0.0) .flex_grow(1.0) .border_right(1.0) - .border_color(*config.get().get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) }), text("Key Binding").style(move |s| { s.width(200.0) @@ -303,7 +293,7 @@ pub fn keymap_view(common: Rc) -> impl View { .padding_horiz(10.0) .height_pct(100.0) .border_right(1.0) - .border_color(*config.get().get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) }), text("Modes").style(move |s| { s.width(200.0) @@ -311,7 +301,7 @@ pub fn keymap_view(common: Rc) -> impl View { .padding_horiz(10.0) .height_pct(100.0) .border_right(1.0) - .border_color(*config.get().get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) .apply_if(!modal.get(), |s| s.hide()) }), container(text("When").style(move |s| { @@ -332,14 +322,14 @@ pub fn keymap_view(common: Rc) -> impl View { .width_pct(100.0) .border_top(1.0) .border_bottom(1.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::EDITOR_CURRENT_LINE)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_CURRENT_LINE)) }), container( scroll( - virtual_list( - VirtualListDirection::Vertical, - VirtualListItemSize::Fixed(Box::new(ui_line_height)), + virtual_stack( + VirtualDirection::Vertical, + VirtualItemSize::Fixed(Box::new(ui_line_height)), items, |(i, (cmd, keymap)): &( usize, @@ -384,7 +374,7 @@ fn keyboard_picker_view( .unwrap_or_default() }) }), - list( + dyn_stack( move || { picker .keys @@ -404,7 +394,7 @@ fn keyboard_picker_view( .border(1.0) .border_radius(6.0) .border_color( - *config.get().get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }) }, @@ -418,8 +408,8 @@ fn keyboard_picker_view( .height(ui_line_height.get() as f32 + 16.0) .border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) }), stack(( text("Save") @@ -430,18 +420,15 @@ fn keyboard_picker_view( .padding_vert(8.0) .border(1.0) .border_radius(6.0) - .border_color( - *config.get_color(LapceColor::LAPCE_BORDER), - ) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get_color( - LapceColor::PANEL_HOVERED_BACKGROUND, - ), + config + .color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) .active(|s| { - s.background(*config.get_color( + s.background(config.color( LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND, )) }) @@ -469,18 +456,15 @@ fn keyboard_picker_view( .padding_vert(8.0) .border(1.0) .border_radius(6.0) - .border_color( - *config.get_color(LapceColor::LAPCE_BORDER), - ) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get_color( - LapceColor::PANEL_HOVERED_BACKGROUND, - ), + config + .color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) .active(|s| { - s.background(*config.get_color( + s.background(config.color( LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND, )) }) @@ -495,7 +479,7 @@ fn keyboard_picker_view( .justify_center() .width_pct(100.0) .margin_top(20.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) }), )) .style(move |s| { @@ -506,8 +490,8 @@ fn keyboard_picker_view( .width(400.0) .border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) }), ) .keyboard_navigatable() diff --git a/lapce-app/src/keypress/condition.rs b/lapce-app/src/keypress/condition.rs index f67df08324..655d84d784 100644 --- a/lapce-app/src/keypress/condition.rs +++ b/lapce-app/src/keypress/condition.rs @@ -49,6 +49,8 @@ pub enum Condition { PaletteFocus, #[strum(serialize = "completion_focus")] CompletionFocus, + #[strum(serialize = "inline_completion_visible")] + InlineCompletionVisible, #[strum(serialize = "modal_focus")] ModalFocus, #[strum(serialize = "in_snippet")] diff --git a/lapce-app/src/keypress/loader.rs b/lapce-app/src/keypress/loader.rs index 54f1e88038..c6363839d2 100644 --- a/lapce-app/src/keypress/loader.rs +++ b/lapce-app/src/keypress/loader.rs @@ -127,6 +127,14 @@ impl KeyMapLoader { } } +fn get_modes(toml_keymap: &toml_edit::Table) -> Modes { + toml_keymap + .get("mode") + .and_then(|v| v.as_str()) + .map(Modes::parse) + .unwrap_or_else(Modes::empty) +} + #[cfg(test)] mod tests { use super::*; @@ -228,11 +236,3 @@ command = "goto_definition" assert_eq!(keymaps.get(&keypress).unwrap().len(), 1); } } - -fn get_modes(toml_keymap: &toml_edit::Table) -> Modes { - toml_keymap - .get("mode") - .and_then(|v| v.as_str()) - .map(Modes::parse) - .unwrap_or_else(Modes::empty) -} diff --git a/lapce-app/src/lib.rs b/lapce-app/src/lib.rs index c98a9c3dd6..bbf8b62166 100644 --- a/lapce-app/src/lib.rs +++ b/lapce-app/src/lib.rs @@ -17,6 +17,7 @@ pub mod global_search; pub mod history; pub mod hover; pub mod id; +pub mod inline_completion; pub mod keymap; pub mod keypress; pub mod listener; diff --git a/lapce-app/src/listener.rs b/lapce-app/src/listener.rs index b610e81841..813da30799 100644 --- a/lapce-app/src/listener.rs +++ b/lapce-app/src/listener.rs @@ -28,11 +28,21 @@ impl Listener { Listener { val, cx } } - /// Listen for values sent to this listener. + pub fn scope(&self) -> Scope { + self.cx + } + + /// Listen for values sent to this listener. pub fn listen(self, on_val: impl Fn(T) + 'static) { + self.listen_with(self.cx, on_val) + } + + /// Listen for values sent to this listener. + /// Allows creating the effect with a custom scope, letting it be disposed of. + pub fn listen_with(self, cx: Scope, on_val: impl Fn(T) + 'static) { let val = self.val; - self.cx.create_effect(move |_| { + cx.create_effect(move |_| { // TODO(minor): Signals could have a `take` method to avoid cloning. if let Some(cmd) = val.get() { on_val(cmd); diff --git a/lapce-app/src/main_split.rs b/lapce-app/src/main_split.rs index f80e6e877a..649a520922 100644 --- a/lapce-app/src/main_split.rs +++ b/lapce-app/src/main_split.rs @@ -33,7 +33,10 @@ use tracing::warn; use crate::{ alert::AlertButton, command::InternalCommand, - doc::{DiagnosticData, DocContent, DocHistory, Document, EditorDiagnostic}, + doc::{ + DiagnosticData, DocContent, DocHistory, Document, DocumentExt, + EditorDiagnostic, + }, editor::{ diff::DiffEditorData, location::{EditorLocation, EditorPosition}, @@ -547,6 +550,16 @@ impl MainSplitData { ); } + pub fn open_diff_files(&self, left_path: PathBuf, right_path: PathBuf) { + let [left, right] = [left_path, right_path].map(|path| self.get_doc(path).0); + + self.get_editor_tab_child( + EditorTabChildSource::DiffEditor { left, right }, + false, + false, + ); + } + fn new_editor_tab( &self, editor_tab_id: EditorTabId, @@ -881,6 +894,7 @@ impl MainSplitData { None, editor_id, doc.clone(), + None, self.common.clone(), ); let editor = Rc::new(editor); @@ -911,6 +925,7 @@ impl MainSplitData { None, editor_id, doc, + None, self.common.clone(), ); let editor = Rc::new(editor); @@ -1391,6 +1406,7 @@ impl MainSplitData { Some(editor_tab_id), None, new_editor_id, + None, ); let editor = Rc::new(editor); self.editors.update(|editors| { @@ -1570,7 +1586,7 @@ impl MainSplitData { let split = splits.get(&split_id).copied()?; let split_chilren = split.with_untracked(|split| split.children.clone()); - let content = split_chilren.get(0)?; + let content = split_chilren.first()?; self.split_content_focus(&content.1); Some(()) @@ -2024,7 +2040,7 @@ impl MainSplitData { None } else { edits - .get(0) + .first() .map(|edit| EditorPosition::Position(edit.range.start)) }; let location = EditorLocation { diff --git a/lapce-app/src/markdown.rs b/lapce-app/src/markdown.rs index 4296bc0d2b..04664389f4 100644 --- a/lapce-app/src/markdown.rs +++ b/lapce-app/src/markdown.rs @@ -28,7 +28,7 @@ pub fn parse_markdown( FamilyOwned::parse_list(&config.editor.font_family).collect(); let default_attrs = Attrs::new() - .color(*config.get_color(LapceColor::EDITOR_FOREGROUND)) + .color(config.color(LapceColor::EDITOR_FOREGROUND)) .font_size(config.ui.font_size() as f32) .line_height(LineHeightValue::Normal(line_height as f32)); let mut attr_list = AttrsList::new(default_attrs); @@ -159,7 +159,7 @@ pub fn parse_markdown( pos..pos + text.len(), default_attrs .family(&code_font_family) - .color(*config.get_color(LapceColor::MARKDOWN_BLOCKQUOTE)), + .color(config.color(LapceColor::MARKDOWN_BLOCKQUOTE)), ); current_text.push_str(&text); pos += text.len(); @@ -219,7 +219,7 @@ fn attribute_for_tag<'a>( Tag::BlockQuote => Some( default_attrs .style(Style::Italic) - .color(*config.get_color(LapceColor::MARKDOWN_BLOCKQUOTE)), + .color(config.color(LapceColor::MARKDOWN_BLOCKQUOTE)), ), Tag::CodeBlock(_) => Some(default_attrs.family(code_font_family)), Tag::Emphasis => Some(default_attrs.style(Style::Italic)), @@ -227,7 +227,7 @@ fn attribute_for_tag<'a>( // TODO: Strikethrough support Tag::Link(_link_type, _target, _title) => { // TODO: Link support - Some(default_attrs.color(*config.get_color(LapceColor::EDITOR_LINK))) + Some(default_attrs.color(config.color(LapceColor::EDITOR_LINK))) } // All other tags are currently ignored _ => None, @@ -276,11 +276,11 @@ pub fn highlight_as_code( if let Some(color) = style .fg_color .as_ref() - .and_then(|fg| config.get_style_color(fg)) + .and_then(|fg| config.style_color(fg)) { attr_list.add_span( start_offset + range.start..start_offset + range.end, - default_attrs.color(*color), + default_attrs.color(color), ); } } diff --git a/lapce-app/src/palette.rs b/lapce-app/src/palette.rs index 0cddfacbf3..8abc048ea1 100644 --- a/lapce-app/src/palette.rs +++ b/lapce-app/src/palette.rs @@ -39,6 +39,7 @@ use crate::{ }, db::LapceDb, debug::{RunDebugConfigs, RunDebugMode}, + doc::DocumentExt, editor::{ location::{EditorLocation, EditorPosition}, EditorData, @@ -102,6 +103,7 @@ pub struct PaletteData { pub references: RwSignal>, pub source_control: SourceControlData, pub common: Rc, + left_diff_path: RwSignal>, } impl PaletteData { @@ -196,6 +198,7 @@ impl PaletteData { } let clicked_index = cx.create_rw_signal(Option::::None); + let left_diff_path = cx.create_rw_signal(None); let palette = Self { run_id_counter, @@ -219,6 +222,7 @@ impl PaletteData { references, source_control, common, + left_diff_path, }; { @@ -326,6 +330,19 @@ impl PaletteData { .update(|cursor| cursor.set_insert(Selection::caret(symbol.len()))); } + /// Get the placeholder text to use in the palette input field. + pub fn placeholder_text(&self) -> &'static str { + if self.kind.get() == PaletteKind::DiffFiles { + if self.left_diff_path.with(Option::is_some) { + "Select right file" + } else { + "Seleft left file" + } + } else { + "" + } + } + /// Execute the internal behavior of the palette for the given kind. This ignores updating and /// focusing the palette input. fn run_inner(&self, kind: PaletteKind) { @@ -336,7 +353,7 @@ impl PaletteData { match kind { PaletteKind::PaletteHelp => self.get_palette_help(), - PaletteKind::File => { + PaletteKind::File | PaletteKind::DiffFiles => { self.get_files(); } PaletteKind::Line => { @@ -360,6 +377,10 @@ impl PaletteData { PaletteKind::SshHost => { self.get_ssh_hosts(); } + #[cfg(windows)] + PaletteKind::WslHost => { + self.get_wsl_hosts(); + } PaletteKind::RunAndDebug => { self.get_run_configs(); } @@ -421,18 +442,18 @@ impl PaletteData { create_ext_action(self.common.scope, move |items: Vec| { let items = items .into_iter() - .map(|path| { - let full_path = path.clone(); + .map(|full_path| { // Strip the workspace prefix off the path, to avoid clutter let path = if let Some(workspace_path) = workspace.path.as_ref() { - path.strip_prefix(workspace_path) + full_path + .strip_prefix(workspace_path) .unwrap_or(&full_path) .to_path_buf() } else { - path + full_path.clone() }; - let filter_text = path.to_str().unwrap_or("").to_string(); + let filter_text = path.to_string_lossy().into_owned(); PaletteItem { content: PaletteItemContent::File { path, full_path }, filter_text, @@ -549,12 +570,12 @@ impl PaletteData { let text = w.path.as_ref()?.to_str()?.to_string(); let filter_text = match &w.kind { LapceWorkspaceType::Local => text, - LapceWorkspaceType::RemoteSSH(ssh) => { - format!("[{ssh}] {text}") + LapceWorkspaceType::RemoteSSH(remote) => { + format!("[{remote}] {text}") } #[cfg(windows)] - LapceWorkspaceType::RemoteWSL => { - format!("[wsl] {text}") + LapceWorkspaceType::RemoteWSL(remote) => { + format!("[{remote}] {text}") } }; Some(PaletteItem { @@ -721,16 +742,76 @@ impl PaletteData { let workspaces = db.recent_workspaces().unwrap_or_default(); let mut hosts = HashSet::new(); for workspace in workspaces.iter() { - if let LapceWorkspaceType::RemoteSSH(ssh) = &workspace.kind { - hosts.insert(ssh.clone()); + if let LapceWorkspaceType::RemoteSSH(host) = &workspace.kind { + hosts.insert(host.clone()); + } + } + + let items = hosts + .iter() + .map(|host| PaletteItem { + content: PaletteItemContent::SshHost { host: host.clone() }, + filter_text: host.to_string(), + score: 0, + indices: vec![], + }) + .collect(); + self.items.set(items); + } + + #[cfg(windows)] + fn get_wsl_hosts(&self) { + use std::os::windows::process::CommandExt; + use std::process; + let cmd = process::Command::new("wsl") + .creation_flags(0x08000000) // CREATE_NO_WINDOW + .arg("-l") + .arg("-v") + .stdout(process::Stdio::piped()) + .output(); + + let distros = if let Ok(proc) = cmd { + let distros = String::from_utf16(bytemuck::cast_slice(&proc.stdout)) + .unwrap_or_default() + .lines() + .skip(1) + .filter_map(|line| { + let line = line.trim_start(); + // let default = line.starts_with('*'); + let name = line + .trim_start_matches('*') + .trim_start() + .split(' ') + .next()?; + Some(name.to_string()) + }) + .collect(); + + distros + } else { + vec![] + }; + + let db: Arc = use_context().unwrap(); + let workspaces = db.recent_workspaces().unwrap_or_default(); + let mut hosts = HashSet::new(); + for distro in distros { + hosts.insert(distro); + } + + for workspace in workspaces.iter() { + if let LapceWorkspaceType::RemoteWSL(host) = &workspace.kind { + hosts.insert(host.host.clone()); } } let items = hosts .iter() - .map(|ssh| PaletteItem { - content: PaletteItemContent::SshHost { host: ssh.clone() }, - filter_text: ssh.to_string(), + .map(|host| PaletteItem { + content: PaletteItemContent::WslHost { + host: crate::workspace::WslHost { host: host.clone() }, + }, + filter_text: host.to_string(), score: 0, indices: vec![], }) @@ -882,7 +963,7 @@ impl PaletteData { if let Some(editor) = self.main_split.active_editor.get_untracked() { let doc = editor.view.doc.get_untracked(); let language = - doc.syntax.with_untracked(|syntax| syntax.language.name()); + doc.syntax().with_untracked(|syntax| syntax.language.name()); self.preselect_matching(&items, language); } self.items.set(items); @@ -977,11 +1058,27 @@ impl PaletteData { self.common.lapce_command.send(cmd); } PaletteItemContent::File { full_path, .. } => { - self.common - .internal_command - .send(InternalCommand::OpenFile { - path: full_path.to_owned(), - }); + if self.kind.get_untracked() == PaletteKind::DiffFiles { + if let Some(left_path) = + self.left_diff_path.try_update(Option::take).flatten() + { + self.common.internal_command.send( + InternalCommand::OpenDiffFiles { + left_path, + right_path: full_path.clone(), + }, + ); + } else { + self.left_diff_path.set(Some(full_path.clone())); + self.run(PaletteKind::DiffFiles); + } + } else { + self.common.internal_command.send( + InternalCommand::OpenFile { + path: full_path.clone(), + }, + ); + } } PaletteItemContent::Line { line, .. } => { let editor = self.main_split.active_editor.get_untracked(); @@ -1039,6 +1136,18 @@ impl PaletteData { }, ); } + #[cfg(windows)] + PaletteItemContent::WslHost { host } => { + self.common.window_common.window_command.send( + WindowCommand::SetWorkspace { + workspace: LapceWorkspace { + kind: LapceWorkspaceType::RemoteWSL(host.clone()), + path: None, + last_open: 0, + }, + }, + ); + } PaletteItemContent::DocumentSymbol { range, .. } => { let editor = self.main_split.active_editor.get_untracked(); let doc = match editor { @@ -1194,6 +1303,8 @@ impl PaletteData { PaletteItemContent::Workspace { .. } => {} PaletteItemContent::RunAndDebug { .. } => {} PaletteItemContent::SshHost { .. } => {} + #[cfg(windows)] + PaletteItemContent::WslHost { .. } => {} PaletteItemContent::Language { .. } => {} PaletteItemContent::Reference { location, .. } => { self.has_preview.set(true); @@ -1277,6 +1388,7 @@ impl PaletteData { .send(InternalCommand::ReloadConfig); } + self.left_diff_path.set(None); self.close(); } diff --git a/lapce-app/src/palette/item.rs b/lapce-app/src/palette/item.rs index eaa66af68b..e80f00b797 100644 --- a/lapce-app/src/palette/item.rs +++ b/lapce-app/src/palette/item.rs @@ -56,6 +56,10 @@ pub enum PaletteItemContent { SshHost { host: SshHost, }, + #[cfg(windows)] + WslHost { + host: crate::workspace::WslHost, + }, RunAndDebug { mode: RunDebugMode, config: RunDebugConfig, diff --git a/lapce-app/src/palette/kind.rs b/lapce-app/src/palette/kind.rs index 623a0baec4..b72e8d700a 100644 --- a/lapce-app/src/palette/kind.rs +++ b/lapce-app/src/palette/kind.rs @@ -13,12 +13,15 @@ pub enum PaletteKind { DocumentSymbol, WorkspaceSymbol, SshHost, + #[cfg(windows)] + WslHost, RunAndDebug, ColorTheme, IconTheme, Language, SCMReferences, TerminalProfile, + DiffFiles, } impl PaletteKind { @@ -40,7 +43,10 @@ impl PaletteKind { | PaletteKind::ColorTheme | PaletteKind::IconTheme | PaletteKind::Language - | PaletteKind::SCMReferences => "", + | PaletteKind::SCMReferences + | PaletteKind::DiffFiles => "", + #[cfg(windows)] + PaletteKind::WslHost => "", } } @@ -74,6 +80,8 @@ impl PaletteKind { PaletteKind::File => Some(LapceWorkbenchCommand::Palette), PaletteKind::Reference => None, // InternalCommand::PaletteReferences PaletteKind::SshHost => Some(LapceWorkbenchCommand::ConnectSshHost), + #[cfg(windows)] + PaletteKind::WslHost => Some(LapceWorkbenchCommand::ConnectWslHost), PaletteKind::RunAndDebug => { Some(LapceWorkbenchCommand::PaletteRunAndDebug) } @@ -84,6 +92,7 @@ impl PaletteKind { Some(LapceWorkbenchCommand::PaletteSCMReferences) } PaletteKind::TerminalProfile => None, // InternalCommand::NewTerminal + PaletteKind::DiffFiles => Some(LapceWorkbenchCommand::DiffFiles), } } @@ -100,6 +109,8 @@ impl PaletteKind { pub fn get_input<'a>(&self, input: &'a str) -> &'a str { match self { + #[cfg(windows)] + PaletteKind::WslHost => input, PaletteKind::File | PaletteKind::Reference | PaletteKind::SshHost @@ -107,7 +118,8 @@ impl PaletteKind { | PaletteKind::ColorTheme | PaletteKind::IconTheme | PaletteKind::Language - | PaletteKind::SCMReferences => input, + | PaletteKind::SCMReferences + | PaletteKind::DiffFiles => input, PaletteKind::PaletteHelp | PaletteKind::Command | PaletteKind::Workspace diff --git a/lapce-app/src/panel/debug_view.rs b/lapce-app/src/panel/debug_view.rs index a2c368b227..05400fe81b 100644 --- a/lapce-app/src/panel/debug_view.rs +++ b/lapce-app/src/panel/debug_view.rs @@ -8,8 +8,8 @@ use floem::{ style::CursorStyle, view::View, views::{ - container, container_box, label, list, scroll, stack, svg, text, - virtual_list, Decorators, VirtualListDirection, VirtualListItemSize, + container, container_box, dyn_stack, label, scroll, stack, svg, text, + virtual_stack, Decorators, VirtualDirection, VirtualItemSize, }, }; use lapce_rpc::{ @@ -242,7 +242,7 @@ fn debug_processes( scroll({ let terminal = terminal.clone(); let local_terminal = terminal.clone(); - list( + dyn_stack( move || local_terminal.run_debug_process(true), |(term_id, p)| (*term_id, p.stopped), move |(term_id, p)| { @@ -267,9 +267,7 @@ fn debug_processes( s.size(size, size) .margin_vert(5.0) .margin_horiz(10.0) - .color( - *config.get_color(LapceColor::LAPCE_ICON_ACTIVE), - ) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) }) }, label(move || p.config.name.clone()).style(|s| { @@ -307,16 +305,13 @@ fn debug_processes( .items_center() .apply_if(is_active(), |s| { s.background( - *config - .get_color(LapceColor::PANEL_CURRENT_BACKGROUND), + config.color(LapceColor::PANEL_CURRENT_BACKGROUND), ) }) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - (*config.get_color( - LapceColor::PANEL_HOVERED_BACKGROUND, - )) - .with_alpha_factor(0.3), + (config.color(LapceColor::PANEL_HOVERED_BACKGROUND)) + .with_alpha_factor(0.3), ) }) }) @@ -333,9 +328,9 @@ fn variables_view(window_tab_data: Rc) -> impl View { let config = window_tab_data.common.config; container( scroll( - virtual_list( - VirtualListDirection::Vertical, - VirtualListItemSize::Fixed(Box::new(move || ui_line_height.get())), + virtual_stack( + VirtualDirection::Vertical, + VirtualItemSize::Fixed(Box::new(move || ui_line_height.get())), move || { let dap = terminal.get_active_dap(true); dap.map(|dap| { @@ -385,7 +380,7 @@ fn variables_view(window_tab_data: Rc) -> impl View { let size = config.ui.icon_size() as f32; let color = if reference > 0 { - *config.get_color(LapceColor::LAPCE_ICON_ACTIVE) + config.color(LapceColor::LAPCE_ICON_ACTIVE) } else { Color::TRANSPARENT }; @@ -396,7 +391,7 @@ fn variables_view(window_tab_data: Rc) -> impl View { s.apply_if(!type_exists || reference == 0, |s| s.hide()) }), text(node.item.ty().unwrap_or("")).style(move |s| { - s.color(*config.get().get_style_color("type").unwrap()) + s.color(config.get().style_color("type").unwrap()) .apply_if(!type_exists || reference == 0, |s| { s.hide() }) @@ -431,9 +426,11 @@ fn variables_view(window_tab_data: Rc) -> impl View { .min_width_pct(100.0) .hover(|s| { s.apply_if(reference > 0, |s| { - s.background(*config.get().get_color( - LapceColor::PANEL_HOVERED_BACKGROUND, - )) + s.background( + config.get().color( + LapceColor::PANEL_HOVERED_BACKGROUND, + ), + ) }) }) }) @@ -465,13 +462,11 @@ fn debug_stack_frames( .style(move |s| { s.padding_horiz(10.0).min_width_pct(100.0).hover(move |s| { s.cursor(CursorStyle::Pointer).background( - *config - .get() - .get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.get().color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }), - list( + dyn_stack( move || { let expanded = stack_trace.expanded.get() && stopped.get(); if expanded { @@ -501,15 +496,15 @@ fn debug_stack_frames( label(move || frame.name.clone()).style(move |s| { s.hover(|s| { s.background( - *config + config .get() - .get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + .color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }), label(move || source_path.clone()).style(move |s| { s.margin_left(10.0) - .color(*config.get().get_color(LapceColor::EDITOR_DIM)) + .color(config.get().color(LapceColor::EDITOR_DIM)) .font_style(FontStyle::Italic) .apply_if(!has_source, |s| s.hide()) }), @@ -542,12 +537,11 @@ fn debug_stack_frames( .padding_right(10.0) .min_width_pct(100.0) .apply_if(!has_source, |s| { - s.color(*config.get_color(LapceColor::EDITOR_DIM)) + s.color(config.color(LapceColor::EDITOR_DIM)) }) .hover(|s| { s.background( - *config - .get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), ) .apply_if(has_source, |s| s.cursor(CursorStyle::Pointer)) }) @@ -567,7 +561,7 @@ fn debug_stack_traces( container( scroll({ let local_terminal = terminal.clone(); - list( + dyn_stack( move || { let dap = local_terminal.get_active_dap(true); if let Some(dap) = dap { @@ -628,7 +622,7 @@ fn breakpoints_view(window_tab_data: Rc) -> impl View { let internal_command = window_tab_data.common.internal_command; container( scroll( - list( + dyn_stack( move || { breakpoints .get() @@ -711,9 +705,7 @@ fn breakpoints_view(window_tab_data: Rc) -> impl View { s.text_ellipsis() .flex_grow(1.0) .flex_basis(0.0) - .color( - *config.get().get_color(LapceColor::EDITOR_DIM), - ) + .color(config.get().color(LapceColor::EDITOR_DIM)) .min_width(0.0) .margin_left(6.0) .apply_if(folder_empty, |s| s.hide()) @@ -723,9 +715,9 @@ fn breakpoints_view(window_tab_data: Rc) -> impl View { s.items_center().padding_horiz(10.0).width_pct(100.0).hover( |s| { s.background( - *config.get().get_color( - LapceColor::PANEL_HOVERED_BACKGROUND, - ), + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }, ) diff --git a/lapce-app/src/panel/global_search_view.rs b/lapce-app/src/panel/global_search_view.rs index e5e22bd696..d18c50ab48 100644 --- a/lapce-app/src/panel/global_search_view.rs +++ b/lapce-app/src/panel/global_search_view.rs @@ -6,8 +6,8 @@ use floem::{ style::{CursorStyle, Style}, view::View, views::{ - container, label, scroll, stack, svg, virtual_list, Decorators, - VirtualListDirection, VirtualListItemSize, + container, label, scroll, stack, svg, virtual_stack, Decorators, + VirtualDirection, VirtualItemSize, }, }; use lapce_xi_rope::find::CaseMatching; @@ -94,7 +94,7 @@ pub fn global_search_panel( .items_center() .border(1.0) .border_radius(6.0) - .border_color(*config.get().get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) }), ) .style(|s| s.width_pct(100.0).padding(10.0)), @@ -112,9 +112,9 @@ fn search_result( let ui_line_height = global_search_data.common.ui_line_height; container({ scroll({ - virtual_list( - VirtualListDirection::Vertical, - VirtualListItemSize::Fn(Box::new( + virtual_stack( + VirtualDirection::Vertical, + VirtualItemSize::Fn(Box::new( |(_, match_data): &(PathBuf, SearchMatchData)| { match_data.height() }, @@ -164,17 +164,14 @@ fn search_result( .size(size, size) .min_size(size, size) .color( - *config.get_color( - LapceColor::LAPCE_ICON_ACTIVE, - ), + config.color(LapceColor::LAPCE_ICON_ACTIVE), ) }), svg(move || config.get().file_svg(&path).0).style( move |s| { let config = config.get(); let size = config.ui.icon_size() as f32; - let color = - config.file_svg(&style_path).1.copied(); + let color = config.file_svg(&style_path).1; s.margin_right(6.0) .size(size, size) .min_size(size, size) @@ -189,9 +186,7 @@ fn search_result( }), label(move || folder.clone()).style(move |s| { s.color( - *config - .get() - .get_color(LapceColor::EDITOR_DIM), + config.get().color(LapceColor::EDITOR_DIM), ) .min_width(0.0) .text_ellipsis() @@ -208,15 +203,15 @@ fn search_result( .items_center() .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get().get_color( + config.get().color( LapceColor::PANEL_HOVERED_BACKGROUND, ), ) }) }), - virtual_list( - VirtualListDirection::Vertical, - VirtualListItemSize::Fixed(Box::new(move || { + virtual_stack( + VirtualDirection::Vertical, + VirtualItemSize::Fixed(Box::new(move || { ui_line_height.get() })), move || { @@ -266,9 +261,7 @@ fn search_result( .collect() }, move || { - *config - .get() - .get_color(LapceColor::EDITOR_FOCUS) + config.get().color(LapceColor::EDITOR_FOCUS) }, ) .style(move |s| { @@ -277,7 +270,7 @@ fn search_result( s.margin_left(10.0 + icon_size + 6.0).hover( |s| { s.cursor(CursorStyle::Pointer) - .background(*config.get_color( + .background(config.color( LapceColor::PANEL_HOVERED_BACKGROUND, )) }, diff --git a/lapce-app/src/panel/plugin_view.rs b/lapce-app/src/panel/plugin_view.rs index 4b41f96a5b..e1c14bbecc 100644 --- a/lapce-app/src/panel/plugin_view.rs +++ b/lapce-app/src/panel/plugin_view.rs @@ -7,8 +7,8 @@ use floem::{ style::CursorStyle, view::View, views::{ - container, dyn_container, img, label, scroll, stack, svg, virtual_list, - Decorators, VirtualListDirection, VirtualListItemSize, VirtualListVector, + container, dyn_container, img, label, scroll, stack, svg, virtual_stack, + Decorators, VirtualDirection, VirtualItemSize, VirtualVector, }, }; use indexmap::IndexMap; @@ -40,16 +40,14 @@ impl IndexMapItems { } } -impl VirtualListVector<(usize, K, V)> +impl VirtualVector<(usize, K, V)> for IndexMapItems { - type ItemIterator = Box>; - fn total_len(&self) -> usize { self.0.len() } - fn slice(&mut self, range: Range) -> Self::ItemIterator { + fn slice(&mut self, range: Range) -> impl Iterator { let start = range.start; Box::new( self.items(range) @@ -178,9 +176,7 @@ fn installed_view(plugin: PluginData) -> impl View { .padding_vert(5.0) .hover(|s| { s.background( - *config - .get() - .get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.get().color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }) @@ -188,9 +184,9 @@ fn installed_view(plugin: PluginData) -> impl View { container( scroll( - virtual_list( - VirtualListDirection::Vertical, - VirtualListItemSize::Fixed(Box::new(move || { + virtual_stack( + VirtualDirection::Vertical, + VirtualItemSize::Fixed(Box::new(move || { ui_line_height.get() * 3.0 + 10.0 })), move || IndexMapItems(volts.get()), @@ -217,54 +213,51 @@ fn available_view(plugin: PluginData) -> impl View { let internal_command = plugin.common.internal_command; let local_plugin = plugin.clone(); - let install_button = move |id: VoltID, - info: RwSignal, - installing: RwSignal| { - let plugin = local_plugin.clone(); - let installed = create_memo(move |_| { - installed.with(|installed| installed.contains_key(&id)) - }); - label(move || { - if installed.get() { - "Installed".to_string() - } else if installing.get() { - "Installing".to_string() - } else { - "Install".to_string() - } - }) - .disabled(move || installed.get() || installing.get()) - .on_click_stop(move |_| { - plugin.install_volt(info.get_untracked()); - }) - .style(move |s| { - let config = config.get(); - s.color(*config.get_color(LapceColor::LAPCE_BUTTON_PRIMARY_FOREGROUND)) - .background( - *config.get_color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND), - ) - .margin_left(6.0) - .padding_horiz(6.0) - .border_radius(6.0) - .hover(|s| { - s.cursor(CursorStyle::Pointer).background( - config - .get_color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND) - .with_alpha_factor(0.8), - ) - }) - .active(|s| { - s.background( - config - .get_color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND) - .with_alpha_factor(0.6), + let install_button = + move |id: VoltID, info: RwSignal, installing: RwSignal| { + let plugin = local_plugin.clone(); + let installed = create_memo(move |_| { + installed.with(|installed| installed.contains_key(&id)) + }); + label(move || { + if installed.get() { + "Installed".to_string() + } else if installing.get() { + "Installing".to_string() + } else { + "Install".to_string() + } + }) + .disabled(move || installed.get() || installing.get()) + .on_click_stop(move |_| { + plugin.install_volt(info.get_untracked()); + }) + .style(move |s| { + let config = config.get(); + s.color(config.color(LapceColor::LAPCE_BUTTON_PRIMARY_FOREGROUND)) + .background( + config.color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND), ) - }) - .disabled(|s| { - s.background(*config.get_color(LapceColor::EDITOR_DIM)) - }) - }) - }; + .margin_left(6.0) + .padding_horiz(6.0) + .border_radius(6.0) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config + .color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND) + .with_alpha_factor(0.8), + ) + }) + .active(|s| { + s.background( + config + .color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND) + .with_alpha_factor(0.6), + ) + }) + .disabled(|s| s.background(config.color(LapceColor::EDITOR_DIM))) + }) + }; let view_fn = move |(_, id, volt): (usize, VoltID, AvailableVoltData)| { let info = volt.info.get_untracked(); @@ -322,9 +315,7 @@ fn available_view(plugin: PluginData) -> impl View { .padding_vert(5.0) .hover(|s| { s.background( - *config - .get() - .get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.get().color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }) @@ -362,18 +353,18 @@ fn available_view(plugin: PluginData) -> impl View { s.width_pct(100.0) .cursor(CursorStyle::Text) .items_center() - .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) .border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) }) }) .style(|s| s.padding(10.0).width_pct(100.0)), container({ scroll({ - virtual_list( - VirtualListDirection::Vertical, - VirtualListItemSize::Fixed(Box::new(move || { + virtual_stack( + VirtualDirection::Vertical, + VirtualItemSize::Fixed(Box::new(move || { ui_line_height.get() * 3.0 + 10.0 })), move || IndexMapItems(volts.get()), diff --git a/lapce-app/src/panel/problem_view.rs b/lapce-app/src/panel/problem_view.rs index 1f1f8856ce..6e29a6182f 100644 --- a/lapce-app/src/panel/problem_view.rs +++ b/lapce-app/src/panel/problem_view.rs @@ -5,7 +5,7 @@ use floem::{ reactive::{create_memo, create_rw_signal, ReadSignal}, style::{CursorStyle, Style}, view::View, - views::{container, label, list, scroll, stack, svg, Decorators}, + views::{container, dyn_stack, label, scroll, stack, svg, Decorators}, }; use lsp_types::{DiagnosticRelatedInformation, DiagnosticSeverity}; @@ -37,7 +37,7 @@ pub fn problem_panel( s.flex_col() .flex_basis(0.0) .flex_grow(1.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .apply_if(is_bottom, |s| s.border_right(1.0)) .apply_if(!is_bottom, |s| s.border_bottom(1.0)) }), @@ -62,7 +62,7 @@ fn problem_section( let internal_command = window_tab_data.common.internal_command; container({ scroll( - list( + dyn_stack( move || main_split.diagnostics.get(), |(p, _)| p.clone(), move |(path, diagnostic_data)| { @@ -125,8 +125,8 @@ fn file_view( let icon_color = move || { let config = config.get(); match severity { - DiagnosticSeverity::ERROR => *config.get_color(LapceColor::LAPCE_ERROR), - _ => *config.get_color(LapceColor::LAPCE_WARN), + DiagnosticSeverity::ERROR => config.color(LapceColor::LAPCE_ERROR), + _ => config.color(LapceColor::LAPCE_WARN), } }; @@ -150,7 +150,7 @@ fn file_view( s.margin_right(6.0).max_width_pct(100.0).text_ellipsis() }), label(move || folder.clone()).style(move |s| { - s.color(*config.get().get_color(LapceColor::EDITOR_DIM)) + s.color(config.get().color(LapceColor::EDITOR_DIM)) .min_width(0.0) .text_ellipsis() }), @@ -168,7 +168,7 @@ fn file_view( .padding_right(10.0) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }), @@ -185,12 +185,12 @@ fn file_view( let size = config.ui.icon_size() as f32; s.margin_right(6.0) .size(size, size) - .color(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) }), svg(move || config.get().file_svg(&path).0).style(move |s| { let config = config.get(); let size = config.ui.icon_size() as f32; - let color = config.file_svg(&style_path).1.copied(); + let color = config.file_svg(&style_path).1; s.min_width(size) .size(size, size) .apply_opt(color, Style::color) @@ -200,7 +200,7 @@ fn file_view( .style(|s| s.absolute().items_center().margin_left(10.0)), )) .style(move |s| s.width_pct(100.0).min_width(0.0)), - list( + dyn_stack( move || { if collpased.get() { im::Vector::new() @@ -274,9 +274,7 @@ fn item_view( .style(move |s| { s.width_pct(100.0).min_width(0.0).hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config - .get() - .get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.get().color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }) @@ -299,7 +297,7 @@ fn related_view( ) -> impl View { let is_empty = related.is_empty(); stack(( - list( + dyn_stack( move || related.clone(), |_| 0, move |related| { @@ -342,8 +340,7 @@ fn related_view( .min_width(0.0) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config - .get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }) @@ -355,7 +352,7 @@ fn related_view( let config = config.get(); let size = config.ui.icon_size() as f32; s.size(size, size) - .color(*config.get_color(LapceColor::EDITOR_DIM)) + .color(config.color(LapceColor::EDITOR_DIM)) }), label(|| " ".to_string()), )) @@ -369,7 +366,7 @@ fn related_view( s.width_pct(100.0) .min_width(0.0) .items_start() - .color(*config.get().get_color(LapceColor::EDITOR_DIM)) + .color(config.get().color(LapceColor::EDITOR_DIM)) .apply_if(is_empty, |s| s.hide()) }) } diff --git a/lapce-app/src/panel/source_control_view.rs b/lapce-app/src/panel/source_control_view.rs index e08f1500e3..56c78a7c6f 100644 --- a/lapce-app/src/panel/source_control_view.rs +++ b/lapce-app/src/panel/source_control_view.rs @@ -8,7 +8,7 @@ use floem::{ reactive::{create_memo, create_rw_signal}, style::{CursorStyle, Style}, view::View, - views::{container, label, list, scroll, stack, svg, Decorators}, + views::{container, dyn_stack, label, scroll, stack, svg, Decorators}, }; use lapce_core::buffer::rope_text::RopeText; use lapce_rpc::source_control::FileDiff; @@ -66,12 +66,13 @@ pub fn source_control_panel( s.absolute() .items_center() .height(config.editor.line_height() as f32) - .color(*config.get_color(LapceColor::EDITOR_DIM)) + .color(config.color(LapceColor::EDITOR_DIM)) .apply_if(!is_empty.get(), |s| s.hide()) }), )) .style(|s| { - s.min_size_pct(100.0, 100.0) + s.absolute() + .min_size_pct(100.0, 100.0) .padding_left(10.0) .padding_vert(6.0) .hover(|s| s.cursor(CursorStyle::Text)) @@ -113,13 +114,18 @@ pub fn source_control_panel( let editor_view = editor.view.clone(); editor_view.doc.track(); editor_view.kind.track(); - let LineRegion { x, width, line } = - cursor_caret(&editor_view, offset, !cursor.is_insert()); + let LineRegion { x, width, rvline } = cursor_caret( + &editor_view, + offset, + !cursor.is_insert(), + cursor.affinity, + ); let config = config.get_untracked(); let line_height = config.editor.line_height(); - + // TODO: is there a way to avoid the calculation of the vline here? + let vline = editor.view.vline_of_rvline(rvline); Rect::from_origin_size( - (x, (line * line_height) as f64), + (x, (vline.get() * line_height) as f64), (width, line_height as f64), ) .inflate(30.0, 10.0) @@ -133,8 +139,8 @@ pub fn source_control_panel( .border(1.0) .padding(-1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) }), { let source_control = source_control.clone(); @@ -150,18 +156,15 @@ pub fn source_control_panel( .justify_center() .border(1.0) .border_radius(6.0) - .border_color( - *config.get_color(LapceColor::LAPCE_BORDER), - ) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get_color( - LapceColor::PANEL_HOVERED_BACKGROUND, - ), + config + .color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) .active(|s| { - s.background(*config.get_color( + s.background(config.color( LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND, )) }) @@ -229,7 +232,7 @@ fn file_diffs_view(source_control: SourceControlData) -> impl View { svg(move || config.get().file_svg(&path).0).style(move |s| { let config = config.get(); let size = config.ui.icon_size() as f32; - let color = config.file_svg(&style_path).1.copied(); + let color = config.file_svg(&style_path).1; s.min_width(size) .size(size, size) .margin(6.0) @@ -253,7 +256,7 @@ fn file_diffs_view(source_control: SourceControlData) -> impl View { s.text_ellipsis() .flex_grow(1.0) .flex_basis(0.0) - .color(*config.get().get_color(LapceColor::EDITOR_DIM)) + .color(config.get().color(LapceColor::EDITOR_DIM)) .min_width(0.0) }), container({ @@ -277,8 +280,8 @@ fn file_diffs_view(source_control: SourceControlData) -> impl View { LapceColor::SOURCE_CONTROL_MODIFIED } }; - let color = config.get_color(color); - s.min_width(size).size(size, size).color(*color) + let color = config.color(color); + s.min_width(size).size(size, size).color(color) }) }) .style(|s| { @@ -322,16 +325,14 @@ fn file_diffs_view(source_control: SourceControlData) -> impl View { .width_pct(100.0) .items_center() .hover(|s| { - s.background( - *config.get_color(LapceColor::PANEL_HOVERED_BACKGROUND), - ) + s.background(config.color(LapceColor::PANEL_HOVERED_BACKGROUND)) }) }) }; container({ scroll({ - list( + dyn_stack( move || file_diffs.get(), |(path, (diff, checked))| { (path.to_path_buf(), diff.clone(), *checked) diff --git a/lapce-app/src/panel/terminal_view.rs b/lapce-app/src/panel/terminal_view.rs index 315a713d98..9f1e421160 100644 --- a/lapce-app/src/panel/terminal_view.rs +++ b/lapce-app/src/panel/terminal_view.rs @@ -6,7 +6,7 @@ use floem::{ reactive::create_rw_signal, view::View, views::{ - container, empty, label, list, + container, dyn_stack, empty, label, scroll::{scroll, Thickness}, stack, svg, tab, Decorators, }, @@ -52,7 +52,7 @@ fn terminal_tab_header(window_tab_data: Rc) -> impl View { let workbench_command = window_tab_data.common.workbench_command; stack(( - scroll(list( + scroll(dyn_stack( move || { let tabs = terminal.tab_info.with(|info| info.tabs.clone()); for (i, (index, _)) in tabs.iter().enumerate() { @@ -115,9 +115,11 @@ fn terminal_tab_header(window_tab_data: Rc) -> impl View { .style(move |s| { let config = config.get(); let size = config.ui.icon_size() as f32; - s.size(size, size).color(*config.get_color( - LapceColor::LAPCE_ICON_ACTIVE, - )) + s.size(size, size).color( + config.color( + LapceColor::LAPCE_ICON_ACTIVE, + ), + ) }), ) .style(|s| s.padding_horiz(10.0).padding_vert(12.0)), @@ -143,15 +145,13 @@ fn terminal_tab_header(window_tab_data: Rc) -> impl View { .height(header_height.get() - 15.0) .border_right(1.0) .border_color( - *config - .get() - .get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }), )) .style(move |s| { s.items_center().width(200.0).border_color( - *config.get().get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }) }) @@ -164,7 +164,7 @@ fn terminal_tab_header(window_tab_data: Rc) -> impl View { } else { 0.0 }) - .border_color(*config.get().get_color( + .border_color(config.get().color( if focus.get() == Focus::Panel(PanelKind::Terminal) { @@ -242,7 +242,7 @@ fn terminal_tab_header(window_tab_data: Rc) -> impl View { s.width_pct(100.0) .items_center() .border_bottom(1.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) }) } @@ -253,7 +253,7 @@ fn terminal_tab_split( let config = terminal_panel_data.common.config; let active = terminal_tab_data.active; let terminal_tab_scope = terminal_tab_data.scope; - list( + dyn_stack( move || { let terminals = terminal_tab_data.terminals.get(); for (i, (index, _)) in terminals.iter().enumerate() { @@ -297,7 +297,7 @@ fn terminal_tab_split( index.get() > 0, |s| { s.border_left(1.0).border_color( - *config.get().get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }, ) diff --git a/lapce-app/src/panel/view.rs b/lapce-app/src/panel/view.rs index 6753b38bf1..39232c4a9f 100644 --- a/lapce-app/src/panel/view.rs +++ b/lapce-app/src/panel/view.rs @@ -6,7 +6,9 @@ use floem::{ reactive::{create_rw_signal, ReadSignal, RwSignal}, style::CursorStyle, view::View, - views::{container, container_box, empty, label, list, stack, tab, Decorators}, + views::{ + container, container_box, dyn_stack, empty, label, stack, tab, Decorators, + }, EventPropagation, }; @@ -76,9 +78,9 @@ pub fn panel_container_view( .style(move |s| { s.size_pct(100.0, 100.0).apply_if(dragging_over.get(), |s| { s.background( - *config + config .get() - .get_color(LapceColor::EDITOR_DRAG_DROP_BACKGROUND), + .color(LapceColor::EDITOR_DRAG_DROP_BACKGROUND), ) }) }) @@ -185,7 +187,7 @@ pub fn panel_container_view( s.width(4.0).margin_left(-2.0).height_pct(100.0) }) .apply_if(is_dragging, |s| { - s.background(*config.get_color(LapceColor::EDITOR_CARET)) + s.background(config.color(LapceColor::EDITOR_CARET)) .apply_if( position == PanelContainerPosition::Bottom, |s| s.cursor(CursorStyle::RowResize), @@ -197,7 +199,7 @@ pub fn panel_container_view( .z_index(2) }) .hover(|s| { - s.background(*config.get_color(LapceColor::EDITOR_CARET)) + s.background(config.color(LapceColor::EDITOR_CARET)) .apply_if( position == PanelContainerPosition::Bottom, |s| s.cursor(CursorStyle::RowResize), @@ -253,17 +255,17 @@ pub fn panel_container_view( s.border_right(1.0) .width(size as f32) .height_pct(100.0) - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) }) .apply_if(position == PanelContainerPosition::Right, |s| { s.border_left(1.0) .width(size as f32) .height_pct(100.0) - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) }) .apply_if(!is_bottom, |s| s.flex_col()) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .color(*config.get_color(LapceColor::PANEL_FOREGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .color(config.color(LapceColor::PANEL_FOREGROUND)) }) } @@ -333,7 +335,7 @@ pub fn panel_header( s.padding_horiz(10.0) .padding_vert(6.0) .width_pct(100.0) - .background(*config.get().get_color(LapceColor::EDITOR_BACKGROUND)) + .background(config.get().color(LapceColor::EDITOR_BACKGROUND)) }) } @@ -347,7 +349,7 @@ fn panel_picker( let dragging = window_tab_data.common.dragging; let is_bottom = position.is_bottom(); let is_first = position.is_first(); - list( + dyn_stack( move || { panel .panels @@ -399,11 +401,11 @@ fn panel_picker( let config = config.get(); s.border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .padding(6.0) .background( config - .get_color(LapceColor::PANEL_BACKGROUND) + .color(LapceColor::PANEL_BACKGROUND) .with_alpha_factor(0.7), ) }) @@ -426,9 +428,9 @@ fn panel_picker( }) }) .border_color( - *config + config .get() - .get_color(LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE), + .color(LapceColor::LAPCE_TAB_ACTIVE_UNDERLINE), ) }), ))) @@ -436,7 +438,7 @@ fn panel_picker( }, ) .style(move |s| { - s.border_color(*config.get().get_color(LapceColor::LAPCE_BORDER)) + s.border_color(config.get().color(LapceColor::LAPCE_BORDER)) .apply_if( panels.with(|p| { p.get(&position).map(|p| p.is_empty()).unwrap_or(true) diff --git a/lapce-app/src/plugin.rs b/lapce-app/src/plugin.rs index 34f75ae160..a4419ca99c 100644 --- a/lapce-app/src/plugin.rs +++ b/lapce-app/src/plugin.rs @@ -17,8 +17,8 @@ use floem::{ style::CursorStyle, view::View, views::{ - container_box, dyn_container, empty, img, label, list, rich_text, scroll, - stack, svg, text, Decorators, + container_box, dyn_container, dyn_stack, empty, img, label, rich_text, + scroll, stack, svg, text, Decorators, }, }; use indexmap::IndexMap; @@ -692,98 +692,98 @@ pub fn plugin_info_view(plugin: PluginData, volt: VoltID) -> impl View { }) }); - let version_view = - move |plugin: PluginData, plugin_info: PluginInfo| { - let version_info = - plugin_info.as_ref().map(|(_, volt, _, latest, _)| { - ( - volt.version.clone(), - latest.as_ref().map(|i| i.version.clone()), - ) - }); - let installing = plugin_info + let version_view = move |plugin: PluginData, plugin_info: PluginInfo| { + let version_info = plugin_info.as_ref().map(|(_, volt, _, latest, _)| { + ( + volt.version.clone(), + latest.as_ref().map(|i| i.version.clone()), + ) + }); + let installing = plugin_info + .as_ref() + .and_then(|(_, _, _, _, installing)| *installing); + let local_version_info = version_info.clone(); + let control = { + move |version_info: Option<(String, Option)>| match version_info .as_ref() - .and_then(|(_, _, _, _, installing)| *installing); - let local_version_info = version_info.clone(); - let control = { - move |version_info: Option<(String, Option)>| { - match version_info.as_ref().map(|(v, l)| match l { - Some(l) => (true, l == v), - None => (false, false), - }) { - Some((true, true)) => "Installed â–¼", - Some((true, false)) => "Upgrade â–¼", - _ => { - if installing.map(|i| i.get()).unwrap_or(false) { - "Installing" - } else { - "Install" - } - } + .map(|(v, l)| match l { + Some(l) => (true, l == v), + None => (false, false), + }) { + Some((true, true)) => "Installed â–¼", + Some((true, false)) => "Upgrade â–¼", + _ => { + if installing.map(|i| i.get()).unwrap_or(false) { + "Installing" + } else { + "Install" } } - }; - let local_plugin_info = plugin_info.clone(); - let local_plugin = plugin.clone(); - stack(( - text( - version_info - .as_ref() - .map(|(v, _)| format!("v{v}")) - .unwrap_or_default(), - ), - label(move || control(local_version_info.clone())) - .style(move |s| { - let config = config.get(); - s.margin_left(10) - .padding_horiz(10) - .border_radius(6.0) - .color(*config.get_color( - LapceColor::LAPCE_BUTTON_PRIMARY_FOREGROUND, - )) - .background(*config.get_color( - LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND, - )) - .hover(|s| { - s.cursor(CursorStyle::Pointer).background( - config - .get_color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND) - .with_alpha_factor(0.8), - ) - }) - .active(|s| { - s.background( - config - .get_color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND) - .with_alpha_factor(0.6), - ) - }) - .disabled(|s| { - s.background( - *config.get_color(LapceColor::EDITOR_DIM), - ) - }) - }) - .disabled(move || installing.map(|i| i.get()).unwrap_or(false)) - .on_click_stop(move |_| { - if let Some((meta, info, _, latest, _)) = - local_plugin_info.as_ref() - { - if let Some(meta) = meta { - let menu = local_plugin.plugin_controls( - meta.to_owned(), - latest - .clone() - .unwrap_or_else(|| info.to_owned()), - ); - show_context_menu(menu, None); - } else { - local_plugin.install_volt(info.to_owned()); - } - } - }), - )) + } }; + let local_plugin_info = plugin_info.clone(); + let local_plugin = plugin.clone(); + stack(( + text( + version_info + .as_ref() + .map(|(v, _)| format!("v{v}")) + .unwrap_or_default(), + ), + label(move || control(local_version_info.clone())) + .style(move |s| { + let config = config.get(); + s.margin_left(10) + .padding_horiz(10) + .border_radius(6.0) + .color( + config + .color(LapceColor::LAPCE_BUTTON_PRIMARY_FOREGROUND), + ) + .background( + config + .color(LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND), + ) + .hover(|s| { + s.cursor(CursorStyle::Pointer).background( + config + .color( + LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND, + ) + .with_alpha_factor(0.8), + ) + }) + .active(|s| { + s.background( + config + .color( + LapceColor::LAPCE_BUTTON_PRIMARY_BACKGROUND, + ) + .with_alpha_factor(0.6), + ) + }) + .disabled(|s| { + s.background(config.color(LapceColor::EDITOR_DIM)) + }) + }) + .disabled(move || installing.map(|i| i.get()).unwrap_or(false)) + .on_click_stop(move |_| { + if let Some((meta, info, _, latest, _)) = + local_plugin_info.as_ref() + { + if let Some(meta) = meta { + let menu = local_plugin.plugin_controls( + meta.to_owned(), + latest.clone().unwrap_or_else(|| info.to_owned()), + ); + show_context_menu(menu, None); + } else { + local_plugin.install_volt(info.to_owned()); + } + } + }), + )) + }; scroll( dyn_container( @@ -862,9 +862,9 @@ pub fn plugin_info_view(plugin: PluginData, volt: VoltID) -> impl View { move || repo.clone(), move || local_repo.clone(), move || { - *config.get().get_color( - LapceColor::EDITOR_LINK, - ) + config + .get() + .color(LapceColor::EDITOR_LINK) }, internal_command, ), @@ -880,9 +880,7 @@ pub fn plugin_info_view(plugin: PluginData, volt: VoltID) -> impl View { ) .style(move |s| { s.color( - *config - .get() - .get_color(LapceColor::EDITOR_DIM), + config.get().color(LapceColor::EDITOR_DIM), ) }), version_view( @@ -904,7 +902,7 @@ pub fn plugin_info_view(plugin: PluginData, volt: VoltID) -> impl View { }), empty().style(move |s| { s.margin_vert(6).height(1).width_full().background( - *config.get().get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }), { @@ -933,7 +931,7 @@ pub fn plugin_info_view(plugin: PluginData, volt: VoltID) -> impl View { }); { let id = AtomicU64::new(0); - list( + dyn_stack( move || { readme.get().unwrap_or_else(|| { parse_markdown( @@ -967,11 +965,9 @@ pub fn plugin_info_view(plugin: PluginData, volt: VoltID) -> impl View { s.width_full() .margin_vert(5.0) .height(1.0) - .background( - *config.get().get_color( - LapceColor::LAPCE_BORDER, - ), - ) + .background(config.get().color( + LapceColor::LAPCE_BORDER, + )) })) } }, diff --git a/lapce-app/src/proxy.rs b/lapce-app/src/proxy.rs index fb60de0c9b..56424ac3f3 100644 --- a/lapce-app/src/proxy.rs +++ b/lapce-app/src/proxy.rs @@ -76,9 +76,11 @@ pub fn new_proxy( proxy_rpc.mainloop(&mut dispatcher); }); } - LapceWorkspaceType::RemoteSSH(ssh) => { + LapceWorkspaceType::RemoteSSH(remote) => { if let Err(e) = start_remote( - SshRemote { ssh: ssh.clone() }, + SshRemote { + ssh: remote.clone(), + }, core_rpc.clone(), proxy_rpc.clone(), ) { @@ -86,20 +88,15 @@ pub fn new_proxy( } } #[cfg(windows)] - LapceWorkspaceType::RemoteWSL => { - use wsl::{WslDistro, WslRemote}; - let distro = WslDistro::all() - .ok() - .and_then(|d| d.into_iter().find(|distro| distro.default)) - .map(|d| d.name); - if let Some(distro) = distro { - if let Err(e) = start_remote( - WslRemote { distro }, - core_rpc.clone(), - proxy_rpc.clone(), - ) { - error!("Failed to start SSH remote: {e}"); - } + LapceWorkspaceType::RemoteWSL(remote) => { + if let Err(e) = start_remote( + wsl::WslRemote { + wsl: remote.clone(), + }, + core_rpc.clone(), + proxy_rpc.clone(), + ) { + error!("Failed to start SSH remote: {e}"); } } } diff --git a/lapce-app/src/proxy/wsl.rs b/lapce-app/src/proxy/wsl.rs index ac1844142e..9f96e19388 100644 --- a/lapce-app/src/proxy/wsl.rs +++ b/lapce-app/src/proxy/wsl.rs @@ -1,61 +1,20 @@ -use std::{ - path::Path, - process::{Command, Stdio}, -}; +use std::{path::Path, process::Command}; -use anyhow::{anyhow, Result}; +use anyhow::Result; -use super::{new_command, remote::Remote}; +use crate::workspace::WslHost; -#[derive(Debug)] -pub struct WslDistro { - pub name: String, - pub default: bool, -} +use super::{new_command, remote::Remote}; pub struct WslRemote { - pub distro: String, -} - -impl WslDistro { - pub fn all() -> Result> { - let cmd = new_command("wsl") - .arg("-l") - .arg("-v") - .stdout(Stdio::piped()) - .output()?; - - if !cmd.status.success() { - return Err(anyhow!("failed to execute `wsl -l -v`")); - } - - let distros = String::from_utf16(bytemuck::cast_slice(&cmd.stdout))? - .lines() - .skip(1) - .filter_map(|line| { - let line = line.trim_start(); - let default = line.starts_with('*'); - let name = line - .trim_start_matches('*') - .trim_start() - .split(' ') - .next()?; - Some(WslDistro { - name: name.to_string(), - default, - }) - }) - .collect(); - - Ok(distros) - } + pub wsl: WslHost, } impl Remote for WslRemote { fn upload_file(&self, local: impl AsRef, remote: &str) -> Result<()> { - let mut wsl_path = Path::new(r"\\wsl.localhost\").join(&self.distro); + let mut wsl_path = Path::new(r"\\wsl.localhost\").join(&self.wsl.host); if !wsl_path.exists() { - wsl_path = Path::new(r"\\wsl$").join(&self.distro); + wsl_path = Path::new(r"\\wsl$").join(&self.wsl.host); } wsl_path = if remote.starts_with('~') { let home_dir = self.home_dir()?; @@ -69,7 +28,7 @@ impl Remote for WslRemote { fn command_builder(&self) -> Command { let mut cmd = new_command("wsl"); - cmd.arg("-d").arg(&self.distro).arg("--"); + cmd.arg("-d").arg(&self.wsl.host).arg("--"); cmd } } diff --git a/lapce-app/src/settings.rs b/lapce-app/src/settings.rs index a99e6a19d0..91e8ba5e8e 100644 --- a/lapce-app/src/settings.rs +++ b/lapce-app/src/settings.rs @@ -16,9 +16,8 @@ use floem::{ style::CursorStyle, view::View, views::{ - container, container_box, empty, label, list, scroll, stack, svg, text, - virtual_list, Decorators, VirtualListDirection, VirtualListItemSize, - VirtualListVector, + container, container_box, dyn_stack, empty, label, scroll, stack, svg, text, + virtual_stack, Decorators, VirtualDirection, VirtualItemSize, VirtualVector, }, }; use indexmap::IndexMap; @@ -117,14 +116,15 @@ impl KeyPressFocus for SettingsData { fn receive_char(&self, _c: &str) {} } -impl VirtualListVector for SettingsData { - type ItemIterator = Box>; - +impl VirtualVector for SettingsData { fn total_len(&self) -> usize { self.filtered_items.get_untracked().len() } - fn slice(&mut self, _range: std::ops::Range) -> Self::ItemIterator { + fn slice( + &mut self, + _range: std::ops::Range, + ) -> impl Iterator { Box::new(self.filtered_items.get().into_iter()) } } @@ -399,19 +399,16 @@ pub fn settings_view( s.padding_horiz(20.0) .width_pct(100.0) .apply_if(kind == current_kind.get(), |s| { - s.background( - *config.get_color(LapceColor::PANEL_CURRENT_BACKGROUND), - ) + s.background(config.color(LapceColor::PANEL_CURRENT_BACKGROUND)) }) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) .active(|s| { s.background( - *config - .get_color(LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND), + config.color(LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND), ) }) }) @@ -419,7 +416,7 @@ pub fn settings_view( let switcher = || { stack(( - list( + dyn_stack( move || kinds.clone(), |(k, _)| k.clone(), move |(k, pos)| switcher_item(k, Box::new(move || Some(pos)), 0.0), @@ -434,7 +431,7 @@ pub fn settings_view( }), 0.0, ), - list( + dyn_stack( move || plugin_kinds.get(), |(k, _)| k.clone(), move |(k, pos)| { @@ -469,7 +466,7 @@ pub fn settings_view( s.height_pct(100.0) .width(200.0) .border_right(1.0) - .border_color(*config.get().get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.get().color(LapceColor::LAPCE_BORDER)) }), stack(( container({ @@ -481,18 +478,18 @@ pub fn settings_view( .border_radius(6.0) .border(1.0) .border_color( - *config.get().get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }) }) .style(|s| s.padding_horiz(50.0).padding_vert(20.0)), container({ scroll({ - virtual_list( - VirtualListDirection::Vertical, - floem::views::VirtualListItemSize::Fn(Box::new( - |item: &SettingsItem| item.size.get().height.max(50.0), - )), + virtual_stack( + VirtualDirection::Vertical, + VirtualItemSize::Fn(Box::new(|item: &SettingsItem| { + item.size.get().height.max(50.0) + })), move || settings_data.clone(), |item| (item.kind.clone(), item.name.clone()), move |item| { @@ -620,9 +617,7 @@ fn settings_item_view(settings_data: SettingsData, item: SettingsItem) -> impl V .border(1.0) .border_radius(6.0) .border_color( - *config - .get() - .get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }, ), @@ -657,9 +652,9 @@ fn settings_item_view(settings_data: SettingsData, item: SettingsItem) -> impl V .style(move |s| { s.text_ellipsis().padding_horiz(10.0).hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get().get_color( - LapceColor::PANEL_HOVERED_BACKGROUND, - ), + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }) @@ -680,9 +675,7 @@ fn settings_item_view(settings_data: SettingsData, item: SettingsItem) -> impl V let config = config.get(); let size = config.ui.icon_size() as f32; s.size(size, size).color( - *config.get_color( - LapceColor::LAPCE_ICON_ACTIVE, - ), + config.color(LapceColor::LAPCE_ICON_ACTIVE), ) }), ) @@ -702,16 +695,14 @@ fn settings_item_view(settings_data: SettingsData, item: SettingsItem) -> impl V .border_radius(6.0) .apply_if(!expanded.get(), |s| { s.border_color( - *config - .get() - .get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }) }), stack(( label(|| " ".to_string()), scroll({ - list( + dyn_stack( move || dropdown.items.clone(), |item| item.to_string(), view_fn, @@ -725,16 +716,14 @@ fn settings_item_view(settings_data: SettingsData, item: SettingsItem) -> impl V .style(move |s| { let config = config.get(); s.background( - *config.get_color(LapceColor::EDITOR_BACKGROUND), + config.color(LapceColor::EDITOR_BACKGROUND), ) .width_pct(100.0) .max_height(300.0) .z_index(1) .border_top(1.0) .border_radius(6.0) - .border_color( - *config.get_color(LapceColor::LAPCE_BORDER), - ) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .apply_if(!expanded.get(), |s| s.hide()) }), )) @@ -751,9 +740,7 @@ fn settings_item_view(settings_data: SettingsData, item: SettingsItem) -> impl V .border(1.0) .border_radius(6.0) .border_color( - *config - .get() - .get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) .apply_if(!expanded.get(), |s| { s.border_color(Color::TRANSPARENT) @@ -770,7 +757,7 @@ fn settings_item_view(settings_data: SettingsData, item: SettingsItem) -> impl V .width_pct(100.0) .padding_horiz(10.0) .font_size(config.ui.font_size() as f32 + 2.0) - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) })) } else { container_box(empty()) @@ -864,10 +851,10 @@ pub fn checkbox( const CHECKBOX_SVG: &str = r#""#; let svg_str = move || if checked() { CHECKBOX_SVG } else { "" }.to_string(); - svg(svg_str).base_style(move |s| { + svg(svg_str).style(move |s| { let config = config.get(); let size = config.ui.font_size() as f32; - let color = *config.get_color(LapceColor::EDITOR_FOREGROUND); + let color = config.color(LapceColor::EDITOR_FOREGROUND); s.min_width(size) .size(size, size) @@ -880,14 +867,15 @@ pub fn checkbox( struct BTreeMapVirtualList(BTreeMap); -impl VirtualListVector<(String, String)> for BTreeMapVirtualList { - type ItemIterator = Box>; - +impl VirtualVector<(String, String)> for BTreeMapVirtualList { fn total_len(&self) -> usize { self.0.len() } - fn slice(&mut self, range: std::ops::Range) -> Self::ItemIterator { + fn slice( + &mut self, + range: std::ops::Range, + ) -> impl Iterator { Box::new( self.0 .iter() @@ -923,9 +911,9 @@ fn color_section_list( .font_bold() .line_height(2.0) }), - virtual_list( - VirtualListDirection::Vertical, - VirtualListItemSize::Fixed(Box::new(move || text_height.get() + 24.0)), + virtual_stack( + VirtualDirection::Vertical, + VirtualItemSize::Fixed(Box::new(move || text_height.get() + 24.0)), move || BTreeMapVirtualList(list()), move |(key, _)| (key.to_owned()), move |(key, value)| { @@ -1041,9 +1029,7 @@ fn color_section_list( .border(1) .border_radius(6) .border_color( - *config - .get() - .get_color(LapceColor::LAPCE_BORDER), + config.get().color(LapceColor::LAPCE_BORDER), ) }, ), @@ -1060,11 +1046,9 @@ fn color_section_list( .border_radius(6) .size(size, size) .margin_left(10) - .border_color( - *config.get_color(LapceColor::LAPCE_BORDER), - ) - .background(*color.unwrap_or_else(|| { - config.get_color(LapceColor::EDITOR_FOREGROUND) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(color.copied().unwrap_or_else(|| { + config.color(LapceColor::EDITOR_FOREGROUND) })) }), { @@ -1106,14 +1090,13 @@ fn color_section_list( .border(1) .border_radius(6) .border_color( - *config.get_color(LapceColor::LAPCE_BORDER), + config.color(LapceColor::LAPCE_BORDER), ) .apply_if(same, |s| s.hide()) .active(|s| { s.background( - *config.get_color( - LapceColor::PANEL_BACKGROUND, - ), + config + .color(LapceColor::PANEL_BACKGROUND), ) }) }) diff --git a/lapce-app/src/status.rs b/lapce-app/src/status.rs index 6464dd9937..febdbb8d5d 100644 --- a/lapce-app/src/status.rs +++ b/lapce-app/src/status.rs @@ -7,7 +7,7 @@ use floem::{ reactive::{create_memo, ReadSignal, RwSignal}, style::{AlignItems, CursorStyle, Display}, view::View, - views::{label, list, stack, svg, Decorators}, + views::{dyn_stack, label, stack, svg, Decorators}, }; use indexmap::IndexMap; use lapce_core::mode::{Mode, VisualMode}; @@ -17,6 +17,7 @@ use crate::{ app::clickable_icon, command::LapceWorkbenchCommand, config::{color::LapceColor, icon::LapceIcons, LapceConfig}, + doc::DocumentExt, listener::Listener, palette::kind::PaletteKind, panel::{kind::PanelKind, position::PanelContainerPosition}, @@ -108,8 +109,8 @@ pub fn status( ), }; - let bg = *config.get_color(bg); - let fg = *config.get_color(fg); + let bg = config.color(bg); + let fg = config.color(fg); s.display(display) .padding_horiz(10.0) @@ -123,12 +124,11 @@ pub fn status( let config = config.get(); let icon_size = config.ui.icon_size() as f32; s.size(icon_size, icon_size) - .color(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) }), label(branch).style(move |s| { - s.margin_left(10.0).color( - *config.get().get_color(LapceColor::STATUS_FOREGROUND), - ) + s.margin_left(10.0) + .color(config.get().color(LapceColor::STATUS_FOREGROUND)) }), )) .style(move |s| { @@ -142,9 +142,7 @@ pub fn status( .align_items(Some(AlignItems::Center)) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config - .get() - .get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.get().color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }) @@ -158,17 +156,14 @@ pub fn status( move |s| { let config = config.get(); let size = config.ui.icon_size() as f32; - s.size(size, size).color( - *config.get_color(LapceColor::LAPCE_ICON_ACTIVE), - ) + s.size(size, size) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) }, ), label(move || diagnostic_count.get().0.to_string()).style( move |s| { s.margin_left(5.0).color( - *config - .get() - .get_color(LapceColor::STATUS_FOREGROUND), + config.get().color(LapceColor::STATUS_FOREGROUND), ) }, ), @@ -176,17 +171,15 @@ pub fn status( move |s| { let config = config.get(); let size = config.ui.icon_size() as f32; - s.size(size, size).margin_left(5.0).color( - *config.get_color(LapceColor::LAPCE_ICON_ACTIVE), - ) + s.size(size, size) + .margin_left(5.0) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) }, ), label(move || diagnostic_count.get().1.to_string()).style( move |s| { s.margin_left(5.0).color( - *config - .get() - .get_color(LapceColor::STATUS_FOREGROUND), + config.get().color(LapceColor::STATUS_FOREGROUND), ) }, ), @@ -200,9 +193,9 @@ pub fn status( .items_center() .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config + config .get() - .get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + .color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }) @@ -294,7 +287,7 @@ pub fn status( .style(move |s| { s.height_pct(100.0) .items_center() - .color(*config.get().get_color(LapceColor::STATUS_FOREGROUND)) + .color(config.get().color(LapceColor::STATUS_FOREGROUND)) }), stack({ let palette_clone = palette.clone(); @@ -351,10 +344,10 @@ pub fn status( .height_pct(100.0) .padding_horiz(10.0) .items_center() - .color(*config.get_color(LapceColor::STATUS_FOREGROUND)) + .color(config.color(LapceColor::STATUS_FOREGROUND)) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }); @@ -362,7 +355,7 @@ pub fn status( let language_info = label(move || { if let Some(editor) = editor.get() { let doc = editor.view.doc.get(); - doc.syntax.with(|s| s.language.name()) + doc.syntax().with(|s| s.language.name()) } else { "unknown" } @@ -388,10 +381,10 @@ pub fn status( .height_pct(100.0) .padding_horiz(10.0) .items_center() - .color(*config.get_color(LapceColor::STATUS_FOREGROUND)) + .color(config.color(LapceColor::STATUS_FOREGROUND)) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) }); @@ -413,8 +406,8 @@ pub fn status( .style(move |s| { let config = config.get(); s.border_top(1.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::STATUS_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::STATUS_BACKGROUND)) .height(config.ui.status_height() as f32) .align_items(Some(AlignItems::Center)) }) @@ -425,7 +418,7 @@ fn progress_view( progresses: RwSignal>, ) -> impl View { let id = AtomicU64::new(0); - list( + dyn_stack( move || progresses.get(), move |_| id.fetch_add(1, std::sync::atomic::Ordering::Relaxed), move |(_, p)| { @@ -440,7 +433,7 @@ fn progress_view( })) .style(move |s| { s.margin_left(10.0) - .color(*config.get().get_color(LapceColor::STATUS_FOREGROUND)) + .color(config.get().color(LapceColor::STATUS_FOREGROUND)) }) }, ) diff --git a/lapce-app/src/terminal/data.rs b/lapce-app/src/terminal/data.rs index 27a6e1f894..b0eb2b0f05 100644 --- a/lapce-app/src/terminal/data.rs +++ b/lapce-app/src/terminal/data.rs @@ -356,10 +356,7 @@ impl TerminalData { common.term_notification_tx.clone(), ))); - let mut profile = match profile { - Some(profile) => profile, - None => TerminalProfile::default(), - }; + let mut profile = profile.unwrap_or_default(); if profile.workdir.is_none() { profile.workdir = if let Ok(path) = url::Url::from_file_path( diff --git a/lapce-app/src/terminal/view.rs b/lapce-app/src/terminal/view.rs index f18f92f737..c19ebdcd5a 100644 --- a/lapce-app/src/terminal/view.rs +++ b/lapce-app/src/terminal/view.rs @@ -215,7 +215,7 @@ impl View for TerminalView { text_layout.set_text( &format!("Terminal failed to launch. Error: {error}"), AttrsList::new( - attrs.color(*config.get_color(LapceColor::EDITOR_FOREGROUND)), + attrs.color(config.color(LapceColor::EDITOR_FOREGROUND)), ), ); cx.draw_text( @@ -259,7 +259,7 @@ impl View for TerminalView { let y1 = y0 + line_height; cx.fill( &Rect::new(x0, y0, x1, y1), - config.get_color(LapceColor::EDITOR_SELECTION), + config.color(LapceColor::EDITOR_SELECTION), 0.0, ); } @@ -269,14 +269,14 @@ impl View for TerminalView { * line_height; cx.fill( &Rect::new(0.0, y, self.size.width, y + line_height), - config.get_color(LapceColor::EDITOR_CURRENT_LINE), + config.color(LapceColor::EDITOR_CURRENT_LINE), 0.0, ); } let cursor_point = &content.cursor.point; - let term_bg = *config.get_color(LapceColor::TERMINAL_BACKGROUND); + let term_bg = config.color(LapceColor::TERMINAL_BACKGROUND); let mut text_layout = TextLayout::new(); for item in content.display_iter { let point = item.point; @@ -321,12 +321,12 @@ impl View for TerminalView { if self.run_config.with_untracked(|run_config| { run_config.as_ref().map(|r| r.stopped).unwrap_or(false) }) { - config.get_color(LapceColor::LAPCE_ERROR) + config.color(LapceColor::LAPCE_ERROR) } else { - config.get_color(LapceColor::TERMINAL_CURSOR) + config.color(LapceColor::TERMINAL_CURSOR) } } else { - config.get_color(LapceColor::EDITOR_CARET) + config.color(LapceColor::EDITOR_CARET) }; if self.is_focused { cx.fill(&rect, cursor_color, 0.0); diff --git a/lapce-app/src/text_area.rs b/lapce-app/src/text_area.rs index 6dc64c16d0..f4fddad814 100644 --- a/lapce-app/src/text_area.rs +++ b/lapce-app/src/text_area.rs @@ -24,9 +24,9 @@ pub fn text_area( let config = config.get(); let font_size = config.ui.font_size(); let font_family = config.ui.font_family(); - let color = config.get_color(LapceColor::EDITOR_FOREGROUND); + let color = config.color(LapceColor::EDITOR_FOREGROUND); let attrs = Attrs::new() - .color(*color) + .color(color) .family(&font_family) .font_size(font_size as f32) .line_height(LineHeightValue::Normal(line_height)); @@ -47,9 +47,9 @@ pub fn text_area( let config = config.get_untracked(); let font_size = config.ui.font_size(); let font_family = config.ui.font_family(); - let color = config.get_color(LapceColor::EDITOR_FOREGROUND); + let color = config.color(LapceColor::EDITOR_FOREGROUND); let attrs = Attrs::new() - .color(*color) + .color(color) .family(&font_family) .font_size(font_size as f32) .line_height(LineHeightValue::Normal(1.2)); @@ -101,9 +101,7 @@ pub fn text_area( .margin_left(cursor_pos.x as f32 - 1.0) .margin_top(cursor_pos.y as f32) .border_left(2.0) - .border_color( - *config.get().get_color(LapceColor::EDITOR_CARET), - ) + .border_color(config.get().color(LapceColor::EDITOR_CARET)) .apply_if(!is_active(), |s| s.hide()) }), )) @@ -111,11 +109,11 @@ pub fn text_area( ) .style(|s| s.absolute().size_pct(100.0, 100.0)), ) - .base_style(move |s| { + .style(move |s| { let config = config.get(); s.border(1.0) .border_radius(6.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) - .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) }) } diff --git a/lapce-app/src/text_input.rs b/lapce-app/src/text_input.rs index f71a6c491d..8a81612910 100644 --- a/lapce-app/src/text_input.rs +++ b/lapce-app/src/text_input.rs @@ -149,7 +149,7 @@ pub fn text_input( hide_cursor: editor.common.window_common.hide_cursor, style: Default::default(), } - .base_style(|s| { + .style(|s| { s.cursor(CursorStyle::Text) .padding_horiz(10.0) .padding_vert(6.0) @@ -194,7 +194,7 @@ pub fn text_input( doc.clear_preedit(); } else { let offset = cursor.with_untracked(|c| c.offset()); - doc.set_preedit(text.clone(), *ime_cursor, offset); + doc.set_preedit(text.clone(), ime_cursor.to_owned(), offset); } } EventPropagation::Stop @@ -207,7 +207,7 @@ pub fn text_input( if let Event::ImeCommit(text) = event { let doc = local_editor.view.doc.get_untracked(); doc.clear_preedit(); - local_editor.receive_char(text); + local_editor.receive_char(text.as_str()); } EventPropagation::Stop }) @@ -632,7 +632,7 @@ impl View for TextInput { &Rect::ZERO .with_size(Size::new(max - min, height)) .with_origin(Point::new(min + point.x, point.y)), - *config.get_color(LapceColor::EDITOR_SELECTION), + config.color(LapceColor::EDITOR_SELECTION), 0.0, ); } @@ -665,11 +665,7 @@ impl View for TextInput { end_point.y + end_position.glyph_descent, ), ); - cx.stroke( - &line, - *config.get_color(LapceColor::EDITOR_FOREGROUND), - 1.0, - ); + cx.stroke(&line, config.color(LapceColor::EDITOR_FOREGROUND), 1.0); } if !self.hide_cursor.get_untracked() @@ -695,10 +691,7 @@ impl View for TextInput { cx.stroke( &line, - *self - .config - .get_untracked() - .get_color(LapceColor::EDITOR_CARET), + self.config.get_untracked().color(LapceColor::EDITOR_CARET), 2.0, ); } diff --git a/lapce-app/src/title.rs b/lapce-app/src/title.rs index 9f2a0a503d..f6fbf03d11 100644 --- a/lapce-app/src/title.rs +++ b/lapce-app/src/title.rs @@ -46,7 +46,7 @@ fn left( move |s| { let config = config.get(); s.size(16.0, 16.0) - .color(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) }, )) .style(move |s| s.margin_horiz(10.0).apply_if(is_macos, |s| s.hide())), @@ -62,11 +62,11 @@ fn left( let config = config.get(); let size = (config.ui.icon_size() as f32 + 2.0).min(30.0); s.size(size, size).color(if is_local { - *config.get_color(LapceColor::LAPCE_ICON_ACTIVE) + config.color(LapceColor::LAPCE_ICON_ACTIVE) } else { match proxy_status.get() { Some(_) => Color::WHITE, - None => *config.get_color(LapceColor::LAPCE_ICON_ACTIVE), + None => config.color(LapceColor::LAPCE_ICON_ACTIVE), } }) }, @@ -80,10 +80,12 @@ fn left( ); #[cfg(windows)] { - menu = - menu.entry(MenuItem::new("Connect to WSL").action(move || { - workbench_command.send(LapceWorkbenchCommand::ConnectWsl); - })); + menu = menu.entry(MenuItem::new("Connect to WSL Host").action( + move || { + workbench_command + .send(LapceWorkbenchCommand::ConnectWslHost); + }, + )); } menu }) @@ -94,13 +96,13 @@ fn left( } else { match proxy_status.get() { Some(ProxyStatus::Connected) => { - *config.get_color(LapceColor::LAPCE_REMOTE_CONNECTED) + config.color(LapceColor::LAPCE_REMOTE_CONNECTED) } Some(ProxyStatus::Connecting) => { - *config.get_color(LapceColor::LAPCE_REMOTE_CONNECTING) + config.color(LapceColor::LAPCE_REMOTE_CONNECTING) } Some(ProxyStatus::Disconnected) => { - *config.get_color(LapceColor::LAPCE_REMOTE_DISCONNECTED) + config.color(LapceColor::LAPCE_REMOTE_DISCONNECTED) } None => Color::TRANSPARENT, } @@ -111,13 +113,12 @@ fn left( .background(color) .hover(|s| { s.cursor(CursorStyle::Pointer).background( - *config.get_color(LapceColor::PANEL_HOVERED_BACKGROUND), + config.color(LapceColor::PANEL_HOVERED_BACKGROUND), ) }) .active(|s| { s.cursor(CursorStyle::Pointer).background( - *config - .get_color(LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND), + config.color(LapceColor::PANEL_HOVERED_ACTIVE_BACKGROUND), ) }) }), @@ -209,7 +210,7 @@ fn middle( let config = config.get(); let icon_size = config.ui.icon_size() as f32; s.size(icon_size, icon_size) - .color(*config.get_color(LapceColor::LAPCE_ICON_ACTIVE)) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) }, ), label(move || { @@ -242,9 +243,9 @@ fn middle( .justify_content(Some(JustifyContent::Center)) .align_items(Some(AlignItems::Center)) .border(1.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) .border_radius(6.0) - .background(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + .background(config.color(LapceColor::EDITOR_BACKGROUND)) }), stack(( clickable_icon( @@ -354,11 +355,11 @@ fn right( container(label(|| "1".to_string()).style(move |s| { let config = config.get(); s.font_size(10.0) - .color(*config.get_color(LapceColor::EDITOR_BACKGROUND)) + .color(config.color(LapceColor::EDITOR_BACKGROUND)) .border_radius(100.0) .margin_left(5.0) .margin_top(10.0) - .background(*config.get_color(LapceColor::EDITOR_CARET)) + .background(config.color(LapceColor::EDITOR_CARET)) })) .style(move |s| { let has_update = has_update(); @@ -433,9 +434,9 @@ pub fn title(window_tab_data: Rc) -> impl View { s.width_pct(100.0) .height(37.0) .items_center() - .background(*config.get_color(LapceColor::PANEL_BACKGROUND)) + .background(config.color(LapceColor::PANEL_BACKGROUND)) .border_bottom(1.0) - .border_color(*config.get_color(LapceColor::LAPCE_BORDER)) + .border_color(config.color(LapceColor::LAPCE_BORDER)) }) } diff --git a/lapce-app/src/web_link.rs b/lapce-app/src/web_link.rs index 3b551cde05..b9e41239df 100644 --- a/lapce-app/src/web_link.rs +++ b/lapce-app/src/web_link.rs @@ -17,7 +17,7 @@ pub fn web_link( .on_click_stop(move |_| { internal_command.send(InternalCommand::OpenWebUri { uri: uri() }); }) - .base_style(move |s| { + .style(move |s| { s.color(color()) .hover(move |s| s.cursor(CursorStyle::Pointer)) }) diff --git a/lapce-app/src/window_tab.rs b/lapce-app/src/window_tab.rs index d08ac212a2..2406a1bf77 100644 --- a/lapce-app/src/window_tab.rs +++ b/lapce-app/src/window_tab.rs @@ -21,16 +21,17 @@ use floem::{ use indexmap::IndexMap; use itertools::Itertools; use lapce_core::{ - command::FocusCommand, directory::Directory, meta, mode::Mode, - register::Register, + command::FocusCommand, cursor::CursorAffinity, directory::Directory, meta, + mode::Mode, register::Register, }; use lapce_rpc::{ core::CoreNotification, dap_types::RunDebugConfig, - file::PathObject, - proxy::{ProxyRpcHandler, ProxyStatus}, + file::{PathObject, RenameState}, + proxy::{ProxyResponse, ProxyRpcHandler, ProxyStatus}, source_control::FileDiff, terminal::TermId, + RpcError, }; use lsp_types::{ProgressParams, ProgressToken, ShowMessageParams}; use serde_json::Value; @@ -48,7 +49,7 @@ use crate::{ config::LapceConfig, db::LapceDb, debug::{DapData, LapceBreakpoint, RunDebugMode, RunDebugProcess}, - doc::{DocContent, EditorDiagnostic}, + doc::{DocContent, DocumentExt, EditorDiagnostic}, editor::location::{EditorLocation, EditorPosition}, editor_tab::EditorTabChild, file_explorer::data::FileExplorerData, @@ -56,6 +57,7 @@ use crate::{ global_search::GlobalSearchData, hover::HoverData, id::WindowTabId, + inline_completion::InlineCompletionData, keypress::{condition::Condition, EventRef, KeyPressData, KeyPressFocus}, listener::Listener, main_split::{MainSplitData, SplitData, SplitDirection, SplitMoveDirection}, @@ -114,6 +116,7 @@ pub struct CommonData { pub focus: RwSignal, pub keypress: RwSignal, pub completion: RwSignal, + pub inline_completion: RwSignal, pub hover: HoverData, pub register: RwSignal, pub find: Find, @@ -295,6 +298,7 @@ impl WindowTabData { let focus = cx.create_rw_signal(Focus::Workbench); let completion = cx.create_rw_signal(CompletionData::new(cx, config)); + let inline_completion = cx.create_rw_signal(InlineCompletionData::new(cx)); let hover = HoverData::new(cx); let register = cx.create_rw_signal(Register::default()); @@ -322,6 +326,7 @@ impl WindowTabData { keypress, focus, completion, + inline_completion, hover, register, find, @@ -895,7 +900,14 @@ impl WindowTabData { // ==== Terminal ==== NewTerminalTab => { - self.terminal.new_tab(None, None); + self.terminal.new_tab( + None, + self.common + .config + .get_untracked() + .terminal + .get_default_profile(), + ); if !self.panel.is_panel_visible(&PanelKind::Terminal) { self.panel.show_panel(&PanelKind::Terminal); } @@ -938,8 +950,9 @@ impl WindowTabData { ConnectSshHost => { self.palette.run(PaletteKind::SshHost); } - ConnectWsl => { - // TODO: + #[cfg(windows)] + ConnectWslHost => { + self.palette.run(PaletteKind::WslHost); } DisconnectRemote => { self.common.window_common.window_command.send( @@ -986,6 +999,7 @@ impl WindowTabData { ChangeFileLanguage => { self.palette.run(PaletteKind::Language); } + DiffFiles => self.palette.run(PaletteKind::DiffFiles), // ==== Running / Debugging ==== RunAndDebugRestart => { @@ -1285,6 +1299,115 @@ impl WindowTabData { InternalCommand::OpenFileChanges { path } => { self.main_split.open_file_changes(path); } + InternalCommand::StartRenamePath { path } => { + self.file_explorer.rename_state.set(RenameState::Renaming { + path, + editor_needs_reset: true, + }); + } + InternalCommand::TestRenamePath { new_path } => { + let rename_state = self.file_explorer.rename_state; + + let send = create_ext_action( + self.scope, + move |response: Result| match response { + Ok(_) => { + rename_state.update(RenameState::set_ok); + } + Err(err) => { + rename_state.update(|rename_state| { + rename_state.set_err(err.message) + }); + } + }, + ); + + self.common.proxy.test_create_at_path(new_path, send); + } + InternalCommand::FinishRenamePath { + current_path, + new_path, + } => { + let send_current_path = current_path.clone(); + let send_new_path = new_path.clone(); + let file_explorer = self.file_explorer.clone(); + let editors = self.main_split.editors; + + let send = create_ext_action( + self.scope, + move |response: Result| match response { + Ok(response) => { + // Get the canonicalized new path from the proxy. + let new_path = + if let ProxyResponse::CreatePathResponse { path } = + response + { + path + } else { + send_new_path + }; + + // If the renamed item is a file, update any editors the file is open + // in to use the new path. + // If the renamed item is a directory, update any editors in which a + // file the renamed directory is an ancestor of is open to use the + // file's new path. + let renamed_editors_content: Vec<_> = editors + .with_untracked(|editors| { + editors + .values() + .map(|editor| { + editor + .view + .doc + .with_untracked(|doc| doc.content) + }) + .filter(|content| { + content.with_untracked(|content| { + match content { + DocContent::File { + path, + .. + } => path.starts_with( + &send_current_path, + ), + _ => false, + } + }) + }) + .collect() + }); + + for content in renamed_editors_content { + content.update(|content| { + if let DocContent::File { path, .. } = content { + if let Ok(suffix) = + path.strip_prefix(&send_current_path) + { + *path = new_path.join(suffix); + } + } + }); + } + + file_explorer.reload(); + file_explorer.rename_state.set(RenameState::NotRenaming); + } + Err(err) => { + file_explorer.rename_state.update(|rename_state| { + rename_state.set_err(err.message) + }); + } + }, + ); + + self.file_explorer + .rename_state + .update(RenameState::set_pending); + self.common + .proxy + .rename_path(current_path.clone(), new_path, send); + } InternalCommand::GoToLocation { location } => { self.main_split.go_to_location(location, None); } @@ -1508,6 +1631,10 @@ impl WindowTabData { self.common.config, ); } + InternalCommand::OpenDiffFiles { + left_path, + right_path, + } => self.main_split.open_diff_files(left_path, right_path), } } @@ -1552,22 +1679,22 @@ impl WindowTabData { } => { self.common.completion.update(|completion| { completion.receive(*request_id, input, resp, *plugin_id); + }); - let editor_data = completion.latest_editor_id.and_then(|id| { - self.main_split - .editors - .with_untracked(|tabs| tabs.get(&id).cloned()) - }); - - if let Some(editor_data) = editor_data { - let cursor_offset = - editor_data.cursor.with_untracked(|c| c.offset()); - completion.update_document_completion( - &editor_data.view, - cursor_offset, - ); - } + let completion = self.common.completion.get_untracked(); + let editor_data = completion.latest_editor_id.and_then(|id| { + self.main_split + .editors + .with_untracked(|tabs| tabs.get(&id).cloned()) }); + if let Some(editor_data) = editor_data { + let cursor_offset = + editor_data.cursor.with_untracked(|c| c.offset()); + completion.update_document_completion( + &editor_data.view, + cursor_offset, + ); + } } CoreNotification::PublishDiagnostics { diagnostics } => { let path = path_from_url(&diagnostics.uri); @@ -1797,8 +1924,11 @@ impl WindowTabData { let (window_origin, viewport, view) = (editor.window_origin, editor.viewport, editor.view.clone()); - let (point_above, point_below) = - view.points_of_offset(self.common.hover.offset.get_untracked()); + // TODO(minor): affinity should be gotten from where the hover was started at. + let (point_above, point_below) = view.points_of_offset( + self.common.hover.offset.get_untracked(), + CursorAffinity::Forward, + ); let window_origin = window_origin.get() - self.common.window_origin.get().to_vec2(); @@ -1840,7 +1970,10 @@ impl WindowTabData { let (window_origin, viewport, view) = (editor.window_origin, editor.viewport, editor.view.clone()); - let (point_above, point_below) = view.points_of_offset(completion.offset); + // TODO(minor): What affinity should we use for this? Probably just use the cursor's + // original affinity.. + let (point_above, point_below) = + view.points_of_offset(completion.offset, CursorAffinity::Forward); let window_origin = window_origin.get() - self.common.window_origin.get().to_vec2(); @@ -1890,7 +2023,9 @@ impl WindowTabData { let (window_origin, viewport, view) = (editor.window_origin, editor.viewport, editor.view.clone()); - let (_point_above, point_below) = view.points_of_offset(code_action.offset); + // TODO(minor): What affinity should we use for this? + let (_point_above, point_below) = + view.points_of_offset(code_action.offset, CursorAffinity::Forward); let window_origin = window_origin.get() - self.common.window_origin.get().to_vec2(); @@ -1940,8 +2075,11 @@ impl WindowTabData { let (window_origin, viewport, view) = (editor.window_origin, editor.viewport, editor.view.clone()); - let (_point_above, point_below) = - view.points_of_offset(self.rename.start.get_untracked()); + // TODO(minor): What affinity should we use for this? + let (_point_above, point_below) = view.points_of_offset( + self.rename.start.get_untracked(), + CursorAffinity::Forward, + ); let window_origin = window_origin.get() - self.common.window_origin.get().to_vec2(); diff --git a/lapce-app/src/workspace.rs b/lapce-app/src/workspace.rs index 6a39e68c4a..7181ac0387 100644 --- a/lapce-app/src/workspace.rs +++ b/lapce-app/src/workspace.rs @@ -48,12 +48,26 @@ impl Display for SshHost { } } +#[cfg(windows)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct WslHost { + pub host: String, +} + +#[cfg(windows)] +impl Display for WslHost { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.host)?; + Ok(()) + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum LapceWorkspaceType { Local, RemoteSSH(SshHost), #[cfg(windows)] - RemoteWSL, + RemoteWSL(WslHost), } impl LapceWorkspaceType { @@ -61,17 +75,14 @@ impl LapceWorkspaceType { matches!(self, LapceWorkspaceType::Local) } - #[cfg(windows)] pub fn is_remote(&self) -> bool { - matches!( - self, - LapceWorkspaceType::RemoteSSH(_) | LapceWorkspaceType::RemoteWSL - ) - } + use LapceWorkspaceType::*; - #[cfg(not(windows))] - pub fn is_remote(&self) -> bool { - matches!(self, LapceWorkspaceType::RemoteSSH(_)) + #[cfg(not(windows))] + return matches!(self, RemoteSSH(_)); + + #[cfg(windows)] + return matches!(self, RemoteSSH(_) | RemoteWSL(_)); } } @@ -79,11 +90,13 @@ impl std::fmt::Display for LapceWorkspaceType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { LapceWorkspaceType::Local => f.write_str("Local"), - LapceWorkspaceType::RemoteSSH(ssh) => { - write!(f, "ssh://{ssh}") + LapceWorkspaceType::RemoteSSH(remote) => { + write!(f, "ssh://{remote}") } #[cfg(windows)] - LapceWorkspaceType::RemoteWSL => f.write_str("WSL"), + LapceWorkspaceType::RemoteWSL(remote) => { + write!(f, "{remote} (WSL)") + } } } } @@ -104,12 +117,14 @@ impl LapceWorkspace { .to_string_lossy() .to_string(); let remote = match &self.kind { - LapceWorkspaceType::Local => "".to_string(), - LapceWorkspaceType::RemoteSSH(ssh) => { - format!(" [SSH: {}]", ssh.host) + LapceWorkspaceType::Local => String::new(), + LapceWorkspaceType::RemoteSSH(remote) => { + format!(" [SSH: {}]", remote.host) } #[cfg(windows)] - LapceWorkspaceType::RemoteWSL => " [WSL]".to_string(), + LapceWorkspaceType::RemoteWSL(remote) => { + format!(" [WSL: {}]", remote.host) + } }; Some(format!("{path}{remote}")) } diff --git a/lapce-core/queries/ruby/highlights.scm b/lapce-core/queries/ruby/highlights.scm new file mode 100644 index 0000000000..14736eea2e --- /dev/null +++ b/lapce-core/queries/ruby/highlights.scm @@ -0,0 +1,154 @@ +; Keywords + +[ + "alias" + "and" + "begin" + "break" + "case" + "class" + "def" + "do" + "else" + "elsif" + "end" + "ensure" + "for" + "if" + "in" + "module" + "next" + "or" + "rescue" + "retry" + "return" + "then" + "unless" + "until" + "when" + "while" + "yield" +] @keyword + +((identifier) @keyword + (#match? @keyword "^(private|protected|public)$")) + +; Function calls + +((identifier) @function.method.builtin + (#eq? @function.method.builtin "require")) + +"defined?" @function.method.builtin + +(call + method: [(identifier) (constant)] @function.method) + +; Function definitions + +(alias (identifier) @function.method) +(setter (identifier) @function.method) +(method name: [(identifier) (constant)] @function.method) +(singleton_method name: [(identifier) (constant)] @function.method) + +; Identifiers + +[ + (class_variable) + (instance_variable) +] @property + +((identifier) @constant.builtin + (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$")) + +(file) @constant.builtin +(line) @constant.builtin +(encoding) @constant.builtin + +(hash_splat_nil + "**" @operator +) @constant.builtin + +((constant) @constant + (#match? @constant "^[A-Z\\d_]+$")) + +(constant) @constructor + +(self) @variable.builtin +(super) @variable.builtin + +(block_parameter (identifier) @variable.parameter) +(block_parameters (identifier) @variable.parameter) +(destructured_parameter (identifier) @variable.parameter) +(hash_splat_parameter (identifier) @variable.parameter) +(lambda_parameters (identifier) @variable.parameter) +(method_parameters (identifier) @variable.parameter) +(splat_parameter (identifier) @variable.parameter) + +(keyword_parameter name: (identifier) @variable.parameter) +(optional_parameter name: (identifier) @variable.parameter) + +((identifier) @function.method + (#is-not? local)) +(identifier) @variable + +; Literals + +[ + (string) + (bare_string) + (subshell) + (heredoc_body) + (heredoc_beginning) +] @string + +[ + (simple_symbol) + (delimited_symbol) + (hash_key_symbol) + (bare_symbol) +] @string.special.symbol + +(regex) @string.special.regex +(escape_sequence) @escape + +[ + (integer) + (float) +] @number + +[ + (nil) + (true) + (false) +]@constant.builtin + +(interpolation + "#{" @punctuation.special + "}" @punctuation.special) @embedded + +(comment) @comment + +; Operators + +[ +"=" +"=>" +"->" +] @operator + +[ + "," + ";" + "." +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "%w(" + "%i(" +] @punctuation.bracket \ No newline at end of file diff --git a/lapce-core/src/buffer/rope_text.rs b/lapce-core/src/buffer/rope_text.rs index 496f8552a0..7a63489f60 100644 --- a/lapce-core/src/buffer/rope_text.rs +++ b/lapce-core/src/buffer/rope_text.rs @@ -433,7 +433,7 @@ impl From for RopeTextVal { Self::new(text) } } -#[derive(Clone)] +#[derive(Copy, Clone)] pub struct RopeTextRef<'a> { pub text: &'a Rope, } diff --git a/lapce-core/src/char_buffer.rs b/lapce-core/src/char_buffer.rs index 7f94059d2b..fd9647fc44 100644 --- a/lapce-core/src/char_buffer.rs +++ b/lapce-core/src/char_buffer.rs @@ -930,10 +930,7 @@ fn test_char_buffer() { Q: std::hash::Hash + ?Sized, S: core::hash::BuildHasher, { - use core::hash::Hasher; - let mut state = hash_builder.build_hasher(); - val.hash(&mut state); - state.finish() + hash_builder.hash_one(val) } for mut char in string.chars() { diff --git a/lapce-core/src/command.rs b/lapce-core/src/command.rs index c1172cd9ea..a14656c4ae 100644 --- a/lapce-core/src/command.rs +++ b/lapce-core/src/command.rs @@ -361,6 +361,21 @@ pub enum FocusCommand { SelectPreviousSyntaxItem, #[strum(serialize = "open_source_file")] OpenSourceFile, + #[strum(serialize = "inline_completion.select")] + #[strum(message = "Inline Completion Select")] + InlineCompletionSelect, + #[strum(serialize = "inline_completion.next")] + #[strum(message = "Inline Completion Next")] + InlineCompletionNext, + #[strum(serialize = "inline_completion.previous")] + #[strum(message = "Inline Completion Previous")] + InlineCompletionPrevious, + #[strum(serialize = "inline_completion.cancel")] + #[strum(message = "Inline Completion Cancel")] + InlineCompletionCancel, + #[strum(serialize = "inline_completion.invoke")] + #[strum(message = "Inline Completion Invoke")] + InlineCompletionInvoke, } #[derive( diff --git a/lapce-core/src/cursor.rs b/lapce-core/src/cursor.rs index 20523e3c40..ef9aa9653a 100644 --- a/lapce-core/src/cursor.rs +++ b/lapce-core/src/cursor.rs @@ -22,6 +22,7 @@ pub struct Cursor { pub horiz: Option, pub motion_mode: Option, pub history_selections: Vec, + pub affinity: CursorAffinity, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -110,6 +111,46 @@ impl CursorMode { } } +/// Decides how the cursor should be placed around special areas of text. +/// Ex: +/// ```rust,ignore +/// let j = // soft linewrap +/// 1 + 2 + 3; +/// ``` +/// where `let j = ` has the issue that there's two positions you might want your cursor to be: +/// `let j = |` or `|1 + 2 + 3;` +/// These are the same offset in the text, but it feels more natural to have it move in a certain +/// way. +/// If you're at `let j =| ` and you press the right-arrow key, then it uses your backwards +/// affinity to keep you on the line at `let j = |`. +/// If you're at `1| + 2 + 3;` and you press the left-arrow key, then it uses your forwards affinity +/// to keep you on the line at `|1 + 2 + 3;`. +/// +/// For other special text, like inlay hints, this can also apply. +/// ```rust,ignore +/// let j<: String> = ... +/// ``` +/// where `<: String>` is our inlay hint, then +/// `let |j<: String> =` and you press the right-arrow key, then it uses your backwards affinity to +/// keep you on the same side of the hint, `let j|<: String>`. +/// `let j<: String> |=` and you press the right-arrow key, then it uses your forwards affinity to +/// keep you on the same side of the hint, `let j<: String>| =`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum CursorAffinity { + /// `<: String>|` + Forward, + /// `|<: String>` + Backward, +} +impl CursorAffinity { + pub fn invert(&self) -> Self { + match self { + CursorAffinity::Forward => CursorAffinity::Backward, + CursorAffinity::Backward => CursorAffinity::Forward, + } + } +} + impl Cursor { pub fn new( mode: CursorMode, @@ -121,6 +162,8 @@ impl Cursor { horiz, motion_mode, history_selections: Vec::new(), + // It should appear before any inlay hints at the very first position + affinity: CursorAffinity::Backward, } } @@ -402,7 +445,7 @@ impl Cursor { return None; } - let x = selection.regions().get(0).unwrap(); + let x = selection.regions().first().unwrap(); let v = buffer.offset_to_line_col(x.start); Some((v.0, v.1, x.start)) diff --git a/lapce-core/src/editor.rs b/lapce-core/src/editor.rs index 527c22393d..c3ef57db2c 100644 --- a/lapce-core/src/editor.rs +++ b/lapce-core/src/editor.rs @@ -16,7 +16,6 @@ use crate::{ has_unmatched_pair, matching_char, matching_pair_direction, str_is_pair_left, str_matching_pair, }, - Syntax, }, word::{get_char_property, CharClassification}, }; @@ -84,7 +83,7 @@ impl Editor { cursor: &mut Cursor, buffer: &mut Buffer, s: &str, - syntax: &Syntax, + prev_unmatched: &dyn Fn(&Buffer, char, usize) -> Option, auto_closing_matching_pairs: bool, auto_surround: bool, ) -> Vec<(RopeDelta, InvalLines, SyntaxEdit)> { @@ -168,12 +167,8 @@ impl Editor { let line_start = buffer.offset_of_line(line); if buffer.slice_to_cow(line_start..offset).trim() == "" { let opening_character = matching_char(c).unwrap(); - if let Some(previous_offset) = buffer - .previous_unmatched( - syntax, - opening_character, - offset, - ) + if let Some(previous_offset) = + prev_unmatched(buffer, opening_character, offset) { // Auto-indent closing character to the same level as the opening. let previous_line = @@ -801,7 +796,7 @@ impl Editor { cursor: &mut Cursor, buffer: &mut Buffer, cmd: &EditCommand, - syntax: &Syntax, + comment_token: &str, clipboard: &mut T, modal: bool, register: &mut Register, @@ -975,7 +970,6 @@ impl Editor { ToggleLineComment => { let mut lines = HashSet::new(); let selection = cursor.edit_selection(buffer); - let comment_token = syntax.language.comment_token(); let mut had_comment = true; let mut smallest_indent = usize::MAX; for region in selection.regions() { @@ -1645,23 +1639,20 @@ mod test { cursor::{Cursor, CursorMode}, editor::{DuplicateDirection, Editor}, selection::{SelRegion, Selection}, - syntax::Syntax, + word::WordCursor, }; + fn prev_unmatched(buffer: &Buffer, c: char, offset: usize) -> Option { + WordCursor::new(buffer.text(), offset).previous_unmatched(c) + } + #[test] fn test_insert_simple() { let mut buffer = Buffer::new("abc"); let mut cursor = Cursor::new(CursorMode::Insert(Selection::caret(1)), None, None); - Editor::insert( - &mut cursor, - &mut buffer, - "e", - &Syntax::plaintext(), - true, - true, - ); + Editor::insert(&mut cursor, &mut buffer, "e", &prev_unmatched, true, true); assert_eq!("aebc", buffer.slice_to_cow(0..buffer.len())); } @@ -1673,14 +1664,7 @@ mod test { selection.add_region(SelRegion::caret(5)); let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); - Editor::insert( - &mut cursor, - &mut buffer, - "i", - &Syntax::plaintext(), - true, - true, - ); + Editor::insert(&mut cursor, &mut buffer, "i", &prev_unmatched, true, true); assert_eq!("aibc\neifg\n", buffer.slice_to_cow(0..buffer.len())); } @@ -1692,41 +1676,13 @@ mod test { selection.add_region(SelRegion::caret(5)); let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); - Editor::insert( - &mut cursor, - &mut buffer, - "i", - &Syntax::plaintext(), - true, - true, - ); + Editor::insert(&mut cursor, &mut buffer, "i", &prev_unmatched, true, true); assert_eq!("aibc\neifg\n", buffer.slice_to_cow(0..buffer.len())); - Editor::insert( - &mut cursor, - &mut buffer, - "j", - &Syntax::plaintext(), - true, - true, - ); + Editor::insert(&mut cursor, &mut buffer, "j", &prev_unmatched, true, true); assert_eq!("aijbc\neijfg\n", buffer.slice_to_cow(0..buffer.len())); - Editor::insert( - &mut cursor, - &mut buffer, - "{", - &Syntax::plaintext(), - true, - true, - ); + Editor::insert(&mut cursor, &mut buffer, "{", &prev_unmatched, true, true); assert_eq!("aij{bc\neij{fg\n", buffer.slice_to_cow(0..buffer.len())); - Editor::insert( - &mut cursor, - &mut buffer, - " ", - &Syntax::plaintext(), - true, - true, - ); + Editor::insert(&mut cursor, &mut buffer, " ", &prev_unmatched, true, true); assert_eq!("aij{ bc\neij{ fg\n", buffer.slice_to_cow(0..buffer.len())); } @@ -1738,23 +1694,9 @@ mod test { selection.add_region(SelRegion::caret(6)); let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); - Editor::insert( - &mut cursor, - &mut buffer, - "{", - &Syntax::plaintext(), - true, - true, - ); + Editor::insert(&mut cursor, &mut buffer, "{", &prev_unmatched, true, true); assert_eq!("a{} bc\ne{} fg\n", buffer.slice_to_cow(0..buffer.len())); - Editor::insert( - &mut cursor, - &mut buffer, - "}", - &Syntax::plaintext(), - true, - true, - ); + Editor::insert(&mut cursor, &mut buffer, "}", &prev_unmatched, true, true); assert_eq!("a{} bc\ne{} fg\n", buffer.slice_to_cow(0..buffer.len())); } @@ -1765,14 +1707,7 @@ mod test { selection.add_region(SelRegion::new(0, 4, None)); selection.add_region(SelRegion::new(5, 9, None)); let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); - Editor::insert( - &mut cursor, - &mut buffer, - "{", - &Syntax::plaintext(), - true, - true, - ); + Editor::insert(&mut cursor, &mut buffer, "{", &prev_unmatched, true, true); assert_eq!("{a bc}\n{e fg}\n", buffer.slice_to_cow(0..buffer.len())); } @@ -1784,23 +1719,9 @@ mod test { selection.add_region(SelRegion::caret(6)); let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); - Editor::insert( - &mut cursor, - &mut buffer, - "{", - &Syntax::plaintext(), - false, - false, - ); + Editor::insert(&mut cursor, &mut buffer, "{", &prev_unmatched, false, false); assert_eq!("a{ bc\ne{ fg\n", buffer.slice_to_cow(0..buffer.len())); - Editor::insert( - &mut cursor, - &mut buffer, - "}", - &Syntax::plaintext(), - false, - false, - ); + Editor::insert(&mut cursor, &mut buffer, "}", &prev_unmatched, false, false); assert_eq!("a{} bc\ne{} fg\n", buffer.slice_to_cow(0..buffer.len())); } @@ -1942,14 +1863,7 @@ mod test { selection.add_region(SelRegion::caret(12)); let mut cursor = Cursor::new(CursorMode::Insert(selection), None, None); - Editor::insert( - &mut cursor, - &mut buffer, - "(", - &Syntax::plaintext(), - true, - true, - ); + Editor::insert(&mut cursor, &mut buffer, "(", &prev_unmatched, true, true); assert_eq!( "() 123() 567() 9ab() def", diff --git a/lapce-core/src/language.rs b/lapce-core/src/language.rs index 5a0623f58c..84c7285897 100644 --- a/lapce-core/src/language.rs +++ b/lapce-core/src/language.rs @@ -354,7 +354,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ id: LapceLanguage::Cmake, indent: " ", - files: &[], + files: &["cmakelists"], extensions: &["cmake"], comment: comment_properties!("#"), @@ -1202,16 +1202,13 @@ const LANGUAGES: &[SyntaxProperties] = &[ comment: comment_properties!("#"), - #[cfg(feature = "lang-ruby")] tree_sitter: Some(TreeSitterProperties { - language: tree_sitter_ruby::language, - highlight: Some(tree_sitter_ruby::HIGHLIGHT_QUERY), - injection: None, + language: None, + grammar: None, + query: None, code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), sticky_headers: &["module", "class", "method", "do_block"], }), - #[cfg(not(feature = "lang-ruby"))] - tree_sitter: None, }, SyntaxProperties { id: LapceLanguage::Rust, @@ -1558,10 +1555,10 @@ impl LapceLanguage { pub fn languages() -> Vec<&'static str> { let mut langs = vec![]; for l in LANGUAGES { - langs.push( - strum::EnumMessage::get_message(&l.id) - .unwrap_or(strum::AsStaticRef::as_static(&l.id)), - ) + // Get only languages with display name to hide inline grammars + if let Some(lang) = strum::EnumMessage::get_message(&l.id) { + langs.push(lang) + } } langs } @@ -1598,7 +1595,7 @@ impl LapceLanguage { } } - pub fn comment_token(&self) -> &str { + pub fn comment_token(&self) -> &'static str { self.properties() .comment .single_line_start diff --git a/lapce-core/src/selection.rs b/lapce-core/src/selection.rs index 2790be67f4..f42c7339ff 100644 --- a/lapce-core/src/selection.rs +++ b/lapce-core/src/selection.rs @@ -226,7 +226,7 @@ impl Selection { /// Get the leftmost [`SelRegion`] in this selection if present. pub fn first(&self) -> Option<&SelRegion> { - self.regions.get(0) + self.regions.first() } /// Get the rightmost [`SelRegion`] in this selection if present. diff --git a/lapce-proxy/src/dispatch.rs b/lapce-proxy/src/dispatch.rs index 9a7ae53e66..92247ffc67 100644 --- a/lapce-proxy/src/dispatch.rs +++ b/lapce-proxy/src/dispatch.rs @@ -1,6 +1,6 @@ use std::{ collections::{HashMap, HashSet}, - fs, + fs, io, path::{Path, PathBuf}, sync::{ atomic::{AtomicU64, Ordering}, @@ -13,7 +13,8 @@ use std::{ use alacritty_terminal::{event::WindowSize, event_loop::Msg}; use anyhow::{anyhow, Context, Result}; use crossbeam_channel::Sender; -use git2::{build::CheckoutBuilder, DiffOptions, Repository}; +use git2::ErrorCode::NotFound; +use git2::{build::CheckoutBuilder, DiffOptions, Oid, Repository}; use grep_matcher::Matcher; use grep_regex::RegexMatcherBuilder; use grep_searcher::{sinks::UTF8, SearcherBuilder}; @@ -31,7 +32,9 @@ use lapce_rpc::{ RequestId, RpcError, }; use lapce_xi_rope::Rope; -use lsp_types::{Position, Range, TextDocumentItem, Url}; +use lsp_types::{ + MessageType, Position, Range, ShowMessageParams, TextDocumentItem, Url, +}; use parking_lot::Mutex; use crate::{ @@ -284,7 +287,15 @@ impl ProxyHandler for Dispatcher { if let Some(workspace) = self.workspace.as_ref() { match git_commit(workspace, &message, diffs) { Ok(()) => (), - Err(e) => eprintln!("{e:?}"), + Err(e) => { + self.core_rpc.show_message( + "Git Commit failure".to_owned(), + ShowMessageParams { + typ: MessageType::ERROR, + message: e.to_string(), + }, + ); + } } } } @@ -521,6 +532,24 @@ impl ProxyHandler for Dispatcher { proxy_rpc.handle_response(id, result); }); } + GetInlineCompletions { + path, + position, + trigger_kind, + } => { + let proxy_rpc = self.proxy_rpc.clone(); + self.catalog_rpc.get_inline_completions( + &path, + position, + trigger_kind, + move |_, result| { + let result = result.map(|completions| { + ProxyResponse::GetInlineCompletions { completions } + }); + proxy_rpc.handle_response(id, result); + }, + ); + } GetSemanticTokens { path } => { let buffer = self.buffers.get(&path).unwrap(); let text = buffer.rope.clone(); @@ -839,18 +868,111 @@ impl ProxyHandler for Dispatcher { // We first check if the destination already exists, because rename can overwrite it // and that's not the default behavior we want for when a user renames a document. let result = if to.exists() { - Err(RpcError { - code: 0, - message: format!("{to:?} already exists"), - }) + Err(format!("{} already exists", to.display())) } else { - std::fs::rename(from, to) - .map(|_| ProxyResponse::Success {}) - .map_err(|e| RpcError { - code: 0, - message: e.to_string(), + Ok(()) + }; + + let result = result.and_then(|_| { + if let Some(parent) = to.parent() { + fs::create_dir_all(parent).map_err(|e| { + if let io::ErrorKind::AlreadyExists = e.kind() { + format!( + "{} has a parent that is not a directory", + to.display() + ) + } else { + e.to_string() + } }) + } else { + Ok(()) + } + }); + + let result = result + .and_then(|_| fs::rename(&from, &to).map_err(|e| e.to_string())); + + let result = result + .map(|_| { + let to = to.canonicalize().unwrap_or(to); + + let (is_dir, is_file) = to + .metadata() + .map(|metadata| (metadata.is_dir(), metadata.is_file())) + .unwrap_or((false, false)); + + if is_dir { + // Update all buffers in which a file the renamed directory is an + // ancestor of is open to use the file's new path. + // This could be written more nicely if `HashMap::extract_if` were + // stable. + let child_buffers: Vec<_> = self + .buffers + .keys() + .filter_map(|path| { + path.strip_prefix(&from).ok().map(|suffix| { + (path.clone(), suffix.to_owned()) + }) + }) + .collect(); + + for (path, suffix) in child_buffers { + if let Some(mut buffer) = self.buffers.remove(&path) + { + let new_path = to.join(suffix); + buffer.path = new_path; + + self.buffers.insert(buffer.path.clone(), buffer); + } + } + } else if is_file { + // If the renamed file is open in a buffer, update it to use the new + // path. + let buffer = self.buffers.remove(&from); + + if let Some(mut buffer) = buffer { + buffer.path = to.clone(); + self.buffers.insert(to.clone(), buffer); + } + } + + ProxyResponse::CreatePathResponse { path: to } + }) + .map_err(|message| RpcError { code: 0, message }); + + self.respond_rpc(id, result); + } + TestCreateAtPath { path } => { + // This performs a best effort test to see if an attempt to create an item at + // `path` or rename an item to `path` will succeed. + // Currently the only conditions that are tested are that `path` doesn't already + // exist and that `path` doesn't have a parent that exists and is not a directory. + let result = if path.exists() { + Err(format!("{} already exists", path.display())) + } else { + Ok(path) }; + + let result = result + .and_then(|path| { + let parent_is_dir = path + .ancestors() + .skip(1) + .find(|parent| parent.exists()) + .map_or(true, |parent| parent.is_dir()); + + if parent_is_dir { + Ok(ProxyResponse::Success {}) + } else { + Err(format!( + "{} has a parent that is not a directory", + path.display() + )) + } + }) + .map_err(|message| RpcError { code: 0, message }); + self.respond_rpc(id, result); } GetSelectionRange { positions, path } => { @@ -1094,18 +1216,35 @@ fn git_commit( index.write()?; let tree = index.write_tree()?; let tree = repo.find_tree(tree)?; - let signature = repo.signature()?; - let parent = repo.head()?.peel_to_commit()?; - - repo.commit( - Some("HEAD"), - &signature, - &signature, - message, - &tree, - &[&parent], - )?; - Ok(()) + + match repo.signature() { + Ok(signature) => { + let parents = repo + .head() + .and_then(|head| Ok(vec![head.peel_to_commit()?])) + .unwrap_or(vec![]); + let parents_refs = parents.iter().collect::>(); + + repo.commit( + Some("HEAD"), + &signature, + &signature, + message, + &tree, + &parents_refs, + )?; + Ok(()) + } + Err(e) => match e.code() { + NotFound => Err(anyhow!( + "No user.name and/or user.email configured for this git repository." + )), + _ => Err(anyhow!( + "Error while creating commit's signature: {}", + e.message() + )), + }, + } } fn git_checkout(workspace_path: &Path, reference: &str) -> Result<()> { @@ -1182,8 +1321,10 @@ fn git_delta_format( fn git_diff_new(workspace_path: &Path) -> Option { let repo = Repository::discover(workspace_path).ok()?; - let head = repo.head().ok()?; - let name = head.shorthand()?.to_string(); + let name = match repo.head() { + Ok(head) => head.shorthand()?.to_string(), + _ => "(No branch)".to_owned(), + }; let mut branches = Vec::new(); for branch in repo.branches(None).ok()? { @@ -1214,18 +1355,21 @@ fn git_diff_new(workspace_path: &Path) -> Option { deltas.push(delta); } } + + let oid = match repo.revparse_single("HEAD^{tree}") { + Ok(obj) => obj.id(), + _ => Oid::zero(), + }; + let cached_diff = repo - .diff_tree_to_index( - repo.find_tree(repo.revparse_single("HEAD^{tree}").ok()?.id()) - .ok() - .as_ref(), - None, - None, - ) - .ok()?; - for delta in cached_diff.deltas() { - if let Some(delta) = git_delta_format(workspace_path, &delta) { - deltas.push(delta); + .diff_tree_to_index(repo.find_tree(oid).ok().as_ref(), None, None) + .ok(); + + if let Some(cached_diff) = cached_diff { + for delta in cached_diff.deltas() { + if let Some(delta) = git_delta_format(workspace_path, &delta) { + deltas.push(delta); + } } } let mut renames = Vec::new(); diff --git a/lapce-proxy/src/plugin/catalog.rs b/lapce-proxy/src/plugin/catalog.rs index f8b3215034..9e264b3132 100644 --- a/lapce-proxy/src/plugin/catalog.rs +++ b/lapce-proxy/src/plugin/catalog.rs @@ -376,7 +376,7 @@ impl PluginCatalog { match result { Ok(resp) => { let scopes = resp.scopes.clone(); - if let Some(scope) = resp.scopes.get(0) { + if let Some(scope) = resp.scopes.first() { let scope = scope.to_owned(); thread::spawn(move || { local_dap.variables_async( diff --git a/lapce-proxy/src/plugin/dap.rs b/lapce-proxy/src/plugin/dap.rs index cbb481a1be..3c30386468 100644 --- a/lapce-proxy/src/plugin/dap.rs +++ b/lapce-proxy/src/plugin/dap.rs @@ -243,7 +243,7 @@ impl DapClient { let active_frame = current_thread .and_then(|thread_id| stack_frames.get(&thread_id)) - .and_then(|stack_frames| stack_frames.get(0)); + .and_then(|stack_frames| stack_frames.first()); let mut vars = Vec::new(); if let Some(frame) = active_frame { diff --git a/lapce-proxy/src/plugin/lsp.rs b/lapce-proxy/src/plugin/lsp.rs index b85c27e62d..1cea6b07cf 100644 --- a/lapce-proxy/src/plugin/lsp.rs +++ b/lapce-proxy/src/plugin/lsp.rs @@ -342,6 +342,7 @@ impl LspClient { }), locale: None, root_path: None, + work_done_progress_params: WorkDoneProgressParams::default(), }; if let Ok(value) = self.server_rpc.server_request( Initialize::METHOD, diff --git a/lapce-proxy/src/plugin/mod.rs b/lapce-proxy/src/plugin/mod.rs index 9d3753391e..b195464278 100644 --- a/lapce-proxy/src/plugin/mod.rs +++ b/lapce-proxy/src/plugin/mod.rs @@ -36,9 +36,9 @@ use lsp_types::{ CodeActionRequest, CodeActionResolveRequest, Completion, DocumentSymbolRequest, Formatting, GotoDefinition, GotoTypeDefinition, GotoTypeDefinitionParams, GotoTypeDefinitionResponse, HoverRequest, - InlayHintRequest, PrepareRenameRequest, References, Rename, Request, - ResolveCompletionItem, SelectionRangeRequest, SemanticTokensFullRequest, - SignatureHelpRequest, WorkspaceSymbol, + InlayHintRequest, InlineCompletionRequest, PrepareRenameRequest, References, + Rename, Request, ResolveCompletionItem, SelectionRangeRequest, + SemanticTokensFullRequest, SignatureHelpRequest, WorkspaceSymbolRequest, }, ClientCapabilities, CodeAction, CodeActionCapabilityResolveSupport, CodeActionClientCapabilities, CodeActionContext, CodeActionKind, @@ -48,8 +48,10 @@ use lsp_types::{ CompletionParams, CompletionResponse, Diagnostic, DocumentFormattingParams, DocumentSymbolParams, DocumentSymbolResponse, FormattingOptions, GotoCapability, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverClientCapabilities, - HoverParams, InlayHint, InlayHintClientCapabilities, InlayHintParams, Location, - MarkupKind, MessageActionItemCapabilities, ParameterInformationSettings, + HoverParams, InlayHint, InlayHintClientCapabilities, InlayHintParams, + InlineCompletionClientCapabilities, InlineCompletionParams, + InlineCompletionResponse, InlineCompletionTriggerKind, Location, MarkupKind, + MessageActionItemCapabilities, ParameterInformationSettings, PartialResultParams, Position, PrepareRenameResponse, PublishDiagnosticsClientCapabilities, Range, ReferenceContext, ReferenceParams, RenameParams, SelectionRange, SelectionRangeParams, SemanticTokens, @@ -622,6 +624,7 @@ impl PluginCatalogRpcHandler { context: CodeActionContext { diagnostics, only: None, + trigger_kind: None, }, work_done_progress_params: WorkDoneProgressParams::default(), partial_result_params: PartialResultParams::default(), @@ -664,6 +667,40 @@ impl PluginCatalogRpcHandler { ); } + pub fn get_inline_completions( + &self, + path: &Path, + position: Position, + trigger_kind: InlineCompletionTriggerKind, + cb: impl FnOnce(PluginId, Result) + + Clone + + Send + + 'static, + ) { + let uri = Url::from_file_path(path).unwrap(); + let method = InlineCompletionRequest::METHOD; + let params = InlineCompletionParams { + text_document_position: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri }, + position, + }, + context: lsp_types::InlineCompletionContext { + trigger_kind, + selected_completion_info: None, + }, + work_done_progress_params: WorkDoneProgressParams::default(), + }; + let language_id = + Some(language_id_from_path(path).unwrap_or("").to_string()); + self.send_request_to_all_plugins( + method, + params, + language_id, + Some(path.to_path_buf()), + cb, + ); + } + pub fn get_document_symbols( &self, path: &Path, @@ -698,7 +735,7 @@ impl PluginCatalogRpcHandler { + Send + 'static, ) { - let method = WorkspaceSymbol::METHOD; + let method = WorkspaceSymbolRequest::METHOD; let params = WorkspaceSymbolParams { query, work_done_progress_params: WorkDoneProgressParams::default(), @@ -1433,6 +1470,9 @@ fn client_capabilities() -> ClientCapabilities { publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { ..Default::default() }), + inline_completion: Some(InlineCompletionClientCapabilities { + ..Default::default() + }), ..Default::default() }), diff --git a/lapce-proxy/src/plugin/psp.rs b/lapce-proxy/src/plugin/psp.rs index e95d77aa14..a855601244 100644 --- a/lapce-proxy/src/plugin/psp.rs +++ b/lapce-proxy/src/plugin/psp.rs @@ -33,10 +33,10 @@ use lsp_types::{ request::{ CodeActionRequest, CodeActionResolveRequest, Completion, DocumentSymbolRequest, Formatting, GotoDefinition, GotoTypeDefinition, - HoverRequest, Initialize, InlayHintRequest, PrepareRenameRequest, - References, RegisterCapability, Rename, ResolveCompletionItem, - SelectionRangeRequest, SemanticTokensFullRequest, SignatureHelpRequest, - WorkDoneProgressCreate, WorkspaceSymbol, + HoverRequest, Initialize, InlayHintRequest, InlineCompletionRequest, + PrepareRenameRequest, References, RegisterCapability, Rename, + ResolveCompletionItem, SelectionRangeRequest, SemanticTokensFullRequest, + SignatureHelpRequest, WorkDoneProgressCreate, WorkspaceSymbolRequest, }, CodeActionProviderCapability, DidChangeTextDocumentParams, DidSaveTextDocumentParams, DocumentSelector, HoverProviderCapability, @@ -760,10 +760,14 @@ impl PluginHostHandler { InlayHintRequest::METHOD => { self.server_capabilities.inlay_hint_provider.is_some() } + InlineCompletionRequest::METHOD => self + .server_capabilities + .inline_completion_provider + .is_some(), DocumentSymbolRequest::METHOD => { self.server_capabilities.document_symbol_provider.is_some() } - WorkspaceSymbol::METHOD => { + WorkspaceSymbolRequest::METHOD => { self.server_capabilities.workspace_symbol_provider.is_some() } PrepareRenameRequest::METHOD => { diff --git a/lapce-proxy/src/plugin/wasi.rs b/lapce-proxy/src/plugin/wasi.rs index b338f344c7..deed5cb663 100644 --- a/lapce-proxy/src/plugin/wasi.rs +++ b/lapce-proxy/src/plugin/wasi.rs @@ -24,6 +24,7 @@ use lsp_types::{ notification::Initialized, request::Initialize, DocumentFilter, InitializeParams, InitializedParams, TextDocumentContentChangeEvent, TextDocumentIdentifier, Url, VersionedTextDocumentIdentifier, + WorkDoneProgressParams, }; use parking_lot::Mutex; use psp_types::{Notification, Request}; @@ -203,6 +204,7 @@ impl Plugin { locale: None, initialization_options: configurations, workspace_folders: None, + work_done_progress_params: WorkDoneProgressParams::default(), }, None, None, diff --git a/lapce-rpc/src/file.rs b/lapce-rpc/src/file.rs index cbd8ffba9e..e20a3fc858 100644 --- a/lapce-rpc/src/file.rs +++ b/lapce-rpc/src/file.rs @@ -1,6 +1,7 @@ use std::{ cmp::{Ord, Ordering, PartialOrd}, collections::HashMap, + mem, path::{Path, PathBuf}, }; @@ -47,11 +48,183 @@ impl PathObject { } } +/// Stores the state of any in progress rename of a path. +/// +/// The `editor_needs_reset` field is `true` if the rename editor should have its contents reset +/// when the view function next runs. +#[derive(Clone)] +pub enum RenameState { + NotRenaming, + Renaming { + path: PathBuf, + editor_needs_reset: bool, + }, + RenameRequestPending { + path: PathBuf, + editor_needs_reset: bool, + }, + RenameErr { + path: PathBuf, + editor_needs_reset: bool, + err: String, + }, +} + +impl RenameState { + pub fn is_accepting_input(&self) -> bool { + match self { + Self::NotRenaming | Self::RenameRequestPending { .. } => false, + Self::Renaming { .. } | Self::RenameErr { .. } => true, + } + } + + pub fn is_err(&self) -> bool { + match self { + Self::NotRenaming + | Self::Renaming { .. } + | Self::RenameRequestPending { .. } => false, + Self::RenameErr { .. } => true, + } + } + + pub fn set_ok(&mut self) { + if let &mut Self::RenameErr { + ref mut path, + editor_needs_reset, + .. + } = self + { + let path = mem::take(path); + + *self = Self::Renaming { + path, + editor_needs_reset, + }; + } + } + + pub fn set_pending(&mut self) { + if let &mut Self::Renaming { + ref mut path, + editor_needs_reset, + } + | &mut Self::RenameErr { + ref mut path, + editor_needs_reset, + .. + } = self + { + let path = mem::take(path); + + *self = Self::RenameRequestPending { + path, + editor_needs_reset, + }; + } + } + + pub fn set_err(&mut self, err: String) { + if let &mut Self::Renaming { + ref mut path, + editor_needs_reset, + } + | &mut Self::RenameRequestPending { + ref mut path, + editor_needs_reset, + } + | &mut Self::RenameErr { + ref mut path, + editor_needs_reset, + .. + } = self + { + let path = mem::take(path); + + *self = Self::RenameErr { + path, + editor_needs_reset, + err, + }; + } + } + + pub fn set_editor_needs_reset(&mut self, needs_reset: bool) { + if let Self::Renaming { + editor_needs_reset, .. + } + | Self::RenameRequestPending { + editor_needs_reset, .. + } + | Self::RenameErr { + editor_needs_reset, .. + } = self + { + *editor_needs_reset = needs_reset; + } + } + + pub fn path(&self) -> Option<&Path> { + match self { + Self::NotRenaming => None, + Self::Renaming { path, .. } + | Self::RenameRequestPending { path, .. } + | Self::RenameErr { path, .. } => Some(path), + } + } + + pub fn editor_needs_reset(&self) -> bool { + match self { + Self::NotRenaming => false, + &Self::Renaming { + editor_needs_reset, .. + } + | &Self::RenameRequestPending { + editor_needs_reset, .. + } + | &Self::RenameErr { + editor_needs_reset, .. + } => editor_needs_reset, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum IsRenaming { + NotRenaming, + Renaming { err: Option }, +} + +impl IsRenaming { + fn is_node_renaming(rename_state: &RenameState, node_path: &Path) -> Self { + match rename_state { + RenameState::NotRenaming => Self::NotRenaming, + RenameState::Renaming { path, .. } + | RenameState::RenameRequestPending { path, .. } => { + if path == node_path { + Self::Renaming { err: None } + } else { + Self::NotRenaming + } + } + RenameState::RenameErr { path, err, .. } => { + if path == node_path { + Self::Renaming { + err: Some(err.clone()), + } + } else { + Self::NotRenaming + } + } + } + } +} + #[derive(Debug)] pub struct FileNodeViewData { pub path: PathBuf, pub is_dir: bool, pub open: bool, + pub is_renaming: IsRenaming, pub level: usize, } @@ -79,9 +252,11 @@ impl Ord for FileNodeItem { _ => { let [self_file_name, other_file_name] = [&self.path, &other.path] .map(|path| { - path.file_name().unwrap_or_default().to_string_lossy() + path.file_name() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase() }); - human_sort::compare(&self_file_name, &other_file_name) } } @@ -223,6 +398,7 @@ impl FileNodeItem { pub fn append_view_slice( &self, view_items: &mut Vec, + rename_state: &RenameState, min: usize, max: usize, current: usize, @@ -241,13 +417,21 @@ impl FileNodeItem { path: self.path.clone(), is_dir: self.is_dir, open: self.open, + is_renaming: IsRenaming::is_node_renaming(rename_state, &self.path), level, }); } if self.open { for item in self.sorted_children() { - i = item.append_view_slice(view_items, min, max, i + 1, level + 1); + i = item.append_view_slice( + view_items, + rename_state, + min, + max, + i + 1, + level + 1, + ); if i > max { return i; } diff --git a/lapce-rpc/src/proxy.rs b/lapce-rpc/src/proxy.rs index dd1affab2f..c4f73d16a0 100644 --- a/lapce-rpc/src/proxy.rs +++ b/lapce-rpc/src/proxy.rs @@ -13,8 +13,9 @@ use lapce_xi_rope::RopeDelta; use lsp_types::{ request::GotoTypeDefinitionResponse, CodeAction, CodeActionResponse, CompletionItem, Diagnostic, DocumentSymbolResponse, GotoDefinitionResponse, - Hover, InlayHint, Location, Position, PrepareRenameResponse, SelectionRange, - SymbolInformation, TextDocumentItem, TextEdit, WorkspaceEdit, + Hover, InlayHint, InlineCompletionResponse, InlineCompletionTriggerKind, + Location, Position, PrepareRenameResponse, SelectionRange, SymbolInformation, + TextDocumentItem, TextEdit, WorkspaceEdit, }; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; @@ -111,6 +112,11 @@ pub enum ProxyRequest { GetInlayHints { path: PathBuf, }, + GetInlineCompletions { + path: PathBuf, + position: Position, + trigger_kind: InlineCompletionTriggerKind, + }, GetSemanticTokens { path: PathBuf, }, @@ -176,6 +182,9 @@ pub enum ProxyRequest { from: PathBuf, to: PathBuf, }, + TestCreateAtPath { + path: PathBuf, + }, DapVariable { dap_id: DapId, reference: usize, @@ -373,6 +382,9 @@ pub enum ProxyResponse { GetInlayHints { hints: Vec, }, + GetInlineCompletions { + completions: InlineCompletionResponse, + }, GetSemanticTokens { styles: SemanticStyles, }, @@ -394,6 +406,9 @@ pub enum ProxyResponse { DapGetScopesResponse { scopes: Vec<(dap_types::Scope, Vec)>, }, + CreatePathResponse { + path: PathBuf, + }, Success {}, SaveResponse {}, } @@ -666,6 +681,14 @@ impl ProxyRpcHandler { self.request_async(ProxyRequest::RenamePath { from, to }, f); } + pub fn test_create_at_path( + &self, + path: PathBuf, + f: impl ProxyCallback + 'static, + ) { + self.request_async(ProxyRequest::TestCreateAtPath { path }, f); + } + pub fn save_buffer_as( &self, buffer_id: BufferId, @@ -917,6 +940,23 @@ impl ProxyRpcHandler { self.request_async(ProxyRequest::GetInlayHints { path }, f); } + pub fn get_inline_completions( + &self, + path: PathBuf, + position: Position, + trigger_kind: InlineCompletionTriggerKind, + f: impl ProxyCallback + 'static, + ) { + self.request_async( + ProxyRequest::GetInlineCompletions { + path, + position, + trigger_kind, + }, + f, + ); + } + pub fn update(&self, path: PathBuf, delta: RopeDelta, rev: u64) { self.notification(ProxyNotification::Update { path, delta, rev }); } diff --git a/lapce.spec b/lapce.spec index 84b312a1e2..b964ec6834 100644 --- a/lapce.spec +++ b/lapce.spec @@ -1,5 +1,5 @@ Name: lapce-git -Version: 0.3.0.{{{ git_dir_version }}} +Version: 0.3.1.{{{ git_dir_version }}} Release: 1 Summary: Lightning-fast and Powerful Code Editor written in Rust License: Apache-2.0