Unified · Split
| @@ -0,0 +1,12 @@ | ||
| 1 | +# This config is different from config.toml in this directory, as the latter is recognized by Cargo. | |
| 2 | +# This file is placed in $HOME/.cargo/config.toml on CI runs. Cargo then merges Zeds .cargo/config.toml with $HOME/.cargo/config.toml | |
| 3 | +# with preference for settings from Zeds config.toml. | |
| 4 | +# TL;DR: If a value is set in both ci-config.toml and config.toml, config.toml value takes precedence. | |
| 5 | +# Arrays are merged together though. See: https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure | |
| 6 | +# The intent for this file is to configure CI build process with a divergance from Zed developers experience; for example, in this config file | |
| 7 | +# we use `-D warnings` for rustflags (which makes compilation fail in presence of warnings during build process). Placing that in developers `config.toml` | |
| 8 | +# would be incovenient. | |
| 9 | +# We *could* override things like RUSTFLAGS manually by setting them as environment variables, but that is less DRY; worse yet, if you forget to set proper environment variables | |
| 10 | +# in one spot, that's going to trigger a rebuild of all of the artifacts. Using ci-config.toml we can define these overrides for CI in one spot and not worry about it. | |
| 11 | +[build] | |
| 12 | +rustflags = ["-D", "warnings"] | |
| @@ -19,16 +19,12 @@ runs: · 18 unmodified lines | ||
| 19 | 19 | |
| 20 | 20 | - name: Limit target directory size |
| 21 | 21 | shell: bash -euxo pipefail {0} |
| 22 | − run: script/clear-target-dir-if-larger-than 70 | |
| 22 | + run: script/clear-target-dir-if-larger-than 100 | |
| 23 | 23 | |
| 24 | 24 | - name: Run check |
| 25 | − env: | |
| 26 | − RUSTFLAGS: -D warnings | |
| 27 | 25 | shell: bash -euxo pipefail {0} |
| 28 | 26 | run: cargo check --tests --workspace |
| 29 | 27 | |
| 30 | 28 | - name: Run tests |
| 31 | − env: | |
| 32 | − RUSTFLAGS: -D warnings | |
| 33 | 29 | shell: bash -euxo pipefail {0} |
| 34 | 30 | run: cargo nextest run --workspace --no-fail-fast |
| @@ -29,6 +29,9 @@ jobs: · 28 unmodified lines | ||
| 29 | 29 | clean: false |
| 30 | 30 | submodules: "recursive" |
| 31 | 31 | |
| 32 | + - name: Set up default .cargo/config.toml | |
| 33 | + run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml | |
| 34 | + | |
| 32 | 35 | - name: Run rustfmt |
| 33 | 36 | uses: ./.github/actions/check_formatting |
| 34 | 37 | |
| @@ -87,7 +90,7 @@ jobs: · 52 unmodified lines | ||
| 87 | 90 | submodules: "recursive" |
| 88 | 91 | |
| 89 | 92 | - name: Limit target directory size |
| 90 | − run: script/clear-target-dir-if-larger-than 70 | |
| 93 | + run: script/clear-target-dir-if-larger-than 100 | |
| 91 | 94 | |
| 92 | 95 | - name: Determine version and release channel |
| 93 | 96 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} |
| @@ -131,8 +134,6 @@ jobs: · 37 unmodified lines | ||
| 131 | 134 | |
| 132 | 135 | - uses: softprops/action-gh-release@v1 |
| 133 | 136 | name: Upload app bundle to release |
| 134 | − # TODO kb seems that zed.dev relies on GitHub releases for release version tracking. | |
| 135 | − # Find alternatives for `nightly` or just go on with more releases? | |
| 136 | 137 | if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }} |
| 137 | 138 | with: |
| 138 | 139 | draft: true |
| @@ -79,7 +79,7 @@ jobs: · 78 unmodified lines | ||
| 79 | 79 | submodules: "recursive" |
| 80 | 80 | |
| 81 | 81 | - name: Limit target directory size |
| 82 | − run: script/clear-target-dir-if-larger-than 70 | |
| 82 | + run: script/clear-target-dir-if-larger-than 100 | |
| 83 | 83 | |
| 84 | 84 | - name: Set release channel to nightly |
| 85 | 85 | run: | |
| @@ -1186,12 +1186,14 @@ version = "0.1.0" · 1185 unmodified lines | ||
| 1186 | 1186 | dependencies = [ |
| 1187 | 1187 | "anyhow", |
| 1188 | 1188 | "async-broadcast", |
| 1189 | + "async-trait", | |
| 1189 | 1190 | "audio2", |
| 1190 | 1191 | "client2", |
| 1191 | 1192 | "collections", |
| 1192 | 1193 | "fs2", |
| 1193 | 1194 | "futures 0.3.28", |
| 1194 | 1195 | "gpui2", |
| 1196 | + "image", | |
| 1195 | 1197 | "language2", |
| 1196 | 1198 | "live_kit_client2", |
| 1197 | 1199 | "log", |
| @@ -1203,7 +1205,10 @@ dependencies = [ · 5 unmodified lines | ||
| 1203 | 1205 | "serde_derive", |
| 1204 | 1206 | "serde_json", |
| 1205 | 1207 | "settings2", |
| 1208 | + "smallvec", | |
| 1209 | + "ui2", | |
| 1206 | 1210 | "util", |
| 1211 | + "workspace2", | |
| 1207 | 1212 | ] |
| 1208 | 1213 | |
| 1209 | 1214 | [[package]] |
| @@ -1398,7 +1403,7 @@ dependencies = [ · 188 unmodified lines | ||
| 1398 | 1403 | "smol", |
| 1399 | 1404 | "sum_tree", |
| 1400 | 1405 | "tempfile", |
| 1401 | − "text", | |
| 1406 | + "text2", | |
| 1402 | 1407 | "thiserror", |
| 1403 | 1408 | "time", |
| 1404 | 1409 | "tiny_http", |
| @@ -1664,7 +1669,7 @@ dependencies = [ · 259 unmodified lines | ||
| 1664 | 1669 | |
| 1665 | 1670 | [[package]] |
| 1666 | 1671 | name = "collab" |
| 1667 | −version = "0.28.0" | |
| 1672 | +version = "0.29.0" | |
| 1668 | 1673 | dependencies = [ |
| 1669 | 1674 | "anyhow", |
| 1670 | 1675 | "async-trait", |
| @@ -1869,7 +1874,7 @@ dependencies = [ · 198 unmodified lines | ||
| 1869 | 1874 | "editor2", |
| 1870 | 1875 | "feature_flags2", |
| 1871 | 1876 | "futures 0.3.28", |
| 1872 | − "fuzzy", | |
| 1877 | + "fuzzy2", | |
| 1873 | 1878 | "gpui2", |
| 1874 | 1879 | "language2", |
| 1875 | 1880 | "lazy_static", |
| @@ -6763,7 +6768,6 @@ dependencies = [ · 4887 unmodified lines | ||
| 6763 | 6768 | "anyhow", |
| 6764 | 6769 | "client2", |
| 6765 | 6770 | "collections", |
| 6766 | − "context_menu", | |
| 6767 | 6771 | "db2", |
| 6768 | 6772 | "editor2", |
| 6769 | 6773 | "futures 0.3.28", |
| @@ -8032,6 +8036,35 @@ dependencies = [ · 1262 unmodified lines | ||
| 8032 | 8036 | "workspace", |
| 8033 | 8037 | ] |
| 8034 | 8038 | |
| 8039 | +[[package]] | |
| 8040 | +name = "search2" | |
| 8041 | +version = "0.1.0" | |
| 8042 | +dependencies = [ | |
| 8043 | + "anyhow", | |
| 8044 | + "bitflags 1.3.2", | |
| 8045 | + "client2", | |
| 8046 | + "collections", | |
| 8047 | + "editor2", | |
| 8048 | + "futures 0.3.28", | |
| 8049 | + "gpui2", | |
| 8050 | + "language2", | |
| 8051 | + "log", | |
| 8052 | + "menu2", | |
| 8053 | + "postage", | |
| 8054 | + "project2", | |
| 8055 | + "serde", | |
| 8056 | + "serde_derive", | |
| 8057 | + "serde_json", | |
| 8058 | + "settings2", | |
| 8059 | + "smallvec", | |
| 8060 | + "smol", | |
| 8061 | + "theme2", | |
| 8062 | + "ui2", | |
| 8063 | + "unindent", | |
| 8064 | + "util", | |
| 8065 | + "workspace2", | |
| 8066 | +] | |
| 8067 | + | |
| 8035 | 8068 | [[package]] |
| 8036 | 8069 | name = "security-framework" |
| 8037 | 8070 | version = "2.9.2" |
| @@ -8855,6 +8888,13 @@ version = "1.1.0" · 817 unmodified lines | ||
| 8855 | 8888 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 8856 | 8889 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" |
| 8857 | 8890 | |
| 8891 | +[[package]] | |
| 8892 | +name = "story" | |
| 8893 | +version = "0.1.0" | |
| 8894 | +dependencies = [ | |
| 8895 | + "gpui2", | |
| 8896 | +] | |
| 8897 | + | |
| 8858 | 8898 | [[package]] |
| 8859 | 8899 | name = "storybook2" |
| 8860 | 8900 | version = "0.1.0" |
| @@ -8876,6 +8916,7 @@ dependencies = [ · 15 unmodified lines | ||
| 8876 | 8916 | "settings2", |
| 8877 | 8917 | "simplelog", |
| 8878 | 8918 | "smallvec", |
| 8919 | + "story", | |
| 8879 | 8920 | "strum", |
| 8880 | 8921 | "theme", |
| 8881 | 8922 | "theme2", |
| @@ -8883,17 +8924,6 @@ dependencies = [ · 1 unmodified line | ||
| 8883 | 8924 | "util", |
| 8884 | 8925 | ] |
| 8885 | 8926 | |
| 8886 | −[[package]] | |
| 8887 | −name = "storybook3" | |
| 8888 | −version = "0.1.0" | |
| 8889 | −dependencies = [ | |
| 8890 | − "anyhow", | |
| 8891 | − "gpui2", | |
| 8892 | − "settings2", | |
| 8893 | − "theme2", | |
| 8894 | − "ui2", | |
| 8895 | −] | |
| 8896 | − | |
| 8897 | 8927 | [[package]] |
| 8898 | 8928 | name = "stringprep" |
| 8899 | 8929 | version = "0.1.4" |
| @@ -9397,6 +9427,7 @@ dependencies = [ · 497 unmodified lines | ||
| 9397 | 9427 | "serde_derive", |
| 9398 | 9428 | "serde_json", |
| 9399 | 9429 | "settings2", |
| 9430 | + "story", | |
| 9400 | 9431 | "toml 0.5.11", |
| 9401 | 9432 | "util", |
| 9402 | 9433 | "uuid 1.4.1", |
| @@ -9899,7 +9930,7 @@ dependencies = [ · 496 unmodified lines | ||
| 9899 | 9930 | [[package]] |
| 9900 | 9931 | name = "tree-sitter" |
| 9901 | 9932 | version = "0.20.10" |
| 9902 | −source = "git+https://github.com/tree-sitter/tree-sitter?rev=35a6052fbcafc5e5fc0f9415b8652be7dcaf7222#35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" | |
| 9933 | +source = "git+https://github.com/tree-sitter/tree-sitter?rev=3b0159d25559b603af566ade3c83d930bf466db1#3b0159d25559b603af566ade3c83d930bf466db1" | |
| 9903 | 9934 | dependencies = [ |
| 9904 | 9935 | "cc", |
| 9905 | 9936 | "regex", |
| @@ -10147,6 +10178,15 @@ dependencies = [ · 241 unmodified lines | ||
| 10147 | 10178 | "tree-sitter", |
| 10148 | 10179 | ] |
| 10149 | 10180 | |
| 10181 | +[[package]] | |
| 10182 | +name = "tree-sitter-uiua" | |
| 10183 | +version = "0.3.3" | |
| 10184 | +source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2#9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2" | |
| 10185 | +dependencies = [ | |
| 10186 | + "cc", | |
| 10187 | + "tree-sitter", | |
| 10188 | +] | |
| 10189 | + | |
| 10150 | 10190 | [[package]] |
| 10151 | 10191 | name = "tree-sitter-vue" |
| 10152 | 10192 | version = "0.0.1" |
| @@ -10240,6 +10280,7 @@ dependencies = [ · 87 unmodified lines | ||
| 10240 | 10280 | "serde", |
| 10241 | 10281 | "settings2", |
| 10242 | 10282 | "smallvec", |
| 10283 | + "story", | |
| 10243 | 10284 | "strum", |
| 10244 | 10285 | "theme2", |
| 10245 | 10286 | ] |
| @@ -11354,6 +11395,7 @@ version = "0.1.0" · 1108 unmodified lines | ||
| 11354 | 11395 | dependencies = [ |
| 11355 | 11396 | "anyhow", |
| 11356 | 11397 | "async-recursion 1.0.5", |
| 11398 | + "async-trait", | |
| 11357 | 11399 | "bincode", |
| 11358 | 11400 | "call2", |
| 11359 | 11401 | "client2", |
| @@ -11466,7 +11508,7 @@ dependencies = [ · 106 unmodified lines | ||
| 11466 | 11508 | |
| 11467 | 11509 | [[package]] |
| 11468 | 11510 | name = "zed" |
| 11469 | −version = "0.114.0" | |
| 11511 | +version = "0.115.0" | |
| 11470 | 11512 | dependencies = [ |
| 11471 | 11513 | "activity_indicator", |
| 11472 | 11514 | "ai", |
| @@ -11583,6 +11625,7 @@ dependencies = [ · 110 unmodified lines | ||
| 11583 | 11625 | "tree-sitter-svelte", |
| 11584 | 11626 | "tree-sitter-toml", |
| 11585 | 11627 | "tree-sitter-typescript", |
| 11628 | + "tree-sitter-uiua", | |
| 11586 | 11629 | "tree-sitter-vue", |
| 11587 | 11630 | "tree-sitter-yaml", |
| 11588 | 11631 | "unindent", |
| @@ -11614,9 +11657,11 @@ dependencies = [ · 25 unmodified lines | ||
| 11614 | 11657 | "async-recursion 0.3.2", |
| 11615 | 11658 | "async-tar", |
| 11616 | 11659 | "async-trait", |
| 11660 | + "audio2", | |
| 11617 | 11661 | "auto_update2", |
| 11618 | 11662 | "backtrace", |
| 11619 | 11663 | "call2", |
| 11664 | + "channel2", | |
| 11620 | 11665 | "chrono", |
| 11621 | 11666 | "cli", |
| 11622 | 11667 | "client2", |
| @@ -11634,7 +11679,6 @@ dependencies = [ · 11 unmodified lines | ||
| 11634 | 11679 | "fs2", |
| 11635 | 11680 | "fsevent", |
| 11636 | 11681 | "futures 0.3.28", |
| 11637 | − "fuzzy", | |
| 11638 | 11682 | "go_to_line2", |
| 11639 | 11683 | "gpui2", |
| 11640 | 11684 | "ignore", |
| @@ -11662,6 +11706,7 @@ dependencies = [ · 21 unmodified lines | ||
| 11662 | 11706 | "rsa 0.4.0", |
| 11663 | 11707 | "rust-embed", |
| 11664 | 11708 | "schemars", |
| 11709 | + "search2", | |
| 11665 | 11710 | "serde", |
| 11666 | 11711 | "serde_derive", |
| 11667 | 11712 | "serde_json", |
| @@ -11704,6 +11749,7 @@ dependencies = [ · 36 unmodified lines | ||
| 11704 | 11749 | "tree-sitter-svelte", |
| 11705 | 11750 | "tree-sitter-toml", |
| 11706 | 11751 | "tree-sitter-typescript", |
| 11752 | + "tree-sitter-uiua", | |
| 11707 | 11753 | "tree-sitter-vue", |
| 11708 | 11754 | "tree-sitter-yaml", |
| 11709 | 11755 | "unindent", |
| @@ -90,6 +90,7 @@ members = [ · 89 unmodified lines | ||
| 90 | 90 | "crates/rpc", |
| 91 | 91 | "crates/rpc2", |
| 92 | 92 | "crates/search", |
| 93 | + "crates/search2", | |
| 93 | 94 | "crates/settings", |
| 94 | 95 | "crates/settings2", |
| 95 | 96 | "crates/snippet", |
| @@ -97,7 +98,6 @@ members = [ · 1 unmodified line | ||
| 97 | 98 | "crates/sqlez_macros", |
| 98 | 99 | "crates/rich_text", |
| 99 | 100 | "crates/storybook2", |
| 100 | − "crates/storybook3", | |
| 101 | 101 | "crates/sum_tree", |
| 102 | 102 | "crates/terminal", |
| 103 | 103 | "crates/terminal2", |
| @@ -110,6 +110,7 @@ members = [ · 6 unmodified lines | ||
| 110 | 110 | "crates/ui2", |
| 111 | 111 | "crates/util", |
| 112 | 112 | "crates/semantic_index", |
| 113 | + "crates/story", | |
| 113 | 114 | "crates/vim", |
| 114 | 115 | "crates/vcs_menu", |
| 115 | 116 | "crates/workspace2", |
| @@ -194,8 +195,10 @@ tree-sitter-lua = "0.0.14" · 78 unmodified lines | ||
| 194 | 195 | tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } |
| 195 | 196 | tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"} |
| 196 | 197 | tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"} |
| 198 | +tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"} | |
| 199 | + | |
| 197 | 200 | [patch.crates-io] |
| 198 | −tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" } | |
| 201 | +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" } | |
| 199 | 202 | async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } |
| 200 | 203 | |
| 201 | 204 | # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 |
| @@ -207,11 +210,12 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07 · 5 unmodified lines | ||
| 207 | 210 | |
| 208 | 211 | [profile.dev] |
| 209 | 212 | split-debuginfo = "unpacked" |
| 213 | +debug = "limited" | |
| 210 | 214 | |
| 211 | 215 | [profile.dev.package.taffy] |
| 212 | 216 | opt-level = 3 |
| 213 | 217 | |
| 214 | 218 | [profile.release] |
| 215 | −debug = true | |
| 219 | +debug = "limited" | |
| 216 | 220 | lto = "thin" |
| 217 | 221 | codegen-units = 1 |
| @@ -0,0 +1,4 @@ | ||
| 1 | +web: cd ../zed.dev && PORT=3000 npm run dev | |
| 2 | +collab: cd crates/collab2 && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve | |
| 3 | +livekit: livekit-server --dev | |
| 4 | +postgrest: postgrest crates/collab2/admin_api.conf | |
| @@ -43,7 +43,7 @@ · 42 unmodified lines | ||
| 43 | 43 | "calt": false |
| 44 | 44 | }, |
| 45 | 45 | // The default font size for text in the UI |
| 46 | − "ui_font_size": 14, | |
| 46 | + "ui_font_size": 16, | |
| 47 | 47 | // The factor to grow the active pane by. Defaults to 1.0 |
| 48 | 48 | // which gives the same size as all other panes. |
| 49 | 49 | "active_pane_magnification": 1.0, |
| @@ -7,5 +7,6 @@ · 6 unmodified lines | ||
| 7 | 7 | // custom settings, run the `open default settings` command |
| 8 | 8 | // from the command palette or from `Zed` application menu. |
| 9 | 9 | { |
| 10 | − "buffer_font_size": 15 | |
| 10 | + "ui_font_size": 16, | |
| 11 | + "buffer_font_size": 16 | |
| 11 | 12 | } |
| @@ -84,8 +84,8 @@ impl Settings for AutoUpdateSetting { · 83 unmodified lines | ||
| 84 | 84 | pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) { |
| 85 | 85 | AutoUpdateSetting::register(cx); |
| 86 | 86 | |
| 87 | − cx.observe_new_views(|wokrspace: &mut Workspace, _cx| { | |
| 88 | − wokrspace | |
| 87 | + cx.observe_new_views(|workspace: &mut Workspace, _cx| { | |
| 88 | + workspace | |
| 89 | 89 | .register_action(|_, action: &Check, cx| check(action, cx)) |
| 90 | 90 | .register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| { |
| 91 | 91 | let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]); |
| @@ -94,6 +94,11 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo · 2 unmodified lines | ||
| 94 | 94 | }) |
| 95 | 95 | .detach(); |
| 96 | 96 | }); |
| 97 | + | |
| 98 | + // @nate - code to trigger update notification on launch | |
| 99 | + // workspace.show_notification(0, _cx, |cx| { | |
| 100 | + // cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap())) | |
| 101 | + // }); | |
| 97 | 102 | }) |
| 98 | 103 | .detach(); |
| 99 | 104 | |
| @@ -131,7 +136,7 @@ pub fn check(_: &Check, cx: &mut AppContext) { · 31 unmodified lines | ||
| 131 | 136 | } |
| 132 | 137 | } |
| 133 | 138 | |
| 134 | −fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { | |
| 139 | +pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { | |
| 135 | 140 | if let Some(auto_updater) = AutoUpdater::get(cx) { |
| 136 | 141 | let auto_updater = auto_updater.read(cx); |
| 137 | 142 | let server_url = &auto_updater.server_url; |
| @@ -1,87 +1,56 @@ | ||
| 1 | −use gpui::{div, Div, EventEmitter, ParentComponent, Render, SemanticVersion, ViewContext}; | |
| 2 | −use menu::Cancel; | |
| 3 | −use workspace::notifications::NotificationEvent; | |
| 1 | +use gpui::{ | |
| 2 | + div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render, | |
| 3 | + SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, | |
| 4 | +}; | |
| 5 | +use util::channel::ReleaseChannel; | |
| 6 | +use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt}; | |
| 4 | 7 | |
| 5 | 8 | pub struct UpdateNotification { |
| 6 | − _version: SemanticVersion, | |
| 9 | + version: SemanticVersion, | |
| 7 | 10 | } |
| 8 | 11 | |
| 9 | −impl EventEmitter<NotificationEvent> for UpdateNotification {} | |
| 12 | +impl EventEmitter<DismissEvent> for UpdateNotification {} | |
| 10 | 13 | |
| 11 | 14 | impl Render for UpdateNotification { |
| 12 | − type Element = Div<Self>; | |
| 13 | − | |
| 14 | − fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element { | |
| 15 | − div().child("Updated zed!") | |
| 16 | − // let theme = theme::current(cx).clone(); | |
| 17 | − // let theme = &theme.update_notification; | |
| 18 | − | |
| 19 | − // let app_name = cx.global::<ReleaseChannel>().display_name(); | |
| 20 | − | |
| 21 | − // MouseEventHandler::new::<ViewReleaseNotes, _>(0, cx, |state, cx| { | |
| 22 | − // Flex::column() | |
| 23 | − // .with_child( | |
| 24 | − // Flex::row() | |
| 25 | − // .with_child( | |
| 26 | − // Text::new( | |
| 27 | − // format!("Updated to {app_name} {}", self.version), | |
| 28 | − // theme.message.text.clone(), | |
| 29 | − // ) | |
| 30 | − // .contained() | |
| 31 | − // .with_style(theme.message.container) | |
| 32 | − // .aligned() | |
| 33 | − // .top() | |
| 34 | − // .left() | |
| 35 | − // .flex(1., true), | |
| 36 | − // ) | |
| 37 | − // .with_child( | |
| 38 | − // MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| { | |
| 39 | − // let style = theme.dismiss_button.style_for(state); | |
| 40 | − // Svg::new("icons/x.svg") | |
| 41 | − // .with_color(style.color) | |
| 42 | − // .constrained() | |
| 43 | − // .with_width(style.icon_width) | |
| 44 | − // .aligned() | |
| 45 | − // .contained() | |
| 46 | − // .with_style(style.container) | |
| 47 | − // .constrained() | |
| 48 | − // .with_width(style.button_width) | |
| 49 | − // .with_height(style.button_width) | |
| 50 | − // }) | |
| 51 | − // .with_padding(Padding::uniform(5.)) | |
| 52 | − // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 53 | − // this.dismiss(&Default::default(), cx) | |
| 54 | − // }) | |
| 55 | − // .aligned() | |
| 56 | − // .constrained() | |
| 57 | − // .with_height(cx.font_cache().line_height(theme.message.text.font_size)) | |
| 58 | − // .aligned() | |
| 59 | − // .top() | |
| 60 | − // .flex_float(), | |
| 61 | − // ), | |
| 62 | − // ) | |
| 63 | − // .with_child({ | |
| 64 | − // let style = theme.action_message.style_for(state); | |
| 65 | − // Text::new("View the release notes", style.text.clone()) | |
| 66 | − // .contained() | |
| 67 | − // .with_style(style.container) | |
| 68 | − // }) | |
| 69 | − // .contained() | |
| 70 | − // }) | |
| 71 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 72 | − // .on_click(MouseButton::Left, |_, _, cx| { | |
| 73 | − // crate::view_release_notes(&Default::default(), cx) | |
| 74 | − // }) | |
| 75 | − // .into_any_named("update notification") | |
| 15 | + type Element = Div; | |
| 16 | + | |
| 17 | + fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element { | |
| 18 | + let app_name = cx.global::<ReleaseChannel>().display_name(); | |
| 19 | + | |
| 20 | + v_stack() | |
| 21 | + .elevation_3(cx) | |
| 22 | + .p_4() | |
| 23 | + .child( | |
| 24 | + h_stack() | |
| 25 | + .justify_between() | |
| 26 | + .child(Label::new(format!( | |
| 27 | + "Updated to {app_name} {}", | |
| 28 | + self.version | |
| 29 | + ))) | |
| 30 | + .child( | |
| 31 | + div() | |
| 32 | + .id("cancel") | |
| 33 | + .child(IconElement::new(Icon::Close)) | |
| 34 | + .cursor_pointer() | |
| 35 | + .on_click(cx.listener(|this, _, cx| this.dismiss(cx))), | |
| 36 | + ), | |
| 37 | + ) | |
| 38 | + .child( | |
| 39 | + div() | |
| 40 | + .id("notes") | |
| 41 | + .child(Label::new("View the release notes")) | |
| 42 | + .cursor_pointer() | |
| 43 | + .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)), | |
| 44 | + ) | |
| 76 | 45 | } |
| 77 | 46 | } |
| 78 | 47 | |
| 79 | 48 | impl UpdateNotification { |
| 80 | 49 | pub fn new(version: SemanticVersion) -> Self { |
| 81 | − Self { _version: version } | |
| 50 | + Self { version } | |
| 82 | 51 | } |
| 83 | 52 | |
| 84 | − pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { | |
| 85 | − cx.emit(NotificationEvent::Dismiss); | |
| 53 | + pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) { | |
| 54 | + cx.emit(DismissEvent::Dismiss); | |
| 86 | 55 | } |
| 87 | 56 | } |
| @@ -31,15 +31,19 @@ media = { path = "../media" } · 30 unmodified lines | ||
| 31 | 31 | project = { package = "project2", path = "../project2" } |
| 32 | 32 | settings = { package = "settings2", path = "../settings2" } |
| 33 | 33 | util = { path = "../util" } |
| 34 | − | |
| 34 | +ui = {package = "ui2", path = "../ui2"} | |
| 35 | +workspace = {package = "workspace2", path = "../workspace2"} | |
| 36 | +async-trait.workspace = true | |
| 35 | 37 | anyhow.workspace = true |
| 36 | 38 | async-broadcast = "0.4" |
| 37 | 39 | futures.workspace = true |
| 40 | +image = "0.23" | |
| 38 | 41 | postage.workspace = true |
| 39 | 42 | schemars.workspace = true |
| 40 | 43 | serde.workspace = true |
| 41 | 44 | serde_json.workspace = true |
| 42 | 45 | serde_derive.workspace = true |
| 46 | +smallvec.workspace = true | |
| 43 | 47 | |
| 44 | 48 | [dev-dependencies] |
| 45 | 49 | client = { package = "client2", path = "../client2", features = ["test-support"] } |
| @@ -1,25 +1,32 @@ | ||
| 1 | 1 | pub mod call_settings; |
| 2 | 2 | pub mod participant; |
| 3 | 3 | pub mod room; |
| 4 | +mod shared_screen; | |
| 4 | 5 | |
| 5 | 6 | use anyhow::{anyhow, Result}; |
| 7 | +use async_trait::async_trait; | |
| 6 | 8 | use audio::Audio; |
| 7 | 9 | use call_settings::CallSettings; |
| 8 | −use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; | |
| 10 | +use client::{ | |
| 11 | + proto::{self, PeerId}, | |
| 12 | + Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE, | |
| 13 | +}; | |
| 9 | 14 | use collections::HashSet; |
| 10 | 15 | use futures::{channel::oneshot, future::Shared, Future, FutureExt}; |
| 11 | 16 | use gpui::{ |
| 12 | 17 | AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, |
| 13 | − WeakModel, | |
| 18 | + View, ViewContext, VisualContext, WeakModel, WeakView, | |
| 14 | 19 | }; |
| 20 | +pub use participant::ParticipantLocation; | |
| 15 | 21 | use postage::watch; |
| 16 | 22 | use project::Project; |
| 17 | 23 | use room::Event; |
| 24 | +pub use room::Room; | |
| 18 | 25 | use settings::Settings; |
| 26 | +use shared_screen::SharedScreen; | |
| 19 | 27 | use std::sync::Arc; |
| 20 | − | |
| 21 | −pub use participant::ParticipantLocation; | |
| 22 | −pub use room::Room; | |
| 28 | +use util::ResultExt; | |
| 29 | +use workspace::{item::ItemHandle, CallHandler, Pane, Workspace}; | |
| 23 | 30 | |
| 24 | 31 | pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) { |
| 25 | 32 | CallSettings::register(cx); |
| @@ -464,7 +471,7 @@ impl ActiveCall { · 438 unmodified lines | ||
| 464 | 471 | &self.pending_invites |
| 465 | 472 | } |
| 466 | 473 | |
| 467 | − pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) { | |
| 474 | + pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) { | |
| 468 | 475 | if let Some(room) = self.room() { |
| 469 | 476 | let room = room.read(cx); |
| 470 | 477 | report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx); |
| @@ -477,7 +484,7 @@ pub fn report_call_event_for_room( · 6 unmodified lines | ||
| 477 | 484 | room_id: u64, |
| 478 | 485 | channel_id: Option<u64>, |
| 479 | 486 | client: &Arc<Client>, |
| 480 | − cx: &AppContext, | |
| 487 | + cx: &mut AppContext, | |
| 481 | 488 | ) { |
| 482 | 489 | let telemetry = client.telemetry(); |
| 483 | 490 | let telemetry_settings = *TelemetrySettings::get_global(cx); |
| @@ -505,6 +512,208 @@ pub fn report_call_event_for_channel( · 21 unmodified lines | ||
| 505 | 512 | ) |
| 506 | 513 | } |
| 507 | 514 | |
| 515 | +pub struct Call { | |
| 516 | + active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>, | |
| 517 | + parent_workspace: WeakView<Workspace>, | |
| 518 | +} | |
| 519 | + | |
| 520 | +impl Call { | |
| 521 | + pub fn new( | |
| 522 | + parent_workspace: WeakView<Workspace>, | |
| 523 | + cx: &mut ViewContext<'_, Workspace>, | |
| 524 | + ) -> Box<dyn CallHandler> { | |
| 525 | + let mut active_call = None; | |
| 526 | + if cx.has_global::<Model<ActiveCall>>() { | |
| 527 | + let call = cx.global::<Model<ActiveCall>>().clone(); | |
| 528 | + let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)]; | |
| 529 | + active_call = Some((call, subscriptions)); | |
| 530 | + } | |
| 531 | + Box::new(Self { | |
| 532 | + active_call, | |
| 533 | + parent_workspace, | |
| 534 | + }) | |
| 535 | + } | |
| 536 | + fn on_active_call_event( | |
| 537 | + workspace: &mut Workspace, | |
| 538 | + _: Model<ActiveCall>, | |
| 539 | + event: &room::Event, | |
| 540 | + cx: &mut ViewContext<Workspace>, | |
| 541 | + ) { | |
| 542 | + match event { | |
| 543 | + room::Event::ParticipantLocationChanged { participant_id } | |
| 544 | + | room::Event::RemoteVideoTracksChanged { participant_id } => { | |
| 545 | + workspace.leader_updated(*participant_id, cx); | |
| 546 | + } | |
| 547 | + _ => {} | |
| 548 | + } | |
| 549 | + } | |
| 550 | +} | |
| 551 | + | |
| 552 | +#[async_trait(?Send)] | |
| 553 | +impl CallHandler for Call { | |
| 554 | + fn peer_state( | |
| 555 | + &mut self, | |
| 556 | + leader_id: PeerId, | |
| 557 | + cx: &mut ViewContext<Workspace>, | |
| 558 | + ) -> Option<(bool, bool)> { | |
| 559 | + let (call, _) = self.active_call.as_ref()?; | |
| 560 | + let room = call.read(cx).room()?.read(cx); | |
| 561 | + let participant = room.remote_participant_for_peer_id(leader_id)?; | |
| 562 | + | |
| 563 | + let leader_in_this_app; | |
| 564 | + let leader_in_this_project; | |
| 565 | + match participant.location { | |
| 566 | + ParticipantLocation::SharedProject { project_id } => { | |
| 567 | + leader_in_this_app = true; | |
| 568 | + leader_in_this_project = Some(project_id) | |
| 569 | + == self | |
| 570 | + .parent_workspace | |
| 571 | + .update(cx, |this, cx| this.project().read(cx).remote_id()) | |
| 572 | + .log_err() | |
| 573 | + .flatten(); | |
| 574 | + } | |
| 575 | + ParticipantLocation::UnsharedProject => { | |
| 576 | + leader_in_this_app = true; | |
| 577 | + leader_in_this_project = false; | |
| 578 | + } | |
| 579 | + ParticipantLocation::External => { | |
| 580 | + leader_in_this_app = false; | |
| 581 | + leader_in_this_project = false; | |
| 582 | + } | |
| 583 | + }; | |
| 584 | + | |
| 585 | + Some((leader_in_this_project, leader_in_this_app)) | |
| 586 | + } | |
| 587 | + | |
| 588 | + fn shared_screen_for_peer( | |
| 589 | + &self, | |
| 590 | + peer_id: PeerId, | |
| 591 | + pane: &View<Pane>, | |
| 592 | + cx: &mut ViewContext<Workspace>, | |
| 593 | + ) -> Option<Box<dyn ItemHandle>> { | |
| 594 | + let (call, _) = self.active_call.as_ref()?; | |
| 595 | + let room = call.read(cx).room()?.read(cx); | |
| 596 | + let participant = room.remote_participant_for_peer_id(peer_id)?; | |
| 597 | + let track = participant.video_tracks.values().next()?.clone(); | |
| 598 | + let user = participant.user.clone(); | |
| 599 | + for item in pane.read(cx).items_of_type::<SharedScreen>() { | |
| 600 | + if item.read(cx).peer_id == peer_id { | |
| 601 | + return Some(Box::new(item)); | |
| 602 | + } | |
| 603 | + } | |
| 604 | + | |
| 605 | + Some(Box::new(cx.build_view(|cx| { | |
| 606 | + SharedScreen::new(&track, peer_id, user.clone(), cx) | |
| 607 | + }))) | |
| 608 | + } | |
| 609 | + fn room_id(&self, cx: &AppContext) -> Option<u64> { | |
| 610 | + Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id()) | |
| 611 | + } | |
| 612 | + fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> { | |
| 613 | + let Some((call, _)) = self.active_call.as_ref() else { | |
| 614 | + return Task::ready(Err(anyhow!("Cannot exit a call; not in a call"))); | |
| 615 | + }; | |
| 616 | + | |
| 617 | + call.update(cx, |this, cx| this.hang_up(cx)) | |
| 618 | + } | |
| 619 | + fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> { | |
| 620 | + ActiveCall::global(cx).read(cx).location().cloned() | |
| 621 | + } | |
| 622 | + fn invite( | |
| 623 | + &mut self, | |
| 624 | + called_user_id: u64, | |
| 625 | + initial_project: Option<Model<Project>>, | |
| 626 | + cx: &mut AppContext, | |
| 627 | + ) -> Task<Result<()>> { | |
| 628 | + ActiveCall::global(cx).update(cx, |this, cx| { | |
| 629 | + this.invite(called_user_id, initial_project, cx) | |
| 630 | + }) | |
| 631 | + } | |
| 632 | + fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> { | |
| 633 | + self.active_call | |
| 634 | + .as_ref() | |
| 635 | + .map(|call| { | |
| 636 | + call.0.read(cx).room().map(|room| { | |
| 637 | + room.read(cx) | |
| 638 | + .remote_participants() | |
| 639 | + .iter() | |
| 640 | + .map(|participant| { | |
| 641 | + (participant.1.user.clone(), participant.1.peer_id.clone()) | |
| 642 | + }) | |
| 643 | + .collect() | |
| 644 | + }) | |
| 645 | + }) | |
| 646 | + .flatten() | |
| 647 | + } | |
| 648 | + fn is_muted(&self, cx: &AppContext) -> Option<bool> { | |
| 649 | + self.active_call | |
| 650 | + .as_ref() | |
| 651 | + .map(|call| { | |
| 652 | + call.0 | |
| 653 | + .read(cx) | |
| 654 | + .room() | |
| 655 | + .map(|room| room.read(cx).is_muted(cx)) | |
| 656 | + }) | |
| 657 | + .flatten() | |
| 658 | + } | |
| 659 | + fn toggle_mute(&self, cx: &mut AppContext) { | |
| 660 | + self.active_call.as_ref().map(|call| { | |
| 661 | + call.0.update(cx, |this, cx| { | |
| 662 | + this.room().map(|room| { | |
| 663 | + let room = room.clone(); | |
| 664 | + cx.spawn(|_, mut cx| async move { | |
| 665 | + room.update(&mut cx, |this, cx| this.toggle_mute(cx))?? | |
| 666 | + .await | |
| 667 | + }) | |
| 668 | + .detach_and_log_err(cx); | |
| 669 | + }) | |
| 670 | + }) | |
| 671 | + }); | |
| 672 | + } | |
| 673 | + fn toggle_screen_share(&self, cx: &mut AppContext) { | |
| 674 | + self.active_call.as_ref().map(|call| { | |
| 675 | + call.0.update(cx, |this, cx| { | |
| 676 | + this.room().map(|room| { | |
| 677 | + room.update(cx, |this, cx| { | |
| 678 | + if this.is_screen_sharing() { | |
| 679 | + this.unshare_screen(cx).log_err(); | |
| 680 | + } else { | |
| 681 | + let t = this.share_screen(cx); | |
| 682 | + cx.spawn(move |_, _| async move { | |
| 683 | + t.await.log_err(); | |
| 684 | + }) | |
| 685 | + .detach(); | |
| 686 | + } | |
| 687 | + }) | |
| 688 | + }) | |
| 689 | + }) | |
| 690 | + }); | |
| 691 | + } | |
| 692 | + fn toggle_deafen(&self, cx: &mut AppContext) { | |
| 693 | + self.active_call.as_ref().map(|call| { | |
| 694 | + call.0.update(cx, |this, cx| { | |
| 695 | + this.room().map(|room| { | |
| 696 | + room.update(cx, |this, cx| { | |
| 697 | + this.toggle_deafen(cx).log_err(); | |
| 698 | + }) | |
| 699 | + }) | |
| 700 | + }) | |
| 701 | + }); | |
| 702 | + } | |
| 703 | + fn is_deafened(&self, cx: &AppContext) -> Option<bool> { | |
| 704 | + self.active_call | |
| 705 | + .as_ref() | |
| 706 | + .map(|call| { | |
| 707 | + call.0 | |
| 708 | + .read(cx) | |
| 709 | + .room() | |
| 710 | + .map(|room| room.read(cx).is_deafened()) | |
| 711 | + }) | |
| 712 | + .flatten() | |
| 713 | + .flatten() | |
| 714 | + } | |
| 715 | +} | |
| 716 | + | |
| 508 | 717 | #[cfg(test)] |
| 509 | 718 | mod test { |
| 510 | 719 | use gpui::TestAppContext; |
| @@ -4,7 +4,7 @@ use client::{proto, User}; · 3 unmodified lines | ||
| 4 | 4 | use collections::HashMap; |
| 5 | 5 | use gpui::WeakModel; |
| 6 | 6 | pub use live_kit_client::Frame; |
| 7 | −use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; | |
| 7 | +pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; | |
| 8 | 8 | use project::Project; |
| 9 | 9 | use std::sync::Arc; |
| 10 | 10 | |
| @@ -21,7 +21,7 @@ use live_kit_client::{ · 20 unmodified lines | ||
| 21 | 21 | }; |
| 22 | 22 | use postage::{sink::Sink, stream::Stream, watch}; |
| 23 | 23 | use project::Project; |
| 24 | −use settings::Settings; | |
| 24 | +use settings::Settings as _; | |
| 25 | 25 | use std::{future::Future, mem, sync::Arc, time::Duration}; |
| 26 | 26 | use util::{post_inc, ResultExt, TryFutureExt}; |
| 27 | 27 | |
| @@ -1267,7 +1267,6 @@ impl Room { · 1239 unmodified lines | ||
| 1267 | 1267 | .ok_or_else(|| anyhow!("live-kit was not initialized"))? |
| 1268 | 1268 | .await |
| 1269 | 1269 | }; |
| 1270 | − | |
| 1271 | 1270 | let publication = publish_track.await; |
| 1272 | 1271 | this.upgrade() |
| 1273 | 1272 | .ok_or_else(|| anyhow!("room was dropped"))? |
| @@ -0,0 +1,157 @@ | ||
| 1 | +use crate::participant::{Frame, RemoteVideoTrack}; | |
| 2 | +use anyhow::Result; | |
| 3 | +use client::{proto::PeerId, User}; | |
| 4 | +use futures::StreamExt; | |
| 5 | +use gpui::{ | |
| 6 | + div, AppContext, Div, Element, EventEmitter, FocusHandle, FocusableView, ParentElement, Render, | |
| 7 | + SharedString, Task, View, ViewContext, VisualContext, WindowContext, | |
| 8 | +}; | |
| 9 | +use std::sync::{Arc, Weak}; | |
| 10 | +use workspace::{item::Item, ItemNavHistory, WorkspaceId}; | |
| 11 | + | |
| 12 | +pub enum Event { | |
| 13 | + Close, | |
| 14 | +} | |
| 15 | + | |
| 16 | +pub struct SharedScreen { | |
| 17 | + track: Weak<RemoteVideoTrack>, | |
| 18 | + frame: Option<Frame>, | |
| 19 | + // temporary addition just to render something interactive. | |
| 20 | + current_frame_id: usize, | |
| 21 | + pub peer_id: PeerId, | |
| 22 | + user: Arc<User>, | |
| 23 | + nav_history: Option<ItemNavHistory>, | |
| 24 | + _maintain_frame: Task<Result<()>>, | |
| 25 | + focus: FocusHandle, | |
| 26 | +} | |
| 27 | + | |
| 28 | +impl SharedScreen { | |
| 29 | + pub fn new( | |
| 30 | + track: &Arc<RemoteVideoTrack>, | |
| 31 | + peer_id: PeerId, | |
| 32 | + user: Arc<User>, | |
| 33 | + cx: &mut ViewContext<Self>, | |
| 34 | + ) -> Self { | |
| 35 | + cx.focus_handle(); | |
| 36 | + let mut frames = track.frames(); | |
| 37 | + Self { | |
| 38 | + track: Arc::downgrade(track), | |
| 39 | + frame: None, | |
| 40 | + peer_id, | |
| 41 | + user, | |
| 42 | + nav_history: Default::default(), | |
| 43 | + _maintain_frame: cx.spawn(|this, mut cx| async move { | |
| 44 | + while let Some(frame) = frames.next().await { | |
| 45 | + this.update(&mut cx, |this, cx| { | |
| 46 | + this.frame = Some(frame); | |
| 47 | + cx.notify(); | |
| 48 | + })?; | |
| 49 | + } | |
| 50 | + this.update(&mut cx, |_, cx| cx.emit(Event::Close))?; | |
| 51 | + Ok(()) | |
| 52 | + }), | |
| 53 | + focus: cx.focus_handle(), | |
| 54 | + current_frame_id: 0, | |
| 55 | + } | |
| 56 | + } | |
| 57 | +} | |
| 58 | + | |
| 59 | +impl EventEmitter<Event> for SharedScreen {} | |
| 60 | +impl EventEmitter<workspace::item::ItemEvent> for SharedScreen {} | |
| 61 | + | |
| 62 | +impl FocusableView for SharedScreen { | |
| 63 | + fn focus_handle(&self, _: &AppContext) -> FocusHandle { | |
| 64 | + self.focus.clone() | |
| 65 | + } | |
| 66 | +} | |
| 67 | +impl Render for SharedScreen { | |
| 68 | + type Element = Div; | |
| 69 | + fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element { | |
| 70 | + let frame = self.frame.clone(); | |
| 71 | + let frame_id = self.current_frame_id; | |
| 72 | + self.current_frame_id = self.current_frame_id.wrapping_add(1); | |
| 73 | + div().children(frame.map(|_| { | |
| 74 | + ui::Label::new(frame_id.to_string()).color(ui::Color::Error) | |
| 75 | + // img().data(Arc::new(ImageData::new(image::ImageBuffer::new( | |
| 76 | + // frame.width() as u32, | |
| 77 | + // frame.height() as u32, | |
| 78 | + // )))) | |
| 79 | + })) | |
| 80 | + } | |
| 81 | +} | |
| 82 | +// impl View for SharedScreen { | |
| 83 | +// fn ui_name() -> &'static str { | |
| 84 | +// "SharedScreen" | |
| 85 | +// } | |
| 86 | + | |
| 87 | +// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { | |
| 88 | +// enum Focus {} | |
| 89 | + | |
| 90 | +// let frame = self.frame.clone(); | |
| 91 | +// MouseEventHandler::new::<Focus, _>(0, cx, |_, cx| { | |
| 92 | +// Canvas::new(move |bounds, _, _, cx| { | |
| 93 | +// if let Some(frame) = frame.clone() { | |
| 94 | +// let size = constrain_size_preserving_aspect_ratio( | |
| 95 | +// bounds.size(), | |
| 96 | +// vec2f(frame.width() as f32, frame.height() as f32), | |
| 97 | +// ); | |
| 98 | +// let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.; | |
| 99 | +// cx.scene().push_surface(gpui::platform::mac::Surface { | |
| 100 | +// bounds: RectF::new(origin, size), | |
| 101 | +// image_buffer: frame.image(), | |
| 102 | +// }); | |
| 103 | +// } | |
| 104 | +// }) | |
| 105 | +// .contained() | |
| 106 | +// .with_style(theme::current(cx).shared_screen) | |
| 107 | +// }) | |
| 108 | +// .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent()) | |
| 109 | +// .into_any() | |
| 110 | +// } | |
| 111 | +// } | |
| 112 | + | |
| 113 | +impl Item for SharedScreen { | |
| 114 | + fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> { | |
| 115 | + Some(format!("{}'s screen", self.user.github_login).into()) | |
| 116 | + } | |
| 117 | + fn deactivated(&mut self, cx: &mut ViewContext<Self>) { | |
| 118 | + if let Some(nav_history) = self.nav_history.as_mut() { | |
| 119 | + nav_history.push::<()>(None, cx); | |
| 120 | + } | |
| 121 | + } | |
| 122 | + | |
| 123 | + fn tab_content(&self, _: Option<usize>, _: &WindowContext<'_>) -> gpui::AnyElement { | |
| 124 | + div().child("Shared screen").into_any() | |
| 125 | + // Flex::row() | |
| 126 | + // .with_child( | |
| 127 | + // Svg::new("icons/desktop.svg") | |
| 128 | + // .with_color(style.label.text.color) | |
| 129 | + // .constrained() | |
| 130 | + // .with_width(style.type_icon_width) | |
| 131 | + // .aligned() | |
| 132 | + // .contained() | |
| 133 | + // .with_margin_right(style.spacing), | |
| 134 | + // ) | |
| 135 | + // .with_child( | |
| 136 | + // Label::new( | |
| 137 | + // format!("{}'s screen", self.user.github_login), | |
| 138 | + // style.label.clone(), | |
| 139 | + // ) | |
| 140 | + // .aligned(), | |
| 141 | + // ) | |
| 142 | + // .into_any() | |
| 143 | + } | |
| 144 | + | |
| 145 | + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) { | |
| 146 | + self.nav_history = Some(history); | |
| 147 | + } | |
| 148 | + | |
| 149 | + fn clone_on_split( | |
| 150 | + &self, | |
| 151 | + _workspace_id: WorkspaceId, | |
| 152 | + cx: &mut ViewContext<Self>, | |
| 153 | + ) -> Option<View<Self>> { | |
| 154 | + let track = self.track.upgrade()?; | |
| 155 | + Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx))) | |
| 156 | + } | |
| 157 | +} | |
| @@ -18,7 +18,7 @@ db = { package = "db2", path = "../db2" } · 17 unmodified lines | ||
| 18 | 18 | gpui = { package = "gpui2", path = "../gpui2" } |
| 19 | 19 | util = { path = "../util" } |
| 20 | 20 | rpc = { package = "rpc2", path = "../rpc2" } |
| 21 | −text = { path = "../text" } | |
| 21 | +text = { package = "text2", path = "../text2" } | |
| 22 | 22 | language = { package = "language2", path = "../language2" } |
| 23 | 23 | settings = { package = "settings2", path = "../settings2" } |
| 24 | 24 | feature_flags = { package = "feature_flags2", path = "../feature_flags2" } |
| @@ -109,6 +109,10 @@ pub enum ClickhouseEvent { · 108 unmodified lines | ||
| 109 | 109 | virtual_memory_in_bytes: u64, |
| 110 | 110 | milliseconds_since_first_event: i64, |
| 111 | 111 | }, |
| 112 | + App { | |
| 113 | + operation: &'static str, | |
| 114 | + milliseconds_since_first_event: i64, | |
| 115 | + }, | |
| 112 | 116 | } |
| 113 | 117 | |
| 114 | 118 | #[cfg(debug_assertions)] |
| @@ -168,13 +172,8 @@ impl Telemetry { · 53 unmodified lines | ||
| 168 | 172 | let mut state = self.state.lock(); |
| 169 | 173 | state.installation_id = installation_id.map(|id| id.into()); |
| 170 | 174 | state.session_id = Some(session_id.into()); |
| 171 | − let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); | |
| 172 | 175 | drop(state); |
| 173 | 176 | |
| 174 | − if has_clickhouse_events { | |
| 175 | − self.flush_clickhouse_events(); | |
| 176 | − } | |
| 177 | − | |
| 178 | 177 | let this = self.clone(); |
| 179 | 178 | cx.spawn(|mut cx| async move { |
| 180 | 179 | // Avoiding calling `System::new_all()`, as there have been crashes related to it |
| @@ -256,7 +255,7 @@ impl Telemetry { · 75 unmodified lines | ||
| 256 | 255 | milliseconds_since_first_event: self.milliseconds_since_first_event(), |
| 257 | 256 | }; |
| 258 | 257 | |
| 259 | − self.report_clickhouse_event(event, telemetry_settings) | |
| 258 | + self.report_clickhouse_event(event, telemetry_settings, false) | |
| 260 | 259 | } |
| 261 | 260 | |
| 262 | 261 | pub fn report_copilot_event( |
| @@ -273,7 +272,7 @@ impl Telemetry { · 10 unmodified lines | ||
| 273 | 272 | milliseconds_since_first_event: self.milliseconds_since_first_event(), |
| 274 | 273 | }; |
| 275 | 274 | |
| 276 | − self.report_clickhouse_event(event, telemetry_settings) | |
| 275 | + self.report_clickhouse_event(event, telemetry_settings, false) | |
| 277 | 276 | } |
| 278 | 277 | |
| 279 | 278 | pub fn report_assistant_event( |
| @@ -290,7 +289,7 @@ impl Telemetry { · 10 unmodified lines | ||
| 290 | 289 | milliseconds_since_first_event: self.milliseconds_since_first_event(), |
| 291 | 290 | }; |
| 292 | 291 | |
| 293 | − self.report_clickhouse_event(event, telemetry_settings) | |
| 292 | + self.report_clickhouse_event(event, telemetry_settings, false) | |
| 294 | 293 | } |
| 295 | 294 | |
| 296 | 295 | pub fn report_call_event( |
| @@ -307,7 +306,7 @@ impl Telemetry { · 10 unmodified lines | ||
| 307 | 306 | milliseconds_since_first_event: self.milliseconds_since_first_event(), |
| 308 | 307 | }; |
| 309 | 308 | |
| 310 | − self.report_clickhouse_event(event, telemetry_settings) | |
| 309 | + self.report_clickhouse_event(event, telemetry_settings, false) | |
| 311 | 310 | } |
| 312 | 311 | |
| 313 | 312 | pub fn report_cpu_event( |
| @@ -322,7 +321,7 @@ impl Telemetry { · 8 unmodified lines | ||
| 322 | 321 | milliseconds_since_first_event: self.milliseconds_since_first_event(), |
| 323 | 322 | }; |
| 324 | 323 | |
| 325 | − self.report_clickhouse_event(event, telemetry_settings) | |
| 324 | + self.report_clickhouse_event(event, telemetry_settings, false) | |
| 326 | 325 | } |
| 327 | 326 | |
| 328 | 327 | pub fn report_memory_event( |
| @@ -337,7 +336,21 @@ impl Telemetry { · 8 unmodified lines | ||
| 337 | 336 | milliseconds_since_first_event: self.milliseconds_since_first_event(), |
| 338 | 337 | }; |
| 339 | 338 | |
| 340 | − self.report_clickhouse_event(event, telemetry_settings) | |
| 339 | + self.report_clickhouse_event(event, telemetry_settings, false) | |
| 340 | + } | |
| 341 | + | |
| 342 | + // app_events are called at app open and app close, so flush is set to immediately send | |
| 343 | + pub fn report_app_event( | |
| 344 | + self: &Arc<Self>, | |
| 345 | + telemetry_settings: TelemetrySettings, | |
| 346 | + operation: &'static str, | |
| 347 | + ) { | |
| 348 | + let event = ClickhouseEvent::App { | |
| 349 | + operation, | |
| 350 | + milliseconds_since_first_event: self.milliseconds_since_first_event(), | |
| 351 | + }; | |
| 352 | + | |
| 353 | + self.report_clickhouse_event(event, telemetry_settings, true) | |
| 341 | 354 | } |
| 342 | 355 | |
| 343 | 356 | fn milliseconds_since_first_event(&self) -> i64 { |
| @@ -358,6 +371,7 @@ impl Telemetry { · 14 unmodified lines | ||
| 358 | 371 | self: &Arc<Self>, |
| 359 | 372 | event: ClickhouseEvent, |
| 360 | 373 | telemetry_settings: TelemetrySettings, |
| 374 | + immediate_flush: bool, | |
| 361 | 375 | ) { |
| 362 | 376 | if !telemetry_settings.metrics { |
| 363 | 377 | return; |
| @@ -370,7 +384,7 @@ impl Telemetry { · 6 unmodified lines | ||
| 370 | 384 | .push(ClickhouseEventWrapper { signed_in, event }); |
| 371 | 385 | |
| 372 | 386 | if state.installation_id.is_some() { |
| 373 | − if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { | |
| 387 | + if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { | |
| 374 | 388 | drop(state); |
| 375 | 389 | self.flush_clickhouse_events(); |
| 376 | 390 | } else { |
| @@ -382,7 +382,7 @@ impl settings::Settings for TelemetrySettings { · 381 unmodified lines | ||
| 382 | 382 | } |
| 383 | 383 | |
| 384 | 384 | impl Client { |
| 385 | − pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> { | |
| 385 | + pub fn new(http: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> { | |
| 386 | 386 | Arc::new(Self { |
| 387 | 387 | id: AtomicU64::new(0), |
| 388 | 388 | peer: Peer::new(0), |
| @@ -551,7 +551,6 @@ impl Client { · 162 unmodified lines | ||
| 551 | 551 | F: 'static + Future<Output = Result<()>>, |
| 552 | 552 | { |
| 553 | 553 | let message_type_id = TypeId::of::<M>(); |
| 554 | − | |
| 555 | 554 | let mut state = self.state.write(); |
| 556 | 555 | state |
| 557 | 556 | .models_by_message_type |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; |
| 2 | 2 | use chrono::{DateTime, Utc}; |
| 3 | +use futures::Future; | |
| 3 | 4 | use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task}; |
| 4 | 5 | use lazy_static::lazy_static; |
| 5 | 6 | use parking_lot::Mutex; |
| @@ -107,6 +108,10 @@ pub enum ClickhouseEvent { · 101 unmodified lines | ||
| 107 | 108 | virtual_memory_in_bytes: u64, |
| 108 | 109 | milliseconds_since_first_event: i64, |
| 109 | 110 | }, |
| 111 | + App { | |
| 112 | + operation: &'static str, | |
| 113 | + milliseconds_since_first_event: i64, | |
| 114 | + }, | |
| 110 | 115 | } |
| 111 | 116 | |
| 112 | 117 | #[cfg(debug_assertions)] |
| @@ -122,12 +127,13 @@ const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1); · 9 unmodified lines | ||
| 122 | 127 | const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); |
| 123 | 128 | |
| 124 | 129 | impl Telemetry { |
| 125 | − pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> { | |
| 130 | + pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> { | |
| 126 | 131 | let release_channel = if cx.has_global::<ReleaseChannel>() { |
| 127 | 132 | Some(cx.global::<ReleaseChannel>().display_name()) |
| 128 | 133 | } else { |
| 129 | 134 | None |
| 130 | 135 | }; |
| 136 | + | |
| 131 | 137 | // TODO: Replace all hardware stuff with nested SystemSpecs json |
| 132 | 138 | let this = Arc::new(Self { |
| 133 | 139 | http_client: client, |
| @@ -147,9 +153,30 @@ impl Telemetry { · 13 unmodified lines | ||
| 147 | 153 | }), |
| 148 | 154 | }); |
| 149 | 155 | |
| 156 | + // We should only ever have one instance of Telemetry, leak the subscription to keep it alive | |
| 157 | + // rather than store in TelemetryState, complicating spawn as subscriptions are not Send | |
| 158 | + std::mem::forget(cx.on_app_quit({ | |
| 159 | + let this = this.clone(); | |
| 160 | + move |cx| this.shutdown_telemetry(cx) | |
| 161 | + })); | |
| 162 | + | |
| 150 | 163 | this |
| 151 | 164 | } |
| 152 | 165 | |
| 166 | + #[cfg(any(test, feature = "test-support"))] | |
| 167 | + fn shutdown_telemetry(self: &Arc<Self>, _: &mut AppContext) -> impl Future<Output = ()> { | |
| 168 | + Task::ready(()) | |
| 169 | + } | |
| 170 | + | |
| 171 | + // Skip calling this function in tests. | |
| 172 | + // TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings | |
| 173 | + #[cfg(not(any(test, feature = "test-support")))] | |
| 174 | + fn shutdown_telemetry(self: &Arc<Self>, cx: &mut AppContext) -> impl Future<Output = ()> { | |
| 175 | + let telemetry_settings = TelemetrySettings::get_global(cx).clone(); | |
| 176 | + self.report_app_event(telemetry_settings, "close"); | |
| 177 | + Task::ready(()) | |
| 178 | + } | |
| 179 | + | |
| 153 | 180 | pub fn log_file_path(&self) -> Option<PathBuf> { |
| 154 | 181 | Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) |
| 155 | 182 | } |
| @@ -163,13 +190,8 @@ impl Telemetry { · 7 unmodified lines | ||
| 163 | 190 | let mut state = self.state.lock(); |
| 164 | 191 | state.installation_id = installation_id.map(|id| id.into()); |
| 165 | 192 | state.session_id = Some(session_id.into()); |
| 166 | − let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); | |
| 167 | 193 | drop(state); |
| 168 | 194 | |
| 169 | − if has_clickhouse_events { | |
| 170 | − self.flush_clickhouse_events(); | |
| 171 | − } | |
| 172 | − | |
| 173 | 195 | let this = self.clone(); |
| 174 | 196 | cx.spawn(|cx| async move { |
| 175 | 197 | // Avoiding calling `System::new_all()`, as there have been crashes related to it |
| @@ -257,7 +279,7 @@ impl Telemetry { · 81 unmodified lines | ||
| 257 | 279 | milliseconds_since_first_event: self.milliseconds_since_first_event(), |
| 258 | 280 | }; |
| 259 | 281 | |
| 260 | − self.report_clickhouse_event(event, telemetry_settings) | |
| 282 | + self.report_clickhouse_event(event, telemetry_settings, false) | |
| 261 | 283 | } |
| 262 | 284 | |
| 263 | 285 | pub fn report_copilot_event( |
| @@ -274,7 +296,7 @@ impl Telemetry { · 10 unmodified lines | ||
| 274 | 296 | milliseconds_since_first_event: self.milliseconds_since_first_event(), |
| 275 | 297 | }; |
| 276 | 298 | |
| 277 | − self.report_clickhouse_event(event, telemetry_settings) | |
| 299 | + self.report_clickhouse_event(event, telemetry_settings, false) | |
| 278 | 300 | } |
| 279 | 301 | |
| 280 | 302 | pub fn report_assistant_event( |
| @@ -291,7 +313,7 @@ impl Telemetry { · 10 unmodified lines | ||
| 291 | 313 | milliseconds_since_first_event: self.milliseconds_since_first_event(), |
| 292 | 314 | }; |
| 293 | 315 | |
| 294 | − self.report_clickhouse_event(event, telemetry_settings) | |
| 316 | + self.report_clickhouse_event(event, telemetry_settings, false) | |
| 295 | 317 | } |
| 296 | 318 | |
| 297 | 319 | pub fn report_call_event( |
| @@ -308,7 +330,7 @@ impl Telemetry { · 10 unmodified lines | ||
| 308 | 330 | milliseconds_since_first_event: self.milliseconds_since_first_event(), |
| 309 | 331 | }; |
| 310 | 332 | |
| 311 | − self.report_clickhouse_event(event, telemetry_settings) | |
| 333 | + self.report_clickhouse_event(event, telemetry_settings, false) | |
| 312 | 334 | } |
| 313 | 335 | |
| 314 | 336 | pub fn report_cpu_event( |
| @@ -323,7 +345,7 @@ impl Telemetry { · 8 unmodified lines | ||
| 323 | 345 | milliseconds_since_first_event: self.milliseconds_since_first_event(), |
| 324 | 346 | }; |
| 325 | 347 | |
| 326 | − self.report_clickhouse_event(event, telemetry_settings) | |
| 348 | + self.report_clickhouse_event(event, telemetry_settings, false) | |
| 327 | 349 | } |
| 328 | 350 | |
| 329 | 351 | pub fn report_memory_event( |
| @@ -338,7 +360,21 @@ impl Telemetry { · 8 unmodified lines | ||
| 338 | 360 | milliseconds_since_first_event: self.milliseconds_since_first_event(), |
| 339 | 361 | }; |
| 340 | 362 | |
| 341 | − self.report_clickhouse_event(event, telemetry_settings) | |
| 363 | + self.report_clickhouse_event(event, telemetry_settings, false) | |
| 364 | + } | |
| 365 | + | |
| 366 | + // app_events are called at app open and app close, so flush is set to immediately send | |
| 367 | + pub fn report_app_event( | |
| 368 | + self: &Arc<Self>, | |
| 369 | + telemetry_settings: TelemetrySettings, | |
| 370 | + operation: &'static str, | |
| 371 | + ) { | |
| 372 | + let event = ClickhouseEvent::App { | |
| 373 | + operation, | |
| 374 | + milliseconds_since_first_event: self.milliseconds_since_first_event(), | |
| 375 | + }; | |
| 376 | + | |
| 377 | + self.report_clickhouse_event(event, telemetry_settings, true) | |
| 342 | 378 | } |
| 343 | 379 | |
| 344 | 380 | fn milliseconds_since_first_event(&self) -> i64 { |
| @@ -359,6 +395,7 @@ impl Telemetry { · 14 unmodified lines | ||
| 359 | 395 | self: &Arc<Self>, |
| 360 | 396 | event: ClickhouseEvent, |
| 361 | 397 | telemetry_settings: TelemetrySettings, |
| 398 | + immediate_flush: bool, | |
| 362 | 399 | ) { |
| 363 | 400 | if !telemetry_settings.metrics { |
| 364 | 401 | return; |
| @@ -371,7 +408,7 @@ impl Telemetry { · 6 unmodified lines | ||
| 371 | 408 | .push(ClickhouseEventWrapper { signed_in, event }); |
| 372 | 409 | |
| 373 | 410 | if state.installation_id.is_some() { |
| 374 | − if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { | |
| 411 | + if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { | |
| 375 | 412 | drop(state); |
| 376 | 413 | self.flush_clickhouse_events(); |
| 377 | 414 | } else { |
| @@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"] · 2 unmodified lines | ||
| 3 | 3 | default-run = "collab" |
| 4 | 4 | edition = "2021" |
| 5 | 5 | name = "collab" |
| 6 | −version = "0.28.0" | |
| 6 | +version = "0.29.0" | |
| 7 | 7 | publish = false |
| 8 | 8 | |
| 9 | 9 | [[bin]] |
| @@ -10,7 +10,7 @@ publish = false · 9 unmodified lines | ||
| 10 | 10 | name = "collab2" |
| 11 | 11 | |
| 12 | 12 | [[bin]] |
| 13 | −name = "seed" | |
| 13 | +name = "seed2" | |
| 14 | 14 | required-features = ["seed-support"] |
| 15 | 15 | |
| 16 | 16 | [dependencies] |
File renamed without changes.
File renamed without changes.
| @@ -149,7 +149,7 @@ impl TestServer { · 148 unmodified lines | ||
| 149 | 149 | .user_id |
| 150 | 150 | }; |
| 151 | 151 | let client_name = name.to_string(); |
| 152 | − let mut client = cx.read(|cx| Client::new(http.clone(), cx)); | |
| 152 | + let mut client = cx.update(|cx| Client::new(http.clone(), cx)); | |
| 153 | 153 | let server = self.server.clone(); |
| 154 | 154 | let db = self.app_state.db.clone(); |
| 155 | 155 | let connection_killers = self.connection_killers.clone(); |
| @@ -221,6 +221,7 @@ impl TestServer { · 65 unmodified lines | ||
| 221 | 221 | fs: fs.clone(), |
| 222 | 222 | build_window_options: |_, _, _| Default::default(), |
| 223 | 223 | node_runtime: FakeNodeRuntime::new(), |
| 224 | + call_factory: |_, _| Box::new(workspace::TestCallHandler), | |
| 224 | 225 | }); |
| 225 | 226 | |
| 226 | 227 | cx.update(|cx| { |
| @@ -33,7 +33,7 @@ collections = { path = "../collections" } · 32 unmodified lines | ||
| 33 | 33 | # drag_and_drop = { path = "../drag_and_drop" } |
| 34 | 34 | editor = { package="editor2", path = "../editor2" } |
| 35 | 35 | #feedback = { path = "../feedback" } |
| 36 | −fuzzy = { path = "../fuzzy" } | |
| 36 | +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } | |
| 37 | 37 | gpui = { package = "gpui2", path = "../gpui2" } |
| 38 | 38 | language = { package = "language2", path = "../language2" } |
| 39 | 39 | menu = { package = "menu2", path = "../menu2" } |
| @@ -1,5 +1,6 @@ | ||
| 1 | +#![allow(unused)] | |
| 1 | 2 | // mod channel_modal; |
| 2 | −// mod contact_finder; | |
| 3 | +mod contact_finder; | |
| 3 | 4 | |
| 4 | 5 | // use crate::{ |
| 5 | 6 | // channel_view::{self, ChannelView}, |
| @@ -15,7 +16,7 @@ · 9 unmodified lines | ||
| 15 | 16 | // proto::{self, PeerId}, |
| 16 | 17 | // Client, Contact, User, UserStore, |
| 17 | 18 | // }; |
| 18 | −// use contact_finder::ContactFinder; | |
| 19 | +use contact_finder::ContactFinder; | |
| 19 | 20 | // use context_menu::{ContextMenu, ContextMenuItem}; |
| 20 | 21 | // use db::kvp::KEY_VALUE_STORE; |
| 21 | 22 | // use drag_and_drop::{DragAndDrop, Draggable}; |
| @@ -155,20 +156,30 @@ actions!( · 133 unmodified lines | ||
| 155 | 156 | |
| 156 | 157 | const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; |
| 157 | 158 | |
| 158 | −use std::sync::Arc; | |
| 159 | +use std::{iter::once, mem, sync::Arc}; | |
| 159 | 160 | |
| 161 | +use call::ActiveCall; | |
| 162 | +use channel::{Channel, ChannelId, ChannelStore}; | |
| 163 | +use client::{Client, Contact, User, UserStore}; | |
| 160 | 164 | use db::kvp::KEY_VALUE_STORE; |
| 165 | +use editor::Editor; | |
| 166 | +use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; | |
| 167 | +use fuzzy::{match_strings, StringMatchCandidate}; | |
| 161 | 168 | use gpui::{ |
| 162 | − actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, | |
| 163 | − Focusable, FocusableView, InteractiveComponent, ParentComponent, Render, View, ViewContext, | |
| 164 | − VisualContext, WeakView, | |
| 169 | + actions, div, img, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, | |
| 170 | + Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, | |
| 171 | + RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, | |
| 165 | 172 | }; |
| 166 | 173 | use project::Fs; |
| 167 | 174 | use serde_derive::{Deserialize, Serialize}; |
| 168 | 175 | use settings::Settings; |
| 169 | −use util::ResultExt; | |
| 176 | +use ui::{ | |
| 177 | + h_stack, v_stack, Avatar, Button, Icon, IconButton, Label, List, ListHeader, ListItem, Tooltip, | |
| 178 | +}; | |
| 179 | +use util::{maybe, ResultExt}; | |
| 170 | 180 | use workspace::{ |
| 171 | 181 | dock::{DockPosition, Panel, PanelEvent}, |
| 182 | + notifications::NotifyResultExt, | |
| 172 | 183 | Workspace, |
| 173 | 184 | }; |
| 174 | 185 | |
| @@ -266,26 +277,26 @@ pub fn init(cx: &mut AppContext) { · 91 unmodified lines | ||
| 266 | 277 | // ); |
| 267 | 278 | } |
| 268 | 279 | |
| 269 | −// #[derive(Debug)] | |
| 270 | −// pub enum ChannelEditingState { | |
| 271 | −// Create { | |
| 272 | −// location: Option<ChannelId>, | |
| 273 | −// pending_name: Option<String>, | |
| 274 | −// }, | |
| 275 | −// Rename { | |
| 276 | −// location: ChannelId, | |
| 277 | −// pending_name: Option<String>, | |
| 278 | −// }, | |
| 279 | −// } | |
| 280 | +#[derive(Debug)] | |
| 281 | +pub enum ChannelEditingState { | |
| 282 | + Create { | |
| 283 | + location: Option<ChannelId>, | |
| 284 | + pending_name: Option<String>, | |
| 285 | + }, | |
| 286 | + Rename { | |
| 287 | + location: ChannelId, | |
| 288 | + pending_name: Option<String>, | |
| 289 | + }, | |
| 290 | +} | |
| 280 | 291 | |
| 281 | −// impl ChannelEditingState { | |
| 282 | −// fn pending_name(&self) -> Option<&str> { | |
| 283 | −// match self { | |
| 284 | −// ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(), | |
| 285 | −// ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(), | |
| 286 | −// } | |
| 287 | −// } | |
| 288 | −// } | |
| 292 | +impl ChannelEditingState { | |
| 293 | + fn pending_name(&self) -> Option<&str> { | |
| 294 | + match self { | |
| 295 | + ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(), | |
| 296 | + ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(), | |
| 297 | + } | |
| 298 | + } | |
| 299 | +} | |
| 289 | 300 | |
| 290 | 301 | pub struct CollabPanel { |
| 291 | 302 | width: Option<f32>, |
| @@ -294,22 +305,22 @@ pub struct CollabPanel { · 2 unmodified lines | ||
| 294 | 305 | // channel_clipboard: Option<ChannelMoveClipboard>, |
| 295 | 306 | // pending_serialization: Task<Option<()>>, |
| 296 | 307 | // context_menu: ViewHandle<ContextMenu>, |
| 297 | − // filter_editor: ViewHandle<Editor>, | |
| 308 | + filter_editor: View<Editor>, | |
| 298 | 309 | // channel_name_editor: ViewHandle<Editor>, |
| 299 | − // channel_editing_state: Option<ChannelEditingState>, | |
| 300 | − // entries: Vec<ListEntry>, | |
| 310 | + channel_editing_state: Option<ChannelEditingState>, | |
| 311 | + entries: Vec<ListEntry>, | |
| 301 | 312 | // selection: Option<usize>, |
| 302 | − // user_store: ModelHandle<UserStore>, | |
| 303 | − // client: Arc<Client>, | |
| 304 | − // channel_store: ModelHandle<ChannelStore>, | |
| 313 | + channel_store: Model<ChannelStore>, | |
| 314 | + user_store: Model<UserStore>, | |
| 315 | + client: Arc<Client>, | |
| 305 | 316 | // project: ModelHandle<Project>, |
| 306 | − // match_candidates: Vec<StringMatchCandidate>, | |
| 317 | + match_candidates: Vec<StringMatchCandidate>, | |
| 307 | 318 | // list_state: ListState<Self>, |
| 308 | − // subscriptions: Vec<Subscription>, | |
| 309 | − // collapsed_sections: Vec<Section>, | |
| 310 | − // collapsed_channels: Vec<ChannelId>, | |
| 319 | + subscriptions: Vec<Subscription>, | |
| 320 | + collapsed_sections: Vec<Section>, | |
| 321 | + collapsed_channels: Vec<ChannelId>, | |
| 311 | 322 | // drag_target_channel: ChannelDragTarget, |
| 312 | − _workspace: WeakView<Workspace>, | |
| 323 | + workspace: WeakView<Workspace>, | |
| 313 | 324 | // context_menu_on_selected: bool, |
| 314 | 325 | } |
| 315 | 326 | |
| @@ -333,58 +344,58 @@ struct SerializedCollabPanel { · 17 unmodified lines | ||
| 333 | 344 | // Dismissed, |
| 334 | 345 | // } |
| 335 | 346 | |
| 336 | −// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] | |
| 337 | −// enum Section { | |
| 338 | −// ActiveCall, | |
| 339 | −// Channels, | |
| 340 | −// ChannelInvites, | |
| 341 | −// ContactRequests, | |
| 342 | −// Contacts, | |
| 343 | −// Online, | |
| 344 | −// Offline, | |
| 345 | −// } | |
| 347 | +#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] | |
| 348 | +enum Section { | |
| 349 | + ActiveCall, | |
| 350 | + Channels, | |
| 351 | + ChannelInvites, | |
| 352 | + ContactRequests, | |
| 353 | + Contacts, | |
| 354 | + Online, | |
| 355 | + Offline, | |
| 356 | +} | |
| 346 | 357 | |
| 347 | −// #[derive(Clone, Debug)] | |
| 348 | −// enum ListEntry { | |
| 349 | −// Header(Section), | |
| 350 | −// CallParticipant { | |
| 351 | −// user: Arc<User>, | |
| 352 | −// peer_id: Option<PeerId>, | |
| 353 | −// is_pending: bool, | |
| 354 | −// }, | |
| 355 | −// ParticipantProject { | |
| 356 | −// project_id: u64, | |
| 357 | −// worktree_root_names: Vec<String>, | |
| 358 | −// host_user_id: u64, | |
| 359 | −// is_last: bool, | |
| 360 | −// }, | |
| 361 | −// ParticipantScreen { | |
| 362 | −// peer_id: Option<PeerId>, | |
| 363 | −// is_last: bool, | |
| 364 | −// }, | |
| 365 | −// IncomingRequest(Arc<User>), | |
| 366 | −// OutgoingRequest(Arc<User>), | |
| 367 | −// ChannelInvite(Arc<Channel>), | |
| 368 | −// Channel { | |
| 369 | −// channel: Arc<Channel>, | |
| 370 | −// depth: usize, | |
| 371 | −// has_children: bool, | |
| 372 | −// }, | |
| 373 | −// ChannelNotes { | |
| 374 | −// channel_id: ChannelId, | |
| 375 | −// }, | |
| 376 | −// ChannelChat { | |
| 377 | −// channel_id: ChannelId, | |
| 378 | −// }, | |
| 379 | −// ChannelEditor { | |
| 380 | −// depth: usize, | |
| 381 | −// }, | |
| 382 | −// Contact { | |
| 383 | −// contact: Arc<Contact>, | |
| 384 | −// calling: bool, | |
| 385 | −// }, | |
| 386 | −// ContactPlaceholder, | |
| 387 | −// } | |
| 358 | +#[derive(Clone, Debug)] | |
| 359 | +enum ListEntry { | |
| 360 | + Header(Section), | |
| 361 | + // CallParticipant { | |
| 362 | + // user: Arc<User>, | |
| 363 | + // peer_id: Option<PeerId>, | |
| 364 | + // is_pending: bool, | |
| 365 | + // }, | |
| 366 | + // ParticipantProject { | |
| 367 | + // project_id: u64, | |
| 368 | + // worktree_root_names: Vec<String>, | |
| 369 | + // host_user_id: u64, | |
| 370 | + // is_last: bool, | |
| 371 | + // }, | |
| 372 | + // ParticipantScreen { | |
| 373 | + // peer_id: Option<PeerId>, | |
| 374 | + // is_last: bool, | |
| 375 | + // }, | |
| 376 | + IncomingRequest(Arc<User>), | |
| 377 | + OutgoingRequest(Arc<User>), | |
| 378 | + // ChannelInvite(Arc<Channel>), | |
| 379 | + Channel { | |
| 380 | + channel: Arc<Channel>, | |
| 381 | + depth: usize, | |
| 382 | + has_children: bool, | |
| 383 | + }, | |
| 384 | + // ChannelNotes { | |
| 385 | + // channel_id: ChannelId, | |
| 386 | + // }, | |
| 387 | + // ChannelChat { | |
| 388 | + // channel_id: ChannelId, | |
| 389 | + // }, | |
| 390 | + ChannelEditor { | |
| 391 | + depth: usize, | |
| 392 | + }, | |
| 393 | + Contact { | |
| 394 | + contact: Arc<Contact>, | |
| 395 | + calling: bool, | |
| 396 | + }, | |
| 397 | + ContactPlaceholder, | |
| 398 | +} | |
| 388 | 399 | |
| 389 | 400 | // impl Entity for CollabPanel { |
| 390 | 401 | // type Event = Event; |
| @@ -395,16 +406,11 @@ impl CollabPanel { · 4 unmodified lines | ||
| 395 | 406 | cx.build_view(|cx| { |
| 396 | 407 | // let view_id = cx.view_id(); |
| 397 | 408 | |
| 398 | − // let filter_editor = cx.add_view(|cx| { | |
| 399 | − // let mut editor = Editor::single_line( | |
| 400 | − // Some(Arc::new(|theme| { | |
| 401 | − // theme.collab_panel.user_query_editor.clone() | |
| 402 | − // })), | |
| 403 | − // cx, | |
| 404 | − // ); | |
| 405 | − // editor.set_placeholder_text("Filter channels, contacts", cx); | |
| 406 | − // editor | |
| 407 | − // }); | |
| 409 | + let filter_editor = cx.build_view(|cx| { | |
| 410 | + let mut editor = Editor::single_line(cx); | |
| 411 | + editor.set_placeholder_text("Filter channels, contacts", cx); | |
| 412 | + editor | |
| 413 | + }); | |
| 408 | 414 | |
| 409 | 415 | // cx.subscribe(&filter_editor, |this, _, event, cx| { |
| 410 | 416 | // if let editor::Event::BufferEdited = event { |
| @@ -583,7 +589,7 @@ impl CollabPanel { · 172 unmodified lines | ||
| 583 | 589 | // } |
| 584 | 590 | // }); |
| 585 | 591 | |
| 586 | − let this = Self { | |
| 592 | + let mut this = Self { | |
| 587 | 593 | width: None, |
| 588 | 594 | focus_handle: cx.focus_handle(), |
| 589 | 595 | // channel_clipboard: None, |
| @@ -591,25 +597,25 @@ impl CollabPanel { · 1 unmodified line | ||
| 591 | 597 | // pending_serialization: Task::ready(None), |
| 592 | 598 | // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), |
| 593 | 599 | // channel_name_editor, |
| 594 | − // filter_editor, | |
| 595 | − // entries: Vec::default(), | |
| 596 | − // channel_editing_state: None, | |
| 600 | + filter_editor, | |
| 601 | + entries: Vec::default(), | |
| 602 | + channel_editing_state: None, | |
| 597 | 603 | // selection: None, |
| 598 | − // user_store: workspace.user_store().clone(), | |
| 599 | − // channel_store: ChannelStore::global(cx), | |
| 604 | + channel_store: ChannelStore::global(cx), | |
| 605 | + user_store: workspace.user_store().clone(), | |
| 600 | 606 | // project: workspace.project().clone(), |
| 601 | − // subscriptions: Vec::default(), | |
| 602 | − // match_candidates: Vec::default(), | |
| 603 | − // collapsed_sections: vec![Section::Offline], | |
| 604 | − // collapsed_channels: Vec::default(), | |
| 605 | − _workspace: workspace.weak_handle(), | |
| 606 | − // client: workspace.app_state().client.clone(), | |
| 607 | + subscriptions: Vec::default(), | |
| 608 | + match_candidates: Vec::default(), | |
| 609 | + collapsed_sections: vec![Section::Offline], | |
| 610 | + collapsed_channels: Vec::default(), | |
| 611 | + workspace: workspace.weak_handle(), | |
| 612 | + client: workspace.app_state().client.clone(), | |
| 607 | 613 | // context_menu_on_selected: true, |
| 608 | 614 | // drag_target_channel: ChannelDragTarget::None, |
| 609 | 615 | // list_state, |
| 610 | 616 | }; |
| 611 | 617 | |
| 612 | − // this.update_entries(false, cx); | |
| 618 | + this.update_entries(false, cx); | |
| 613 | 619 | |
| 614 | 620 | // // Update the dock position when the setting changes. |
| 615 | 621 | // let mut old_dock_position = this.position(cx); |
| @@ -626,10 +632,10 @@ impl CollabPanel { · 10 unmodified lines | ||
| 626 | 632 | // ); |
| 627 | 633 | |
| 628 | 634 | // let active_call = ActiveCall::global(cx); |
| 629 | − // this.subscriptions | |
| 630 | − // .push(cx.observe(&this.user_store, |this, _, cx| { | |
| 631 | − // this.update_entries(true, cx) | |
| 632 | − // })); | |
| 635 | + this.subscriptions | |
| 636 | + .push(cx.observe(&this.user_store, |this, _, cx| { | |
| 637 | + this.update_entries(true, cx) | |
| 638 | + })); | |
| 633 | 639 | // this.subscriptions |
| 634 | 640 | // .push(cx.observe(&this.channel_store, |this, _, cx| { |
| 635 | 641 | // this.update_entries(true, cx) |
| @@ -663,6 +669,9 @@ impl CollabPanel { · 27 unmodified lines | ||
| 663 | 669 | }) |
| 664 | 670 | } |
| 665 | 671 | |
| 672 | + fn contacts(&self, cx: &AppContext) -> Option<Vec<Arc<Contact>>> { | |
| 673 | + Some(self.user_store.read(cx).contacts().to_owned()) | |
| 674 | + } | |
| 666 | 675 | pub async fn load( |
| 667 | 676 | workspace: WeakView<Workspace>, |
| 668 | 677 | mut cx: AsyncWindowContext, |
| @@ -715,449 +724,449 @@ impl CollabPanel { · 46 unmodified lines | ||
| 715 | 724 | // ); |
| 716 | 725 | // } |
| 717 | 726 | |
| 718 | − // fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) { | |
| 719 | − // let channel_store = self.channel_store.read(cx); | |
| 720 | − // let user_store = self.user_store.read(cx); | |
| 721 | − // let query = self.filter_editor.read(cx).text(cx); | |
| 722 | − // let executor = cx.background().clone(); | |
| 723 | − | |
| 724 | − // let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); | |
| 725 | − // let old_entries = mem::take(&mut self.entries); | |
| 726 | − // let mut scroll_to_top = false; | |
| 727 | − | |
| 728 | − // if let Some(room) = ActiveCall::global(cx).read(cx).room() { | |
| 729 | − // self.entries.push(ListEntry::Header(Section::ActiveCall)); | |
| 730 | − // if !old_entries | |
| 731 | − // .iter() | |
| 732 | − // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) | |
| 733 | − // { | |
| 734 | − // scroll_to_top = true; | |
| 735 | − // } | |
| 736 | − | |
| 737 | − // if !self.collapsed_sections.contains(&Section::ActiveCall) { | |
| 738 | − // let room = room.read(cx); | |
| 739 | − | |
| 740 | − // if let Some(channel_id) = room.channel_id() { | |
| 741 | − // self.entries.push(ListEntry::ChannelNotes { channel_id }); | |
| 742 | − // self.entries.push(ListEntry::ChannelChat { channel_id }) | |
| 743 | − // } | |
| 744 | − | |
| 745 | − // // Populate the active user. | |
| 746 | − // if let Some(user) = user_store.current_user() { | |
| 747 | − // self.match_candidates.clear(); | |
| 748 | − // self.match_candidates.push(StringMatchCandidate { | |
| 749 | − // id: 0, | |
| 750 | − // string: user.github_login.clone(), | |
| 751 | − // char_bag: user.github_login.chars().collect(), | |
| 752 | − // }); | |
| 753 | − // let matches = executor.block(match_strings( | |
| 754 | − // &self.match_candidates, | |
| 755 | − // &query, | |
| 756 | − // true, | |
| 757 | − // usize::MAX, | |
| 758 | − // &Default::default(), | |
| 759 | − // executor.clone(), | |
| 760 | − // )); | |
| 761 | − // if !matches.is_empty() { | |
| 762 | − // let user_id = user.id; | |
| 763 | − // self.entries.push(ListEntry::CallParticipant { | |
| 764 | − // user, | |
| 765 | − // peer_id: None, | |
| 766 | − // is_pending: false, | |
| 767 | − // }); | |
| 768 | − // let mut projects = room.local_participant().projects.iter().peekable(); | |
| 769 | − // while let Some(project) = projects.next() { | |
| 770 | − // self.entries.push(ListEntry::ParticipantProject { | |
| 771 | − // project_id: project.id, | |
| 772 | − // worktree_root_names: project.worktree_root_names.clone(), | |
| 773 | − // host_user_id: user_id, | |
| 774 | − // is_last: projects.peek().is_none() && !room.is_screen_sharing(), | |
| 775 | − // }); | |
| 776 | − // } | |
| 777 | − // if room.is_screen_sharing() { | |
| 778 | − // self.entries.push(ListEntry::ParticipantScreen { | |
| 779 | − // peer_id: None, | |
| 780 | − // is_last: true, | |
| 781 | − // }); | |
| 782 | − // } | |
| 783 | − // } | |
| 784 | − // } | |
| 785 | − | |
| 786 | − // // Populate remote participants. | |
| 787 | − // self.match_candidates.clear(); | |
| 788 | − // self.match_candidates | |
| 789 | − // .extend(room.remote_participants().iter().map(|(_, participant)| { | |
| 790 | − // StringMatchCandidate { | |
| 791 | − // id: participant.user.id as usize, | |
| 792 | − // string: participant.user.github_login.clone(), | |
| 793 | − // char_bag: participant.user.github_login.chars().collect(), | |
| 794 | − // } | |
| 795 | − // })); | |
| 796 | − // let matches = executor.block(match_strings( | |
| 797 | − // &self.match_candidates, | |
| 798 | − // &query, | |
| 799 | − // true, | |
| 800 | − // usize::MAX, | |
| 801 | − // &Default::default(), | |
| 802 | − // executor.clone(), | |
| 803 | − // )); | |
| 804 | − // for mat in matches { | |
| 805 | − // let user_id = mat.candidate_id as u64; | |
| 806 | − // let participant = &room.remote_participants()[&user_id]; | |
| 807 | − // self.entries.push(ListEntry::CallParticipant { | |
| 808 | − // user: participant.user.clone(), | |
| 809 | − // peer_id: Some(participant.peer_id), | |
| 810 | − // is_pending: false, | |
| 811 | − // }); | |
| 812 | − // let mut projects = participant.projects.iter().peekable(); | |
| 813 | − // while let Some(project) = projects.next() { | |
| 814 | − // self.entries.push(ListEntry::ParticipantProject { | |
| 815 | − // project_id: project.id, | |
| 816 | − // worktree_root_names: project.worktree_root_names.clone(), | |
| 817 | − // host_user_id: participant.user.id, | |
| 818 | − // is_last: projects.peek().is_none() | |
| 819 | − // && participant.video_tracks.is_empty(), | |
| 820 | − // }); | |
| 821 | − // } | |
| 822 | − // if !participant.video_tracks.is_empty() { | |
| 823 | − // self.entries.push(ListEntry::ParticipantScreen { | |
| 824 | − // peer_id: Some(participant.peer_id), | |
| 825 | − // is_last: true, | |
| 826 | − // }); | |
| 827 | − // } | |
| 828 | − // } | |
| 829 | − | |
| 830 | − // // Populate pending participants. | |
| 831 | − // self.match_candidates.clear(); | |
| 832 | − // self.match_candidates | |
| 833 | − // .extend(room.pending_participants().iter().enumerate().map( | |
| 834 | − // |(id, participant)| StringMatchCandidate { | |
| 835 | − // id, | |
| 836 | − // string: participant.github_login.clone(), | |
| 837 | − // char_bag: participant.github_login.chars().collect(), | |
| 838 | − // }, | |
| 839 | − // )); | |
| 840 | − // let matches = executor.block(match_strings( | |
| 841 | − // &self.match_candidates, | |
| 842 | − // &query, | |
| 843 | − // true, | |
| 844 | − // usize::MAX, | |
| 845 | − // &Default::default(), | |
| 846 | − // executor.clone(), | |
| 847 | − // )); | |
| 848 | − // self.entries | |
| 849 | − // .extend(matches.iter().map(|mat| ListEntry::CallParticipant { | |
| 850 | − // user: room.pending_participants()[mat.candidate_id].clone(), | |
| 851 | − // peer_id: None, | |
| 852 | − // is_pending: true, | |
| 853 | − // })); | |
| 854 | − // } | |
| 855 | − // } | |
| 856 | − | |
| 857 | − // let mut request_entries = Vec::new(); | |
| 858 | − | |
| 859 | − // if cx.has_flag::<ChannelsAlpha>() { | |
| 860 | − // self.entries.push(ListEntry::Header(Section::Channels)); | |
| 861 | − | |
| 862 | − // if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { | |
| 863 | − // self.match_candidates.clear(); | |
| 864 | − // self.match_candidates | |
| 865 | − // .extend(channel_store.ordered_channels().enumerate().map( | |
| 866 | − // |(ix, (_, channel))| StringMatchCandidate { | |
| 867 | − // id: ix, | |
| 868 | − // string: channel.name.clone(), | |
| 869 | − // char_bag: channel.name.chars().collect(), | |
| 870 | − // }, | |
| 871 | − // )); | |
| 872 | − // let matches = executor.block(match_strings( | |
| 873 | − // &self.match_candidates, | |
| 874 | − // &query, | |
| 875 | − // true, | |
| 876 | − // usize::MAX, | |
| 877 | − // &Default::default(), | |
| 878 | − // executor.clone(), | |
| 879 | − // )); | |
| 880 | − // if let Some(state) = &self.channel_editing_state { | |
| 881 | − // if matches!(state, ChannelEditingState::Create { location: None, .. }) { | |
| 882 | − // self.entries.push(ListEntry::ChannelEditor { depth: 0 }); | |
| 883 | − // } | |
| 884 | − // } | |
| 885 | − // let mut collapse_depth = None; | |
| 886 | − // for mat in matches { | |
| 887 | − // let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); | |
| 888 | − // let depth = channel.parent_path.len(); | |
| 889 | − | |
| 890 | − // if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { | |
| 891 | − // collapse_depth = Some(depth); | |
| 892 | − // } else if let Some(collapsed_depth) = collapse_depth { | |
| 893 | − // if depth > collapsed_depth { | |
| 894 | − // continue; | |
| 895 | − // } | |
| 896 | − // if self.is_channel_collapsed(channel.id) { | |
| 897 | − // collapse_depth = Some(depth); | |
| 898 | − // } else { | |
| 899 | − // collapse_depth = None; | |
| 900 | − // } | |
| 901 | − // } | |
| 902 | − | |
| 903 | − // let has_children = channel_store | |
| 904 | − // .channel_at_index(mat.candidate_id + 1) | |
| 905 | − // .map_or(false, |next_channel| { | |
| 906 | − // next_channel.parent_path.ends_with(&[channel.id]) | |
| 907 | − // }); | |
| 908 | − | |
| 909 | − // match &self.channel_editing_state { | |
| 910 | − // Some(ChannelEditingState::Create { | |
| 911 | − // location: parent_id, | |
| 912 | − // .. | |
| 913 | − // }) if *parent_id == Some(channel.id) => { | |
| 914 | − // self.entries.push(ListEntry::Channel { | |
| 915 | − // channel: channel.clone(), | |
| 916 | − // depth, | |
| 917 | − // has_children: false, | |
| 918 | − // }); | |
| 919 | − // self.entries | |
| 920 | − // .push(ListEntry::ChannelEditor { depth: depth + 1 }); | |
| 921 | − // } | |
| 922 | − // Some(ChannelEditingState::Rename { | |
| 923 | − // location: parent_id, | |
| 924 | − // .. | |
| 925 | − // }) if parent_id == &channel.id => { | |
| 926 | − // self.entries.push(ListEntry::ChannelEditor { depth }); | |
| 927 | − // } | |
| 928 | − // _ => { | |
| 929 | − // self.entries.push(ListEntry::Channel { | |
| 930 | − // channel: channel.clone(), | |
| 931 | − // depth, | |
| 932 | − // has_children, | |
| 933 | − // }); | |
| 934 | − // } | |
| 935 | − // } | |
| 936 | − // } | |
| 937 | − // } | |
| 938 | − | |
| 939 | − // let channel_invites = channel_store.channel_invitations(); | |
| 940 | − // if !channel_invites.is_empty() { | |
| 941 | − // self.match_candidates.clear(); | |
| 942 | − // self.match_candidates | |
| 943 | − // .extend(channel_invites.iter().enumerate().map(|(ix, channel)| { | |
| 944 | − // StringMatchCandidate { | |
| 945 | − // id: ix, | |
| 946 | − // string: channel.name.clone(), | |
| 947 | − // char_bag: channel.name.chars().collect(), | |
| 948 | − // } | |
| 949 | − // })); | |
| 950 | − // let matches = executor.block(match_strings( | |
| 951 | − // &self.match_candidates, | |
| 952 | − // &query, | |
| 953 | − // true, | |
| 954 | − // usize::MAX, | |
| 955 | − // &Default::default(), | |
| 956 | − // executor.clone(), | |
| 957 | − // )); | |
| 958 | − // request_entries.extend(matches.iter().map(|mat| { | |
| 959 | − // ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone()) | |
| 960 | − // })); | |
| 961 | − | |
| 962 | − // if !request_entries.is_empty() { | |
| 963 | − // self.entries | |
| 964 | − // .push(ListEntry::Header(Section::ChannelInvites)); | |
| 965 | − // if !self.collapsed_sections.contains(&Section::ChannelInvites) { | |
| 966 | − // self.entries.append(&mut request_entries); | |
| 967 | − // } | |
| 968 | − // } | |
| 969 | − // } | |
| 970 | − // } | |
| 971 | − | |
| 972 | − // self.entries.push(ListEntry::Header(Section::Contacts)); | |
| 973 | − | |
| 974 | − // request_entries.clear(); | |
| 975 | − // let incoming = user_store.incoming_contact_requests(); | |
| 976 | − // if !incoming.is_empty() { | |
| 977 | − // self.match_candidates.clear(); | |
| 978 | − // self.match_candidates | |
| 979 | − // .extend( | |
| 980 | − // incoming | |
| 981 | − // .iter() | |
| 982 | − // .enumerate() | |
| 983 | − // .map(|(ix, user)| StringMatchCandidate { | |
| 984 | − // id: ix, | |
| 985 | − // string: user.github_login.clone(), | |
| 986 | − // char_bag: user.github_login.chars().collect(), | |
| 987 | − // }), | |
| 988 | − // ); | |
| 989 | − // let matches = executor.block(match_strings( | |
| 990 | − // &self.match_candidates, | |
| 991 | − // &query, | |
| 992 | − // true, | |
| 993 | − // usize::MAX, | |
| 994 | − // &Default::default(), | |
| 995 | − // executor.clone(), | |
| 996 | − // )); | |
| 997 | − // request_entries.extend( | |
| 998 | − // matches | |
| 999 | − // .iter() | |
| 1000 | − // .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())), | |
| 1001 | − // ); | |
| 1002 | − // } | |
| 1003 | − | |
| 1004 | − // let outgoing = user_store.outgoing_contact_requests(); | |
| 1005 | − // if !outgoing.is_empty() { | |
| 1006 | − // self.match_candidates.clear(); | |
| 1007 | − // self.match_candidates | |
| 1008 | − // .extend( | |
| 1009 | − // outgoing | |
| 1010 | − // .iter() | |
| 1011 | − // .enumerate() | |
| 1012 | − // .map(|(ix, user)| StringMatchCandidate { | |
| 1013 | − // id: ix, | |
| 1014 | − // string: user.github_login.clone(), | |
| 1015 | − // char_bag: user.github_login.chars().collect(), | |
| 1016 | − // }), | |
| 1017 | − // ); | |
| 1018 | − // let matches = executor.block(match_strings( | |
| 1019 | − // &self.match_candidates, | |
| 1020 | − // &query, | |
| 1021 | − // true, | |
| 1022 | − // usize::MAX, | |
| 1023 | − // &Default::default(), | |
| 1024 | − // executor.clone(), | |
| 1025 | − // )); | |
| 1026 | − // request_entries.extend( | |
| 1027 | − // matches | |
| 1028 | − // .iter() | |
| 1029 | − // .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), | |
| 1030 | − // ); | |
| 1031 | − // } | |
| 1032 | − | |
| 1033 | − // if !request_entries.is_empty() { | |
| 1034 | − // self.entries | |
| 1035 | − // .push(ListEntry::Header(Section::ContactRequests)); | |
| 1036 | − // if !self.collapsed_sections.contains(&Section::ContactRequests) { | |
| 1037 | − // self.entries.append(&mut request_entries); | |
| 1038 | − // } | |
| 1039 | − // } | |
| 1040 | − | |
| 1041 | − // let contacts = user_store.contacts(); | |
| 1042 | − // if !contacts.is_empty() { | |
| 1043 | − // self.match_candidates.clear(); | |
| 1044 | − // self.match_candidates | |
| 1045 | − // .extend( | |
| 1046 | − // contacts | |
| 1047 | − // .iter() | |
| 1048 | − // .enumerate() | |
| 1049 | − // .map(|(ix, contact)| StringMatchCandidate { | |
| 1050 | − // id: ix, | |
| 1051 | − // string: contact.user.github_login.clone(), | |
| 1052 | − // char_bag: contact.user.github_login.chars().collect(), | |
| 1053 | − // }), | |
| 1054 | − // ); | |
| 1055 | − | |
| 1056 | − // let matches = executor.block(match_strings( | |
| 1057 | − // &self.match_candidates, | |
| 1058 | − // &query, | |
| 1059 | − // true, | |
| 1060 | − // usize::MAX, | |
| 1061 | − // &Default::default(), | |
| 1062 | − // executor.clone(), | |
| 1063 | − // )); | |
| 1064 | − | |
| 1065 | − // let (online_contacts, offline_contacts) = matches | |
| 1066 | − // .iter() | |
| 1067 | − // .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online); | |
| 1068 | − | |
| 1069 | − // for (matches, section) in [ | |
| 1070 | − // (online_contacts, Section::Online), | |
| 1071 | − // (offline_contacts, Section::Offline), | |
| 1072 | − // ] { | |
| 1073 | − // if !matches.is_empty() { | |
| 1074 | − // self.entries.push(ListEntry::Header(section)); | |
| 1075 | − // if !self.collapsed_sections.contains(§ion) { | |
| 1076 | − // let active_call = &ActiveCall::global(cx).read(cx); | |
| 1077 | − // for mat in matches { | |
| 1078 | − // let contact = &contacts[mat.candidate_id]; | |
| 1079 | − // self.entries.push(ListEntry::Contact { | |
| 1080 | − // contact: contact.clone(), | |
| 1081 | − // calling: active_call.pending_invites().contains(&contact.user.id), | |
| 1082 | − // }); | |
| 1083 | − // } | |
| 1084 | − // } | |
| 1085 | − // } | |
| 1086 | − // } | |
| 1087 | − // } | |
| 1088 | − | |
| 1089 | − // if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() { | |
| 1090 | − // self.entries.push(ListEntry::ContactPlaceholder); | |
| 1091 | − // } | |
| 1092 | − | |
| 1093 | − // if select_same_item { | |
| 1094 | − // if let Some(prev_selected_entry) = prev_selected_entry { | |
| 1095 | − // self.selection.take(); | |
| 1096 | − // for (ix, entry) in self.entries.iter().enumerate() { | |
| 1097 | − // if *entry == prev_selected_entry { | |
| 1098 | − // self.selection = Some(ix); | |
| 1099 | − // break; | |
| 1100 | − // } | |
| 1101 | − // } | |
| 1102 | − // } | |
| 1103 | − // } else { | |
| 1104 | − // self.selection = self.selection.and_then(|prev_selection| { | |
| 1105 | − // if self.entries.is_empty() { | |
| 1106 | − // None | |
| 1107 | − // } else { | |
| 1108 | − // Some(prev_selection.min(self.entries.len() - 1)) | |
| 1109 | − // } | |
| 1110 | − // }); | |
| 1111 | − // } | |
| 1112 | − | |
| 1113 | − // let old_scroll_top = self.list_state.logical_scroll_top(); | |
| 1114 | − | |
| 1115 | − // self.list_state.reset(self.entries.len()); | |
| 727 | + fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) { | |
| 728 | + let channel_store = self.channel_store.read(cx); | |
| 729 | + let user_store = self.user_store.read(cx); | |
| 730 | + let query = self.filter_editor.read(cx).text(cx); | |
| 731 | + let executor = cx.background_executor().clone(); | |
| 732 | + | |
| 733 | + // let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); | |
| 734 | + let _old_entries = mem::take(&mut self.entries); | |
| 735 | + // let mut scroll_to_top = false; | |
| 736 | + | |
| 737 | + // if let Some(room) = ActiveCall::global(cx).read(cx).room() { | |
| 738 | + // self.entries.push(ListEntry::Header(Section::ActiveCall)); | |
| 739 | + // if !old_entries | |
| 740 | + // .iter() | |
| 741 | + // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) | |
| 742 | + // { | |
| 743 | + // scroll_to_top = true; | |
| 744 | + // } | |
| 745 | + | |
| 746 | + // if !self.collapsed_sections.contains(&Section::ActiveCall) { | |
| 747 | + // let room = room.read(cx); | |
| 748 | + | |
| 749 | + // if let Some(channel_id) = room.channel_id() { | |
| 750 | + // self.entries.push(ListEntry::ChannelNotes { channel_id }); | |
| 751 | + // self.entries.push(ListEntry::ChannelChat { channel_id }) | |
| 752 | + // } | |
| 753 | + | |
| 754 | + // // Populate the active user. | |
| 755 | + // if let Some(user) = user_store.current_user() { | |
| 756 | + // self.match_candidates.clear(); | |
| 757 | + // self.match_candidates.push(StringMatchCandidate { | |
| 758 | + // id: 0, | |
| 759 | + // string: user.github_login.clone(), | |
| 760 | + // char_bag: user.github_login.chars().collect(), | |
| 761 | + // }); | |
| 762 | + // let matches = executor.block(match_strings( | |
| 763 | + // &self.match_candidates, | |
| 764 | + // &query, | |
| 765 | + // true, | |
| 766 | + // usize::MAX, | |
| 767 | + // &Default::default(), | |
| 768 | + // executor.clone(), | |
| 769 | + // )); | |
| 770 | + // if !matches.is_empty() { | |
| 771 | + // let user_id = user.id; | |
| 772 | + // self.entries.push(ListEntry::CallParticipant { | |
| 773 | + // user, | |
| 774 | + // peer_id: None, | |
| 775 | + // is_pending: false, | |
| 776 | + // }); | |
| 777 | + // let mut projects = room.local_participant().projects.iter().peekable(); | |
| 778 | + // while let Some(project) = projects.next() { | |
| 779 | + // self.entries.push(ListEntry::ParticipantProject { | |
| 780 | + // project_id: project.id, | |
| 781 | + // worktree_root_names: project.worktree_root_names.clone(), | |
| 782 | + // host_user_id: user_id, | |
| 783 | + // is_last: projects.peek().is_none() && !room.is_screen_sharing(), | |
| 784 | + // }); | |
| 785 | + // } | |
| 786 | + // if room.is_screen_sharing() { | |
| 787 | + // self.entries.push(ListEntry::ParticipantScreen { | |
| 788 | + // peer_id: None, | |
| 789 | + // is_last: true, | |
| 790 | + // }); | |
| 791 | + // } | |
| 792 | + // } | |
| 793 | + // } | |
| 794 | + | |
| 795 | + // // Populate remote participants. | |
| 796 | + // self.match_candidates.clear(); | |
| 797 | + // self.match_candidates | |
| 798 | + // .extend(room.remote_participants().iter().map(|(_, participant)| { | |
| 799 | + // StringMatchCandidate { | |
| 800 | + // id: participant.user.id as usize, | |
| 801 | + // string: participant.user.github_login.clone(), | |
| 802 | + // char_bag: participant.user.github_login.chars().collect(), | |
| 803 | + // } | |
| 804 | + // })); | |
| 805 | + // let matches = executor.block(match_strings( | |
| 806 | + // &self.match_candidates, | |
| 807 | + // &query, | |
| 808 | + // true, | |
| 809 | + // usize::MAX, | |
| 810 | + // &Default::default(), | |
| 811 | + // executor.clone(), | |
| 812 | + // )); | |
| 813 | + // for mat in matches { | |
| 814 | + // let user_id = mat.candidate_id as u64; | |
| 815 | + // let participant = &room.remote_participants()[&user_id]; | |
| 816 | + // self.entries.push(ListEntry::CallParticipant { | |
| 817 | + // user: participant.user.clone(), | |
| 818 | + // peer_id: Some(participant.peer_id), | |
| 819 | + // is_pending: false, | |
| 820 | + // }); | |
| 821 | + // let mut projects = participant.projects.iter().peekable(); | |
| 822 | + // while let Some(project) = projects.next() { | |
| 823 | + // self.entries.push(ListEntry::ParticipantProject { | |
| 824 | + // project_id: project.id, | |
| 825 | + // worktree_root_names: project.worktree_root_names.clone(), | |
| 826 | + // host_user_id: participant.user.id, | |
| 827 | + // is_last: projects.peek().is_none() | |
| 828 | + // && participant.video_tracks.is_empty(), | |
| 829 | + // }); | |
| 830 | + // } | |
| 831 | + // if !participant.video_tracks.is_empty() { | |
| 832 | + // self.entries.push(ListEntry::ParticipantScreen { | |
| 833 | + // peer_id: Some(participant.peer_id), | |
| 834 | + // is_last: true, | |
| 835 | + // }); | |
| 836 | + // } | |
| 837 | + // } | |
| 838 | + | |
| 839 | + // // Populate pending participants. | |
| 840 | + // self.match_candidates.clear(); | |
| 841 | + // self.match_candidates | |
| 842 | + // .extend(room.pending_participants().iter().enumerate().map( | |
| 843 | + // |(id, participant)| StringMatchCandidate { | |
| 844 | + // id, | |
| 845 | + // string: participant.github_login.clone(), | |
| 846 | + // char_bag: participant.github_login.chars().collect(), | |
| 847 | + // }, | |
| 848 | + // )); | |
| 849 | + // let matches = executor.block(match_strings( | |
| 850 | + // &self.match_candidates, | |
| 851 | + // &query, | |
| 852 | + // true, | |
| 853 | + // usize::MAX, | |
| 854 | + // &Default::default(), | |
| 855 | + // executor.clone(), | |
| 856 | + // )); | |
| 857 | + // self.entries | |
| 858 | + // .extend(matches.iter().map(|mat| ListEntry::CallParticipant { | |
| 859 | + // user: room.pending_participants()[mat.candidate_id].clone(), | |
| 860 | + // peer_id: None, | |
| 861 | + // is_pending: true, | |
| 862 | + // })); | |
| 863 | + // } | |
| 864 | + // } | |
| 865 | + | |
| 866 | + let mut request_entries = Vec::new(); | |
| 867 | + | |
| 868 | + if cx.has_flag::<ChannelsAlpha>() { | |
| 869 | + self.entries.push(ListEntry::Header(Section::Channels)); | |
| 870 | + | |
| 871 | + if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { | |
| 872 | + self.match_candidates.clear(); | |
| 873 | + self.match_candidates | |
| 874 | + .extend(channel_store.ordered_channels().enumerate().map( | |
| 875 | + |(ix, (_, channel))| StringMatchCandidate { | |
| 876 | + id: ix, | |
| 877 | + string: channel.name.clone(), | |
| 878 | + char_bag: channel.name.chars().collect(), | |
| 879 | + }, | |
| 880 | + )); | |
| 881 | + let matches = executor.block(match_strings( | |
| 882 | + &self.match_candidates, | |
| 883 | + &query, | |
| 884 | + true, | |
| 885 | + usize::MAX, | |
| 886 | + &Default::default(), | |
| 887 | + executor.clone(), | |
| 888 | + )); | |
| 889 | + if let Some(state) = &self.channel_editing_state { | |
| 890 | + if matches!(state, ChannelEditingState::Create { location: None, .. }) { | |
| 891 | + self.entries.push(ListEntry::ChannelEditor { depth: 0 }); | |
| 892 | + } | |
| 893 | + } | |
| 894 | + let mut collapse_depth = None; | |
| 895 | + for mat in matches { | |
| 896 | + let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); | |
| 897 | + let depth = channel.parent_path.len(); | |
| 898 | + | |
| 899 | + if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { | |
| 900 | + collapse_depth = Some(depth); | |
| 901 | + } else if let Some(collapsed_depth) = collapse_depth { | |
| 902 | + if depth > collapsed_depth { | |
| 903 | + continue; | |
| 904 | + } | |
| 905 | + if self.is_channel_collapsed(channel.id) { | |
| 906 | + collapse_depth = Some(depth); | |
| 907 | + } else { | |
| 908 | + collapse_depth = None; | |
| 909 | + } | |
| 910 | + } | |
| 911 | + | |
| 912 | + let has_children = channel_store | |
| 913 | + .channel_at_index(mat.candidate_id + 1) | |
| 914 | + .map_or(false, |next_channel| { | |
| 915 | + next_channel.parent_path.ends_with(&[channel.id]) | |
| 916 | + }); | |
| 917 | + | |
| 918 | + match &self.channel_editing_state { | |
| 919 | + Some(ChannelEditingState::Create { | |
| 920 | + location: parent_id, | |
| 921 | + .. | |
| 922 | + }) if *parent_id == Some(channel.id) => { | |
| 923 | + self.entries.push(ListEntry::Channel { | |
| 924 | + channel: channel.clone(), | |
| 925 | + depth, | |
| 926 | + has_children: false, | |
| 927 | + }); | |
| 928 | + self.entries | |
| 929 | + .push(ListEntry::ChannelEditor { depth: depth + 1 }); | |
| 930 | + } | |
| 931 | + Some(ChannelEditingState::Rename { | |
| 932 | + location: parent_id, | |
| 933 | + .. | |
| 934 | + }) if parent_id == &channel.id => { | |
| 935 | + self.entries.push(ListEntry::ChannelEditor { depth }); | |
| 936 | + } | |
| 937 | + _ => { | |
| 938 | + self.entries.push(ListEntry::Channel { | |
| 939 | + channel: channel.clone(), | |
| 940 | + depth, | |
| 941 | + has_children, | |
| 942 | + }); | |
| 943 | + } | |
| 944 | + } | |
| 945 | + } | |
| 946 | + } | |
| 1116 | 947 | |
| 1117 | − // if scroll_to_top { | |
| 1118 | − // self.list_state.scroll_to(ListOffset::default()); | |
| 1119 | − // } else { | |
| 1120 | − // // Attempt to maintain the same scroll position. | |
| 1121 | − // if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { | |
| 1122 | − // let new_scroll_top = self | |
| 1123 | − // .entries | |
| 1124 | − // .iter() | |
| 1125 | − // .position(|entry| entry == old_top_entry) | |
| 1126 | − // .map(|item_ix| ListOffset { | |
| 1127 | − // item_ix, | |
| 1128 | − // offset_in_item: old_scroll_top.offset_in_item, | |
| 1129 | − // }) | |
| 1130 | − // .or_else(|| { | |
| 1131 | − // let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; | |
| 1132 | − // let item_ix = self | |
| 1133 | − // .entries | |
| 1134 | − // .iter() | |
| 1135 | − // .position(|entry| entry == entry_after_old_top)?; | |
| 1136 | − // Some(ListOffset { | |
| 1137 | − // item_ix, | |
| 1138 | − // offset_in_item: 0., | |
| 1139 | − // }) | |
| 1140 | − // }) | |
| 1141 | − // .or_else(|| { | |
| 1142 | − // let entry_before_old_top = | |
| 1143 | − // old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; | |
| 1144 | − // let item_ix = self | |
| 1145 | − // .entries | |
| 1146 | − // .iter() | |
| 1147 | − // .position(|entry| entry == entry_before_old_top)?; | |
| 1148 | − // Some(ListOffset { | |
| 1149 | − // item_ix, | |
| 1150 | − // offset_in_item: 0., | |
| 1151 | − // }) | |
| 1152 | − // }); | |
| 948 | + // let channel_invites = channel_store.channel_invitations(); | |
| 949 | + // if !channel_invites.is_empty() { | |
| 950 | + // self.match_candidates.clear(); | |
| 951 | + // self.match_candidates | |
| 952 | + // .extend(channel_invites.iter().enumerate().map(|(ix, channel)| { | |
| 953 | + // StringMatchCandidate { | |
| 954 | + // id: ix, | |
| 955 | + // string: channel.name.clone(), | |
| 956 | + // char_bag: channel.name.chars().collect(), | |
| 957 | + // } | |
| 958 | + // })); | |
| 959 | + // let matches = executor.block(match_strings( | |
| 960 | + // &self.match_candidates, | |
| 961 | + // &query, | |
| 962 | + // true, | |
| 963 | + // usize::MAX, | |
| 964 | + // &Default::default(), | |
| 965 | + // executor.clone(), | |
| 966 | + // )); | |
| 967 | + // request_entries.extend(matches.iter().map(|mat| { | |
| 968 | + // ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone()) | |
| 969 | + // })); | |
| 1153 | 970 | |
| 1154 | − // self.list_state | |
| 1155 | − // .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); | |
| 1156 | − // } | |
| 1157 | − // } | |
| 971 | + // if !request_entries.is_empty() { | |
| 972 | + // self.entries | |
| 973 | + // .push(ListEntry::Header(Section::ChannelInvites)); | |
| 974 | + // if !self.collapsed_sections.contains(&Section::ChannelInvites) { | |
| 975 | + // self.entries.append(&mut request_entries); | |
| 976 | + // } | |
| 977 | + // } | |
| 978 | + // } | |
| 979 | + } | |
| 980 | + | |
| 981 | + self.entries.push(ListEntry::Header(Section::Contacts)); | |
| 982 | + | |
| 983 | + request_entries.clear(); | |
| 984 | + let incoming = user_store.incoming_contact_requests(); | |
| 985 | + if !incoming.is_empty() { | |
| 986 | + self.match_candidates.clear(); | |
| 987 | + self.match_candidates | |
| 988 | + .extend( | |
| 989 | + incoming | |
| 990 | + .iter() | |
| 991 | + .enumerate() | |
| 992 | + .map(|(ix, user)| StringMatchCandidate { | |
| 993 | + id: ix, | |
| 994 | + string: user.github_login.clone(), | |
| 995 | + char_bag: user.github_login.chars().collect(), | |
| 996 | + }), | |
| 997 | + ); | |
| 998 | + let matches = executor.block(match_strings( | |
| 999 | + &self.match_candidates, | |
| 1000 | + &query, | |
| 1001 | + true, | |
| 1002 | + usize::MAX, | |
| 1003 | + &Default::default(), | |
| 1004 | + executor.clone(), | |
| 1005 | + )); | |
| 1006 | + request_entries.extend( | |
| 1007 | + matches | |
| 1008 | + .iter() | |
| 1009 | + .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())), | |
| 1010 | + ); | |
| 1011 | + } | |
| 1012 | + | |
| 1013 | + let outgoing = user_store.outgoing_contact_requests(); | |
| 1014 | + if !outgoing.is_empty() { | |
| 1015 | + self.match_candidates.clear(); | |
| 1016 | + self.match_candidates | |
| 1017 | + .extend( | |
| 1018 | + outgoing | |
| 1019 | + .iter() | |
| 1020 | + .enumerate() | |
| 1021 | + .map(|(ix, user)| StringMatchCandidate { | |
| 1022 | + id: ix, | |
| 1023 | + string: user.github_login.clone(), | |
| 1024 | + char_bag: user.github_login.chars().collect(), | |
| 1025 | + }), | |
| 1026 | + ); | |
| 1027 | + let matches = executor.block(match_strings( | |
| 1028 | + &self.match_candidates, | |
| 1029 | + &query, | |
| 1030 | + true, | |
| 1031 | + usize::MAX, | |
| 1032 | + &Default::default(), | |
| 1033 | + executor.clone(), | |
| 1034 | + )); | |
| 1035 | + request_entries.extend( | |
| 1036 | + matches | |
| 1037 | + .iter() | |
| 1038 | + .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), | |
| 1039 | + ); | |
| 1040 | + } | |
| 1041 | + | |
| 1042 | + if !request_entries.is_empty() { | |
| 1043 | + self.entries | |
| 1044 | + .push(ListEntry::Header(Section::ContactRequests)); | |
| 1045 | + if !self.collapsed_sections.contains(&Section::ContactRequests) { | |
| 1046 | + self.entries.append(&mut request_entries); | |
| 1047 | + } | |
| 1048 | + } | |
| 1049 | + | |
| 1050 | + let contacts = user_store.contacts(); | |
| 1051 | + if !contacts.is_empty() { | |
| 1052 | + self.match_candidates.clear(); | |
| 1053 | + self.match_candidates | |
| 1054 | + .extend( | |
| 1055 | + contacts | |
| 1056 | + .iter() | |
| 1057 | + .enumerate() | |
| 1058 | + .map(|(ix, contact)| StringMatchCandidate { | |
| 1059 | + id: ix, | |
| 1060 | + string: contact.user.github_login.clone(), | |
| 1061 | + char_bag: contact.user.github_login.chars().collect(), | |
| 1062 | + }), | |
| 1063 | + ); | |
| 1064 | + | |
| 1065 | + let matches = executor.block(match_strings( | |
| 1066 | + &self.match_candidates, | |
| 1067 | + &query, | |
| 1068 | + true, | |
| 1069 | + usize::MAX, | |
| 1070 | + &Default::default(), | |
| 1071 | + executor.clone(), | |
| 1072 | + )); | |
| 1073 | + | |
| 1074 | + let (online_contacts, offline_contacts) = matches | |
| 1075 | + .iter() | |
| 1076 | + .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online); | |
| 1077 | + | |
| 1078 | + for (matches, section) in [ | |
| 1079 | + (online_contacts, Section::Online), | |
| 1080 | + (offline_contacts, Section::Offline), | |
| 1081 | + ] { | |
| 1082 | + if !matches.is_empty() { | |
| 1083 | + self.entries.push(ListEntry::Header(section)); | |
| 1084 | + if !self.collapsed_sections.contains(§ion) { | |
| 1085 | + let active_call = &ActiveCall::global(cx).read(cx); | |
| 1086 | + for mat in matches { | |
| 1087 | + let contact = &contacts[mat.candidate_id]; | |
| 1088 | + self.entries.push(ListEntry::Contact { | |
| 1089 | + contact: contact.clone(), | |
| 1090 | + calling: active_call.pending_invites().contains(&contact.user.id), | |
| 1091 | + }); | |
| 1092 | + } | |
| 1093 | + } | |
| 1094 | + } | |
| 1095 | + } | |
| 1096 | + } | |
| 1097 | + | |
| 1098 | + if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() { | |
| 1099 | + self.entries.push(ListEntry::ContactPlaceholder); | |
| 1100 | + } | |
| 1101 | + | |
| 1102 | + // if select_same_item { | |
| 1103 | + // if let Some(prev_selected_entry) = prev_selected_entry { | |
| 1104 | + // self.selection.take(); | |
| 1105 | + // for (ix, entry) in self.entries.iter().enumerate() { | |
| 1106 | + // if *entry == prev_selected_entry { | |
| 1107 | + // self.selection = Some(ix); | |
| 1108 | + // break; | |
| 1109 | + // } | |
| 1110 | + // } | |
| 1111 | + // } | |
| 1112 | + // } else { | |
| 1113 | + // self.selection = self.selection.and_then(|prev_selection| { | |
| 1114 | + // if self.entries.is_empty() { | |
| 1115 | + // None | |
| 1116 | + // } else { | |
| 1117 | + // Some(prev_selection.min(self.entries.len() - 1)) | |
| 1118 | + // } | |
| 1119 | + // }); | |
| 1120 | + // } | |
| 1121 | + | |
| 1122 | + // let old_scroll_top = self.list_state.logical_scroll_top(); | |
| 1123 | + | |
| 1124 | + // self.list_state.reset(self.entries.len()); | |
| 1125 | + | |
| 1126 | + // if scroll_to_top { | |
| 1127 | + // self.list_state.scroll_to(ListOffset::default()); | |
| 1128 | + // } else { | |
| 1129 | + // // Attempt to maintain the same scroll position. | |
| 1130 | + // if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { | |
| 1131 | + // let new_scroll_top = self | |
| 1132 | + // .entries | |
| 1133 | + // .iter() | |
| 1134 | + // .position(|entry| entry == old_top_entry) | |
| 1135 | + // .map(|item_ix| ListOffset { | |
| 1136 | + // item_ix, | |
| 1137 | + // offset_in_item: old_scroll_top.offset_in_item, | |
| 1138 | + // }) | |
| 1139 | + // .or_else(|| { | |
| 1140 | + // let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; | |
| 1141 | + // let item_ix = self | |
| 1142 | + // .entries | |
| 1143 | + // .iter() | |
| 1144 | + // .position(|entry| entry == entry_after_old_top)?; | |
| 1145 | + // Some(ListOffset { | |
| 1146 | + // item_ix, | |
| 1147 | + // offset_in_item: 0., | |
| 1148 | + // }) | |
| 1149 | + // }) | |
| 1150 | + // .or_else(|| { | |
| 1151 | + // let entry_before_old_top = | |
| 1152 | + // old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; | |
| 1153 | + // let item_ix = self | |
| 1154 | + // .entries | |
| 1155 | + // .iter() | |
| 1156 | + // .position(|entry| entry == entry_before_old_top)?; | |
| 1157 | + // Some(ListOffset { | |
| 1158 | + // item_ix, | |
| 1159 | + // offset_in_item: 0., | |
| 1160 | + // }) | |
| 1161 | + // }); | |
| 1162 | + | |
| 1163 | + // self.list_state | |
| 1164 | + // .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); | |
| 1165 | + // } | |
| 1166 | + // } | |
| 1158 | 1167 | |
| 1159 | − // cx.notify(); | |
| 1160 | − // } | |
| 1168 | + cx.notify(); | |
| 1169 | + } | |
| 1161 | 1170 | |
| 1162 | 1171 | // fn render_call_participant( |
| 1163 | 1172 | // user: &User, |
| @@ -1455,389 +1464,6 @@ impl CollabPanel { · 291 unmodified lines | ||
| 1455 | 1464 | // } |
| 1456 | 1465 | // } |
| 1457 | 1466 | |
| 1458 | − // fn render_header( | |
| 1459 | − // &self, | |
| 1460 | − // section: Section, | |
| 1461 | − // theme: &theme::Theme, | |
| 1462 | − // is_selected: bool, | |
| 1463 | − // is_collapsed: bool, | |
| 1464 | − // cx: &mut ViewContext<Self>, | |
| 1465 | − // ) -> AnyElement<Self> { | |
| 1466 | − // enum Header {} | |
| 1467 | − // enum LeaveCallContactList {} | |
| 1468 | − // enum AddChannel {} | |
| 1469 | − | |
| 1470 | − // let tooltip_style = &theme.tooltip; | |
| 1471 | − // let mut channel_link = None; | |
| 1472 | − // let mut channel_tooltip_text = None; | |
| 1473 | − // let mut channel_icon = None; | |
| 1474 | − // let mut is_dragged_over = false; | |
| 1475 | − | |
| 1476 | − // let text = match section { | |
| 1477 | − // Section::ActiveCall => { | |
| 1478 | − // let channel_name = maybe!({ | |
| 1479 | − // let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; | |
| 1480 | − | |
| 1481 | − // let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; | |
| 1482 | − | |
| 1483 | − // channel_link = Some(channel.link()); | |
| 1484 | − // (channel_icon, channel_tooltip_text) = match channel.visibility { | |
| 1485 | − // proto::ChannelVisibility::Public => { | |
| 1486 | − // (Some("icons/public.svg"), Some("Copy public channel link.")) | |
| 1487 | − // } | |
| 1488 | − // proto::ChannelVisibility::Members => { | |
| 1489 | − // (Some("icons/hash.svg"), Some("Copy private channel link.")) | |
| 1490 | − // } | |
| 1491 | − // }; | |
| 1492 | − | |
| 1493 | − // Some(channel.name.as_str()) | |
| 1494 | − // }); | |
| 1495 | − | |
| 1496 | − // if let Some(name) = channel_name { | |
| 1497 | − // Cow::Owned(format!("{}", name)) | |
| 1498 | − // } else { | |
| 1499 | − // Cow::Borrowed("Current Call") | |
| 1500 | − // } | |
| 1501 | − // } | |
| 1502 | − // Section::ContactRequests => Cow::Borrowed("Requests"), | |
| 1503 | − // Section::Contacts => Cow::Borrowed("Contacts"), | |
| 1504 | − // Section::Channels => Cow::Borrowed("Channels"), | |
| 1505 | − // Section::ChannelInvites => Cow::Borrowed("Invites"), | |
| 1506 | − // Section::Online => Cow::Borrowed("Online"), | |
| 1507 | − // Section::Offline => Cow::Borrowed("Offline"), | |
| 1508 | − // }; | |
| 1509 | − | |
| 1510 | − // enum AddContact {} | |
| 1511 | − // let button = match section { | |
| 1512 | − // Section::ActiveCall => channel_link.map(|channel_link| { | |
| 1513 | − // let channel_link_copy = channel_link.clone(); | |
| 1514 | − // MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| { | |
| 1515 | − // render_icon_button( | |
| 1516 | − // theme | |
| 1517 | − // .collab_panel | |
| 1518 | − // .leave_call_button | |
| 1519 | − // .style_for(is_selected, state), | |
| 1520 | − // "icons/link.svg", | |
| 1521 | − // ) | |
| 1522 | − // }) | |
| 1523 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 1524 | − // .on_click(MouseButton::Left, move |_, _, cx| { | |
| 1525 | − // let item = ClipboardItem::new(channel_link_copy.clone()); | |
| 1526 | − // cx.write_to_clipboard(item) | |
| 1527 | − // }) | |
| 1528 | − // .with_tooltip::<AddContact>( | |
| 1529 | − // 0, | |
| 1530 | − // channel_tooltip_text.unwrap(), | |
| 1531 | − // None, | |
| 1532 | − // tooltip_style.clone(), | |
| 1533 | − // cx, | |
| 1534 | − // ) | |
| 1535 | − // }), | |
| 1536 | − // Section::Contacts => Some( | |
| 1537 | − // MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| { | |
| 1538 | − // render_icon_button( | |
| 1539 | − // theme | |
| 1540 | − // .collab_panel | |
| 1541 | − // .add_contact_button | |
| 1542 | − // .style_for(is_selected, state), | |
| 1543 | − // "icons/plus.svg", | |
| 1544 | − // ) | |
| 1545 | − // }) | |
| 1546 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 1547 | − // .on_click(MouseButton::Left, |_, this, cx| { | |
| 1548 | − // this.toggle_contact_finder(cx); | |
| 1549 | − // }) | |
| 1550 | − // .with_tooltip::<LeaveCallContactList>( | |
| 1551 | − // 0, | |
| 1552 | − // "Search for new contact", | |
| 1553 | − // None, | |
| 1554 | − // tooltip_style.clone(), | |
| 1555 | − // cx, | |
| 1556 | − // ), | |
| 1557 | − // ), | |
| 1558 | − // Section::Channels => { | |
| 1559 | − // if cx | |
| 1560 | − // .global::<DragAndDrop<Workspace>>() | |
| 1561 | − // .currently_dragged::<Channel>(cx.window()) | |
| 1562 | − // .is_some() | |
| 1563 | − // && self.drag_target_channel == ChannelDragTarget::Root | |
| 1564 | − // { | |
| 1565 | − // is_dragged_over = true; | |
| 1566 | − // } | |
| 1567 | − | |
| 1568 | − // Some( | |
| 1569 | − // MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| { | |
| 1570 | − // render_icon_button( | |
| 1571 | − // theme | |
| 1572 | − // .collab_panel | |
| 1573 | − // .add_contact_button | |
| 1574 | − // .style_for(is_selected, state), | |
| 1575 | − // "icons/plus.svg", | |
| 1576 | − // ) | |
| 1577 | − // }) | |
| 1578 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 1579 | − // .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) | |
| 1580 | − // .with_tooltip::<AddChannel>( | |
| 1581 | − // 0, | |
| 1582 | − // "Create a channel", | |
| 1583 | − // None, | |
| 1584 | − // tooltip_style.clone(), | |
| 1585 | − // cx, | |
| 1586 | − // ), | |
| 1587 | − // ) | |
| 1588 | − // } | |
| 1589 | − // _ => None, | |
| 1590 | − // }; | |
| 1591 | − | |
| 1592 | − // let can_collapse = match section { | |
| 1593 | − // Section::ActiveCall | Section::Channels | Section::Contacts => false, | |
| 1594 | − // Section::ChannelInvites | |
| 1595 | − // | Section::ContactRequests | |
| 1596 | − // | Section::Online | |
| 1597 | − // | Section::Offline => true, | |
| 1598 | − // }; | |
| 1599 | − // let icon_size = (&theme.collab_panel).section_icon_size; | |
| 1600 | − // let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| { | |
| 1601 | − // let header_style = if can_collapse { | |
| 1602 | − // theme | |
| 1603 | − // .collab_panel | |
| 1604 | − // .subheader_row | |
| 1605 | − // .in_state(is_selected) | |
| 1606 | − // .style_for(state) | |
| 1607 | − // } else { | |
| 1608 | − // &theme.collab_panel.header_row | |
| 1609 | − // }; | |
| 1610 | − | |
| 1611 | − // Flex::row() | |
| 1612 | − // .with_children(if can_collapse { | |
| 1613 | − // Some( | |
| 1614 | − // Svg::new(if is_collapsed { | |
| 1615 | − // "icons/chevron_right.svg" | |
| 1616 | − // } else { | |
| 1617 | − // "icons/chevron_down.svg" | |
| 1618 | − // }) | |
| 1619 | − // .with_color(header_style.text.color) | |
| 1620 | − // .constrained() | |
| 1621 | − // .with_max_width(icon_size) | |
| 1622 | − // .with_max_height(icon_size) | |
| 1623 | − // .aligned() | |
| 1624 | − // .constrained() | |
| 1625 | − // .with_width(icon_size) | |
| 1626 | − // .contained() | |
| 1627 | − // .with_margin_right( | |
| 1628 | − // theme.collab_panel.contact_username.container.margin.left, | |
| 1629 | − // ), | |
| 1630 | − // ) | |
| 1631 | − // } else if let Some(channel_icon) = channel_icon { | |
| 1632 | − // Some( | |
| 1633 | − // Svg::new(channel_icon) | |
| 1634 | − // .with_color(header_style.text.color) | |
| 1635 | − // .constrained() | |
| 1636 | − // .with_max_width(icon_size) | |
| 1637 | − // .with_max_height(icon_size) | |
| 1638 | − // .aligned() | |
| 1639 | − // .constrained() | |
| 1640 | − // .with_width(icon_size) | |
| 1641 | − // .contained() | |
| 1642 | − // .with_margin_right( | |
| 1643 | − // theme.collab_panel.contact_username.container.margin.left, | |
| 1644 | − // ), | |
| 1645 | − // ) | |
| 1646 | − // } else { | |
| 1647 | − // None | |
| 1648 | − // }) | |
| 1649 | − // .with_child( | |
| 1650 | − // Label::new(text, header_style.text.clone()) | |
| 1651 | − // .aligned() | |
| 1652 | − // .left() | |
| 1653 | − // .flex(1., true), | |
| 1654 | − // ) | |
| 1655 | − // .with_children(button.map(|button| button.aligned().right())) | |
| 1656 | − // .constrained() | |
| 1657 | − // .with_height(theme.collab_panel.row_height) | |
| 1658 | − // .contained() | |
| 1659 | − // .with_style(if is_dragged_over { | |
| 1660 | − // theme.collab_panel.dragged_over_header | |
| 1661 | − // } else { | |
| 1662 | − // header_style.container | |
| 1663 | − // }) | |
| 1664 | − // }); | |
| 1665 | − | |
| 1666 | − // result = result | |
| 1667 | − // .on_move(move |_, this, cx| { | |
| 1668 | − // if cx | |
| 1669 | − // .global::<DragAndDrop<Workspace>>() | |
| 1670 | − // .currently_dragged::<Channel>(cx.window()) | |
| 1671 | − // .is_some() | |
| 1672 | − // { | |
| 1673 | − // this.drag_target_channel = ChannelDragTarget::Root; | |
| 1674 | − // cx.notify() | |
| 1675 | − // } | |
| 1676 | − // }) | |
| 1677 | − // .on_up(MouseButton::Left, move |_, this, cx| { | |
| 1678 | − // if let Some((_, dragged_channel)) = cx | |
| 1679 | − // .global::<DragAndDrop<Workspace>>() | |
| 1680 | − // .currently_dragged::<Channel>(cx.window()) | |
| 1681 | − // { | |
| 1682 | − // this.channel_store | |
| 1683 | − // .update(cx, |channel_store, cx| { | |
| 1684 | − // channel_store.move_channel(dragged_channel.id, None, cx) | |
| 1685 | − // }) | |
| 1686 | − // .detach_and_log_err(cx) | |
| 1687 | − // } | |
| 1688 | − // }); | |
| 1689 | − | |
| 1690 | − // if can_collapse { | |
| 1691 | − // result = result | |
| 1692 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 1693 | − // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 1694 | − // if can_collapse { | |
| 1695 | − // this.toggle_section_expanded(section, cx); | |
| 1696 | − // } | |
| 1697 | − // }) | |
| 1698 | − // } | |
| 1699 | − | |
| 1700 | − // result.into_any() | |
| 1701 | − // } | |
| 1702 | − | |
| 1703 | − // fn render_contact( | |
| 1704 | − // contact: &Contact, | |
| 1705 | − // calling: bool, | |
| 1706 | − // project: &ModelHandle<Project>, | |
| 1707 | − // theme: &theme::Theme, | |
| 1708 | − // is_selected: bool, | |
| 1709 | − // cx: &mut ViewContext<Self>, | |
| 1710 | − // ) -> AnyElement<Self> { | |
| 1711 | − // enum ContactTooltip {} | |
| 1712 | − | |
| 1713 | − // let collab_theme = &theme.collab_panel; | |
| 1714 | − // let online = contact.online; | |
| 1715 | − // let busy = contact.busy || calling; | |
| 1716 | − // let user_id = contact.user.id; | |
| 1717 | − // let github_login = contact.user.github_login.clone(); | |
| 1718 | − // let initial_project = project.clone(); | |
| 1719 | − | |
| 1720 | − // let event_handler = | |
| 1721 | − // MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| { | |
| 1722 | − // Flex::row() | |
| 1723 | − // .with_children(contact.user.avatar.clone().map(|avatar| { | |
| 1724 | − // let status_badge = if contact.online { | |
| 1725 | − // Some( | |
| 1726 | − // Empty::new() | |
| 1727 | − // .collapsed() | |
| 1728 | − // .contained() | |
| 1729 | − // .with_style(if busy { | |
| 1730 | − // collab_theme.contact_status_busy | |
| 1731 | − // } else { | |
| 1732 | − // collab_theme.contact_status_free | |
| 1733 | − // }) | |
| 1734 | − // .aligned(), | |
| 1735 | − // ) | |
| 1736 | − // } else { | |
| 1737 | − // None | |
| 1738 | − // }; | |
| 1739 | − // Stack::new() | |
| 1740 | − // .with_child( | |
| 1741 | − // Image::from_data(avatar) | |
| 1742 | − // .with_style(collab_theme.contact_avatar) | |
| 1743 | − // .aligned() | |
| 1744 | − // .left(), | |
| 1745 | − // ) | |
| 1746 | − // .with_children(status_badge) | |
| 1747 | − // })) | |
| 1748 | − // .with_child( | |
| 1749 | − // Label::new( | |
| 1750 | − // contact.user.github_login.clone(), | |
| 1751 | − // collab_theme.contact_username.text.clone(), | |
| 1752 | − // ) | |
| 1753 | − // .contained() | |
| 1754 | − // .with_style(collab_theme.contact_username.container) | |
| 1755 | − // .aligned() | |
| 1756 | − // .left() | |
| 1757 | − // .flex(1., true), | |
| 1758 | − // ) | |
| 1759 | − // .with_children(if state.hovered() { | |
| 1760 | − // Some( | |
| 1761 | − // MouseEventHandler::new::<Cancel, _>( | |
| 1762 | − // contact.user.id as usize, | |
| 1763 | − // cx, | |
| 1764 | − // |mouse_state, _| { | |
| 1765 | − // let button_style = | |
| 1766 | − // collab_theme.contact_button.style_for(mouse_state); | |
| 1767 | − // render_icon_button(button_style, "icons/x.svg") | |
| 1768 | − // .aligned() | |
| 1769 | − // .flex_float() | |
| 1770 | − // }, | |
| 1771 | − // ) | |
| 1772 | − // .with_padding(Padding::uniform(2.)) | |
| 1773 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 1774 | − // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 1775 | − // this.remove_contact(user_id, &github_login, cx); | |
| 1776 | − // }) | |
| 1777 | − // .flex_float(), | |
| 1778 | − // ) | |
| 1779 | − // } else { | |
| 1780 | − // None | |
| 1781 | − // }) | |
| 1782 | − // .with_children(if calling { | |
| 1783 | − // Some( | |
| 1784 | − // Label::new("Calling", collab_theme.calling_indicator.text.clone()) | |
| 1785 | − // .contained() | |
| 1786 | − // .with_style(collab_theme.calling_indicator.container) | |
| 1787 | − // .aligned(), | |
| 1788 | − // ) | |
| 1789 | − // } else { | |
| 1790 | − // None | |
| 1791 | − // }) | |
| 1792 | − // .constrained() | |
| 1793 | − // .with_height(collab_theme.row_height) | |
| 1794 | − // .contained() | |
| 1795 | − // .with_style( | |
| 1796 | − // *collab_theme | |
| 1797 | − // .contact_row | |
| 1798 | − // .in_state(is_selected) | |
| 1799 | − // .style_for(state), | |
| 1800 | − // ) | |
| 1801 | − // }); | |
| 1802 | − | |
| 1803 | − // if online && !busy { | |
| 1804 | − // let room = ActiveCall::global(cx).read(cx).room(); | |
| 1805 | − // let label = if room.is_some() { | |
| 1806 | − // format!("Invite {} to join call", contact.user.github_login) | |
| 1807 | − // } else { | |
| 1808 | − // format!("Call {}", contact.user.github_login) | |
| 1809 | − // }; | |
| 1810 | − | |
| 1811 | − // event_handler | |
| 1812 | − // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 1813 | − // this.call(user_id, Some(initial_project.clone()), cx); | |
| 1814 | − // }) | |
| 1815 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 1816 | − // .with_tooltip::<ContactTooltip>( | |
| 1817 | − // contact.user.id as usize, | |
| 1818 | − // label, | |
| 1819 | − // None, | |
| 1820 | − // theme.tooltip.clone(), | |
| 1821 | − // cx, | |
| 1822 | − // ) | |
| 1823 | − // .into_any() | |
| 1824 | − // } else { | |
| 1825 | − // event_handler | |
| 1826 | − // .with_tooltip::<ContactTooltip>( | |
| 1827 | − // contact.user.id as usize, | |
| 1828 | − // format!( | |
| 1829 | − // "{} is {}", | |
| 1830 | − // contact.user.github_login, | |
| 1831 | − // if busy { "on a call" } else { "offline" } | |
| 1832 | − // ), | |
| 1833 | − // None, | |
| 1834 | − // theme.tooltip.clone(), | |
| 1835 | − // cx, | |
| 1836 | − // ) | |
| 1837 | − // .into_any() | |
| 1838 | − // } | |
| 1839 | − // } | |
| 1840 | − | |
| 1841 | 1467 | // fn render_contact_placeholder( |
| 1842 | 1468 | // &self, |
| 1843 | 1469 | // theme: &theme::CollabPanel, |
| @@ -1933,335 +1559,6 @@ impl CollabPanel { · 89 unmodified lines | ||
| 1933 | 1559 | // .into_any() |
| 1934 | 1560 | // } |
| 1935 | 1561 | |
| 1936 | − // fn render_channel( | |
| 1937 | − // &self, | |
| 1938 | − // channel: &Channel, | |
| 1939 | − // depth: usize, | |
| 1940 | − // theme: &theme::Theme, | |
| 1941 | − // is_selected: bool, | |
| 1942 | − // has_children: bool, | |
| 1943 | − // ix: usize, | |
| 1944 | − // cx: &mut ViewContext<Self>, | |
| 1945 | − // ) -> AnyElement<Self> { | |
| 1946 | − // let channel_id = channel.id; | |
| 1947 | − // let collab_theme = &theme.collab_panel; | |
| 1948 | − // let is_public = self | |
| 1949 | − // .channel_store | |
| 1950 | − // .read(cx) | |
| 1951 | − // .channel_for_id(channel_id) | |
| 1952 | − // .map(|channel| channel.visibility) | |
| 1953 | − // == Some(proto::ChannelVisibility::Public); | |
| 1954 | − // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); | |
| 1955 | − // let disclosed = | |
| 1956 | − // has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); | |
| 1957 | − | |
| 1958 | − // let is_active = maybe!({ | |
| 1959 | − // let call_channel = ActiveCall::global(cx) | |
| 1960 | − // .read(cx) | |
| 1961 | − // .room()? | |
| 1962 | − // .read(cx) | |
| 1963 | − // .channel_id()?; | |
| 1964 | − // Some(call_channel == channel_id) | |
| 1965 | − // }) | |
| 1966 | − // .unwrap_or(false); | |
| 1967 | − | |
| 1968 | − // const FACEPILE_LIMIT: usize = 3; | |
| 1969 | − | |
| 1970 | − // enum ChannelCall {} | |
| 1971 | − // enum ChannelNote {} | |
| 1972 | − // enum NotesTooltip {} | |
| 1973 | − // enum ChatTooltip {} | |
| 1974 | − // enum ChannelTooltip {} | |
| 1975 | − | |
| 1976 | − // let mut is_dragged_over = false; | |
| 1977 | − // if cx | |
| 1978 | − // .global::<DragAndDrop<Workspace>>() | |
| 1979 | − // .currently_dragged::<Channel>(cx.window()) | |
| 1980 | − // .is_some() | |
| 1981 | − // && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) | |
| 1982 | − // { | |
| 1983 | − // is_dragged_over = true; | |
| 1984 | − // } | |
| 1985 | − | |
| 1986 | − // let has_messages_notification = channel.unseen_message_id.is_some(); | |
| 1987 | − | |
| 1988 | − // MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| { | |
| 1989 | − // let row_hovered = state.hovered(); | |
| 1990 | − | |
| 1991 | − // let mut select_state = |interactive: &Interactive<ContainerStyle>| { | |
| 1992 | − // if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() { | |
| 1993 | − // interactive.clicked.as_ref().unwrap().clone() | |
| 1994 | − // } else if state.hovered() || other_selected { | |
| 1995 | − // interactive | |
| 1996 | − // .hovered | |
| 1997 | − // .as_ref() | |
| 1998 | − // .unwrap_or(&interactive.default) | |
| 1999 | − // .clone() | |
| 2000 | − // } else { | |
| 2001 | − // interactive.default.clone() | |
| 2002 | − // } | |
| 2003 | − // }; | |
| 2004 | − | |
| 2005 | − // Flex::<Self>::row() | |
| 2006 | − // .with_child( | |
| 2007 | − // Svg::new(if is_public { | |
| 2008 | − // "icons/public.svg" | |
| 2009 | − // } else { | |
| 2010 | − // "icons/hash.svg" | |
| 2011 | − // }) | |
| 2012 | − // .with_color(collab_theme.channel_hash.color) | |
| 2013 | − // .constrained() | |
| 2014 | − // .with_width(collab_theme.channel_hash.width) | |
| 2015 | − // .aligned() | |
| 2016 | − // .left(), | |
| 2017 | − // ) | |
| 2018 | − // .with_child({ | |
| 2019 | − // let style = collab_theme.channel_name.inactive_state(); | |
| 2020 | − // Flex::row() | |
| 2021 | − // .with_child( | |
| 2022 | − // Label::new(channel.name.clone(), style.text.clone()) | |
| 2023 | − // .contained() | |
| 2024 | − // .with_style(style.container) | |
| 2025 | − // .aligned() | |
| 2026 | − // .left() | |
| 2027 | − // .with_tooltip::<ChannelTooltip>( | |
| 2028 | − // ix, | |
| 2029 | − // "Join channel", | |
| 2030 | − // None, | |
| 2031 | − // theme.tooltip.clone(), | |
| 2032 | − // cx, | |
| 2033 | − // ), | |
| 2034 | − // ) | |
| 2035 | − // .with_children({ | |
| 2036 | − // let participants = | |
| 2037 | − // self.channel_store.read(cx).channel_participants(channel_id); | |
| 2038 | − | |
| 2039 | − // if !participants.is_empty() { | |
| 2040 | − // let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); | |
| 2041 | − | |
| 2042 | − // let result = FacePile::new(collab_theme.face_overlap) | |
| 2043 | − // .with_children( | |
| 2044 | − // participants | |
| 2045 | − // .iter() | |
| 2046 | − // .filter_map(|user| { | |
| 2047 | − // Some( | |
| 2048 | − // Image::from_data(user.avatar.clone()?) | |
| 2049 | − // .with_style(collab_theme.channel_avatar), | |
| 2050 | − // ) | |
| 2051 | − // }) | |
| 2052 | − // .take(FACEPILE_LIMIT), | |
| 2053 | − // ) | |
| 2054 | − // .with_children((extra_count > 0).then(|| { | |
| 2055 | − // Label::new( | |
| 2056 | − // format!("+{}", extra_count), | |
| 2057 | − // collab_theme.extra_participant_label.text.clone(), | |
| 2058 | − // ) | |
| 2059 | − // .contained() | |
| 2060 | − // .with_style(collab_theme.extra_participant_label.container) | |
| 2061 | − // })); | |
| 2062 | − | |
| 2063 | − // Some(result) | |
| 2064 | − // } else { | |
| 2065 | − // None | |
| 2066 | − // } | |
| 2067 | − // }) | |
| 2068 | − // .with_spacing(8.) | |
| 2069 | − // .align_children_center() | |
| 2070 | − // .flex(1., true) | |
| 2071 | − // }) | |
| 2072 | − // .with_child( | |
| 2073 | − // MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| { | |
| 2074 | − // let container_style = collab_theme | |
| 2075 | − // .disclosure | |
| 2076 | − // .button | |
| 2077 | − // .style_for(mouse_state) | |
| 2078 | − // .container; | |
| 2079 | − | |
| 2080 | − // if channel.unseen_message_id.is_some() { | |
| 2081 | − // Svg::new("icons/conversations.svg") | |
| 2082 | − // .with_color(collab_theme.channel_note_active_color) | |
| 2083 | − // .constrained() | |
| 2084 | − // .with_width(collab_theme.channel_hash.width) | |
| 2085 | − // .contained() | |
| 2086 | − // .with_style(container_style) | |
| 2087 | − // .with_uniform_padding(4.) | |
| 2088 | − // .into_any() | |
| 2089 | − // } else if row_hovered { | |
| 2090 | − // Svg::new("icons/conversations.svg") | |
| 2091 | − // .with_color(collab_theme.channel_hash.color) | |
| 2092 | − // .constrained() | |
| 2093 | − // .with_width(collab_theme.channel_hash.width) | |
| 2094 | − // .contained() | |
| 2095 | − // .with_style(container_style) | |
| 2096 | − // .with_uniform_padding(4.) | |
| 2097 | − // .into_any() | |
| 2098 | − // } else { | |
| 2099 | − // Empty::new().into_any() | |
| 2100 | − // } | |
| 2101 | − // }) | |
| 2102 | − // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 2103 | − // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); | |
| 2104 | − // }) | |
| 2105 | − // .with_tooltip::<ChatTooltip>( | |
| 2106 | − // ix, | |
| 2107 | − // "Open channel chat", | |
| 2108 | − // None, | |
| 2109 | − // theme.tooltip.clone(), | |
| 2110 | − // cx, | |
| 2111 | − // ) | |
| 2112 | − // .contained() | |
| 2113 | − // .with_margin_right(4.), | |
| 2114 | − // ) | |
| 2115 | − // .with_child( | |
| 2116 | − // MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| { | |
| 2117 | − // let container_style = collab_theme | |
| 2118 | − // .disclosure | |
| 2119 | − // .button | |
| 2120 | − // .style_for(mouse_state) | |
| 2121 | − // .container; | |
| 2122 | − // if row_hovered || channel.unseen_note_version.is_some() { | |
| 2123 | − // Svg::new("icons/file.svg") | |
| 2124 | − // .with_color(if channel.unseen_note_version.is_some() { | |
| 2125 | − // collab_theme.channel_note_active_color | |
| 2126 | − // } else { | |
| 2127 | − // collab_theme.channel_hash.color | |
| 2128 | − // }) | |
| 2129 | − // .constrained() | |
| 2130 | − // .with_width(collab_theme.channel_hash.width) | |
| 2131 | − // .contained() | |
| 2132 | − // .with_style(container_style) | |
| 2133 | − // .with_uniform_padding(4.) | |
| 2134 | − // .with_margin_right(collab_theme.channel_hash.container.margin.left) | |
| 2135 | − // .with_tooltip::<NotesTooltip>( | |
| 2136 | − // ix as usize, | |
| 2137 | − // "Open channel notes", | |
| 2138 | − // None, | |
| 2139 | − // theme.tooltip.clone(), | |
| 2140 | − // cx, | |
| 2141 | − // ) | |
| 2142 | − // .into_any() | |
| 2143 | − // } else if has_messages_notification { | |
| 2144 | − // Empty::new() | |
| 2145 | − // .constrained() | |
| 2146 | − // .with_width(collab_theme.channel_hash.width) | |
| 2147 | − // .contained() | |
| 2148 | − // .with_uniform_padding(4.) | |
| 2149 | − // .with_margin_right(collab_theme.channel_hash.container.margin.left) | |
| 2150 | − // .into_any() | |
| 2151 | − // } else { | |
| 2152 | − // Empty::new().into_any() | |
| 2153 | − // } | |
| 2154 | − // }) | |
| 2155 | − // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 2156 | − // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); | |
| 2157 | − // }), | |
| 2158 | − // ) | |
| 2159 | − // .align_children_center() | |
| 2160 | − // .styleable_component() | |
| 2161 | − // .disclosable( | |
| 2162 | − // disclosed, | |
| 2163 | − // Box::new(ToggleCollapse { | |
| 2164 | − // location: channel.id.clone(), | |
| 2165 | − // }), | |
| 2166 | − // ) | |
| 2167 | − // .with_id(ix) | |
| 2168 | − // .with_style(collab_theme.disclosure.clone()) | |
| 2169 | − // .element() | |
| 2170 | − // .constrained() | |
| 2171 | − // .with_height(collab_theme.row_height) | |
| 2172 | − // .contained() | |
| 2173 | − // .with_style(select_state( | |
| 2174 | − // collab_theme | |
| 2175 | − // .channel_row | |
| 2176 | − // .in_state(is_selected || is_active || is_dragged_over), | |
| 2177 | − // )) | |
| 2178 | − // .with_padding_left( | |
| 2179 | − // collab_theme.channel_row.default_style().padding.left | |
| 2180 | − // + collab_theme.channel_indent * depth as f32, | |
| 2181 | − // ) | |
| 2182 | − // }) | |
| 2183 | − // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 2184 | − // if this.drag_target_channel == ChannelDragTarget::None { | |
| 2185 | − // if is_active { | |
| 2186 | − // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) | |
| 2187 | − // } else { | |
| 2188 | − // this.join_channel(channel_id, cx) | |
| 2189 | − // } | |
| 2190 | − // } | |
| 2191 | − // }) | |
| 2192 | − // .on_click(MouseButton::Right, { | |
| 2193 | − // let channel = channel.clone(); | |
| 2194 | − // move |e, this, cx| { | |
| 2195 | − // this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); | |
| 2196 | − // } | |
| 2197 | − // }) | |
| 2198 | − // .on_up(MouseButton::Left, move |_, this, cx| { | |
| 2199 | − // if let Some((_, dragged_channel)) = cx | |
| 2200 | − // .global::<DragAndDrop<Workspace>>() | |
| 2201 | − // .currently_dragged::<Channel>(cx.window()) | |
| 2202 | − // { | |
| 2203 | − // this.channel_store | |
| 2204 | − // .update(cx, |channel_store, cx| { | |
| 2205 | − // channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) | |
| 2206 | − // }) | |
| 2207 | − // .detach_and_log_err(cx) | |
| 2208 | − // } | |
| 2209 | − // }) | |
| 2210 | − // .on_move({ | |
| 2211 | − // let channel = channel.clone(); | |
| 2212 | − // move |_, this, cx| { | |
| 2213 | − // if let Some((_, dragged_channel)) = cx | |
| 2214 | − // .global::<DragAndDrop<Workspace>>() | |
| 2215 | − // .currently_dragged::<Channel>(cx.window()) | |
| 2216 | − // { | |
| 2217 | − // if channel.id != dragged_channel.id { | |
| 2218 | − // this.drag_target_channel = ChannelDragTarget::Channel(channel.id); | |
| 2219 | − // } | |
| 2220 | − // cx.notify() | |
| 2221 | − // } | |
| 2222 | − // } | |
| 2223 | − // }) | |
| 2224 | − // .as_draggable::<_, Channel>( | |
| 2225 | − // channel.clone(), | |
| 2226 | − // move |_, channel, cx: &mut ViewContext<Workspace>| { | |
| 2227 | − // let theme = &theme::current(cx).collab_panel; | |
| 2228 | − | |
| 2229 | − // Flex::<Workspace>::row() | |
| 2230 | − // .with_child( | |
| 2231 | − // Svg::new("icons/hash.svg") | |
| 2232 | − // .with_color(theme.channel_hash.color) | |
| 2233 | − // .constrained() | |
| 2234 | − // .with_width(theme.channel_hash.width) | |
| 2235 | − // .aligned() | |
| 2236 | − // .left(), | |
| 2237 | − // ) | |
| 2238 | − // .with_child( | |
| 2239 | − // Label::new(channel.name.clone(), theme.channel_name.text.clone()) | |
| 2240 | − // .contained() | |
| 2241 | − // .with_style(theme.channel_name.container) | |
| 2242 | − // .aligned() | |
| 2243 | − // .left(), | |
| 2244 | − // ) | |
| 2245 | − // .align_children_center() | |
| 2246 | − // .contained() | |
| 2247 | − // .with_background_color( | |
| 2248 | − // theme | |
| 2249 | − // .container | |
| 2250 | − // .background_color | |
| 2251 | − // .unwrap_or(gpui::color::Color::transparent_black()), | |
| 2252 | − // ) | |
| 2253 | − // .contained() | |
| 2254 | − // .with_padding_left( | |
| 2255 | − // theme.channel_row.default_style().padding.left | |
| 2256 | − // + theme.channel_indent * depth as f32, | |
| 2257 | − // ) | |
| 2258 | − // .into_any() | |
| 2259 | − // }, | |
| 2260 | − // ) | |
| 2261 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 2262 | − // .into_any() | |
| 2263 | − // } | |
| 2264 | − | |
| 2265 | 1562 | // fn render_channel_notes( |
| 2266 | 1563 | // &self, |
| 2267 | 1564 | // channel_id: ChannelId, |
| @@ -2948,9 +2245,9 @@ impl CollabPanel { · 680 unmodified lines | ||
| 2948 | 2245 | // cx.focus_self(); |
| 2949 | 2246 | // } |
| 2950 | 2247 | |
| 2951 | − // fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { | |
| 2952 | − // self.collapsed_channels.binary_search(&channel_id).is_ok() | |
| 2953 | − // } | |
| 2248 | + fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { | |
| 2249 | + self.collapsed_channels.binary_search(&channel_id).is_ok() | |
| 2250 | + } | |
| 2954 | 2251 | |
| 2955 | 2252 | // fn leave_call(cx: &mut ViewContext<Self>) { |
| 2956 | 2253 | // ActiveCall::global(cx) |
| @@ -2958,19 +2255,17 @@ impl CollabPanel { · 1 unmodified line | ||
| 2958 | 2255 | // .detach_and_log_err(cx); |
| 2959 | 2256 | // } |
| 2960 | 2257 | |
| 2961 | − // fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) { | |
| 2962 | − // if let Some(workspace) = self.workspace.upgrade(cx) { | |
| 2963 | − // workspace.update(cx, |workspace, cx| { | |
| 2964 | − // workspace.toggle_modal(cx, |_, cx| { | |
| 2965 | − // cx.add_view(|cx| { | |
| 2966 | − // let mut finder = ContactFinder::new(self.user_store.clone(), cx); | |
| 2967 | − // finder.set_query(self.filter_editor.read(cx).text(cx), cx); | |
| 2968 | − // finder | |
| 2969 | − // }) | |
| 2970 | − // }); | |
| 2971 | − // }); | |
| 2972 | − // } | |
| 2973 | − // } | |
| 2258 | + fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) { | |
| 2259 | + if let Some(workspace) = self.workspace.upgrade() { | |
| 2260 | + workspace.update(cx, |workspace, cx| { | |
| 2261 | + workspace.toggle_modal(cx, |cx| { | |
| 2262 | + let mut finder = ContactFinder::new(self.user_store.clone(), cx); | |
| 2263 | + finder.set_query(self.filter_editor.read(cx).text(cx), cx); | |
| 2264 | + finder | |
| 2265 | + }); | |
| 2266 | + }); | |
| 2267 | + } | |
| 2268 | + } | |
| 2974 | 2269 | |
| 2975 | 2270 | // fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) { |
| 2976 | 2271 | // self.channel_editing_state = Some(ChannelEditingState::Create { |
| @@ -3247,6 +2542,802 @@ impl CollabPanel { · 270 unmodified lines | ||
| 3247 | 2542 | // let item = ClipboardItem::new(channel.link()); |
| 3248 | 2543 | // cx.write_to_clipboard(item) |
| 3249 | 2544 | // } |
| 2545 | + | |
| 2546 | + fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div { | |
| 2547 | + v_stack().child(Button::new("Sign in to collaborate").on_click(cx.listener( | |
| 2548 | + |this, _, cx| { | |
| 2549 | + let client = this.client.clone(); | |
| 2550 | + cx.spawn(|_, mut cx| async move { | |
| 2551 | + client | |
| 2552 | + .authenticate_and_connect(true, &cx) | |
| 2553 | + .await | |
| 2554 | + .notify_async_err(&mut cx); | |
| 2555 | + }) | |
| 2556 | + .detach() | |
| 2557 | + }, | |
| 2558 | + ))) | |
| 2559 | + } | |
| 2560 | + | |
| 2561 | + fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> List { | |
| 2562 | + let is_selected = false; // todo!() this.selection == Some(ix); | |
| 2563 | + | |
| 2564 | + List::new().children(self.entries.clone().into_iter().map(|entry| { | |
| 2565 | + match entry { | |
| 2566 | + ListEntry::Header(section) => { | |
| 2567 | + let is_collapsed = self.collapsed_sections.contains(§ion); | |
| 2568 | + self.render_header(section, is_selected, is_collapsed, cx) | |
| 2569 | + .into_any_element() | |
| 2570 | + } | |
| 2571 | + ListEntry::Contact { contact, calling } => self | |
| 2572 | + .render_contact(&*contact, calling, is_selected, cx) | |
| 2573 | + .into_any_element(), | |
| 2574 | + ListEntry::ContactPlaceholder => self | |
| 2575 | + .render_contact_placeholder(is_selected, cx) | |
| 2576 | + .into_any_element(), | |
| 2577 | + ListEntry::IncomingRequest(user) => self | |
| 2578 | + .render_contact_request(user, true, is_selected, cx) | |
| 2579 | + .into_any_element(), | |
| 2580 | + ListEntry::OutgoingRequest(user) => self | |
| 2581 | + .render_contact_request(user, false, is_selected, cx) | |
| 2582 | + .into_any_element(), | |
| 2583 | + ListEntry::Channel { | |
| 2584 | + channel, | |
| 2585 | + depth, | |
| 2586 | + has_children, | |
| 2587 | + } => self | |
| 2588 | + .render_channel(&*channel, depth, has_children, is_selected, cx) | |
| 2589 | + .into_any_element(), | |
| 2590 | + ListEntry::ChannelEditor { depth } => todo!(), | |
| 2591 | + } | |
| 2592 | + })) | |
| 2593 | + } | |
| 2594 | + | |
| 2595 | + fn render_header( | |
| 2596 | + &mut self, | |
| 2597 | + section: Section, | |
| 2598 | + is_selected: bool, | |
| 2599 | + is_collapsed: bool, | |
| 2600 | + cx: &ViewContext<Self>, | |
| 2601 | + ) -> impl IntoElement { | |
| 2602 | + // let mut channel_link = None; | |
| 2603 | + // let mut channel_tooltip_text = None; | |
| 2604 | + // let mut channel_icon = None; | |
| 2605 | + // let mut is_dragged_over = false; | |
| 2606 | + | |
| 2607 | + let text = match section { | |
| 2608 | + Section::ActiveCall => { | |
| 2609 | + // let channel_name = maybe!({ | |
| 2610 | + // let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; | |
| 2611 | + | |
| 2612 | + // let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; | |
| 2613 | + | |
| 2614 | + // channel_link = Some(channel.link()); | |
| 2615 | + // (channel_icon, channel_tooltip_text) = match channel.visibility { | |
| 2616 | + // proto::ChannelVisibility::Public => { | |
| 2617 | + // (Some("icons/public.svg"), Some("Copy public channel link.")) | |
| 2618 | + // } | |
| 2619 | + // proto::ChannelVisibility::Members => { | |
| 2620 | + // (Some("icons/hash.svg"), Some("Copy private channel link.")) | |
| 2621 | + // } | |
| 2622 | + // }; | |
| 2623 | + | |
| 2624 | + // Some(channel.name.as_str()) | |
| 2625 | + // }); | |
| 2626 | + | |
| 2627 | + // if let Some(name) = channel_name { | |
| 2628 | + // SharedString::from(format!("{}", name)) | |
| 2629 | + // } else { | |
| 2630 | + // SharedString::from("Current Call") | |
| 2631 | + // } | |
| 2632 | + todo!() | |
| 2633 | + } | |
| 2634 | + Section::ContactRequests => SharedString::from("Requests"), | |
| 2635 | + Section::Contacts => SharedString::from("Contacts"), | |
| 2636 | + Section::Channels => SharedString::from("Channels"), | |
| 2637 | + Section::ChannelInvites => SharedString::from("Invites"), | |
| 2638 | + Section::Online => SharedString::from("Online"), | |
| 2639 | + Section::Offline => SharedString::from("Offline"), | |
| 2640 | + }; | |
| 2641 | + | |
| 2642 | + let button = match section { | |
| 2643 | + Section::ActiveCall => | |
| 2644 | + // channel_link.map(|channel_link| { | |
| 2645 | + // let channel_link_copy = channel_link.clone(); | |
| 2646 | + // MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| { | |
| 2647 | + // render_icon_button( | |
| 2648 | + // theme | |
| 2649 | + // .collab_panel | |
| 2650 | + // .leave_call_button | |
| 2651 | + // .style_for(is_selected, state), | |
| 2652 | + // "icons/link.svg", | |
| 2653 | + // ) | |
| 2654 | + // }) | |
| 2655 | + // .with_cursor_style(CursorStyle::PointingHand) | |
| 2656 | + // .on_click(MouseButton::Left, move |_, _, cx| { | |
| 2657 | + // let item = ClipboardItem::new(channel_link_copy.clone()); | |
| 2658 | + // cx.write_to_clipboard(item) | |
| 2659 | + // }) | |
| 2660 | + // .with_tooltip::<AddContact>( | |
| 2661 | + // 0, | |
| 2662 | + // channel_tooltip_text.unwrap(), | |
| 2663 | + // None, | |
| 2664 | + // tooltip_style.clone(), | |
| 2665 | + // cx, | |
| 2666 | + // ) | |
| 2667 | + // }), | |
| 2668 | + { | |
| 2669 | + todo!() | |
| 2670 | + } | |
| 2671 | + Section::Contacts => Some( | |
| 2672 | + IconButton::new("add-contact", Icon::Plus) | |
| 2673 | + .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) | |
| 2674 | + .tooltip(|cx| Tooltip::text("Search for new contact", cx)), | |
| 2675 | + ), | |
| 2676 | + Section::Channels => { | |
| 2677 | + // todo!() | |
| 2678 | + // if cx | |
| 2679 | + // .global::<DragAndDrop<Workspace>>() | |
| 2680 | + // .currently_dragged::<Channel>(cx.window()) | |
| 2681 | + // .is_some() | |
| 2682 | + // && self.drag_target_channel == ChannelDragTarget::Root | |
| 2683 | + // { | |
| 2684 | + // is_dragged_over = true; | |
| 2685 | + // } | |
| 2686 | + | |
| 2687 | + Some( | |
| 2688 | + IconButton::new("add-channel", Icon::Plus) | |
| 2689 | + .on_click(cx.listener(|this, _, cx| { | |
| 2690 | + todo!() | |
| 2691 | + // this.new_root_channel(cx) | |
| 2692 | + })) | |
| 2693 | + .tooltip(|cx| Tooltip::text("Create a channel", cx)), | |
| 2694 | + ) | |
| 2695 | + } | |
| 2696 | + _ => None, | |
| 2697 | + }; | |
| 2698 | + | |
| 2699 | + let can_collapse = match section { | |
| 2700 | + Section::ActiveCall | Section::Channels | Section::Contacts => false, | |
| 2701 | + Section::ChannelInvites | |
| 2702 | + | Section::ContactRequests | |
| 2703 | + | Section::Online | |
| 2704 | + | Section::Offline => true, | |
| 2705 | + }; | |
| 2706 | + | |
| 2707 | + let mut header = ListHeader::new(text); | |
| 2708 | + if let Some(button) = button { | |
| 2709 | + header = header.right_button(button) | |
| 2710 | + } | |
| 2711 | + // todo!() is selected | |
| 2712 | + if can_collapse { | |
| 2713 | + // todo!() on click to toggle | |
| 2714 | + header = header.toggle(ui::Toggle::Toggled(is_collapsed)); | |
| 2715 | + } | |
| 2716 | + | |
| 2717 | + header | |
| 2718 | + } | |
| 2719 | + fn render_contact( | |
| 2720 | + &mut self, | |
| 2721 | + contact: &Contact, | |
| 2722 | + calling: bool, | |
| 2723 | + is_selected: bool, | |
| 2724 | + cx: &mut ViewContext<Self>, | |
| 2725 | + ) -> impl IntoElement { | |
| 2726 | + enum ContactTooltip {} | |
| 2727 | + | |
| 2728 | + let online = contact.online; | |
| 2729 | + let busy = contact.busy || calling; | |
| 2730 | + let user_id = contact.user.id; | |
| 2731 | + let github_login = SharedString::from(contact.user.github_login.clone()); | |
| 2732 | + let mut item = ListItem::new(github_login.clone()) | |
| 2733 | + .on_click(cx.listener(move |this, _, cx| { | |
| 2734 | + this.workspace | |
| 2735 | + .update(cx, |this, cx| { | |
| 2736 | + this.call_state() | |
| 2737 | + .invite(user_id, None, cx) | |
| 2738 | + .detach_and_log_err(cx) | |
| 2739 | + }) | |
| 2740 | + .log_err(); | |
| 2741 | + })) | |
| 2742 | + .child(Label::new(github_login.clone())); | |
| 2743 | + if let Some(avatar) = contact.user.avatar.clone() { | |
| 2744 | + //item = item.left_avatar(avatar); | |
| 2745 | + } | |
| 2746 | + // let event_handler = | |
| 2747 | + // MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| { | |
| 2748 | + // Flex::row() | |
| 2749 | + // .with_children(contact.user.avatar.clone().map(|avatar| { | |
| 2750 | + // let status_badge = if contact.online { | |
| 2751 | + // Some( | |
| 2752 | + // Empty::new() | |
| 2753 | + // .collapsed() | |
| 2754 | + // .contained() | |
| 2755 | + // .with_style(if busy { | |
| 2756 | + // collab_theme.contact_status_busy | |
| 2757 | + // } else { | |
| 2758 | + // collab_theme.contact_status_free | |
| 2759 | + // }) | |
| 2760 | + // .aligned(), | |
| 2761 | + // ) | |
| 2762 | + // } else { | |
| 2763 | + // None | |
| 2764 | + // }; | |
| 2765 | + // Stack::new() | |
| 2766 | + // .with_child( | |
| 2767 | + // Image::from_data(avatar) | |
| 2768 | + // .with_style(collab_theme.contact_avatar) | |
| 2769 | + // .aligned() | |
| 2770 | + // .left(), | |
| 2771 | + // ) | |
| 2772 | + // .with_children(status_badge) | |
| 2773 | + // })) | |
| 2774 | + // .with_child( | |
| 2775 | + // Label::new( | |
| 2776 | + // contact.user.github_login.clone(), | |
| 2777 | + // collab_theme.contact_username.text.clone(), | |
| 2778 | + // ) | |
| 2779 | + // .contained() | |
| 2780 | + // .with_style(collab_theme.contact_username.container) | |
| 2781 | + // .aligned() | |
| 2782 | + // .left() | |
| 2783 | + // .flex(1., true), | |
| 2784 | + // ) | |
| 2785 | + // .with_children(if state.hovered() { | |
| 2786 | + // Some( | |
| 2787 | + // MouseEventHandler::new::<Cancel, _>( | |
| 2788 | + // contact.user.id as usize, | |
| 2789 | + // cx, | |
| 2790 | + // |mouse_state, _| { | |
| 2791 | + // let button_style = | |
| 2792 | + // collab_theme.contact_button.style_for(mouse_state); | |
| 2793 | + // render_icon_button(button_style, "icons/x.svg") | |
| 2794 | + // .aligned() | |
| 2795 | + // .flex_float() | |
| 2796 | + // }, | |
| 2797 | + // ) | |
| 2798 | + // .with_padding(Padding::uniform(2.)) | |
| 2799 | + // .with_cursor_style(CursorStyle::PointingHand) | |
| 2800 | + // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 2801 | + // this.remove_contact(user_id, &github_login, cx); | |
| 2802 | + // }) | |
| 2803 | + // .flex_float(), | |
| 2804 | + // ) | |
| 2805 | + // } else { | |
| 2806 | + // None | |
| 2807 | + // }) | |
| 2808 | + // .with_children(if calling { | |
| 2809 | + // Some( | |
| 2810 | + // Label::new("Calling", collab_theme.calling_indicator.text.clone()) | |
| 2811 | + // .contained() | |
| 2812 | + // .with_style(collab_theme.calling_indicator.container) | |
| 2813 | + // .aligned(), | |
| 2814 | + // ) | |
| 2815 | + // } else { | |
| 2816 | + // None | |
| 2817 | + // }) | |
| 2818 | + // .constrained() | |
| 2819 | + // .with_height(collab_theme.row_height) | |
| 2820 | + // .contained() | |
| 2821 | + // .with_style( | |
| 2822 | + // *collab_theme | |
| 2823 | + // .contact_row | |
| 2824 | + // .in_state(is_selected) | |
| 2825 | + // .style_for(state), | |
| 2826 | + // ) | |
| 2827 | + // }); | |
| 2828 | + | |
| 2829 | + // if online && !busy { | |
| 2830 | + // let room = ActiveCall::global(cx).read(cx).room(); | |
| 2831 | + // let label = if room.is_some() { | |
| 2832 | + // format!("Invite {} to join call", contact.user.github_login) | |
| 2833 | + // } else { | |
| 2834 | + // format!("Call {}", contact.user.github_login) | |
| 2835 | + // }; | |
| 2836 | + | |
| 2837 | + // event_handler | |
| 2838 | + // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 2839 | + // this.call(user_id, Some(initial_project.clone()), cx); | |
| 2840 | + // }) | |
| 2841 | + // .with_cursor_style(CursorStyle::PointingHand) | |
| 2842 | + // .with_tooltip::<ContactTooltip>( | |
| 2843 | + // contact.user.id as usize, | |
| 2844 | + // label, | |
| 2845 | + // None, | |
| 2846 | + // theme.tooltip.clone(), | |
| 2847 | + // cx, | |
| 2848 | + // ) | |
| 2849 | + // .into_any() | |
| 2850 | + // } else { | |
| 2851 | + // event_handler | |
| 2852 | + // .with_tooltip::<ContactTooltip>( | |
| 2853 | + // contact.user.id as usize, | |
| 2854 | + // format!( | |
| 2855 | + // "{} is {}", | |
| 2856 | + // contact.user.github_login, | |
| 2857 | + // if busy { "on a call" } else { "offline" } | |
| 2858 | + // ), | |
| 2859 | + // None, | |
| 2860 | + // theme.tooltip.clone(), | |
| 2861 | + // cx, | |
| 2862 | + // ) | |
| 2863 | + // .into_any() | |
| 2864 | + // }; | |
| 2865 | + | |
| 2866 | + item | |
| 2867 | + } | |
| 2868 | + | |
| 2869 | + fn render_contact_request( | |
| 2870 | + &mut self, | |
| 2871 | + user: Arc<User>, | |
| 2872 | + is_incoming: bool, | |
| 2873 | + is_selected: bool, | |
| 2874 | + cx: &mut ViewContext<Self>, | |
| 2875 | + ) -> impl IntoElement { | |
| 2876 | + let github_login = SharedString::from(user.github_login.clone()); | |
| 2877 | + | |
| 2878 | + let mut item = ListItem::new(github_login.clone()) | |
| 2879 | + .child(Label::new(github_login.clone())) | |
| 2880 | + .on_click(cx.listener(|this, _, cx| { | |
| 2881 | + todo!(); | |
| 2882 | + })); | |
| 2883 | + if let Some(avatar) = user.avatar.clone() { | |
| 2884 | + item = item.left_avatar(avatar); | |
| 2885 | + } | |
| 2886 | + // .with_children(user.avatar.clone().map(|avatar| { | |
| 2887 | + // Image::from_data(avatar) | |
| 2888 | + // .with_style(theme.contact_avatar) | |
| 2889 | + // .aligned() | |
| 2890 | + // .left() | |
| 2891 | + // })) | |
| 2892 | + // .with_child( | |
| 2893 | + // Label::new( | |
| 2894 | + // user.github_login.clone(), | |
| 2895 | + // theme.contact_username.text.clone(), | |
| 2896 | + // ) | |
| 2897 | + // .contained() | |
| 2898 | + // .with_style(theme.contact_username.container) | |
| 2899 | + // .aligned() | |
| 2900 | + // .left() | |
| 2901 | + // .flex(1., true), | |
| 2902 | + // ); | |
| 2903 | + | |
| 2904 | + // let user_id = user.id; | |
| 2905 | + // let github_login = user.github_login.clone(); | |
| 2906 | + // let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); | |
| 2907 | + // let button_spacing = theme.contact_button_spacing; | |
| 2908 | + | |
| 2909 | + // if is_incoming { | |
| 2910 | + // row.add_child( | |
| 2911 | + // MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| { | |
| 2912 | + // let button_style = if is_contact_request_pending { | |
| 2913 | + // &theme.disabled_button | |
| 2914 | + // } else { | |
| 2915 | + // theme.contact_button.style_for(mouse_state) | |
| 2916 | + // }; | |
| 2917 | + // render_icon_button(button_style, "icons/x.svg").aligned() | |
| 2918 | + // }) | |
| 2919 | + // .with_cursor_style(CursorStyle::PointingHand) | |
| 2920 | + // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 2921 | + // this.respond_to_contact_request(user_id, false, cx); | |
| 2922 | + // }) | |
| 2923 | + // .contained() | |
| 2924 | + // .with_margin_right(button_spacing), | |
| 2925 | + // ); | |
| 2926 | + | |
| 2927 | + // row.add_child( | |
| 2928 | + // MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| { | |
| 2929 | + // let button_style = if is_contact_request_pending { | |
| 2930 | + // &theme.disabled_button | |
| 2931 | + // } else { | |
| 2932 | + // theme.contact_button.style_for(mouse_state) | |
| 2933 | + // }; | |
| 2934 | + // render_icon_button(button_style, "icons/check.svg") | |
| 2935 | + // .aligned() | |
| 2936 | + // .flex_float() | |
| 2937 | + // }) | |
| 2938 | + // .with_cursor_style(CursorStyle::PointingHand) | |
| 2939 | + // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 2940 | + // this.respond_to_contact_request(user_id, true, cx); | |
| 2941 | + // }), | |
| 2942 | + // ); | |
| 2943 | + // } else { | |
| 2944 | + // row.add_child( | |
| 2945 | + // MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| { | |
| 2946 | + // let button_style = if is_contact_request_pending { | |
| 2947 | + // &theme.disabled_button | |
| 2948 | + // } else { | |
| 2949 | + // theme.contact_button.style_for(mouse_state) | |
| 2950 | + // }; | |
| 2951 | + // render_icon_button(button_style, "icons/x.svg") | |
| 2952 | + // .aligned() | |
| 2953 | + // .flex_float() | |
| 2954 | + // }) | |
| 2955 | + // .with_padding(Padding::uniform(2.)) | |
| 2956 | + // .with_cursor_style(CursorStyle::PointingHand) | |
| 2957 | + // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 2958 | + // this.remove_contact(user_id, &github_login, cx); | |
| 2959 | + // }) | |
| 2960 | + // .flex_float(), | |
| 2961 | + // ); | |
| 2962 | + // } | |
| 2963 | + | |
| 2964 | + // row.constrained() | |
| 2965 | + // .with_height(theme.row_height) | |
| 2966 | + // .contained() | |
| 2967 | + // .with_style( | |
| 2968 | + // *theme | |
| 2969 | + // .contact_row | |
| 2970 | + // .in_state(is_selected) | |
| 2971 | + // .style_for(&mut Default::default()), | |
| 2972 | + // ) | |
| 2973 | + // .into_any() | |
| 2974 | + item | |
| 2975 | + } | |
| 2976 | + | |
| 2977 | + fn render_contact_placeholder( | |
| 2978 | + &self, | |
| 2979 | + is_selected: bool, | |
| 2980 | + cx: &mut ViewContext<Self>, | |
| 2981 | + ) -> impl IntoElement { | |
| 2982 | + ListItem::new("contact-placeholder") | |
| 2983 | + .child(Label::new("Add a Contact")) | |
| 2984 | + .on_click(cx.listener(|this, _, cx| todo!())) | |
| 2985 | + // enum AddContacts {} | |
| 2986 | + // MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| { | |
| 2987 | + // let style = theme.list_empty_state.style_for(is_selected, state); | |
| 2988 | + // Flex::row() | |
| 2989 | + // .with_child( | |
| 2990 | + // Svg::new("icons/plus.svg") | |
| 2991 | + // .with_color(theme.list_empty_icon.color) | |
| 2992 | + // .constrained() | |
| 2993 | + // .with_width(theme.list_empty_icon.width) | |
| 2994 | + // .aligned() | |
| 2995 | + // .left(), | |
| 2996 | + // ) | |
| 2997 | + // .with_child( | |
| 2998 | + // Label::new("Add a contact", style.text.clone()) | |
| 2999 | + // .contained() | |
| 3000 | + // .with_style(theme.list_empty_label_container), | |
| 3001 | + // ) | |
| 3002 | + // .align_children_center() | |
| 3003 | + // .contained() | |
| 3004 | + // .with_style(style.container) | |
| 3005 | + // .into_any() | |
| 3006 | + // }) | |
| 3007 | + // .on_click(MouseButton::Left, |_, this, cx| { | |
| 3008 | + // this.toggle_contact_finder(cx); | |
| 3009 | + // }) | |
| 3010 | + // .into_any() | |
| 3011 | + } | |
| 3012 | + | |
| 3013 | + fn render_channel( | |
| 3014 | + &self, | |
| 3015 | + channel: &Channel, | |
| 3016 | + depth: usize, | |
| 3017 | + has_children: bool, | |
| 3018 | + is_selected: bool, | |
| 3019 | + cx: &mut ViewContext<Self>, | |
| 3020 | + ) -> impl IntoElement { | |
| 3021 | + let channel_id = channel.id; | |
| 3022 | + ListItem::new(channel_id as usize).child(Label::new(channel.name.clone())) | |
| 3023 | + // let channel_id = channel.id; | |
| 3024 | + // let collab_theme = &theme.collab_panel; | |
| 3025 | + // let is_public = self | |
| 3026 | + // .channel_store | |
| 3027 | + // .read(cx) | |
| 3028 | + // .channel_for_id(channel_id) | |
| 3029 | + // .map(|channel| channel.visibility) | |
| 3030 | + // == Some(proto::ChannelVisibility::Public); | |
| 3031 | + // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); | |
| 3032 | + // let disclosed = | |
| 3033 | + // has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); | |
| 3034 | + | |
| 3035 | + // let is_active = maybe!({ | |
| 3036 | + // let call_channel = ActiveCall::global(cx) | |
| 3037 | + // .read(cx) | |
| 3038 | + // .room()? | |
| 3039 | + // .read(cx) | |
| 3040 | + // .channel_id()?; | |
| 3041 | + // Some(call_channel == channel_id) | |
| 3042 | + // }) | |
| 3043 | + // .unwrap_or(false); | |
| 3044 | + | |
| 3045 | + // const FACEPILE_LIMIT: usize = 3; | |
| 3046 | + | |
| 3047 | + // enum ChannelCall {} | |
| 3048 | + // enum ChannelNote {} | |
| 3049 | + // enum NotesTooltip {} | |
| 3050 | + // enum ChatTooltip {} | |
| 3051 | + // enum ChannelTooltip {} | |
| 3052 | + | |
| 3053 | + // let mut is_dragged_over = false; | |
| 3054 | + // if cx | |
| 3055 | + // .global::<DragAndDrop<Workspace>>() | |
| 3056 | + // .currently_dragged::<Channel>(cx.window()) | |
| 3057 | + // .is_some() | |
| 3058 | + // && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) | |
| 3059 | + // { | |
| 3060 | + // is_dragged_over = true; | |
| 3061 | + // } | |
| 3062 | + | |
| 3063 | + // let has_messages_notification = channel.unseen_message_id.is_some(); | |
| 3064 | + | |
| 3065 | + // MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| { | |
| 3066 | + // let row_hovered = state.hovered(); | |
| 3067 | + | |
| 3068 | + // let mut select_state = |interactive: &Interactive<ContainerStyle>| { | |
| 3069 | + // if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() { | |
| 3070 | + // interactive.clicked.as_ref().unwrap().clone() | |
| 3071 | + // } else if state.hovered() || other_selected { | |
| 3072 | + // interactive | |
| 3073 | + // .hovered | |
| 3074 | + // .as_ref() | |
| 3075 | + // .unwrap_or(&interactive.default) | |
| 3076 | + // .clone() | |
| 3077 | + // } else { | |
| 3078 | + // interactive.default.clone() | |
| 3079 | + // } | |
| 3080 | + // }; | |
| 3081 | + | |
| 3082 | + // Flex::<Self>::row() | |
| 3083 | + // .with_child( | |
| 3084 | + // Svg::new(if is_public { | |
| 3085 | + // "icons/public.svg" | |
| 3086 | + // } else { | |
| 3087 | + // "icons/hash.svg" | |
| 3088 | + // }) | |
| 3089 | + // .with_color(collab_theme.channel_hash.color) | |
| 3090 | + // .constrained() | |
| 3091 | + // .with_width(collab_theme.channel_hash.width) | |
| 3092 | + // .aligned() | |
| 3093 | + // .left(), | |
| 3094 | + // ) | |
| 3095 | + // .with_child({ | |
| 3096 | + // let style = collab_theme.channel_name.inactive_state(); | |
| 3097 | + // Flex::row() | |
| 3098 | + // .with_child( | |
| 3099 | + // Label::new(channel.name.clone(), style.text.clone()) | |
| 3100 | + // .contained() | |
| 3101 | + // .with_style(style.container) | |
| 3102 | + // .aligned() | |
| 3103 | + // .left() | |
| 3104 | + // .with_tooltip::<ChannelTooltip>( | |
| 3105 | + // ix, | |
| 3106 | + // "Join channel", | |
| 3107 | + // None, | |
| 3108 | + // theme.tooltip.clone(), | |
| 3109 | + // cx, | |
| 3110 | + // ), | |
| 3111 | + // ) | |
| 3112 | + // .with_children({ | |
| 3113 | + // let participants = | |
| 3114 | + // self.channel_store.read(cx).channel_participants(channel_id); | |
| 3115 | + | |
| 3116 | + // if !participants.is_empty() { | |
| 3117 | + // let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); | |
| 3118 | + | |
| 3119 | + // let result = FacePile::new(collab_theme.face_overlap) | |
| 3120 | + // .with_children( | |
| 3121 | + // participants | |
| 3122 | + // .iter() | |
| 3123 | + // .filter_map(|user| { | |
| 3124 | + // Some( | |
| 3125 | + // Image::from_data(user.avatar.clone()?) | |
| 3126 | + // .with_style(collab_theme.channel_avatar), | |
| 3127 | + // ) | |
| 3128 | + // }) | |
| 3129 | + // .take(FACEPILE_LIMIT), | |
| 3130 | + // ) | |
| 3131 | + // .with_children((extra_count > 0).then(|| { | |
| 3132 | + // Label::new( | |
| 3133 | + // format!("+{}", extra_count), | |
| 3134 | + // collab_theme.extra_participant_label.text.clone(), | |
| 3135 | + // ) | |
| 3136 | + // .contained() | |
| 3137 | + // .with_style(collab_theme.extra_participant_label.container) | |
| 3138 | + // })); | |
| 3139 | + | |
| 3140 | + // Some(result) | |
| 3141 | + // } else { | |
| 3142 | + // None | |
| 3143 | + // } | |
| 3144 | + // }) | |
| 3145 | + // .with_spacing(8.) | |
| 3146 | + // .align_children_center() | |
| 3147 | + // .flex(1., true) | |
| 3148 | + // }) | |
| 3149 | + // .with_child( | |
| 3150 | + // MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| { | |
| 3151 | + // let container_style = collab_theme | |
| 3152 | + // .disclosure | |
| 3153 | + // .button | |
| 3154 | + // .style_for(mouse_state) | |
| 3155 | + // .container; | |
| 3156 | + | |
| 3157 | + // if channel.unseen_message_id.is_some() { | |
| 3158 | + // Svg::new("icons/conversations.svg") | |
| 3159 | + // .with_color(collab_theme.channel_note_active_color) | |
| 3160 | + // .constrained() | |
| 3161 | + // .with_width(collab_theme.channel_hash.width) | |
| 3162 | + // .contained() | |
| 3163 | + // .with_style(container_style) | |
| 3164 | + // .with_uniform_padding(4.) | |
| 3165 | + // .into_any() | |
| 3166 | + // } else if row_hovered { | |
| 3167 | + // Svg::new("icons/conversations.svg") | |
| 3168 | + // .with_color(collab_theme.channel_hash.color) | |
| 3169 | + // .constrained() | |
| 3170 | + // .with_width(collab_theme.channel_hash.width) | |
| 3171 | + // .contained() | |
| 3172 | + // .with_style(container_style) | |
| 3173 | + // .with_uniform_padding(4.) | |
| 3174 | + // .into_any() | |
| 3175 | + // } else { | |
| 3176 | + // Empty::new().into_any() | |
| 3177 | + // } | |
| 3178 | + // }) | |
| 3179 | + // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 3180 | + // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); | |
| 3181 | + // }) | |
| 3182 | + // .with_tooltip::<ChatTooltip>( | |
| 3183 | + // ix, | |
| 3184 | + // "Open channel chat", | |
| 3185 | + // None, | |
| 3186 | + // theme.tooltip.clone(), | |
| 3187 | + // cx, | |
| 3188 | + // ) | |
| 3189 | + // .contained() | |
| 3190 | + // .with_margin_right(4.), | |
| 3191 | + // ) | |
| 3192 | + // .with_child( | |
| 3193 | + // MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| { | |
| 3194 | + // let container_style = collab_theme | |
| 3195 | + // .disclosure | |
| 3196 | + // .button | |
| 3197 | + // .style_for(mouse_state) | |
| 3198 | + // .container; | |
| 3199 | + // if row_hovered || channel.unseen_note_version.is_some() { | |
| 3200 | + // Svg::new("icons/file.svg") | |
| 3201 | + // .with_color(if channel.unseen_note_version.is_some() { | |
| 3202 | + // collab_theme.channel_note_active_color | |
| 3203 | + // } else { | |
| 3204 | + // collab_theme.channel_hash.color | |
| 3205 | + // }) | |
| 3206 | + // .constrained() | |
| 3207 | + // .with_width(collab_theme.channel_hash.width) | |
| 3208 | + // .contained() | |
| 3209 | + // .with_style(container_style) | |
| 3210 | + // .with_uniform_padding(4.) | |
| 3211 | + // .with_margin_right(collab_theme.channel_hash.container.margin.left) | |
| 3212 | + // .with_tooltip::<NotesTooltip>( | |
| 3213 | + // ix as usize, | |
| 3214 | + // "Open channel notes", | |
| 3215 | + // None, | |
| 3216 | + // theme.tooltip.clone(), | |
| 3217 | + // cx, | |
| 3218 | + // ) | |
| 3219 | + // .into_any() | |
| 3220 | + // } else if has_messages_notification { | |
| 3221 | + // Empty::new() | |
| 3222 | + // .constrained() | |
| 3223 | + // .with_width(collab_theme.channel_hash.width) | |
| 3224 | + // .contained() | |
| 3225 | + // .with_uniform_padding(4.) | |
| 3226 | + // .with_margin_right(collab_theme.channel_hash.container.margin.left) | |
| 3227 | + // .into_any() | |
| 3228 | + // } else { | |
| 3229 | + // Empty::new().into_any() | |
| 3230 | + // } | |
| 3231 | + // }) | |
| 3232 | + // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 3233 | + // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); | |
| 3234 | + // }), | |
| 3235 | + // ) | |
| 3236 | + // .align_children_center() | |
| 3237 | + // .styleable_component() | |
| 3238 | + // .disclosable( | |
| 3239 | + // disclosed, | |
| 3240 | + // Box::new(ToggleCollapse { | |
| 3241 | + // location: channel.id.clone(), | |
| 3242 | + // }), | |
| 3243 | + // ) | |
| 3244 | + // .with_id(ix) | |
| 3245 | + // .with_style(collab_theme.disclosure.clone()) | |
| 3246 | + // .element() | |
| 3247 | + // .constrained() | |
| 3248 | + // .with_height(collab_theme.row_height) | |
| 3249 | + // .contained() | |
| 3250 | + // .with_style(select_state( | |
| 3251 | + // collab_theme | |
| 3252 | + // .channel_row | |
| 3253 | + // .in_state(is_selected || is_active || is_dragged_over), | |
| 3254 | + // )) | |
| 3255 | + // .with_padding_left( | |
| 3256 | + // collab_theme.channel_row.default_style().padding.left | |
| 3257 | + // + collab_theme.channel_indent * depth as f32, | |
| 3258 | + // ) | |
| 3259 | + // }) | |
| 3260 | + // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 3261 | + // if this.drag_target_channel == ChannelDragTarget::None { | |
| 3262 | + // if is_active { | |
| 3263 | + // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) | |
| 3264 | + // } else { | |
| 3265 | + // this.join_channel(channel_id, cx) | |
| 3266 | + // } | |
| 3267 | + // } | |
| 3268 | + // }) | |
| 3269 | + // .on_click(MouseButton::Right, { | |
| 3270 | + // let channel = channel.clone(); | |
| 3271 | + // move |e, this, cx| { | |
| 3272 | + // this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); | |
| 3273 | + // } | |
| 3274 | + // }) | |
| 3275 | + // .on_up(MouseButton::Left, move |_, this, cx| { | |
| 3276 | + // if let Some((_, dragged_channel)) = cx | |
| 3277 | + // .global::<DragAndDrop<Workspace>>() | |
| 3278 | + // .currently_dragged::<Channel>(cx.window()) | |
| 3279 | + // { | |
| 3280 | + // this.channel_store | |
| 3281 | + // .update(cx, |channel_store, cx| { | |
| 3282 | + // channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) | |
| 3283 | + // }) | |
| 3284 | + // .detach_and_log_err(cx) | |
| 3285 | + // } | |
| 3286 | + // }) | |
| 3287 | + // .on_move({ | |
| 3288 | + // let channel = channel.clone(); | |
| 3289 | + // move |_, this, cx| { | |
| 3290 | + // if let Some((_, dragged_channel)) = cx | |
| 3291 | + // .global::<DragAndDrop<Workspace>>() | |
| 3292 | + // .currently_dragged::<Channel>(cx.window()) | |
| 3293 | + // { | |
| 3294 | + // if channel.id != dragged_channel.id { | |
| 3295 | + // this.drag_target_channel = ChannelDragTarget::Channel(channel.id); | |
| 3296 | + // } | |
| 3297 | + // cx.notify() | |
| 3298 | + // } | |
| 3299 | + // } | |
| 3300 | + // }) | |
| 3301 | + // .as_draggable::<_, Channel>( | |
| 3302 | + // channel.clone(), | |
| 3303 | + // move |_, channel, cx: &mut ViewContext<Workspace>| { | |
| 3304 | + // let theme = &theme::current(cx).collab_panel; | |
| 3305 | + | |
| 3306 | + // Flex::<Workspace>::row() | |
| 3307 | + // .with_child( | |
| 3308 | + // Svg::new("icons/hash.svg") | |
| 3309 | + // .with_color(theme.channel_hash.color) | |
| 3310 | + // .constrained() | |
| 3311 | + // .with_width(theme.channel_hash.width) | |
| 3312 | + // .aligned() | |
| 3313 | + // .left(), | |
| 3314 | + // ) | |
| 3315 | + // .with_child( | |
| 3316 | + // Label::new(channel.name.clone(), theme.channel_name.text.clone()) | |
| 3317 | + // .contained() | |
| 3318 | + // .with_style(theme.channel_name.container) | |
| 3319 | + // .aligned() | |
| 3320 | + // .left(), | |
| 3321 | + // ) | |
| 3322 | + // .align_children_center() | |
| 3323 | + // .contained() | |
| 3324 | + // .with_background_color( | |
| 3325 | + // theme | |
| 3326 | + // .container | |
| 3327 | + // .background_color | |
| 3328 | + // .unwrap_or(gpui::color::Color::transparent_black()), | |
| 3329 | + // ) | |
| 3330 | + // .contained() | |
| 3331 | + // .with_padding_left( | |
| 3332 | + // theme.channel_row.default_style().padding.left | |
| 3333 | + // + theme.channel_indent * depth as f32, | |
| 3334 | + // ) | |
| 3335 | + // .into_any() | |
| 3336 | + // }, | |
| 3337 | + // ) | |
| 3338 | + // .with_cursor_style(CursorStyle::PointingHand) | |
| 3339 | + // .into_any() | |
| 3340 | + } | |
| 3250 | 3341 | } |
| 3251 | 3342 | |
| 3252 | 3343 | // fn render_tree_branch( |
| @@ -3295,13 +3386,19 @@ impl CollabPanel { · 42 unmodified lines | ||
| 3295 | 3386 | // } |
| 3296 | 3387 | |
| 3297 | 3388 | impl Render for CollabPanel { |
| 3298 | − type Element = Focusable<Self, Div<Self>>; | |
| 3389 | + type Element = Focusable<Div>; | |
| 3299 | 3390 | |
| 3300 | − fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { | |
| 3391 | + fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { | |
| 3301 | 3392 | div() |
| 3302 | 3393 | .key_context("CollabPanel") |
| 3303 | 3394 | .track_focus(&self.focus_handle) |
| 3304 | − .child("COLLAB PANEL") | |
| 3395 | + .map(|el| { | |
| 3396 | + if self.user_store.read(cx).current_user().is_none() { | |
| 3397 | + el.child(self.render_signed_out(cx)) | |
| 3398 | + } else { | |
| 3399 | + el.child(self.render_signed_in(cx)) | |
| 3400 | + } | |
| 3401 | + }) | |
| 3305 | 3402 | } |
| 3306 | 3403 | } |
| 3307 | 3404 | |
| @@ -1,37 +1,34 @@ | ||
| 1 | 1 | use client::{ContactRequestStatus, User, UserStore}; |
| 2 | 2 | use gpui::{ |
| 3 | − elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle, | |
| 3 | + div, img, svg, AnyElement, AppContext, DismissEvent, Div, Entity, EventEmitter, FocusHandle, | |
| 4 | + FocusableView, Img, IntoElement, Model, ParentElement as _, Render, Styled, Task, View, | |
| 5 | + ViewContext, VisualContext, WeakView, | |
| 4 | 6 | }; |
| 5 | −use picker::{Picker, PickerDelegate, PickerEvent}; | |
| 7 | +use picker::{Picker, PickerDelegate}; | |
| 6 | 8 | use std::sync::Arc; |
| 7 | −use util::TryFutureExt; | |
| 8 | −use workspace::Modal; | |
| 9 | +use theme::ActiveTheme as _; | |
| 10 | +use ui::{h_stack, v_stack, Label}; | |
| 11 | +use util::{ResultExt as _, TryFutureExt}; | |
| 9 | 12 | |
| 10 | 13 | pub fn init(cx: &mut AppContext) { |
| 11 | − Picker::<ContactFinderDelegate>::init(cx); | |
| 12 | − cx.add_action(ContactFinder::dismiss) | |
| 14 | + //Picker::<ContactFinderDelegate>::init(cx); | |
| 15 | + //cx.add_action(ContactFinder::dismiss) | |
| 13 | 16 | } |
| 14 | 17 | |
| 15 | 18 | pub struct ContactFinder { |
| 16 | − picker: ViewHandle<Picker<ContactFinderDelegate>>, | |
| 19 | + picker: View<Picker<ContactFinderDelegate>>, | |
| 17 | 20 | has_focus: bool, |
| 18 | 21 | } |
| 19 | 22 | |
| 20 | 23 | impl ContactFinder { |
| 21 | − pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self { | |
| 22 | − let picker = cx.add_view(|cx| { | |
| 23 | − Picker::new( | |
| 24 | − ContactFinderDelegate { | |
| 25 | − user_store, | |
| 26 | − potential_contacts: Arc::from([]), | |
| 27 | − selected_index: 0, | |
| 28 | − }, | |
| 29 | − cx, | |
| 30 | − ) | |
| 31 | − .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone()) | |
| 32 | − }); | |
| 33 | − | |
| 34 | − cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach(); | |
| 24 | + pub fn new(user_store: Model<UserStore>, cx: &mut ViewContext<Self>) -> Self { | |
| 25 | + let delegate = ContactFinderDelegate { | |
| 26 | + parent: cx.view().downgrade(), | |
| 27 | + user_store, | |
| 28 | + potential_contacts: Arc::from([]), | |
| 29 | + selected_index: 0, | |
| 30 | + }; | |
| 31 | + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); | |
| 35 | 32 | |
| 36 | 33 | Self { |
| 37 | 34 | picker, |
| @@ -41,105 +38,72 @@ impl ContactFinder { · 3 unmodified lines | ||
| 41 | 38 | |
| 42 | 39 | pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) { |
| 43 | 40 | self.picker.update(cx, |picker, cx| { |
| 44 | − picker.set_query(query, cx); | |
| 41 | + // todo!() | |
| 42 | + // picker.set_query(query, cx); | |
| 45 | 43 | }); |
| 46 | 44 | } |
| 47 | − | |
| 48 | − fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) { | |
| 49 | − cx.emit(PickerEvent::Dismiss); | |
| 50 | − } | |
| 51 | −} | |
| 52 | − | |
| 53 | −impl Entity for ContactFinder { | |
| 54 | − type Event = PickerEvent; | |
| 55 | 45 | } |
| 56 | 46 | |
| 57 | −impl View for ContactFinder { | |
| 58 | − fn ui_name() -> &'static str { | |
| 59 | − "ContactFinder" | |
| 60 | − } | |
| 61 | − | |
| 62 | − fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { | |
| 63 | − let full_theme = &theme::current(cx); | |
| 64 | − let theme = &full_theme.collab_panel.tabbed_modal; | |
| 65 | − | |
| 66 | − fn render_mode_button( | |
| 67 | − text: &'static str, | |
| 68 | − theme: &theme::TabbedModal, | |
| 69 | − _cx: &mut ViewContext<ContactFinder>, | |
| 70 | − ) -> AnyElement<ContactFinder> { | |
| 71 | − let contained_text = &theme.tab_button.active_state().default; | |
| 72 | − Label::new(text, contained_text.text.clone()) | |
| 73 | − .contained() | |
| 74 | − .with_style(contained_text.container.clone()) | |
| 75 | − .into_any() | |
| 47 | +impl Render for ContactFinder { | |
| 48 | + fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { | |
| 49 | + fn render_mode_button(text: &'static str) -> AnyElement { | |
| 50 | + Label::new(text).into_any_element() | |
| 76 | 51 | } |
| 77 | 52 | |
| 78 | − Flex::column() | |
| 79 | − .with_child( | |
| 80 | − Flex::column() | |
| 81 | − .with_child( | |
| 82 | − Label::new("Contacts", theme.title.text.clone()) | |
| 83 | − .contained() | |
| 84 | − .with_style(theme.title.container.clone()), | |
| 85 | − ) | |
| 86 | − .with_child(Flex::row().with_children([render_mode_button( | |
| 87 | − "Invite new contacts", | |
| 88 | − &theme, | |
| 89 | − cx, | |
| 90 | − )])) | |
| 91 | − .expanded() | |
| 92 | − .contained() | |
| 93 | − .with_style(theme.header), | |
| 94 | − ) | |
| 95 | − .with_child( | |
| 96 | − ChildView::new(&self.picker, cx) | |
| 97 | − .contained() | |
| 98 | − .with_style(theme.body), | |
| 53 | + v_stack() | |
| 54 | + .child( | |
| 55 | + v_stack() | |
| 56 | + .child(Label::new("Contacts")) | |
| 57 | + .child(h_stack().children([render_mode_button("Invite new contacts")])) | |
| 58 | + .bg(cx.theme().colors().element_background), | |
| 99 | 59 | ) |
| 100 | − .constrained() | |
| 101 | − .with_max_height(theme.max_height) | |
| 102 | − .with_max_width(theme.max_width) | |
| 103 | − .contained() | |
| 104 | − .with_style(theme.modal) | |
| 105 | − .into_any() | |
| 60 | + .child(self.picker.clone()) | |
| 61 | + .w_96() | |
| 106 | 62 | } |
| 107 | 63 | |
| 108 | − fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) { | |
| 109 | − self.has_focus = true; | |
| 110 | − if cx.is_self_focused() { | |
| 111 | − cx.focus(&self.picker) | |
| 112 | − } | |
| 113 | − } | |
| 64 | + // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) { | |
| 65 | + // self.has_focus = true; | |
| 66 | + // if cx.is_self_focused() { | |
| 67 | + // cx.focus(&self.picker) | |
| 68 | + // } | |
| 69 | + // } | |
| 114 | 70 | |
| 115 | − fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) { | |
| 116 | − self.has_focus = false; | |
| 117 | − } | |
| 71 | + // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) { | |
| 72 | + // self.has_focus = false; | |
| 73 | + // } | |
| 74 | + | |
| 75 | + type Element = Div; | |
| 118 | 76 | } |
| 119 | 77 | |
| 120 | −impl Modal for ContactFinder { | |
| 121 | − fn has_focus(&self) -> bool { | |
| 122 | − self.has_focus | |
| 123 | − } | |
| 78 | +// impl Modal for ContactFinder { | |
| 79 | +// fn has_focus(&self) -> bool { | |
| 80 | +// self.has_focus | |
| 81 | +// } | |
| 124 | 82 | |
| 125 | − fn dismiss_on_event(event: &Self::Event) -> bool { | |
| 126 | − match event { | |
| 127 | − PickerEvent::Dismiss => true, | |
| 128 | − } | |
| 129 | − } | |
| 130 | −} | |
| 83 | +// fn dismiss_on_event(event: &Self::Event) -> bool { | |
| 84 | +// match event { | |
| 85 | +// PickerEvent::Dismiss => true, | |
| 86 | +// } | |
| 87 | +// } | |
| 88 | +// } | |
| 131 | 89 | |
| 132 | 90 | pub struct ContactFinderDelegate { |
| 91 | + parent: WeakView<ContactFinder>, | |
| 133 | 92 | potential_contacts: Arc<[Arc<User>]>, |
| 134 | − user_store: ModelHandle<UserStore>, | |
| 93 | + user_store: Model<UserStore>, | |
| 135 | 94 | selected_index: usize, |
| 136 | 95 | } |
| 137 | 96 | |
| 138 | −impl PickerDelegate for ContactFinderDelegate { | |
| 139 | − fn placeholder_text(&self) -> Arc<str> { | |
| 140 | − "Search collaborator by username...".into() | |
| 97 | +impl EventEmitter<DismissEvent> for ContactFinder {} | |
| 98 | + | |
| 99 | +impl FocusableView for ContactFinder { | |
| 100 | + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { | |
| 101 | + self.picker.focus_handle(cx) | |
| 141 | 102 | } |
| 103 | +} | |
| 142 | 104 | |
| 105 | +impl PickerDelegate for ContactFinderDelegate { | |
| 106 | + type ListItem = Div; | |
| 143 | 107 | fn match_count(&self) -> usize { |
| 144 | 108 | self.potential_contacts.len() |
| 145 | 109 | } |
| @@ -152,6 +116,10 @@ impl PickerDelegate for ContactFinderDelegate { · 6 unmodified lines | ||
| 152 | 116 | self.selected_index = ix; |
| 153 | 117 | } |
| 154 | 118 | |
| 119 | + fn placeholder_text(&self) -> Arc<str> { | |
| 120 | + "Search collaborator by username...".into() | |
| 121 | + } | |
| 122 | + | |
| 155 | 123 | fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> { |
| 156 | 124 | let search_users = self |
| 157 | 125 | .user_store |
| @@ -161,7 +129,7 @@ impl PickerDelegate for ContactFinderDelegate { · 3 unmodified lines | ||
| 161 | 129 | async { |
| 162 | 130 | let potential_contacts = search_users.await?; |
| 163 | 131 | picker.update(&mut cx, |picker, cx| { |
| 164 | − picker.delegate_mut().potential_contacts = potential_contacts.into(); | |
| 132 | + picker.delegate.potential_contacts = potential_contacts.into(); | |
| 165 | 133 | cx.notify(); |
| 166 | 134 | })?; |
| 167 | 135 | anyhow::Ok(()) |
| @@ -191,19 +159,18 @@ impl PickerDelegate for ContactFinderDelegate { · 23 unmodified lines | ||
| 191 | 159 | } |
| 192 | 160 | |
| 193 | 161 | fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) { |
| 194 | − cx.emit(PickerEvent::Dismiss); | |
| 162 | + //cx.emit(PickerEvent::Dismiss); | |
| 163 | + self.parent | |
| 164 | + .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) | |
| 165 | + .log_err(); | |
| 195 | 166 | } |
| 196 | 167 | |
| 197 | 168 | fn render_match( |
| 198 | 169 | &self, |
| 199 | 170 | ix: usize, |
| 200 | − mouse_state: &mut MouseState, | |
| 201 | 171 | selected: bool, |
| 202 | − cx: &gpui::AppContext, | |
| 203 | − ) -> AnyElement<Picker<Self>> { | |
| 204 | − let full_theme = &theme::current(cx); | |
| 205 | − let theme = &full_theme.collab_panel.contact_finder; | |
| 206 | − let tabbed_modal = &full_theme.collab_panel.tabbed_modal; | |
| 172 | + cx: &mut ViewContext<Picker<Self>>, | |
| 173 | + ) -> Option<Self::ListItem> { | |
| 207 | 174 | let user = &self.potential_contacts[ix]; |
| 208 | 175 | let request_status = self.user_store.read(cx).contact_request_status(user); |
| 209 | 176 | |
| @@ -214,48 +181,47 @@ impl PickerDelegate for ContactFinderDelegate { · 4 unmodified lines | ||
| 214 | 181 | ContactRequestStatus::RequestSent => Some("icons/x.svg"), |
| 215 | 182 | ContactRequestStatus::RequestAccepted => None, |
| 216 | 183 | }; |
| 217 | − let button_style = if self.user_store.read(cx).is_contact_request_pending(user) { | |
| 218 | − &theme.disabled_contact_button | |
| 219 | − } else { | |
| 220 | − &theme.contact_button | |
| 221 | − }; | |
| 222 | − let style = tabbed_modal | |
| 223 | − .picker | |
| 224 | − .item | |
| 225 | − .in_state(selected) | |
| 226 | − .style_for(mouse_state); | |
| 227 | − Flex::row() | |
| 228 | − .with_children(user.avatar.clone().map(|avatar| { | |
| 229 | − Image::from_data(avatar) | |
| 230 | − .with_style(theme.contact_avatar) | |
| 231 | − .aligned() | |
| 232 | − .left() | |
| 233 | − })) | |
| 234 | − .with_child( | |
| 235 | − Label::new(user.github_login.clone(), style.label.clone()) | |
| 236 | − .contained() | |
| 237 | − .with_style(theme.contact_username) | |
| 238 | − .aligned() | |
| 239 | − .left(), | |
| 240 | − ) | |
| 241 | − .with_children(icon_path.map(|icon_path| { | |
| 242 | − Svg::new(icon_path) | |
| 243 | − .with_color(button_style.color) | |
| 244 | − .constrained() | |
| 245 | − .with_width(button_style.icon_width) | |
| 246 | − .aligned() | |
| 247 | − .contained() | |
| 248 | − .with_style(button_style.container) | |
| 249 | − .constrained() | |
| 250 | − .with_width(button_style.button_width) | |
| 251 | − .with_height(button_style.button_width) | |
| 252 | − .aligned() | |
| 253 | − .flex_float() | |
| 254 | − })) | |
| 255 | − .contained() | |
| 256 | − .with_style(style.container) | |
| 257 | − .constrained() | |
| 258 | − .with_height(tabbed_modal.row_height) | |
| 259 | − .into_any() | |
| 184 | + dbg!(icon_path); | |
| 185 | + Some( | |
| 186 | + div() | |
| 187 | + .flex_1() | |
| 188 | + .justify_between() | |
| 189 | + .children(user.avatar.clone().map(|avatar| img().data(avatar))) | |
| 190 | + .child(Label::new(user.github_login.clone())) | |
| 191 | + .children(icon_path.map(|icon_path| svg().path(icon_path))), | |
| 192 | + ) | |
| 193 | + // Flex::row() | |
| 194 | + // .with_children(user.avatar.clone().map(|avatar| { | |
| 195 | + // Image::from_data(avatar) | |
| 196 | + // .with_style(theme.contact_avatar) | |
| 197 | + // .aligned() | |
| 198 | + // .left() | |
| 199 | + // })) | |
| 200 | + // .with_child( | |
| 201 | + // Label::new(user.github_login.clone(), style.label.clone()) | |
| 202 | + // .contained() | |
| 203 | + // .with_style(theme.contact_username) | |
| 204 | + // .aligned() | |
| 205 | + // .left(), | |
| 206 | + // ) | |
| 207 | + // .with_children(icon_path.map(|icon_path| { | |
| 208 | + // Svg::new(icon_path) | |
| 209 | + // .with_color(button_style.color) | |
| 210 | + // .constrained() | |
| 211 | + // .with_width(button_style.icon_width) | |
| 212 | + // .aligned() | |
| 213 | + // .contained() | |
| 214 | + // .with_style(button_style.container) | |
| 215 | + // .constrained() | |
| 216 | + // .with_width(button_style.button_width) | |
| 217 | + // .with_height(button_style.button_width) | |
| 218 | + // .aligned() | |
| 219 | + // .flex_float() | |
| 220 | + // })) | |
| 221 | + // .contained() | |
| 222 | + // .with_style(style.container) | |
| 223 | + // .constrained() | |
| 224 | + // .with_height(tabbed_modal.row_height) | |
| 225 | + // .into_any() | |
| 260 | 226 | } |
| 261 | 227 | } |
| @@ -31,14 +31,17 @@ use std::sync::Arc; · 30 unmodified lines | ||
| 31 | 31 | use call::ActiveCall; |
| 32 | 32 | use client::{Client, UserStore}; |
| 33 | 33 | use gpui::{ |
| 34 | − div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent, | |
| 35 | − Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext, | |
| 36 | − VisualContext, WeakView, WindowBounds, | |
| 34 | + div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton, | |
| 35 | + ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription, | |
| 36 | + ViewContext, VisualContext, WeakView, WindowBounds, | |
| 37 | 37 | }; |
| 38 | 38 | use project::Project; |
| 39 | 39 | use theme::ActiveTheme; |
| 40 | −use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, Tooltip}; | |
| 41 | −use workspace::Workspace; | |
| 40 | +use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip}; | |
| 41 | +use util::ResultExt; | |
| 42 | +use workspace::{notifications::NotifyResultExt, Workspace}; | |
| 43 | + | |
| 44 | +use crate::face_pile::FacePile; | |
| 42 | 45 | |
| 43 | 46 | // const MAX_PROJECT_NAME_LENGTH: usize = 40; |
| 44 | 47 | // const MAX_BRANCH_NAME_LENGTH: usize = 40; |
| @@ -82,9 +85,44 @@ pub struct CollabTitlebarItem { · 37 unmodified lines | ||
| 82 | 85 | } |
| 83 | 86 | |
| 84 | 87 | impl Render for CollabTitlebarItem { |
| 85 | − type Element = Stateful<Self, Div<Self>>; | |
| 88 | + type Element = Stateful<Div>; | |
| 86 | 89 | |
| 87 | 90 | fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { |
| 91 | + let is_in_room = self | |
| 92 | + .workspace | |
| 93 | + .update(cx, |this, cx| this.call_state().is_in_room(cx)) | |
| 94 | + .unwrap_or_default(); | |
| 95 | + let is_shared = is_in_room && self.project.read(cx).is_shared(); | |
| 96 | + let current_user = self.user_store.read(cx).current_user(); | |
| 97 | + let client = self.client.clone(); | |
| 98 | + let users = self | |
| 99 | + .workspace | |
| 100 | + .update(cx, |this, cx| this.call_state().remote_participants(cx)) | |
| 101 | + .log_err() | |
| 102 | + .flatten(); | |
| 103 | + let mic_icon = if self | |
| 104 | + .workspace | |
| 105 | + .update(cx, |this, cx| this.call_state().is_muted(cx)) | |
| 106 | + .log_err() | |
| 107 | + .flatten() | |
| 108 | + .unwrap_or_default() | |
| 109 | + { | |
| 110 | + ui::Icon::MicMute | |
| 111 | + } else { | |
| 112 | + ui::Icon::Mic | |
| 113 | + }; | |
| 114 | + let speakers_icon = if self | |
| 115 | + .workspace | |
| 116 | + .update(cx, |this, cx| this.call_state().is_deafened(cx)) | |
| 117 | + .log_err() | |
| 118 | + .flatten() | |
| 119 | + .unwrap_or_default() | |
| 120 | + { | |
| 121 | + ui::Icon::AudioOff | |
| 122 | + } else { | |
| 123 | + ui::Icon::AudioOn | |
| 124 | + }; | |
| 125 | + let workspace = self.workspace.clone(); | |
| 88 | 126 | h_stack() |
| 89 | 127 | .id("titlebar") |
| 90 | 128 | .justify_between() |
| @@ -100,7 +138,7 @@ impl Render for CollabTitlebarItem { · 9 unmodified lines | ||
| 100 | 138 | |s| s.pl(px(68.)), |
| 101 | 139 | ) |
| 102 | 140 | .bg(cx.theme().colors().title_bar_background) |
| 103 | − .on_click(|_, event, cx| { | |
| 141 | + .on_click(|event, cx| { | |
| 104 | 142 | if event.up.click_count == 2 { |
| 105 | 143 | cx.zoom_window(); |
| 106 | 144 | } |
| @@ -111,31 +149,37 @@ impl Render for CollabTitlebarItem { · 4 unmodified lines | ||
| 111 | 149 | // TODO - Add player menu |
| 112 | 150 | .child( |
| 113 | 151 | div() |
| 152 | + .border() | |
| 153 | + .border_color(gpui::red()) | |
| 114 | 154 | .id("project_owner_indicator") |
| 115 | 155 | .child( |
| 116 | 156 | Button::new("player") |
| 117 | 157 | .variant(ButtonVariant::Ghost) |
| 118 | − .color(Some(TextColor::Player(0))), | |
| 158 | + .color(Some(Color::Player(0))), | |
| 119 | 159 | ) |
| 120 | − .tooltip(move |_, cx| Tooltip::text("Toggle following", cx)), | |
| 160 | + .tooltip(move |cx| Tooltip::text("Toggle following", cx)), | |
| 121 | 161 | ) |
| 122 | 162 | // TODO - Add project menu |
| 123 | 163 | .child( |
| 124 | 164 | div() |
| 165 | + .border() | |
| 166 | + .border_color(gpui::red()) | |
| 125 | 167 | .id("titlebar_project_menu_button") |
| 126 | 168 | .child(Button::new("project_name").variant(ButtonVariant::Ghost)) |
| 127 | − .tooltip(move |_, cx| Tooltip::text("Recent Projects", cx)), | |
| 169 | + .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), | |
| 128 | 170 | ) |
| 129 | 171 | // TODO - Add git menu |
| 130 | 172 | .child( |
| 131 | 173 | div() |
| 174 | + .border() | |
| 175 | + .border_color(gpui::red()) | |
| 132 | 176 | .id("titlebar_git_menu_button") |
| 133 | 177 | .child( |
| 134 | 178 | Button::new("branch_name") |
| 135 | 179 | .variant(ButtonVariant::Ghost) |
| 136 | − .color(Some(TextColor::Muted)), | |
| 180 | + .color(Some(Color::Muted)), | |
| 137 | 181 | ) |
| 138 | − .tooltip(move |_, cx| { | |
| 182 | + .tooltip(move |cx| { | |
| 139 | 183 | cx.build_view(|_| { |
| 140 | 184 | Tooltip::new("Recent Branches") |
| 141 | 185 | .key_binding(KeyBinding::new(gpui::KeyBinding::new( |
| @@ -149,8 +193,113 @@ impl Render for CollabTitlebarItem { · 7 unmodified lines | ||
| 149 | 193 | .into() |
| 150 | 194 | }), |
| 151 | 195 | ), |
| 152 | − ) // self.titlebar_item | |
| 153 | − .child(h_stack().child(Label::new("Right side titlebar item"))) | |
| 196 | + ) | |
| 197 | + .when_some( | |
| 198 | + users.zip(current_user.clone()), | |
| 199 | + |this, (remote_participants, current_user)| { | |
| 200 | + let mut pile = FacePile::default(); | |
| 201 | + pile.extend( | |
| 202 | + current_user | |
| 203 | + .avatar | |
| 204 | + .clone() | |
| 205 | + .map(|avatar| { | |
| 206 | + div().child(Avatar::data(avatar.clone())).into_any_element() | |
| 207 | + }) | |
| 208 | + .into_iter() | |
| 209 | + .chain(remote_participants.into_iter().flat_map(|(user, peer_id)| { | |
| 210 | + user.avatar.as_ref().map(|avatar| { | |
| 211 | + div() | |
| 212 | + .child( | |
| 213 | + Avatar::data(avatar.clone()).into_element().into_any(), | |
| 214 | + ) | |
| 215 | + .on_mouse_down(MouseButton::Left, { | |
| 216 | + let workspace = workspace.clone(); | |
| 217 | + move |_, cx| { | |
| 218 | + workspace | |
| 219 | + .update(cx, |this, cx| { | |
| 220 | + this.open_shared_screen(peer_id, cx); | |
| 221 | + }) | |
| 222 | + .log_err(); | |
| 223 | + } | |
| 224 | + }) | |
| 225 | + .into_any_element() | |
| 226 | + }) | |
| 227 | + })), | |
| 228 | + ); | |
| 229 | + this.child(pile.render(cx)) | |
| 230 | + }, | |
| 231 | + ) | |
| 232 | + .child(div().flex_1()) | |
| 233 | + .when(is_in_room, |this| { | |
| 234 | + this.child( | |
| 235 | + h_stack() | |
| 236 | + .child( | |
| 237 | + h_stack() | |
| 238 | + .child(Button::new(if is_shared { "Unshare" } else { "Share" })) | |
| 239 | + .child(IconButton::new("leave-call", ui::Icon::Exit).on_click({ | |
| 240 | + let workspace = workspace.clone(); | |
| 241 | + move |_, cx| { | |
| 242 | + workspace | |
| 243 | + .update(cx, |this, cx| { | |
| 244 | + this.call_state().hang_up(cx).detach(); | |
| 245 | + }) | |
| 246 | + .log_err(); | |
| 247 | + } | |
| 248 | + })), | |
| 249 | + ) | |
| 250 | + .child( | |
| 251 | + h_stack() | |
| 252 | + .child(IconButton::new("mute-microphone", mic_icon).on_click({ | |
| 253 | + let workspace = workspace.clone(); | |
| 254 | + move |_, cx| { | |
| 255 | + workspace | |
| 256 | + .update(cx, |this, cx| { | |
| 257 | + this.call_state().toggle_mute(cx); | |
| 258 | + }) | |
| 259 | + .log_err(); | |
| 260 | + } | |
| 261 | + })) | |
| 262 | + .child(IconButton::new("mute-sound", speakers_icon).on_click({ | |
| 263 | + let workspace = workspace.clone(); | |
| 264 | + move |_, cx| { | |
| 265 | + workspace | |
| 266 | + .update(cx, |this, cx| { | |
| 267 | + this.call_state().toggle_deafen(cx); | |
| 268 | + }) | |
| 269 | + .log_err(); | |
| 270 | + } | |
| 271 | + })) | |
| 272 | + .child(IconButton::new("screen-share", ui::Icon::Screen).on_click( | |
| 273 | + move |_, cx| { | |
| 274 | + workspace | |
| 275 | + .update(cx, |this, cx| { | |
| 276 | + this.call_state().toggle_screen_share(cx); | |
| 277 | + }) | |
| 278 | + .log_err(); | |
| 279 | + }, | |
| 280 | + )) | |
| 281 | + .pl_2(), | |
| 282 | + ), | |
| 283 | + ) | |
| 284 | + }) | |
| 285 | + .map(|this| { | |
| 286 | + if let Some(user) = current_user { | |
| 287 | + this.when_some(user.avatar.clone(), |this, avatar| { | |
| 288 | + this.child(ui::Avatar::data(avatar)) | |
| 289 | + }) | |
| 290 | + } else { | |
| 291 | + this.child(Button::new("Sign in").on_click(move |_, cx| { | |
| 292 | + let client = client.clone(); | |
| 293 | + cx.spawn(move |mut cx| async move { | |
| 294 | + client | |
| 295 | + .authenticate_and_connect(true, &cx) | |
| 296 | + .await | |
| 297 | + .notify_async_err(&mut cx); | |
| 298 | + }) | |
| 299 | + .detach(); | |
| 300 | + })) | |
| 301 | + } | |
| 302 | + }) | |
| 154 | 303 | } |
| 155 | 304 | } |
| 156 | 305 | |
| @@ -7,11 +7,14 @@ pub mod notification_panel; · 6 unmodified lines | ||
| 7 | 7 | pub mod notifications; |
| 8 | 8 | mod panel_settings; |
| 9 | 9 | |
| 10 | −use std::sync::Arc; | |
| 10 | +use std::{rc::Rc, sync::Arc}; | |
| 11 | 11 | |
| 12 | 12 | pub use collab_panel::CollabPanel; |
| 13 | 13 | pub use collab_titlebar_item::CollabTitlebarItem; |
| 14 | −use gpui::AppContext; | |
| 14 | +use gpui::{ | |
| 15 | + point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind, | |
| 16 | + WindowOptions, | |
| 17 | +}; | |
| 15 | 18 | pub use panel_settings::{ |
| 16 | 19 | ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings, |
| 17 | 20 | }; |
| @@ -23,7 +26,7 @@ use workspace::AppState; · 5 unmodified lines | ||
| 23 | 26 | // [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall] |
| 24 | 27 | // ); |
| 25 | 28 | |
| 26 | −pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) { | |
| 29 | +pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { | |
| 27 | 30 | CollaborationPanelSettings::register(cx); |
| 28 | 31 | ChatPanelSettings::register(cx); |
| 29 | 32 | NotificationPanelSettings::register(cx); |
| @@ -32,7 +35,7 @@ pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) { · 2 unmodified lines | ||
| 32 | 35 | collab_titlebar_item::init(cx); |
| 33 | 36 | collab_panel::init(cx); |
| 34 | 37 | // chat_panel::init(cx); |
| 35 | − // notifications::init(&app_state, cx); | |
| 38 | + notifications::init(&app_state, cx); | |
| 36 | 39 | |
| 37 | 40 | // cx.add_global_action(toggle_screen_sharing); |
| 38 | 41 | // cx.add_global_action(toggle_mute); |
| @@ -95,31 +98,36 @@ pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) { · 56 unmodified lines | ||
| 95 | 98 | // } |
| 96 | 99 | // } |
| 97 | 100 | |
| 98 | −// fn notification_window_options( | |
| 99 | −// screen: Rc<dyn Screen>, | |
| 100 | −// window_size: Vector2F, | |
| 101 | −// ) -> WindowOptions<'static> { | |
| 102 | −// const NOTIFICATION_PADDING: f32 = 16.; | |
| 101 | +fn notification_window_options( | |
| 102 | + screen: Rc<dyn PlatformDisplay>, | |
| 103 | + window_size: Size<Pixels>, | |
| 104 | +) -> WindowOptions { | |
| 105 | + let notification_margin_width = GlobalPixels::from(16.); | |
| 106 | + let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.); | |
| 103 | 107 | |
| 104 | −// let screen_bounds = screen.content_bounds(); | |
| 105 | −// WindowOptions { | |
| 106 | −// bounds: WindowBounds::Fixed(RectF::new( | |
| 107 | −// screen_bounds.upper_right() | |
| 108 | −// + vec2f( | |
| 109 | −// -NOTIFICATION_PADDING - window_size.x(), | |
| 110 | −// NOTIFICATION_PADDING, | |
| 111 | −// ), | |
| 112 | −// window_size, | |
| 113 | −// )), | |
| 114 | −// titlebar: None, | |
| 115 | −// center: false, | |
| 116 | −// focus: false, | |
| 117 | −// show: true, | |
| 118 | −// kind: WindowKind::PopUp, | |
| 119 | −// is_movable: false, | |
| 120 | −// screen: Some(screen), | |
| 121 | −// } | |
| 122 | −// } | |
| 108 | + let screen_bounds = screen.bounds(); | |
| 109 | + let size: Size<GlobalPixels> = window_size.into(); | |
| 110 | + | |
| 111 | + // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument. | |
| 112 | + let bounds = gpui::Bounds::<GlobalPixels> { | |
| 113 | + origin: screen_bounds.upper_right() | |
| 114 | + - point( | |
| 115 | + size.width + notification_margin_width, | |
| 116 | + notification_margin_height, | |
| 117 | + ), | |
| 118 | + size: window_size.into(), | |
| 119 | + }; | |
| 120 | + WindowOptions { | |
| 121 | + bounds: WindowBounds::Fixed(bounds), | |
| 122 | + titlebar: None, | |
| 123 | + center: false, | |
| 124 | + focus: false, | |
| 125 | + show: true, | |
| 126 | + kind: WindowKind::PopUp, | |
| 127 | + is_movable: false, | |
| 128 | + display_id: Some(screen.id()), | |
| 129 | + } | |
| 130 | +} | |
| 123 | 131 | |
| 124 | 132 | // fn render_avatar<T: 'static>( |
| 125 | 133 | // avatar: Option<Arc<ImageData>>, |
| @@ -1,54 +1,48 @@ | ||
| 1 | −// use std::ops::Range; | |
| 1 | +use gpui::{ | |
| 2 | + div, AnyElement, Div, IntoElement as _, ParentElement as _, RenderOnce, Styled, WindowContext, | |
| 3 | +}; | |
| 2 | 4 | |
| 3 | −// use gpui::{ | |
| 4 | −// geometry::{ | |
| 5 | −// rect::RectF, | |
| 6 | −// vector::{vec2f, Vector2F}, | |
| 7 | −// }, | |
| 8 | −// json::ToJson, | |
| 9 | −// serde_json::{self, json}, | |
| 10 | −// AnyElement, Axis, Element, View, ViewContext, | |
| 11 | −// }; | |
| 5 | +#[derive(Default)] | |
| 6 | +pub(crate) struct FacePile { | |
| 7 | + faces: Vec<AnyElement>, | |
| 8 | +} | |
| 12 | 9 | |
| 13 | −// pub(crate) struct FacePile<V: View> { | |
| 14 | −// overlap: f32, | |
| 15 | −// faces: Vec<AnyElement<V>>, | |
| 16 | −// } | |
| 10 | +impl RenderOnce for FacePile { | |
| 11 | + type Rendered = Div; | |
| 17 | 12 | |
| 18 | −// impl<V: View> FacePile<V> { | |
| 19 | −// pub fn new(overlap: f32) -> Self { | |
| 20 | −// Self { | |
| 21 | −// overlap, | |
| 22 | −// faces: Vec::new(), | |
| 23 | −// } | |
| 24 | −// } | |
| 25 | −// } | |
| 13 | + fn render(self, _: &mut WindowContext) -> Self::Rendered { | |
| 14 | + let player_count = self.faces.len(); | |
| 15 | + let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| { | |
| 16 | + let isnt_last = ix < player_count - 1; | |
| 26 | 17 | |
| 27 | −// impl<V: View> Element<V> for FacePile<V> { | |
| 28 | −// type LayoutState = (); | |
| 29 | −// type PaintState = (); | |
| 18 | + div().when(isnt_last, |div| div.neg_mr_1()).child(player) | |
| 19 | + }); | |
| 20 | + div().p_1().flex().items_center().children(player_list) | |
| 21 | + } | |
| 22 | +} | |
| 30 | 23 | |
| 24 | +// impl Element for FacePile { | |
| 25 | +// type State = (); | |
| 31 | 26 | // fn layout( |
| 32 | 27 | // &mut self, |
| 33 | −// constraint: gpui::SizeConstraint, | |
| 34 | −// view: &mut V, | |
| 35 | −// cx: &mut ViewContext<V>, | |
| 36 | −// ) -> (Vector2F, Self::LayoutState) { | |
| 37 | −// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY); | |
| 38 | − | |
| 28 | +// state: Option<Self::State>, | |
| 29 | +// cx: &mut WindowContext, | |
| 30 | +// ) -> (LayoutId, Self::State) { | |
| 39 | 31 | // let mut width = 0.; |
| 40 | 32 | // let mut max_height = 0.; |
| 33 | +// let mut faces = Vec::with_capacity(self.faces.len()); | |
| 41 | 34 | // for face in &mut self.faces { |
| 42 | −// let layout = face.layout(constraint, view, cx); | |
| 35 | +// let layout = face.layout(cx); | |
| 43 | 36 | // width += layout.x(); |
| 44 | 37 | // max_height = f32::max(max_height, layout.y()); |
| 38 | +// faces.push(layout); | |
| 45 | 39 | // } |
| 46 | 40 | // width -= self.overlap * self.faces.len().saturating_sub(1) as f32; |
| 47 | − | |
| 48 | −// ( | |
| 49 | −// Vector2F::new(width, max_height.clamp(1., constraint.max.y())), | |
| 50 | −// (), | |
| 51 | −// ) | |
| 41 | +// (cx.request_layout(&Style::default(), faces), ()) | |
| 42 | +// // ( | |
| 43 | +// // Vector2F::new(width, max_height.clamp(1., constraint.max.y())), | |
| 44 | +// // (), | |
| 45 | +// // )) | |
| 52 | 46 | // } |
| 53 | 47 | |
| 54 | 48 | // fn paint( |
| @@ -77,37 +71,10 @@ · 22 unmodified lines | ||
| 77 | 71 | |
| 78 | 72 | // () |
| 79 | 73 | // } |
| 80 | − | |
| 81 | −// fn rect_for_text_range( | |
| 82 | −// &self, | |
| 83 | −// _: Range<usize>, | |
| 84 | −// _: RectF, | |
| 85 | −// _: RectF, | |
| 86 | −// _: &Self::LayoutState, | |
| 87 | −// _: &Self::PaintState, | |
| 88 | −// _: &V, | |
| 89 | −// _: &ViewContext<V>, | |
| 90 | −// ) -> Option<RectF> { | |
| 91 | −// None | |
| 92 | −// } | |
| 93 | − | |
| 94 | −// fn debug( | |
| 95 | −// &self, | |
| 96 | −// bounds: RectF, | |
| 97 | −// _: &Self::LayoutState, | |
| 98 | −// _: &Self::PaintState, | |
| 99 | −// _: &V, | |
| 100 | −// _: &ViewContext<V>, | |
| 101 | −// ) -> serde_json::Value { | |
| 102 | −// json!({ | |
| 103 | −// "type": "FacePile", | |
| 104 | −// "bounds": bounds.to_json() | |
| 105 | −// }) | |
| 106 | −// } | |
| 107 | 74 | // } |
| 108 | 75 | |
| 109 | −// impl<V: View> Extend<AnyElement<V>> for FacePile<V> { | |
| 110 | −// fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) { | |
| 111 | −// self.faces.extend(children); | |
| 112 | −// } | |
| 113 | −// } | |
| 76 | +impl Extend<AnyElement> for FacePile { | |
| 77 | + fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) { | |
| 78 | + self.faces.extend(children); | |
| 79 | + } | |
| 80 | +} | |
| @@ -1,11 +1,11 @@ | ||
| 1 | −// use gpui::AppContext; | |
| 2 | −// use std::sync::Arc; | |
| 3 | −// use workspace::AppState; | |
| 1 | +use gpui::AppContext; | |
| 2 | +use std::sync::Arc; | |
| 3 | +use workspace::AppState; | |
| 4 | 4 | |
| 5 | −// pub mod incoming_call_notification; | |
| 5 | +pub mod incoming_call_notification; | |
| 6 | 6 | // pub mod project_shared_notification; |
| 7 | 7 | |
| 8 | −// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { | |
| 9 | −// incoming_call_notification::init(app_state, cx); | |
| 10 | −// project_shared_notification::init(app_state, cx); | |
| 11 | −// } | |
| 8 | +pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { | |
| 9 | + incoming_call_notification::init(app_state, cx); | |
| 10 | + //project_shared_notification::init(app_state, cx); | |
| 11 | +} | |
| @@ -1,14 +1,12 @@ | ||
| 1 | 1 | use crate::notification_window_options; |
| 2 | 2 | use call::{ActiveCall, IncomingCall}; |
| 3 | −use client::proto; | |
| 4 | 3 | use futures::StreamExt; |
| 5 | 4 | use gpui::{ |
| 6 | − elements::*, | |
| 7 | − geometry::vector::vec2f, | |
| 8 | − platform::{CursorStyle, MouseButton}, | |
| 9 | − AnyElement, AppContext, Entity, View, ViewContext, WindowHandle, | |
| 5 | + div, green, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, | |
| 6 | + StatefulInteractiveElement, Styled, ViewContext, VisualContext as _, WindowHandle, | |
| 10 | 7 | }; |
| 11 | 8 | use std::sync::{Arc, Weak}; |
| 9 | +use ui::{h_stack, v_stack, Avatar, Button, Label}; | |
| 12 | 10 | use util::ResultExt; |
| 13 | 11 | use workspace::AppState; |
| 14 | 12 | |
| @@ -19,23 +17,44 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { · 4 unmodified lines | ||
| 19 | 17 | let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new(); |
| 20 | 18 | while let Some(incoming_call) = incoming_call.next().await { |
| 21 | 19 | for window in notification_windows.drain(..) { |
| 22 | − window.remove(&mut cx); | |
| 20 | + window | |
| 21 | + .update(&mut cx, |_, cx| { | |
| 22 | + // todo!() | |
| 23 | + cx.remove_window(); | |
| 24 | + }) | |
| 25 | + .log_err(); | |
| 23 | 26 | } |
| 24 | 27 | |
| 25 | 28 | if let Some(incoming_call) = incoming_call { |
| 26 | − let window_size = cx.read(|cx| { | |
| 27 | − let theme = &theme::current(cx).incoming_call_notification; | |
| 28 | − vec2f(theme.window_width, theme.window_height) | |
| 29 | − }); | |
| 29 | + let unique_screens = cx.update(|cx| cx.displays()).unwrap(); | |
| 30 | + let window_size = gpui::Size { | |
| 31 | + width: px(380.), | |
| 32 | + height: px(64.), | |
| 33 | + }; | |
| 30 | 34 | |
| 31 | − for screen in cx.platform().screens() { | |
| 35 | + for window in unique_screens { | |
| 36 | + let options = notification_window_options(window, window_size); | |
| 32 | 37 | let window = cx |
| 33 | − .add_window(notification_window_options(screen, window_size), |_| { | |
| 34 | − IncomingCallNotification::new(incoming_call.clone(), app_state.clone()) | |
| 35 | − }); | |
| 36 | − | |
| 38 | + .open_window(options, |cx| { | |
| 39 | + cx.build_view(|_| { | |
| 40 | + IncomingCallNotification::new( | |
| 41 | + incoming_call.clone(), | |
| 42 | + app_state.clone(), | |
| 43 | + ) | |
| 44 | + }) | |
| 45 | + }) | |
| 46 | + .unwrap(); | |
| 37 | 47 | notification_windows.push(window); |
| 38 | 48 | } |
| 49 | + | |
| 50 | + // for screen in cx.platform().screens() { | |
| 51 | + // let window = cx | |
| 52 | + // .add_window(notification_window_options(screen, window_size), |_| { | |
| 53 | + // IncomingCallNotification::new(incoming_call.clone(), app_state.clone()) | |
| 54 | + // }); | |
| 55 | + | |
| 56 | + // notification_windows.push(window); | |
| 57 | + // } | |
| 39 | 58 | } |
| 40 | 59 | } |
| 41 | 60 | }) |
| @@ -47,167 +66,196 @@ struct RespondToCall { · 5 unmodified lines | ||
| 47 | 66 | accept: bool, |
| 48 | 67 | } |
| 49 | 68 | |
| 50 | −pub struct IncomingCallNotification { | |
| 69 | +struct IncomingCallNotificationState { | |
| 51 | 70 | call: IncomingCall, |
| 52 | 71 | app_state: Weak<AppState>, |
| 53 | 72 | } |
| 54 | 73 | |
| 55 | −impl IncomingCallNotification { | |
| 74 | +pub struct IncomingCallNotification { | |
| 75 | + state: Arc<IncomingCallNotificationState>, | |
| 76 | +} | |
| 77 | +impl IncomingCallNotificationState { | |
| 56 | 78 | pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self { |
| 57 | 79 | Self { call, app_state } |
| 58 | 80 | } |
| 59 | 81 | |
| 60 | − fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) { | |
| 82 | + fn respond(&self, accept: bool, cx: &mut AppContext) { | |
| 61 | 83 | let active_call = ActiveCall::global(cx); |
| 62 | 84 | if accept { |
| 63 | 85 | let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx)); |
| 64 | − let caller_user_id = self.call.calling_user.id; | |
| 65 | 86 | let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id); |
| 66 | 87 | let app_state = self.app_state.clone(); |
| 67 | − cx.app_context() | |
| 68 | − .spawn(|mut cx| async move { | |
| 69 | − join.await?; | |
| 70 | − if let Some(project_id) = initial_project_id { | |
| 71 | − cx.update(|cx| { | |
| 72 | − if let Some(app_state) = app_state.upgrade() { | |
| 73 | − workspace::join_remote_project( | |
| 74 | − project_id, | |
| 75 | − caller_user_id, | |
| 76 | − app_state, | |
| 77 | − cx, | |
| 78 | − ) | |
| 79 | − .detach_and_log_err(cx); | |
| 80 | − } | |
| 81 | − }); | |
| 82 | − } | |
| 83 | − anyhow::Ok(()) | |
| 84 | − }) | |
| 85 | − .detach_and_log_err(cx); | |
| 88 | + let cx: &mut AppContext = cx; | |
| 89 | + cx.spawn(|cx| async move { | |
| 90 | + join.await?; | |
| 91 | + if let Some(_project_id) = initial_project_id { | |
| 92 | + cx.update(|_cx| { | |
| 93 | + if let Some(_app_state) = app_state.upgrade() { | |
| 94 | + // workspace::join_remote_project( | |
| 95 | + // project_id, | |
| 96 | + // caller_user_id, | |
| 97 | + // app_state, | |
| 98 | + // cx, | |
| 99 | + // ) | |
| 100 | + // .detach_and_log_err(cx); | |
| 101 | + } | |
| 102 | + }) | |
| 103 | + .log_err(); | |
| 104 | + } | |
| 105 | + anyhow::Ok(()) | |
| 106 | + }) | |
| 107 | + .detach_and_log_err(cx); | |
| 86 | 108 | } else { |
| 87 | 109 | active_call.update(cx, |active_call, cx| { |
| 88 | 110 | active_call.decline_incoming(cx).log_err(); |
| 89 | 111 | }); |
| 90 | 112 | } |
| 91 | 113 | } |
| 114 | +} | |
| 92 | 115 | |
| 93 | − fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { | |
| 94 | − let theme = &theme::current(cx).incoming_call_notification; | |
| 95 | − let default_project = proto::ParticipantProject::default(); | |
| 96 | − let initial_project = self | |
| 97 | − .call | |
| 98 | − .initial_project | |
| 99 | − .as_ref() | |
| 100 | − .unwrap_or(&default_project); | |
| 101 | − Flex::row() | |
| 102 | − .with_children(self.call.calling_user.avatar.clone().map(|avatar| { | |
| 103 | − Image::from_data(avatar) | |
| 104 | − .with_style(theme.caller_avatar) | |
| 105 | − .aligned() | |
| 106 | − })) | |
| 107 | − .with_child( | |
| 108 | − Flex::column() | |
| 109 | − .with_child( | |
| 110 | − Label::new( | |
| 111 | − self.call.calling_user.github_login.clone(), | |
| 112 | − theme.caller_username.text.clone(), | |
| 113 | − ) | |
| 114 | − .contained() | |
| 115 | − .with_style(theme.caller_username.container), | |
| 116 | − ) | |
| 117 | − .with_child( | |
| 118 | − Label::new( | |
| 119 | − format!( | |
| 120 | − "is sharing a project in Zed{}", | |
| 121 | − if initial_project.worktree_root_names.is_empty() { | |
| 122 | − "" | |
| 123 | − } else { | |
| 124 | − ":" | |
| 125 | − } | |
| 126 | − ), | |
| 127 | − theme.caller_message.text.clone(), | |
| 128 | − ) | |
| 129 | − .contained() | |
| 130 | − .with_style(theme.caller_message.container), | |
| 131 | − ) | |
| 132 | − .with_children(if initial_project.worktree_root_names.is_empty() { | |
| 133 | − None | |
| 134 | − } else { | |
| 135 | − Some( | |
| 136 | − Label::new( | |
| 137 | − initial_project.worktree_root_names.join(", "), | |
| 138 | − theme.worktree_roots.text.clone(), | |
| 139 | − ) | |
| 140 | − .contained() | |
| 141 | − .with_style(theme.worktree_roots.container), | |
| 142 | − ) | |
| 143 | − }) | |
| 144 | − .contained() | |
| 145 | − .with_style(theme.caller_metadata) | |
| 146 | − .aligned(), | |
| 147 | − ) | |
| 148 | − .contained() | |
| 149 | − .with_style(theme.caller_container) | |
| 150 | − .flex(1., true) | |
| 151 | − .into_any() | |
| 116 | +impl IncomingCallNotification { | |
| 117 | + pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self { | |
| 118 | + Self { | |
| 119 | + state: Arc::new(IncomingCallNotificationState::new(call, app_state)), | |
| 120 | + } | |
| 152 | 121 | } |
| 153 | − | |
| 154 | − fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { | |
| 155 | − enum Accept {} | |
| 156 | − enum Decline {} | |
| 157 | − | |
| 158 | − let theme = theme::current(cx); | |
| 159 | − Flex::column() | |
| 160 | − .with_child( | |
| 161 | − MouseEventHandler::new::<Accept, _>(0, cx, |_, _| { | |
| 162 | − let theme = &theme.incoming_call_notification; | |
| 163 | − Label::new("Accept", theme.accept_button.text.clone()) | |
| 164 | − .aligned() | |
| 165 | − .contained() | |
| 166 | − .with_style(theme.accept_button.container) | |
| 167 | − }) | |
| 168 | − .with_cursor_style(CursorStyle::PointingHand) | |
| 169 | − .on_click(MouseButton::Left, |_, this, cx| { | |
| 170 | − this.respond(true, cx); | |
| 171 | − }) | |
| 172 | − .flex(1., true), | |
| 122 | + fn render_caller(&self, cx: &mut ViewContext<Self>) -> impl Element { | |
| 123 | + h_stack() | |
| 124 | + .children( | |
| 125 | + self.state | |
| 126 | + .call | |
| 127 | + .calling_user | |
| 128 | + .avatar | |
| 129 | + .as_ref() | |
| 130 | + .map(|avatar| Avatar::data(avatar.clone())), | |
| 173 | 131 | ) |
| 174 | − .with_child( | |
| 175 | − MouseEventHandler::new::<Decline, _>(0, cx, |_, _| { | |
| 176 | − let theme = &theme.incoming_call_notification; | |
| 177 | − Label::new("Decline", theme.decline_button.text.clone()) | |
| 178 | − .aligned() | |
| 179 | − .contained() | |
| 180 | − .with_style(theme.decline_button.container) | |
| 181 | − }) | |
| 182 | − .with_cursor_style(CursorStyle::PointingHand) | |
| 183 | − .on_click(MouseButton::Left, |_, this, cx| { | |
| 184 | − this.respond(false, cx); | |
| 185 | − }) | |
| 186 | − .flex(1., true), | |
| 132 | + .child( | |
| 133 | + v_stack() | |
| 134 | + .child(Label::new(format!( | |
| 135 | + "{} is sharing a project in Zed", | |
| 136 | + self.state.call.calling_user.github_login | |
| 137 | + ))) | |
| 138 | + .child(self.render_buttons(cx)), | |
| 187 | 139 | ) |
| 188 | − .constrained() | |
| 189 | − .with_width(theme.incoming_call_notification.button_width) | |
| 190 | − .into_any() | |
| 140 | + // let theme = &theme::current(cx).incoming_call_notification; | |
| 141 | + // let default_project = proto::ParticipantProject::default(); | |
| 142 | + // let initial_project = self | |
| 143 | + // .call | |
| 144 | + // .initial_project | |
| 145 | + // .as_ref() | |
| 146 | + // .unwrap_or(&default_project); | |
| 147 | + // Flex::row() | |
| 148 | + // .with_children(self.call.calling_user.avatar.clone().map(|avatar| { | |
| 149 | + // Image::from_data(avatar) | |
| 150 | + // .with_style(theme.caller_avatar) | |
| 151 | + // .aligned() | |
| 152 | + // })) | |
| 153 | + // .with_child( | |
| 154 | + // Flex::column() | |
| 155 | + // .with_child( | |
| 156 | + // Label::new( | |
| 157 | + // self.call.calling_user.github_login.clone(), | |
| 158 | + // theme.caller_username.text.clone(), | |
| 159 | + // ) | |
| 160 | + // .contained() | |
| 161 | + // .with_style(theme.caller_username.container), | |
| 162 | + // ) | |
| 163 | + // .with_child( | |
| 164 | + // Label::new( | |
| 165 | + // format!( | |
| 166 | + // "is sharing a project in Zed{}", | |
| 167 | + // if initial_project.worktree_root_names.is_empty() { | |
| 168 | + // "" | |
| 169 | + // } else { | |
| 170 | + // ":" | |
| 171 | + // } | |
| 172 | + // ), | |
| 173 | + // theme.caller_message.text.clone(), | |
| 174 | + // ) | |
| 175 | + // .contained() | |
| 176 | + // .with_style(theme.caller_message.container), | |
| 177 | + // ) | |
| 178 | + // .with_children(if initial_project.worktree_root_names.is_empty() { | |
| 179 | + // None | |
| 180 | + // } else { | |
| 181 | + // Some( | |
| 182 | + // Label::new( | |
| 183 | + // initial_project.worktree_root_names.join(", "), | |
| 184 | + // theme.worktree_roots.text.clone(), | |
| 185 | + // ) | |
| 186 | + // .contained() | |
| 187 | + // .with_style(theme.worktree_roots.container), | |
| 188 | + // ) | |
| 189 | + // }) | |
| 190 | + // .contained() | |
| 191 | + // .with_style(theme.caller_metadata) | |
| 192 | + // .aligned(), | |
| 193 | + // ) | |
| 194 | + // .contained() | |
| 195 | + // .with_style(theme.caller_container) | |
| 196 | + // .flex(1., true) | |
| 197 | + // .into_any() | |
| 191 | 198 | } |
| 192 | −} | |
| 193 | 199 | |
| 194 | −impl Entity for IncomingCallNotification { | |
| 195 | − type Event = (); | |
| 196 | −} | |
| 200 | + fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element { | |
| 201 | + h_stack() | |
| 202 | + .child(Button::new("Accept").render(cx).bg(green()).on_click({ | |
| 203 | + let state = self.state.clone(); | |
| 204 | + move |_, cx| state.respond(true, cx) | |
| 205 | + })) | |
| 206 | + .child(Button::new("Decline").render(cx).bg(red()).on_click({ | |
| 207 | + let state = self.state.clone(); | |
| 208 | + move |_, cx| state.respond(false, cx) | |
| 209 | + })) | |
| 197 | 210 | |
| 198 | −impl View for IncomingCallNotification { | |
| 199 | − fn ui_name() -> &'static str { | |
| 200 | − "IncomingCallNotification" | |
| 201 | − } | |
| 211 | + // enum Accept {} | |
| 212 | + // enum Decline {} | |
| 202 | 213 | |
| 203 | − fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { | |
| 204 | − let background = theme::current(cx).incoming_call_notification.background; | |
| 205 | − Flex::row() | |
| 206 | − .with_child(self.render_caller(cx)) | |
| 207 | − .with_child(self.render_buttons(cx)) | |
| 208 | − .contained() | |
| 209 | − .with_background_color(background) | |
| 210 | − .expanded() | |
| 211 | − .into_any() | |
| 214 | + // let theme = theme::current(cx); | |
| 215 | + // Flex::column() | |
| 216 | + // .with_child( | |
| 217 | + // MouseEventHandler::new::<Accept, _>(0, cx, |_, _| { | |
| 218 | + // let theme = &theme.incoming_call_notification; | |
| 219 | + // Label::new("Accept", theme.accept_button.text.clone()) | |
| 220 | + // .aligned() | |
| 221 | + // .contained() | |
| 222 | + // .with_style(theme.accept_button.container) | |
| 223 | + // }) | |
| 224 | + // .with_cursor_style(CursorStyle::PointingHand) | |
| 225 | + // .on_click(MouseButton::Left, |_, this, cx| { | |
| 226 | + // this.respond(true, cx); | |
| 227 | + // }) | |
| 228 | + // .flex(1., true), | |
| 229 | + // ) | |
| 230 | + // .with_child( | |
| 231 | + // MouseEventHandler::new::<Decline, _>(0, cx, |_, _| { | |
| 232 | + // let theme = &theme.incoming_call_notification; | |
| 233 | + // Label::new("Decline", theme.decline_button.text.clone()) | |
| 234 | + // .aligned() | |
| 235 | + // .contained() | |
| 236 | + // .with_style(theme.decline_button.container) | |
| 237 | + // }) | |
| 238 | + // .with_cursor_style(CursorStyle::PointingHand) | |
| 239 | + // .on_click(MouseButton::Left, |_, this, cx| { | |
| 240 | + // this.respond(false, cx); | |
| 241 | + // }) | |
| 242 | + // .flex(1., true), | |
| 243 | + // ) | |
| 244 | + // .constrained() | |
| 245 | + // .with_width(theme.incoming_call_notification.button_width) | |
| 246 | + // .into_any() | |
| 247 | + } | |
| 248 | +} | |
| 249 | +impl Render for IncomingCallNotification { | |
| 250 | + type Element = Div; | |
| 251 | + fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { | |
| 252 | + div().bg(red()).flex_none().child(self.render_caller(cx)) | |
| 253 | + // Flex::row() | |
| 254 | + // .with_child() | |
| 255 | + // .with_child(self.render_buttons(cx)) | |
| 256 | + // .contained() | |
| 257 | + // .with_background_color(background) | |
| 258 | + // .expanded() | |
| 259 | + // .into_any() | |
| 212 | 260 | } |
| 213 | 261 | } |
| @@ -1,17 +1,15 @@ | ||
| 1 | 1 | use collections::{CommandPaletteFilter, HashMap}; |
| 2 | 2 | use fuzzy::{StringMatch, StringMatchCandidate}; |
| 3 | 3 | use gpui::{ |
| 4 | − actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle, | |
| 5 | − FocusableView, Keystroke, Manager, ParentComponent, Render, Styled, View, ViewContext, | |
| 6 | − VisualContext, WeakView, | |
| 4 | + actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, | |
| 5 | + Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, | |
| 7 | 6 | }; |
| 8 | 7 | use picker::{Picker, PickerDelegate}; |
| 9 | 8 | use std::{ |
| 10 | 9 | cmp::{self, Reverse}, |
| 11 | 10 | sync::Arc, |
| 12 | 11 | }; |
| 13 | −use theme::ActiveTheme; | |
| 14 | −use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt}; | |
| 12 | +use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem}; | |
| 15 | 13 | use util::{ |
| 16 | 14 | channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, |
| 17 | 15 | ResultExt, |
| @@ -69,7 +67,7 @@ impl CommandPalette { · 51 unmodified lines | ||
| 69 | 67 | } |
| 70 | 68 | } |
| 71 | 69 | |
| 72 | −impl EventEmitter<Manager> for CommandPalette {} | |
| 70 | +impl EventEmitter<DismissEvent> for CommandPalette {} | |
| 73 | 71 | |
| 74 | 72 | impl FocusableView for CommandPalette { |
| 75 | 73 | fn focus_handle(&self, cx: &AppContext) -> FocusHandle { |
| @@ -78,7 +76,7 @@ impl FocusableView for CommandPalette { · 2 unmodified lines | ||
| 78 | 76 | } |
| 79 | 77 | |
| 80 | 78 | impl Render for CommandPalette { |
| 81 | − type Element = Div<Self>; | |
| 79 | + type Element = Div; | |
| 82 | 80 | |
| 83 | 81 | fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { |
| 84 | 82 | v_stack().w_96().child(self.picker.clone()) |
| @@ -141,7 +139,7 @@ impl CommandPaletteDelegate { · 56 unmodified lines | ||
| 141 | 139 | } |
| 142 | 140 | |
| 143 | 141 | impl PickerDelegate for CommandPaletteDelegate { |
| 144 | − type ListItem = Div<Picker<Self>>; | |
| 142 | + type ListItem = ListItem; | |
| 145 | 143 | |
| 146 | 144 | fn placeholder_text(&self) -> Arc<str> { |
| 147 | 145 | "Execute a command...".into() |
| @@ -269,7 +267,7 @@ impl PickerDelegate for CommandPaletteDelegate { · 121 unmodified lines | ||
| 269 | 267 | |
| 270 | 268 | fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) { |
| 271 | 269 | self.command_palette |
| 272 | − .update(cx, |_, cx| cx.emit(Manager::Dismiss)) | |
| 270 | + .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) | |
| 273 | 271 | .log_err(); |
| 274 | 272 | } |
| 275 | 273 | |
| @@ -294,24 +292,16 @@ impl PickerDelegate for CommandPaletteDelegate { · 18 unmodified lines | ||
| 294 | 292 | ix: usize, |
| 295 | 293 | selected: bool, |
| 296 | 294 | cx: &mut ViewContext<Picker<Self>>, |
| 297 | − ) -> Self::ListItem { | |
| 298 | − let colors = cx.theme().colors(); | |
| 295 | + ) -> Option<Self::ListItem> { | |
| 299 | 296 | let Some(r#match) = self.matches.get(ix) else { |
| 300 | − return div(); | |
| 297 | + return None; | |
| 301 | 298 | }; |
| 302 | 299 | let Some(command) = self.commands.get(r#match.candidate_id) else { |
| 303 | − return div(); | |
| 300 | + return None; | |
| 304 | 301 | }; |
| 305 | 302 | |
| 306 | − div() | |
| 307 | − .px_1() | |
| 308 | − .text_color(colors.text) | |
| 309 | − .text_ui() | |
| 310 | − .bg(colors.ghost_element_background) | |
| 311 | − .rounded_md() | |
| 312 | − .when(selected, |this| this.bg(colors.ghost_element_selected)) | |
| 313 | − .hover(|this| this.bg(colors.ghost_element_hover)) | |
| 314 | − .child( | |
| 303 | + Some( | |
| 304 | + ListItem::new(ix).selected(selected).child( | |
| 315 | 305 | h_stack() |
| 316 | 306 | .justify_between() |
| 317 | 307 | .child(HighlightedLabel::new( |
| @@ -319,7 +309,8 @@ impl PickerDelegate for CommandPaletteDelegate { · 1 unmodified line | ||
| 319 | 309 | r#match.positions.clone(), |
| 320 | 310 | )) |
| 321 | 311 | .children(KeyBinding::for_action(&*command.action, cx)), |
| 322 | − ) | |
| 312 | + ), | |
| 313 | + ) | |
| 323 | 314 | } |
| 324 | 315 | } |
| 325 | 316 | |
| @@ -13,10 +13,10 @@ use editor::{ · 12 unmodified lines | ||
| 13 | 13 | }; |
| 14 | 14 | use futures::future::try_join_all; |
| 15 | 15 | use gpui::{ |
| 16 | − actions, div, AnyElement, AnyView, AppContext, Component, Context, Div, EventEmitter, | |
| 17 | − FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InteractiveComponent, | |
| 18 | − Model, ParentComponent, Render, SharedString, Styled, Subscription, Task, View, ViewContext, | |
| 19 | − VisualContext, WeakView, | |
| 16 | + actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent, | |
| 17 | + FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, IntoElement, | |
| 18 | + Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext, | |
| 19 | + VisualContext, WeakView, WindowContext, | |
| 20 | 20 | }; |
| 21 | 21 | use language::{ |
| 22 | 22 | Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, |
| @@ -36,7 +36,7 @@ use std::{ · 13 unmodified lines | ||
| 36 | 36 | }; |
| 37 | 37 | use theme::ActiveTheme; |
| 38 | 38 | pub use toolbar_controls::ToolbarControls; |
| 39 | −use ui::{h_stack, HighlightedLabel, Icon, IconElement, Label, TextColor}; | |
| 39 | +use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label}; | |
| 40 | 40 | use util::TryFutureExt; |
| 41 | 41 | use workspace::{ |
| 42 | 42 | item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, |
| @@ -91,7 +91,7 @@ struct DiagnosticGroupState { · 48 unmodified lines | ||
| 91 | 91 | impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {} |
| 92 | 92 | |
| 93 | 93 | impl Render for ProjectDiagnosticsEditor { |
| 94 | − type Element = Focusable<Self, Div<Self>>; | |
| 94 | + type Element = Focusable<Div>; | |
| 95 | 95 | |
| 96 | 96 | fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { |
| 97 | 97 | let child = if self.path_states.is_empty() { |
| @@ -109,8 +109,8 @@ impl Render for ProjectDiagnosticsEditor { · 11 unmodified lines | ||
| 109 | 109 | div() |
| 110 | 110 | .track_focus(&self.focus_handle) |
| 111 | 111 | .size_full() |
| 112 | − .on_focus_in(Self::focus_in) | |
| 113 | − .on_action(Self::toggle_warnings) | |
| 112 | + .on_focus_in(cx.listener(Self::focus_in)) | |
| 113 | + .on_action(cx.listener(Self::toggle_warnings)) | |
| 114 | 114 | .child(child) |
| 115 | 115 | } |
| 116 | 116 | } |
| @@ -662,7 +662,7 @@ impl Item for ProjectDiagnosticsEditor { · 545 unmodified lines | ||
| 662 | 662 | Some("Project Diagnostics".into()) |
| 663 | 663 | } |
| 664 | 664 | |
| 665 | − fn tab_content<T: 'static>(&self, _detail: Option<usize>, _: &AppContext) -> AnyElement<T> { | |
| 665 | + fn tab_content(&self, _detail: Option<usize>, _: &WindowContext) -> AnyElement { | |
| 666 | 666 | render_summary(&self.summary) |
| 667 | 667 | } |
| 668 | 668 | |
| @@ -742,7 +742,7 @@ impl Item for ProjectDiagnosticsEditor { · 73 unmodified lines | ||
| 742 | 742 | } |
| 743 | 743 | |
| 744 | 744 | fn breadcrumb_location(&self) -> ToolbarItemLocation { |
| 745 | − ToolbarItemLocation::PrimaryLeft { flex: None } | |
| 745 | + ToolbarItemLocation::PrimaryLeft | |
| 746 | 746 | } |
| 747 | 747 | |
| 748 | 748 | fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> { |
| @@ -778,27 +778,28 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { · 29 unmodified lines | ||
| 778 | 778 | .bg(gpui::red()) |
| 779 | 779 | .map(|stack| { |
| 780 | 780 | let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { |
| 781 | − IconElement::new(Icon::XCircle).color(TextColor::Error) | |
| 781 | + IconElement::new(Icon::XCircle).color(Color::Error) | |
| 782 | 782 | } else { |
| 783 | − IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning) | |
| 783 | + IconElement::new(Icon::ExclamationTriangle).color(Color::Warning) | |
| 784 | 784 | }; |
| 785 | 785 | |
| 786 | 786 | stack.child(div().pl_8().child(icon)) |
| 787 | 787 | }) |
| 788 | 788 | .when_some(diagnostic.source.as_ref(), |stack, source| { |
| 789 | − stack.child(Label::new(format!("{source}:")).color(TextColor::Accent)) | |
| 789 | + stack.child(Label::new(format!("{source}:")).color(Color::Accent)) | |
| 790 | 790 | }) |
| 791 | 791 | .child(HighlightedLabel::new(message.clone(), highlights.clone())) |
| 792 | 792 | .when_some(diagnostic.code.as_ref(), |stack, code| { |
| 793 | 793 | stack.child(Label::new(code.clone())) |
| 794 | 794 | }) |
| 795 | − .render() | |
| 795 | + .into_any_element() | |
| 796 | 796 | }) |
| 797 | 797 | } |
| 798 | 798 | |
| 799 | −pub(crate) fn render_summary<T: 'static>(summary: &DiagnosticSummary) -> AnyElement<T> { | |
| 799 | +pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement { | |
| 800 | 800 | if summary.error_count == 0 && summary.warning_count == 0 { |
| 801 | − Label::new("No problems").render() | |
| 801 | + let label = Label::new("No problems"); | |
| 802 | + label.into_any_element() | |
| 802 | 803 | } else { |
| 803 | 804 | h_stack() |
| 804 | 805 | .bg(gpui::red()) |
| @@ -806,7 +807,7 @@ pub(crate) fn render_summary<T: 'static>(summary: &DiagnosticSummary) -> AnyElem · 1 unmodified line | ||
| 806 | 807 | .child(Label::new(summary.error_count.to_string())) |
| 807 | 808 | .child(IconElement::new(Icon::ExclamationTriangle)) |
| 808 | 809 | .child(Label::new(summary.warning_count.to_string())) |
| 809 | − .render() | |
| 810 | + .into_any_element() | |
| 810 | 811 | } |
| 811 | 812 | } |
| 812 | 813 | |
| @@ -1549,7 +1550,7 @@ mod tests { · 736 unmodified lines | ||
| 1549 | 1550 | block_id: ix, |
| 1550 | 1551 | editor_style: &editor::EditorStyle::default(), |
| 1551 | 1552 | }) |
| 1552 | − .element_id()? | |
| 1553 | + .inner_id()? | |
| 1553 | 1554 | .try_into() |
| 1554 | 1555 | .ok()?, |
| 1555 | 1556 | |
| @@ -1,13 +1,13 @@ | ||
| 1 | 1 | use collections::HashSet; |
| 2 | 2 | use editor::{Editor, GoToDiagnostic}; |
| 3 | 3 | use gpui::{ |
| 4 | − rems, Div, EventEmitter, InteractiveComponent, ParentComponent, Render, Stateful, | |
| 5 | − StatefulInteractiveComponent, Styled, Subscription, View, ViewContext, WeakView, | |
| 4 | + rems, Div, EventEmitter, InteractiveElement, ParentElement, Render, Stateful, | |
| 5 | + StatefulInteractiveElement, Styled, Subscription, View, ViewContext, WeakView, | |
| 6 | 6 | }; |
| 7 | 7 | use language::Diagnostic; |
| 8 | 8 | use lsp::LanguageServerId; |
| 9 | 9 | use theme::ActiveTheme; |
| 10 | −use ui::{h_stack, Icon, IconElement, Label, TextColor, Tooltip}; | |
| 10 | +use ui::{h_stack, Color, Icon, IconElement, Label, Tooltip}; | |
| 11 | 11 | use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace}; |
| 12 | 12 | |
| 13 | 13 | use crate::ProjectDiagnosticsEditor; |
| @@ -22,30 +22,30 @@ pub struct DiagnosticIndicator { · 8 unmodified lines | ||
| 22 | 22 | } |
| 23 | 23 | |
| 24 | 24 | impl Render for DiagnosticIndicator { |
| 25 | − type Element = Stateful<Self, Div<Self>>; | |
| 25 | + type Element = Stateful<Div>; | |
| 26 | 26 | |
| 27 | 27 | fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { |
| 28 | 28 | let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { |
| 29 | − (0, 0) => h_stack().child(IconElement::new(Icon::Check).color(TextColor::Success)), | |
| 29 | + (0, 0) => h_stack().child(IconElement::new(Icon::Check).color(Color::Success)), | |
| 30 | 30 | (0, warning_count) => h_stack() |
| 31 | 31 | .gap_1() |
| 32 | − .child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)) | |
| 32 | + .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)) | |
| 33 | 33 | .child(Label::new(warning_count.to_string())), |
| 34 | 34 | (error_count, 0) => h_stack() |
| 35 | 35 | .gap_1() |
| 36 | − .child(IconElement::new(Icon::XCircle).color(TextColor::Error)) | |
| 36 | + .child(IconElement::new(Icon::XCircle).color(Color::Error)) | |
| 37 | 37 | .child(Label::new(error_count.to_string())), |
| 38 | 38 | (error_count, warning_count) => h_stack() |
| 39 | 39 | .gap_1() |
| 40 | − .child(IconElement::new(Icon::XCircle).color(TextColor::Error)) | |
| 40 | + .child(IconElement::new(Icon::XCircle).color(Color::Error)) | |
| 41 | 41 | .child(Label::new(error_count.to_string())) |
| 42 | − .child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)) | |
| 42 | + .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)) | |
| 43 | 43 | .child(Label::new(warning_count.to_string())), |
| 44 | 44 | }; |
| 45 | 45 | |
| 46 | 46 | h_stack() |
| 47 | − .id(cx.entity_id()) | |
| 48 | − .on_action(Self::go_to_next_diagnostic) | |
| 47 | + .id("diagnostic-indicator") | |
| 48 | + .on_action(cx.listener(Self::go_to_next_diagnostic)) | |
| 49 | 49 | .rounded_md() |
| 50 | 50 | .flex_none() |
| 51 | 51 | .h(rems(1.375)) |
| @@ -54,14 +54,14 @@ impl Render for DiagnosticIndicator { · 2 unmodified lines | ||
| 54 | 54 | .bg(cx.theme().colors().ghost_element_background) |
| 55 | 55 | .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) |
| 56 | 56 | .active(|style| style.bg(cx.theme().colors().ghost_element_active)) |
| 57 | − .tooltip(|_, cx| Tooltip::text("Project Diagnostics", cx)) | |
| 58 | − .on_click(|this, _, cx| { | |
| 57 | + .tooltip(|cx| Tooltip::text("Project Diagnostics", cx)) | |
| 58 | + .on_click(cx.listener(|this, _, cx| { | |
| 59 | 59 | if let Some(workspace) = this.workspace.upgrade() { |
| 60 | 60 | workspace.update(cx, |workspace, cx| { |
| 61 | 61 | ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx) |
| 62 | 62 | }) |
| 63 | 63 | } |
| 64 | − }) | |
| 64 | + })) | |
| 65 | 65 | .child(diagnostic_indicator) |
| 66 | 66 | } |
| 67 | 67 | } |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | use crate::ProjectDiagnosticsEditor; |
| 2 | −use gpui::{div, Div, EventEmitter, ParentComponent, Render, ViewContext, WeakView}; | |
| 2 | +use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView}; | |
| 3 | 3 | use ui::{Icon, IconButton, Tooltip}; |
| 4 | 4 | use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; |
| 5 | 5 | |
| @@ -8,7 +8,7 @@ pub struct ToolbarControls { · 2 unmodified lines | ||
| 8 | 8 | } |
| 9 | 9 | |
| 10 | 10 | impl Render for ToolbarControls { |
| 11 | − type Element = Div<Self>; | |
| 11 | + type Element = Div; | |
| 12 | 12 | |
| 13 | 13 | fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { |
| 14 | 14 | let include_warnings = self |
| @@ -26,14 +26,14 @@ impl Render for ToolbarControls { · 11 unmodified lines | ||
| 26 | 26 | |
| 27 | 27 | div().child( |
| 28 | 28 | IconButton::new("toggle-warnings", Icon::ExclamationTriangle) |
| 29 | − .tooltip(move |_, cx| Tooltip::text(tooltip, cx)) | |
| 30 | − .on_click(|this: &mut Self, cx| { | |
| 29 | + .tooltip(move |cx| Tooltip::text(tooltip, cx)) | |
| 30 | + .on_click(cx.listener(|this, _, cx| { | |
| 31 | 31 | if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) { |
| 32 | 32 | editor.update(cx, |editor, cx| { |
| 33 | 33 | editor.toggle_warnings(&Default::default(), cx); |
| 34 | 34 | }); |
| 35 | 35 | } |
| 36 | − }), | |
| 36 | + })), | |
| 37 | 37 | ) |
| 38 | 38 | } |
| 39 | 39 | } |
| @@ -49,7 +49,7 @@ impl ToolbarItemView for ToolbarControls { · 9 unmodified lines | ||
| 49 | 49 | if let Some(pane_item) = active_pane_item.as_ref() { |
| 50 | 50 | if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() { |
| 51 | 51 | self.editor = Some(editor.downgrade()); |
| 52 | − ToolbarItemLocation::PrimaryRight { flex: None } | |
| 52 | + ToolbarItemLocation::PrimaryRight | |
| 53 | 53 | } else { |
| 54 | 54 | ToolbarItemLocation::Hidden |
| 55 | 55 | } |
| @@ -1001,17 +1001,18 @@ impl CompletionsMenu { · 1000 unmodified lines | ||
| 1001 | 1001 | |
| 1002 | 1002 | fn pre_resolve_completion_documentation( |
| 1003 | 1003 | &self, |
| 1004 | − project: Option<ModelHandle<Project>>, | |
| 1004 | + editor: &Editor, | |
| 1005 | 1005 | cx: &mut ViewContext<Editor>, |
| 1006 | − ) { | |
| 1006 | + ) -> Option<Task<()>> { | |
| 1007 | 1007 | let settings = settings::get::<EditorSettings>(cx); |
| 1008 | 1008 | if !settings.show_completion_documentation { |
| 1009 | − return; | |
| 1009 | + return None; | |
| 1010 | 1010 | } |
| 1011 | 1011 | |
| 1012 | − let Some(project) = project else { | |
| 1013 | − return; | |
| 1012 | + let Some(project) = editor.project.clone() else { | |
| 1013 | + return None; | |
| 1014 | 1014 | }; |
| 1015 | + | |
| 1015 | 1016 | let client = project.read(cx).client(); |
| 1016 | 1017 | let language_registry = project.read(cx).languages().clone(); |
| 1017 | 1018 | |
| @@ -1021,7 +1022,7 @@ impl CompletionsMenu { · 3 unmodified lines | ||
| 1021 | 1022 | let completions = self.completions.clone(); |
| 1022 | 1023 | let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); |
| 1023 | 1024 | |
| 1024 | − cx.spawn(move |this, mut cx| async move { | |
| 1025 | + Some(cx.spawn(move |this, mut cx| async move { | |
| 1025 | 1026 | if is_remote { |
| 1026 | 1027 | let Some(project_id) = project_id else { |
| 1027 | 1028 | log::error!("Remote project without remote_id"); |
| @@ -1083,8 +1084,7 @@ impl CompletionsMenu { · 55 unmodified lines | ||
| 1083 | 1084 | _ = this.update(&mut cx, |_, cx| cx.notify()); |
| 1084 | 1085 | } |
| 1085 | 1086 | } |
| 1086 | − }) | |
| 1087 | − .detach(); | |
| 1087 | + })) | |
| 1088 | 1088 | } |
| 1089 | 1089 | |
| 1090 | 1090 | fn attempt_resolve_selected_completion_documentation( |
| @@ -3423,7 +3423,7 @@ impl Editor { · 2332 unmodified lines | ||
| 3423 | 3423 | to_insert, |
| 3424 | 3424 | }) = self.inlay_hint_cache.spawn_hint_refresh( |
| 3425 | 3425 | reason_description, |
| 3426 | − self.excerpt_visible_offsets(required_languages.as_ref(), cx), | |
| 3426 | + self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx), | |
| 3427 | 3427 | invalidate_cache, |
| 3428 | 3428 | cx, |
| 3429 | 3429 | ) { |
| @@ -3442,11 +3442,15 @@ impl Editor { · 12 unmodified lines | ||
| 3442 | 3442 | .collect() |
| 3443 | 3443 | } |
| 3444 | 3444 | |
| 3445 | − pub fn excerpt_visible_offsets( | |
| 3445 | + pub fn excerpts_for_inlay_hints_query( | |
| 3446 | 3446 | &self, |
| 3447 | 3447 | restrict_to_languages: Option<&HashSet<Arc<Language>>>, |
| 3448 | 3448 | cx: &mut ViewContext<'_, '_, Editor>, |
| 3449 | 3449 | ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> { |
| 3450 | + let Some(project) = self.project.as_ref() else { | |
| 3451 | + return HashMap::default(); | |
| 3452 | + }; | |
| 3453 | + let project = project.read(cx); | |
| 3450 | 3454 | let multi_buffer = self.buffer().read(cx); |
| 3451 | 3455 | let multi_buffer_snapshot = multi_buffer.snapshot(cx); |
| 3452 | 3456 | let multi_buffer_visible_start = self |
| @@ -3466,6 +3470,14 @@ impl Editor { · 13 unmodified lines | ||
| 3466 | 3470 | .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) |
| 3467 | 3471 | .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| { |
| 3468 | 3472 | let buffer = buffer_handle.read(cx); |
| 3473 | + let buffer_file = project::worktree::File::from_dyn(buffer.file())?; | |
| 3474 | + let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; | |
| 3475 | + let worktree_entry = buffer_worktree | |
| 3476 | + .read(cx) | |
| 3477 | + .entry_for_id(buffer_file.project_entry_id(cx)?)?; | |
| 3478 | + if worktree_entry.is_ignored { | |
| 3479 | + return None; | |
| 3480 | + } | |
| 3469 | 3481 | let language = buffer.language()?; |
| 3470 | 3482 | if let Some(restrict_to_languages) = restrict_to_languages { |
| 3471 | 3483 | if !restrict_to_languages.contains(language) { |
| @@ -3580,7 +3592,8 @@ impl Editor { · 108 unmodified lines | ||
| 3580 | 3592 | let id = post_inc(&mut self.next_completion_id); |
| 3581 | 3593 | let task = cx.spawn(|this, mut cx| { |
| 3582 | 3594 | async move { |
| 3583 | − let menu = if let Some(completions) = completions.await.log_err() { | |
| 3595 | + let completions = completions.await.log_err(); | |
| 3596 | + let (menu, pre_resolve_task) = if let Some(completions) = completions { | |
| 3584 | 3597 | let mut menu = CompletionsMenu { |
| 3585 | 3598 | id, |
| 3586 | 3599 | initial_position: position, |
| @@ -3601,21 +3614,26 @@ impl Editor { · 14 unmodified lines | ||
| 3601 | 3614 | selected_item: 0, |
| 3602 | 3615 | list: Default::default(), |
| 3603 | 3616 | }; |
| 3617 | + | |
| 3604 | 3618 | menu.filter(query.as_deref(), cx.background()).await; |
| 3619 | + | |
| 3605 | 3620 | if menu.matches.is_empty() { |
| 3606 | − None | |
| 3621 | + (None, None) | |
| 3607 | 3622 | } else { |
| 3608 | − _ = this.update(&mut cx, |editor, cx| { | |
| 3609 | − menu.pre_resolve_completion_documentation(editor.project.clone(), cx); | |
| 3610 | − }); | |
| 3611 | − Some(menu) | |
| 3623 | + let pre_resolve_task = this | |
| 3624 | + .update(&mut cx, |editor, cx| { | |
| 3625 | + menu.pre_resolve_completion_documentation(editor, cx) | |
| 3626 | + }) | |
| 3627 | + .ok() | |
| 3628 | + .flatten(); | |
| 3629 | + (Some(menu), pre_resolve_task) | |
| 3612 | 3630 | } |
| 3613 | 3631 | } else { |
| 3614 | − None | |
| 3632 | + (None, None) | |
| 3615 | 3633 | }; |
| 3616 | 3634 | |
| 3617 | 3635 | this.update(&mut cx, |this, cx| { |
| 3618 | − this.completion_tasks.retain(|(task_id, _)| *task_id > id); | |
| 3636 | + this.completion_tasks.retain(|(task_id, _)| *task_id >= id); | |
| 3619 | 3637 | |
| 3620 | 3638 | let mut context_menu = this.context_menu.write(); |
| 3621 | 3639 | match context_menu.as_ref() { |
| @@ -3636,10 +3654,10 @@ impl Editor { · 14 unmodified lines | ||
| 3636 | 3654 | drop(context_menu); |
| 3637 | 3655 | this.discard_copilot_suggestion(cx); |
| 3638 | 3656 | cx.notify(); |
| 3639 | − } else if this.completion_tasks.is_empty() { | |
| 3640 | − // If there are no more completion tasks and the last menu was | |
| 3641 | − // empty, we should hide it. If it was already hidden, we should | |
| 3642 | − // also show the copilot suggestion when available. | |
| 3657 | + } else if this.completion_tasks.len() <= 1 { | |
| 3658 | + // If there are no more completion tasks (omitting ourself) and | |
| 3659 | + // the last menu was empty, we should hide it. If it was already | |
| 3660 | + // hidden, we should also show the copilot suggestion when available. | |
| 3643 | 3661 | drop(context_menu); |
| 3644 | 3662 | if this.hide_context_menu(cx).is_none() { |
| 3645 | 3663 | this.update_visible_copilot_suggestion(cx); |
| @@ -3647,10 +3665,15 @@ impl Editor { · 1 unmodified line | ||
| 3647 | 3665 | } |
| 3648 | 3666 | })?; |
| 3649 | 3667 | |
| 3668 | + if let Some(pre_resolve_task) = pre_resolve_task { | |
| 3669 | + pre_resolve_task.await; | |
| 3670 | + } | |
| 3671 | + | |
| 3650 | 3672 | Ok::<_, anyhow::Error>(()) |
| 3651 | 3673 | } |
| 3652 | 3674 | .log_err() |
| 3653 | 3675 | }); |
| 3676 | + | |
| 3654 | 3677 | self.completion_tasks.push((id, task)); |
| 3655 | 3678 | } |
| 3656 | 3679 | |
| @@ -861,7 +861,7 @@ async fn fetch_and_update_hints( · 860 unmodified lines | ||
| 861 | 861 | let inlay_hints_fetch_task = editor |
| 862 | 862 | .update(&mut cx, |editor, cx| { |
| 863 | 863 | if got_throttled { |
| 864 | − let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { | |
| 864 | + let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) { | |
| 865 | 865 | Some((_, _, current_visible_range)) => { |
| 866 | 866 | let visible_offset_length = current_visible_range.len(); |
| 867 | 867 | let double_visible_range = current_visible_range |
| @@ -2237,7 +2237,9 @@ pub mod tests { · 1369 unmodified lines | ||
| 2237 | 2237 | editor: &ViewHandle<Editor>, |
| 2238 | 2238 | cx: &mut gpui::TestAppContext, |
| 2239 | 2239 | ) -> Range<Point> { |
| 2240 | − let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); | |
| 2240 | + let ranges = editor.update(cx, |editor, cx| { | |
| 2241 | + editor.excerpts_for_inlay_hints_query(None, cx) | |
| 2242 | + }); | |
| 2241 | 2243 | assert_eq!( |
| 2242 | 2244 | ranges.len(), |
| 2243 | 2245 | 1, |
| @@ -50,7 +50,7 @@ struct BlockRow(u32); · 49 unmodified lines | ||
| 50 | 50 | #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] |
| 51 | 51 | struct WrapRow(u32); |
| 52 | 52 | |
| 53 | −pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>; | |
| 53 | +pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement>; | |
| 54 | 54 | |
| 55 | 55 | pub struct Block { |
| 56 | 56 | id: BlockId, |
| @@ -69,7 +69,7 @@ where · 12 unmodified lines | ||
| 69 | 69 | pub position: P, |
| 70 | 70 | pub height: u8, |
| 71 | 71 | pub style: BlockStyle, |
| 72 | − pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>, | |
| 72 | + pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement>, | |
| 73 | 73 | pub disposition: BlockDisposition, |
| 74 | 74 | } |
| 75 | 75 | |
| @@ -947,7 +947,7 @@ impl DerefMut for BlockContext<'_, '_> { · 871 unmodified lines | ||
| 947 | 947 | } |
| 948 | 948 | |
| 949 | 949 | impl Block { |
| 950 | − pub fn render(&self, cx: &mut BlockContext) -> AnyElement<Editor> { | |
| 950 | + pub fn render(&self, cx: &mut BlockContext) -> AnyElement { | |
| 951 | 951 | self.render.lock()(cx) |
| 952 | 952 | } |
| 953 | 953 | |
| @@ -40,11 +40,12 @@ use fuzzy::{StringMatch, StringMatchCandidate}; · 39 unmodified lines | ||
| 40 | 40 | use git::diff_hunk_to_display; |
| 41 | 41 | use gpui::{ |
| 42 | 42 | actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, |
| 43 | − AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, | |
| 44 | − EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, | |
| 45 | − Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled, | |
| 46 | − Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, | |
| 47 | − WeakView, WindowContext, | |
| 43 | + AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, | |
| 44 | + DispatchPhase, Div, ElementId, EventEmitter, FocusHandle, FocusableView, FontFeatures, | |
| 45 | + FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, | |
| 46 | + MouseButton, ParentElement, Pixels, Render, RenderOnce, SharedString, Styled, StyledText, | |
| 47 | + Subscription, Task, TextRun, TextStyle, UniformListScrollHandle, View, ViewContext, | |
| 48 | + VisualContext, WeakView, WhiteSpace, WindowContext, | |
| 48 | 49 | }; |
| 49 | 50 | use highlight_matching_bracket::refresh_matching_bracket_highlights; |
| 50 | 51 | use hover_popover::{hide_hover, HoverState}; |
| @@ -54,13 +55,14 @@ use itertools::Itertools; · 3 unmodified lines | ||
| 54 | 55 | pub use language::{char_kind, CharKind}; |
| 55 | 56 | use language::{ |
| 56 | 57 | language_settings::{self, all_language_settings, InlayHintSettings}, |
| 57 | − point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, Completion, CursorShape, | |
| 58 | − Diagnostic, IndentKind, IndentSize, Language, LanguageRegistry, LanguageServerName, | |
| 59 | − OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, | |
| 58 | + markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, | |
| 59 | + Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, | |
| 60 | + LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, | |
| 61 | + TransactionId, | |
| 60 | 62 | }; |
| 61 | 63 | use lazy_static::lazy_static; |
| 62 | 64 | use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState}; |
| 63 | −use lsp::{DiagnosticSeverity, Documentation, LanguageServerId}; | |
| 65 | +use lsp::{DiagnosticSeverity, LanguageServerId}; | |
| 64 | 66 | use movement::TextLayoutDetails; |
| 65 | 67 | use multi_buffer::ToOffsetUtf16; |
| 66 | 68 | pub use multi_buffer::{ |
| @@ -97,12 +99,12 @@ use text::{OffsetUtf16, Rope}; · 30 unmodified lines | ||
| 97 | 99 | use theme::{ |
| 98 | 100 | ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, |
| 99 | 101 | }; |
| 100 | −use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip}; | |
| 102 | +use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, StyledExt, Tooltip}; | |
| 101 | 103 | use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; |
| 102 | 104 | use workspace::{ |
| 103 | 105 | item::{ItemEvent, ItemHandle}, |
| 104 | 106 | searchable::SearchEvent, |
| 105 | − ItemNavHistory, SplitDirection, ViewId, Workspace, | |
| 107 | + ItemNavHistory, Pane, SplitDirection, ViewId, Workspace, | |
| 106 | 108 | }; |
| 107 | 109 | |
| 108 | 110 | const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); |
| @@ -115,70 +117,70 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis · 6 unmodified lines | ||
| 115 | 117 | |
| 116 | 118 | pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); |
| 117 | 119 | |
| 118 | −// pub fn render_parsed_markdown<Tag: 'static>( | |
| 119 | −// parsed: &language::ParsedMarkdown, | |
| 120 | −// editor_style: &EditorStyle, | |
| 121 | −// workspace: Option<WeakView<Workspace>>, | |
| 122 | −// cx: &mut ViewContext<Editor>, | |
| 123 | −// ) -> Text { | |
| 124 | −// enum RenderedMarkdown {} | |
| 125 | − | |
| 126 | −// let parsed = parsed.clone(); | |
| 127 | −// let view_id = cx.view_id(); | |
| 128 | −// let code_span_background_color = editor_style.document_highlight_read_background; | |
| 129 | − | |
| 130 | −// let mut region_id = 0; | |
| 131 | − | |
| 132 | −// todo!() | |
| 133 | −// // Text::new(parsed.text, editor_style.text.clone()) | |
| 134 | −// // .with_highlights( | |
| 135 | −// // parsed | |
| 136 | −// // .highlights | |
| 137 | −// // .iter() | |
| 138 | −// // .filter_map(|(range, highlight)| { | |
| 139 | −// // let highlight = highlight.to_highlight_style(&editor_style.syntax)?; | |
| 140 | −// // Some((range.clone(), highlight)) | |
| 141 | −// // }) | |
| 142 | −// // .collect::<Vec<_>>(), | |
| 143 | −// // ) | |
| 144 | −// // .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| { | |
| 145 | −// // region_id += 1; | |
| 146 | −// // let region = parsed.regions[ix].clone(); | |
| 147 | − | |
| 148 | −// // if let Some(link) = region.link { | |
| 149 | −// // cx.scene().push_cursor_region(CursorRegion { | |
| 150 | −// // bounds, | |
| 151 | −// // style: CursorStyle::PointingHand, | |
| 152 | −// // }); | |
| 153 | −// // cx.scene().push_mouse_region( | |
| 154 | −// // MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) | |
| 155 | −// // .on_down::<Editor, _>(MouseButton::Left, move |_, _, cx| match &link { | |
| 156 | −// // markdown::Link::Web { url } => cx.platform().open_url(url), | |
| 157 | −// // markdown::Link::Path { path } => { | |
| 158 | −// // if let Some(workspace) = &workspace { | |
| 159 | −// // _ = workspace.update(cx, |workspace, cx| { | |
| 160 | −// // workspace.open_abs_path(path.clone(), false, cx).detach(); | |
| 161 | −// // }); | |
| 162 | −// // } | |
| 163 | −// // } | |
| 164 | −// // }), | |
| 165 | −// // ); | |
| 166 | −// // } | |
| 167 | − | |
| 168 | −// // if region.code { | |
| 169 | −// // cx.draw_quad(Quad { | |
| 170 | −// // bounds, | |
| 171 | −// // background: Some(code_span_background_color), | |
| 172 | −// // corner_radii: (2.0).into(), | |
| 173 | −// // order: todo!(), | |
| 174 | −// // content_mask: todo!(), | |
| 175 | −// // border_color: todo!(), | |
| 176 | −// // border_widths: todo!(), | |
| 177 | −// // }); | |
| 178 | −// // } | |
| 179 | −// // }) | |
| 180 | −// // .with_soft_wrap(true) | |
| 181 | −// } | |
| 120 | +pub fn render_parsed_markdown( | |
| 121 | + element_id: impl Into<ElementId>, | |
| 122 | + parsed: &language::ParsedMarkdown, | |
| 123 | + editor_style: &EditorStyle, | |
| 124 | + workspace: Option<WeakView<Workspace>>, | |
| 125 | + cx: &mut ViewContext<Editor>, | |
| 126 | +) -> InteractiveText { | |
| 127 | + let code_span_background_color = cx | |
| 128 | + .theme() | |
| 129 | + .colors() | |
| 130 | + .editor_document_highlight_read_background; | |
| 131 | + | |
| 132 | + let highlights = gpui::combine_highlights( | |
| 133 | + parsed.highlights.iter().filter_map(|(range, highlight)| { | |
| 134 | + let highlight = highlight.to_highlight_style(&editor_style.syntax)?; | |
| 135 | + Some((range.clone(), highlight)) | |
| 136 | + }), | |
| 137 | + parsed | |
| 138 | + .regions | |
| 139 | + .iter() | |
| 140 | + .zip(&parsed.region_ranges) | |
| 141 | + .filter_map(|(region, range)| { | |
| 142 | + if region.code { | |
| 143 | + Some(( | |
| 144 | + range.clone(), | |
| 145 | + HighlightStyle { | |
| 146 | + background_color: Some(code_span_background_color), | |
| 147 | + ..Default::default() | |
| 148 | + }, | |
| 149 | + )) | |
| 150 | + } else { | |
| 151 | + None | |
| 152 | + } | |
| 153 | + }), | |
| 154 | + ); | |
| 155 | + let runs = text_runs_for_highlights(&parsed.text, &editor_style.text, highlights); | |
| 156 | + | |
| 157 | + // todo!("add the ability to change cursor style for link ranges") | |
| 158 | + let mut links = Vec::new(); | |
| 159 | + let mut link_ranges = Vec::new(); | |
| 160 | + for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { | |
| 161 | + if let Some(link) = region.link.clone() { | |
| 162 | + links.push(link); | |
| 163 | + link_ranges.push(range.clone()); | |
| 164 | + } | |
| 165 | + } | |
| 166 | + | |
| 167 | + InteractiveText::new( | |
| 168 | + element_id, | |
| 169 | + StyledText::new(parsed.text.clone()).with_runs(runs), | |
| 170 | + ) | |
| 171 | + .on_click(link_ranges, move |clicked_range_ix, cx| { | |
| 172 | + match &links[clicked_range_ix] { | |
| 173 | + markdown::Link::Web { url } => cx.open_url(url), | |
| 174 | + markdown::Link::Path { path } => { | |
| 175 | + if let Some(workspace) = &workspace { | |
| 176 | + _ = workspace.update(cx, |workspace, cx| { | |
| 177 | + workspace.open_abs_path(path.clone(), false, cx).detach(); | |
| 178 | + }); | |
| 179 | + } | |
| 180 | + } | |
| 181 | + } | |
| 182 | + }) | |
| 183 | +} | |
| 182 | 184 | |
| 183 | 185 | #[derive(PartialEq, Clone, Deserialize, Default, Action)] |
| 184 | 186 | pub struct SelectNext { |
| @@ -529,8 +531,6 @@ pub fn init(cx: &mut AppContext) { · 344 unmodified lines | ||
| 529 | 531 | // cx.register_action_type(Editor::context_menu_next); |
| 530 | 532 | // cx.register_action_type(Editor::context_menu_last); |
| 531 | 533 | |
| 532 | − hover_popover::init(cx); | |
| 533 | − | |
| 534 | 534 | workspace::register_project_item::<Editor>(cx); |
| 535 | 535 | workspace::register_followable_item::<Editor>(cx); |
| 536 | 536 | workspace::register_deserializable_item::<Editor>(cx); |
| @@ -663,6 +663,7 @@ pub struct Editor { · 126 unmodified lines | ||
| 663 | 663 | pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>, |
| 664 | 664 | gutter_width: Pixels, |
| 665 | 665 | style: Option<EditorStyle>, |
| 666 | + editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>, | |
| 666 | 667 | } |
| 667 | 668 | |
| 668 | 669 | pub struct EditorSnapshot { |
| @@ -905,12 +906,16 @@ impl ContextMenu { · 236 unmodified lines | ||
| 905 | 906 | &self, |
| 906 | 907 | cursor_position: DisplayPoint, |
| 907 | 908 | style: &EditorStyle, |
| 909 | + max_height: Pixels, | |
| 908 | 910 | workspace: Option<WeakView<Workspace>>, |
| 909 | 911 | cx: &mut ViewContext<Editor>, |
| 910 | − ) -> (DisplayPoint, AnyElement<Editor>) { | |
| 912 | + ) -> (DisplayPoint, AnyElement) { | |
| 911 | 913 | match self { |
| 912 | − ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), | |
| 913 | − ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), | |
| 914 | + ContextMenu::Completions(menu) => ( | |
| 915 | + cursor_position, | |
| 916 | + menu.render(style, max_height, workspace, cx), | |
| 917 | + ), | |
| 918 | + ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, max_height, cx), | |
| 914 | 919 | } |
| 915 | 920 | } |
| 916 | 921 | } |
| @@ -966,20 +971,22 @@ impl CompletionsMenu { · 49 unmodified lines | ||
| 966 | 971 | |
| 967 | 972 | fn pre_resolve_completion_documentation( |
| 968 | 973 | &self, |
| 969 | − project: Option<Model<Project>>, | |
| 970 | − cx: &mut ViewContext<Editor>, | |
| 971 | − ) { | |
| 974 | + _editor: &Editor, | |
| 975 | + _cx: &mut ViewContext<Editor>, | |
| 976 | + ) -> Option<Task<()>> { | |
| 972 | 977 | // todo!("implementation below "); |
| 978 | + None | |
| 973 | 979 | } |
| 974 | − // ) { | |
| 980 | + // { | |
| 975 | 981 | // let settings = EditorSettings::get_global(cx); |
| 976 | 982 | // if !settings.show_completion_documentation { |
| 977 | − // return; | |
| 983 | + // return None; | |
| 978 | 984 | // } |
| 979 | 985 | |
| 980 | − // let Some(project) = project else { | |
| 981 | − // return; | |
| 986 | + // let Some(project) = editor.project.clone() else { | |
| 987 | + // return None; | |
| 982 | 988 | // }; |
| 989 | + | |
| 983 | 990 | // let client = project.read(cx).client(); |
| 984 | 991 | // let language_registry = project.read(cx).languages().clone(); |
| 985 | 992 | |
| @@ -989,7 +996,7 @@ impl CompletionsMenu { · 3 unmodified lines | ||
| 989 | 996 | // let completions = self.completions.clone(); |
| 990 | 997 | // let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); |
| 991 | 998 | |
| 992 | − // cx.spawn(move |this, mut cx| async move { | |
| 999 | + // Some(cx.spawn(move |this, mut cx| async move { | |
| 993 | 1000 | // if is_remote { |
| 994 | 1001 | // let Some(project_id) = project_id else { |
| 995 | 1002 | // log::error!("Remote project without remote_id"); |
| @@ -1051,8 +1058,7 @@ impl CompletionsMenu { · 55 unmodified lines | ||
| 1051 | 1058 | // _ = this.update(&mut cx, |_, cx| cx.notify()); |
| 1052 | 1059 | // } |
| 1053 | 1060 | // } |
| 1054 | − // }) | |
| 1055 | − // .detach(); | |
| 1061 | + // })) | |
| 1056 | 1062 | // } |
| 1057 | 1063 | |
| 1058 | 1064 | fn attempt_resolve_selected_completion_documentation( |
| @@ -1221,210 +1227,156 @@ impl CompletionsMenu { · 162 unmodified lines | ||
| 1221 | 1227 | fn render( |
| 1222 | 1228 | &self, |
| 1223 | 1229 | style: &EditorStyle, |
| 1230 | + max_height: Pixels, | |
| 1224 | 1231 | workspace: Option<WeakView<Workspace>>, |
| 1225 | 1232 | cx: &mut ViewContext<Editor>, |
| 1226 | − ) -> AnyElement<Editor> { | |
| 1227 | − todo!("old implementation below") | |
| 1228 | − } | |
| 1233 | + ) -> AnyElement { | |
| 1234 | + let settings = EditorSettings::get_global(cx); | |
| 1235 | + let show_completion_documentation = settings.show_completion_documentation; | |
| 1229 | 1236 | |
| 1230 | − // enum CompletionTag {} | |
| 1237 | + let widest_completion_ix = self | |
| 1238 | + .matches | |
| 1239 | + .iter() | |
| 1240 | + .enumerate() | |
| 1241 | + .max_by_key(|(_, mat)| { | |
| 1242 | + let completions = self.completions.read(); | |
| 1243 | + let completion = &completions[mat.candidate_id]; | |
| 1244 | + let documentation = &completion.documentation; | |
| 1245 | + | |
| 1246 | + let mut len = completion.label.text.chars().count(); | |
| 1247 | + if let Some(Documentation::SingleLine(text)) = documentation { | |
| 1248 | + if show_completion_documentation { | |
| 1249 | + len += text.chars().count(); | |
| 1250 | + } | |
| 1251 | + } | |
| 1231 | 1252 | |
| 1232 | − // let settings = EditorSettings>(cx); | |
| 1233 | − // let show_completion_documentation = settings.show_completion_documentation; | |
| 1253 | + len | |
| 1254 | + }) | |
| 1255 | + .map(|(ix, _)| ix); | |
| 1234 | 1256 | |
| 1235 | − // let widest_completion_ix = self | |
| 1236 | − // .matches | |
| 1237 | − // .iter() | |
| 1238 | − // .enumerate() | |
| 1239 | − // .max_by_key(|(_, mat)| { | |
| 1240 | − // let completions = self.completions.read(); | |
| 1241 | − // let completion = &completions[mat.candidate_id]; | |
| 1242 | − // let documentation = &completion.documentation; | |
| 1257 | + let completions = self.completions.clone(); | |
| 1258 | + let matches = self.matches.clone(); | |
| 1259 | + let selected_item = self.selected_item; | |
| 1260 | + let style = style.clone(); | |
| 1243 | 1261 | |
| 1244 | − // let mut len = completion.label.text.chars().count(); | |
| 1245 | − // if let Some(Documentation::SingleLine(text)) = documentation { | |
| 1246 | − // if show_completion_documentation { | |
| 1247 | − // len += text.chars().count(); | |
| 1248 | − // } | |
| 1249 | − // } | |
| 1262 | + let multiline_docs = { | |
| 1263 | + let mat = &self.matches[selected_item]; | |
| 1264 | + let multiline_docs = match &self.completions.read()[mat.candidate_id].documentation { | |
| 1265 | + Some(Documentation::MultiLinePlainText(text)) => { | |
| 1266 | + Some(div().child(SharedString::from(text.clone()))) | |
| 1267 | + } | |
| 1268 | + Some(Documentation::MultiLineMarkdown(parsed)) => Some(div().child( | |
| 1269 | + render_parsed_markdown("completions_markdown", parsed, &style, workspace, cx), | |
| 1270 | + )), | |
| 1271 | + _ => None, | |
| 1272 | + }; | |
| 1273 | + multiline_docs.map(|div| { | |
| 1274 | + div.id("multiline_docs") | |
| 1275 | + .max_h(max_height) | |
| 1276 | + .flex_1() | |
| 1277 | + .px_1p5() | |
| 1278 | + .py_1() | |
| 1279 | + .min_w(px(260.)) | |
| 1280 | + .max_w(px(640.)) | |
| 1281 | + .w(px(500.)) | |
| 1282 | + .text_ui() | |
| 1283 | + .overflow_y_scroll() | |
| 1284 | + // Prevent a mouse down on documentation from being propagated to the editor, | |
| 1285 | + // because that would move the cursor. | |
| 1286 | + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) | |
| 1287 | + }) | |
| 1288 | + }; | |
| 1289 | + let list = uniform_list( | |
| 1290 | + cx.view().clone(), | |
| 1291 | + "completions", | |
| 1292 | + matches.len(), | |
| 1293 | + move |editor, range, cx| { | |
| 1294 | + let start_ix = range.start; | |
| 1295 | + let completions_guard = completions.read(); | |
| 1250 | 1296 | |
| 1251 | − // len | |
| 1252 | − // }) | |
| 1253 | − // .map(|(ix, _)| ix); | |
| 1297 | + matches[range] | |
| 1298 | + .iter() | |
| 1299 | + .enumerate() | |
| 1300 | + .map(|(ix, mat)| { | |
| 1301 | + let item_ix = start_ix + ix; | |
| 1302 | + let candidate_id = mat.candidate_id; | |
| 1303 | + let completion = &completions_guard[candidate_id]; | |
| 1254 | 1304 | |
| 1255 | − // let completions = self.completions.clone(); | |
| 1256 | − // let matches = self.matches.clone(); | |
| 1257 | − // let selected_item = self.selected_item; | |
| 1258 | − | |
| 1259 | − // let list = UniformList::new(self.list.clone(), matches.len(), cx, { | |
| 1260 | − // let style = style.clone(); | |
| 1261 | − // move |_, range, items, cx| { | |
| 1262 | − // let start_ix = range.start; | |
| 1263 | − // let completions_guard = completions.read(); | |
| 1264 | − | |
| 1265 | − // for (ix, mat) in matches[range].iter().enumerate() { | |
| 1266 | − // let item_ix = start_ix + ix; | |
| 1267 | − // let candidate_id = mat.candidate_id; | |
| 1268 | − // let completion = &completions_guard[candidate_id]; | |
| 1269 | − | |
| 1270 | − // let documentation = if show_completion_documentation { | |
| 1271 | − // &completion.documentation | |
| 1272 | − // } else { | |
| 1273 | − // &None | |
| 1274 | − // }; | |
| 1305 | + let documentation = if show_completion_documentation { | |
| 1306 | + &completion.documentation | |
| 1307 | + } else { | |
| 1308 | + &None | |
| 1309 | + }; | |
| 1275 | 1310 | |
| 1276 | − // items.push( | |
| 1277 | − // MouseEventHandler::new::<CompletionTag, _>( | |
| 1278 | − // mat.candidate_id, | |
| 1279 | − // cx, | |
| 1280 | − // |state, _| { | |
| 1281 | − // let item_style = if item_ix == selected_item { | |
| 1282 | − // style.autocomplete.selected_item | |
| 1283 | − // } else if state.hovered() { | |
| 1284 | − // style.autocomplete.hovered_item | |
| 1285 | − // } else { | |
| 1286 | − // style.autocomplete.item | |
| 1287 | − // }; | |
| 1288 | − | |
| 1289 | − // let completion_label = | |
| 1290 | − // Text::new(completion.label.text.clone(), style.text.clone()) | |
| 1291 | − // .with_soft_wrap(false) | |
| 1292 | − // .with_highlights( | |
| 1293 | − // combine_syntax_and_fuzzy_match_highlights( | |
| 1294 | − // &completion.label.text, | |
| 1295 | − // style.text.color.into(), | |
| 1296 | − // styled_runs_for_code_label( | |
| 1297 | − // &completion.label, | |
| 1298 | − // &style.syntax, | |
| 1299 | − // ), | |
| 1300 | − // &mat.positions, | |
| 1301 | − // ), | |
| 1302 | − // ); | |
| 1303 | − | |
| 1304 | − // if let Some(Documentation::SingleLine(text)) = documentation { | |
| 1305 | − // Flex::row() | |
| 1306 | − // .with_child(completion_label) | |
| 1307 | − // .with_children((|| { | |
| 1308 | − // let text_style = TextStyle { | |
| 1309 | − // color: style.autocomplete.inline_docs_color, | |
| 1310 | − // font_size: style.text.font_size | |
| 1311 | − // * style.autocomplete.inline_docs_size_percent, | |
| 1312 | − // ..style.text.clone() | |
| 1313 | − // }; | |
| 1314 | − | |
| 1315 | − // let label = Text::new(text.clone(), text_style) | |
| 1316 | − // .aligned() | |
| 1317 | − // .constrained() | |
| 1318 | − // .dynamically(move |constraint, _, _| { | |
| 1319 | − // gpui::SizeConstraint { | |
| 1320 | − // min: constraint.min, | |
| 1321 | − // max: vec2f( | |
| 1322 | − // constraint.max.x(), | |
| 1323 | − // constraint.min.y(), | |
| 1324 | − // ), | |
| 1325 | − // } | |
| 1326 | − // }); | |
| 1327 | − | |
| 1328 | − // if Some(item_ix) == widest_completion_ix { | |
| 1329 | − // Some( | |
| 1330 | − // label | |
| 1331 | − // .contained() | |
| 1332 | − // .with_style( | |
| 1333 | − // style | |
| 1334 | − // .autocomplete | |
| 1335 | − // .inline_docs_container, | |
| 1336 | − // ) | |
| 1337 | − // .into_any(), | |
| 1338 | − // ) | |
| 1339 | − // } else { | |
| 1340 | − // Some(label.flex_float().into_any()) | |
| 1341 | − // } | |
| 1342 | − // })()) | |
| 1343 | − // .into_any() | |
| 1344 | − // } else { | |
| 1345 | − // completion_label.into_any() | |
| 1346 | − // } | |
| 1347 | − // .contained() | |
| 1348 | − // .with_style(item_style) | |
| 1349 | − // .constrained() | |
| 1350 | − // .dynamically( | |
| 1351 | − // move |constraint, _, _| { | |
| 1352 | − // if Some(item_ix) == widest_completion_ix { | |
| 1353 | − // constraint | |
| 1354 | − // } else { | |
| 1355 | − // gpui::SizeConstraint { | |
| 1356 | − // min: constraint.min, | |
| 1357 | − // max: constraint.min, | |
| 1358 | − // } | |
| 1359 | − // } | |
| 1360 | − // }, | |
| 1361 | − // ) | |
| 1362 | − // }, | |
| 1363 | − // ) | |
| 1364 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 1365 | − // .on_down(MouseButton::Left, move |_, this, cx| { | |
| 1366 | − // this.confirm_completion( | |
| 1367 | − // &ConfirmCompletion { | |
| 1368 | − // item_ix: Some(item_ix), | |
| 1369 | − // }, | |
| 1370 | − // cx, | |
| 1371 | − // ) | |
| 1372 | − // .map(|task| task.detach()); | |
| 1373 | − // }) | |
| 1374 | − // .constrained() | |
| 1375 | − // .with_min_width(style.autocomplete.completion_min_width) | |
| 1376 | − // .with_max_width(style.autocomplete.completion_max_width) | |
| 1377 | − // .into_any(), | |
| 1378 | − // ); | |
| 1379 | − // } | |
| 1380 | − // } | |
| 1381 | − // }) | |
| 1382 | − // .with_width_from_item(widest_completion_ix); | |
| 1383 | − | |
| 1384 | − // enum MultiLineDocumentation {} | |
| 1385 | − | |
| 1386 | − // Flex::row() | |
| 1387 | − // .with_child(list.flex(1., false)) | |
| 1388 | − // .with_children({ | |
| 1389 | − // let mat = &self.matches[selected_item]; | |
| 1390 | − // let completions = self.completions.read(); | |
| 1391 | − // let completion = &completions[mat.candidate_id]; | |
| 1392 | − // let documentation = &completion.documentation; | |
| 1393 | − | |
| 1394 | − // match documentation { | |
| 1395 | − // Some(Documentation::MultiLinePlainText(text)) => Some( | |
| 1396 | − // Flex::column() | |
| 1397 | − // .scrollable::<MultiLineDocumentation>(0, None, cx) | |
| 1398 | − // .with_child( | |
| 1399 | − // Text::new(text.clone(), style.text.clone()).with_soft_wrap(true), | |
| 1400 | − // ) | |
| 1401 | − // .contained() | |
| 1402 | − // .with_style(style.autocomplete.alongside_docs_container) | |
| 1403 | − // .constrained() | |
| 1404 | − // .with_max_width(style.autocomplete.alongside_docs_max_width) | |
| 1405 | − // .flex(1., false), | |
| 1406 | − // ), | |
| 1407 | − | |
| 1408 | − // Some(Documentation::MultiLineMarkdown(parsed)) => Some( | |
| 1409 | − // Flex::column() | |
| 1410 | − // .scrollable::<MultiLineDocumentation>(0, None, cx) | |
| 1411 | − // .with_child(render_parsed_markdown::<MultiLineDocumentation>( | |
| 1412 | − // parsed, &style, workspace, cx, | |
| 1413 | − // )) | |
| 1414 | − // .contained() | |
| 1415 | − // .with_style(style.autocomplete.alongside_docs_container) | |
| 1416 | − // .constrained() | |
| 1417 | − // .with_max_width(style.autocomplete.alongside_docs_max_width) | |
| 1418 | − // .flex(1., false), | |
| 1419 | − // ), | |
| 1420 | − | |
| 1421 | − // _ => None, | |
| 1422 | − // } | |
| 1423 | − // }) | |
| 1424 | − // .contained() | |
| 1425 | − // .with_style(style.autocomplete.container) | |
| 1426 | − // .into_any() | |
| 1427 | − // } | |
| 1311 | + let highlights = gpui::combine_highlights( | |
| 1312 | + mat.ranges().map(|range| (range, FontWeight::BOLD.into())), | |
| 1313 | + styled_runs_for_code_label(&completion.label, &style.syntax).map( | |
| 1314 | + |(range, mut highlight)| { | |
| 1315 | + // Ignore font weight for syntax highlighting, as we'll use it | |
| 1316 | + // for fuzzy matches. | |
| 1317 | + highlight.font_weight = None; | |
| 1318 | + (range, highlight) | |
| 1319 | + }, | |
| 1320 | + ), | |
| 1321 | + ); | |
| 1322 | + let completion_label = StyledText::new(completion.label.text.clone()) | |
| 1323 | + .with_runs(text_runs_for_highlights( | |
| 1324 | + &completion.label.text, | |
| 1325 | + &style.text, | |
| 1326 | + highlights, | |
| 1327 | + )); | |
| 1328 | + let documentation_label = | |
| 1329 | + if let Some(Documentation::SingleLine(text)) = documentation { | |
| 1330 | + Some(SharedString::from(text.clone())) | |
| 1331 | + } else { | |
| 1332 | + None | |
| 1333 | + }; | |
| 1334 | + | |
| 1335 | + div() | |
| 1336 | + .id(mat.candidate_id) | |
| 1337 | + .min_w(px(220.)) | |
| 1338 | + .max_w(px(540.)) | |
| 1339 | + .whitespace_nowrap() | |
| 1340 | + .overflow_hidden() | |
| 1341 | + .text_ui() | |
| 1342 | + .px_1() | |
| 1343 | + .rounded(px(4.)) | |
| 1344 | + .bg(cx.theme().colors().ghost_element_background) | |
| 1345 | + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) | |
| 1346 | + .when(item_ix == selected_item, |div| { | |
| 1347 | + div.bg(cx.theme().colors().ghost_element_selected) | |
| 1348 | + }) | |
| 1349 | + .on_mouse_down( | |
| 1350 | + MouseButton::Left, | |
| 1351 | + cx.listener(move |editor, event, cx| { | |
| 1352 | + cx.stop_propagation(); | |
| 1353 | + editor | |
| 1354 | + .confirm_completion( | |
| 1355 | + &ConfirmCompletion { | |
| 1356 | + item_ix: Some(item_ix), | |
| 1357 | + }, | |
| 1358 | + cx, | |
| 1359 | + ) | |
| 1360 | + .map(|task| task.detach_and_log_err(cx)); | |
| 1361 | + }), | |
| 1362 | + ) | |
| 1363 | + .child(completion_label) | |
| 1364 | + .children(documentation_label) | |
| 1365 | + }) | |
| 1366 | + .collect() | |
| 1367 | + }, | |
| 1368 | + ) | |
| 1369 | + .max_h(max_height) | |
| 1370 | + .track_scroll(self.scroll_handle.clone()) | |
| 1371 | + .with_width_from_item(widest_completion_ix); | |
| 1372 | + | |
| 1373 | + Popover::new() | |
| 1374 | + .child(list) | |
| 1375 | + .when_some(multiline_docs, |popover, multiline_docs| { | |
| 1376 | + popover.aside(multiline_docs) | |
| 1377 | + }) | |
| 1378 | + .into_any_element() | |
| 1379 | + } | |
| 1428 | 1380 | |
| 1429 | 1381 | pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) { |
| 1430 | 1382 | let mut matches = if let Some(query) = query { |
| @@ -1540,14 +1492,17 @@ impl CodeActionsMenu { · 109 unmodified lines | ||
| 1540 | 1492 | &self, |
| 1541 | 1493 | mut cursor_position: DisplayPoint, |
| 1542 | 1494 | style: &EditorStyle, |
| 1495 | + max_height: Pixels, | |
| 1543 | 1496 | cx: &mut ViewContext<Editor>, |
| 1544 | − ) -> (DisplayPoint, AnyElement<Editor>) { | |
| 1497 | + ) -> (DisplayPoint, AnyElement) { | |
| 1545 | 1498 | let actions = self.actions.clone(); |
| 1546 | 1499 | let selected_item = self.selected_item; |
| 1500 | + | |
| 1547 | 1501 | let element = uniform_list( |
| 1502 | + cx.view().clone(), | |
| 1548 | 1503 | "code_actions_menu", |
| 1549 | 1504 | self.actions.len(), |
| 1550 | − move |editor, range, cx| { | |
| 1505 | + move |this, range, cx| { | |
| 1551 | 1506 | actions[range.clone()] |
| 1552 | 1507 | .iter() |
| 1553 | 1508 | .enumerate() |
| @@ -1569,18 +1524,22 @@ impl CodeActionsMenu { · 15 unmodified lines | ||
| 1569 | 1524 | .bg(colors.element_hover) |
| 1570 | 1525 | .text_color(colors.text_accent) |
| 1571 | 1526 | }) |
| 1572 | − .on_mouse_down(MouseButton::Left, move |editor: &mut Editor, _, cx| { | |
| 1573 | − cx.stop_propagation(); | |
| 1574 | − editor | |
| 1575 | − .confirm_code_action( | |
| 1576 | − &ConfirmCodeAction { | |
| 1577 | − item_ix: Some(item_ix), | |
| 1578 | − }, | |
| 1579 | − cx, | |
| 1580 | − ) | |
| 1581 | − .map(|task| task.detach_and_log_err(cx)); | |
| 1582 | − }) | |
| 1583 | − .child(action.lsp_action.title.clone()) | |
| 1527 | + .on_mouse_down( | |
| 1528 | + MouseButton::Left, | |
| 1529 | + cx.listener(move |editor, _, cx| { | |
| 1530 | + cx.stop_propagation(); | |
| 1531 | + editor | |
| 1532 | + .confirm_code_action( | |
| 1533 | + &ConfirmCodeAction { | |
| 1534 | + item_ix: Some(item_ix), | |
| 1535 | + }, | |
| 1536 | + cx, | |
| 1537 | + ) | |
| 1538 | + .map(|task| task.detach_and_log_err(cx)); | |
| 1539 | + }), | |
| 1540 | + ) | |
| 1541 | + // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here. | |
| 1542 | + .child(SharedString::from(action.lsp_action.title.clone())) | |
| 1584 | 1543 | }) |
| 1585 | 1544 | .collect() |
| 1586 | 1545 | }, |
| @@ -1588,6 +1547,8 @@ impl CodeActionsMenu { · 1 unmodified line | ||
| 1588 | 1547 | .elevation_1(cx) |
| 1589 | 1548 | .px_2() |
| 1590 | 1549 | .py_1() |
| 1550 | + .max_h(max_height) | |
| 1551 | + .track_scroll(self.scroll_handle.clone()) | |
| 1591 | 1552 | .with_width_from_item( |
| 1592 | 1553 | self.actions |
| 1593 | 1554 | .iter() |
| @@ -1595,7 +1556,7 @@ impl CodeActionsMenu { · 1 unmodified line | ||
| 1595 | 1556 | .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) |
| 1596 | 1557 | .map(|(ix, _)| ix), |
| 1597 | 1558 | ) |
| 1598 | − .render(); | |
| 1559 | + .into_any_element(); | |
| 1599 | 1560 | |
| 1600 | 1561 | if self.deployed_from_indicator { |
| 1601 | 1562 | *cursor_position.column_mut() = 0; |
| @@ -1943,6 +1904,7 @@ impl Editor { · 341 unmodified lines | ||
| 1943 | 1904 | pixel_position_of_newest_cursor: None, |
| 1944 | 1905 | gutter_width: Default::default(), |
| 1945 | 1906 | style: None, |
| 1907 | + editor_actions: Default::default(), | |
| 1946 | 1908 | _subscriptions: vec![ |
| 1947 | 1909 | cx.observe(&buffer, Self::on_buffer_changed), |
| 1948 | 1910 | cx.subscribe(&buffer, Self::on_buffer_event), |
| @@ -2074,10 +2036,14 @@ impl Editor { · 125 unmodified lines | ||
| 2074 | 2036 | &self.buffer |
| 2075 | 2037 | } |
| 2076 | 2038 | |
| 2077 | − fn workspace(&self) -> Option<View<Workspace>> { | |
| 2039 | + pub fn workspace(&self) -> Option<View<Workspace>> { | |
| 2078 | 2040 | self.workspace.as_ref()?.0.upgrade() |
| 2079 | 2041 | } |
| 2080 | 2042 | |
| 2043 | + pub fn pane(&self, cx: &AppContext) -> Option<View<Pane>> { | |
| 2044 | + self.workspace()?.read(cx).pane_for(&self.handle.upgrade()?) | |
| 2045 | + } | |
| 2046 | + | |
| 2081 | 2047 | pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> { |
| 2082 | 2048 | self.buffer().read(cx).title(cx) |
| 2083 | 2049 | } |
| @@ -2319,6 +2285,7 @@ impl Editor { · 235 unmodified lines | ||
| 2319 | 2285 | |
| 2320 | 2286 | self.blink_manager.update(cx, BlinkManager::pause_blinking); |
| 2321 | 2287 | cx.emit(EditorEvent::SelectionsChanged { local }); |
| 2288 | + cx.emit(SearchEvent::MatchesInvalidated); | |
| 2322 | 2289 | |
| 2323 | 2290 | if self.selections.disjoint_anchors().len() == 1 { |
| 2324 | 2291 | cx.emit(SearchEvent::ActiveMatchChanged) |
| @@ -3437,7 +3404,7 @@ impl Editor { · 1112 unmodified lines | ||
| 3437 | 3404 | to_insert, |
| 3438 | 3405 | }) = self.inlay_hint_cache.spawn_hint_refresh( |
| 3439 | 3406 | reason_description, |
| 3440 | − self.excerpt_visible_offsets(required_languages.as_ref(), cx), | |
| 3407 | + self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx), | |
| 3441 | 3408 | invalidate_cache, |
| 3442 | 3409 | cx, |
| 3443 | 3410 | ) { |
| @@ -3456,11 +3423,15 @@ impl Editor { · 12 unmodified lines | ||
| 3456 | 3423 | .collect() |
| 3457 | 3424 | } |
| 3458 | 3425 | |
| 3459 | − pub fn excerpt_visible_offsets( | |
| 3426 | + pub fn excerpts_for_inlay_hints_query( | |
| 3460 | 3427 | &self, |
| 3461 | 3428 | restrict_to_languages: Option<&HashSet<Arc<Language>>>, |
| 3462 | 3429 | cx: &mut ViewContext<Editor>, |
| 3463 | 3430 | ) -> HashMap<ExcerptId, (Model<Buffer>, clock::Global, Range<usize>)> { |
| 3431 | + let Some(project) = self.project.as_ref() else { | |
| 3432 | + return HashMap::default(); | |
| 3433 | + }; | |
| 3434 | + let project = project.read(cx); | |
| 3464 | 3435 | let multi_buffer = self.buffer().read(cx); |
| 3465 | 3436 | let multi_buffer_snapshot = multi_buffer.snapshot(cx); |
| 3466 | 3437 | let multi_buffer_visible_start = self |
| @@ -3480,6 +3451,15 @@ impl Editor { · 13 unmodified lines | ||
| 3480 | 3451 | .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) |
| 3481 | 3452 | .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| { |
| 3482 | 3453 | let buffer = buffer_handle.read(cx); |
| 3454 | + let buffer_file = project::worktree::File::from_dyn(buffer.file())?; | |
| 3455 | + let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; | |
| 3456 | + let worktree_entry = buffer_worktree | |
| 3457 | + .read(cx) | |
| 3458 | + .entry_for_id(buffer_file.project_entry_id(cx)?)?; | |
| 3459 | + if worktree_entry.is_ignored { | |
| 3460 | + return None; | |
| 3461 | + } | |
| 3462 | + | |
| 3483 | 3463 | let language = buffer.language()?; |
| 3484 | 3464 | if let Some(restrict_to_languages) = restrict_to_languages { |
| 3485 | 3465 | if !restrict_to_languages.contains(language) { |
| @@ -3594,7 +3574,8 @@ impl Editor { · 108 unmodified lines | ||
| 3594 | 3574 | let id = post_inc(&mut self.next_completion_id); |
| 3595 | 3575 | let task = cx.spawn(|this, mut cx| { |
| 3596 | 3576 | async move { |
| 3597 | − let menu = if let Some(completions) = completions.await.log_err() { | |
| 3577 | + let completions = completions.await.log_err(); | |
| 3578 | + let (menu, pre_resolve_task) = if let Some(completions) = completions { | |
| 3598 | 3579 | let mut menu = CompletionsMenu { |
| 3599 | 3580 | id, |
| 3600 | 3581 | initial_position: position, |
| @@ -3617,20 +3598,24 @@ impl Editor { · 16 unmodified lines | ||
| 3617 | 3598 | }; |
| 3618 | 3599 | menu.filter(query.as_deref(), cx.background_executor().clone()) |
| 3619 | 3600 | .await; |
| 3601 | + | |
| 3620 | 3602 | if menu.matches.is_empty() { |
| 3621 | − None | |
| 3603 | + (None, None) | |
| 3622 | 3604 | } else { |
| 3623 | − _ = this.update(&mut cx, |editor, cx| { | |
| 3624 | − menu.pre_resolve_completion_documentation(editor.project.clone(), cx); | |
| 3625 | − }); | |
| 3626 | − Some(menu) | |
| 3605 | + let pre_resolve_task = this | |
| 3606 | + .update(&mut cx, |editor, cx| { | |
| 3607 | + menu.pre_resolve_completion_documentation(editor, cx) | |
| 3608 | + }) | |
| 3609 | + .ok() | |
| 3610 | + .flatten(); | |
| 3611 | + (Some(menu), pre_resolve_task) | |
| 3627 | 3612 | } |
| 3628 | 3613 | } else { |
| 3629 | − None | |
| 3614 | + (None, None) | |
| 3630 | 3615 | }; |
| 3631 | 3616 | |
| 3632 | 3617 | this.update(&mut cx, |this, cx| { |
| 3633 | − this.completion_tasks.retain(|(task_id, _)| *task_id > id); | |
| 3618 | + this.completion_tasks.retain(|(task_id, _)| *task_id >= id); | |
| 3634 | 3619 | |
| 3635 | 3620 | let mut context_menu = this.context_menu.write(); |
| 3636 | 3621 | match context_menu.as_ref() { |
| @@ -3662,142 +3647,147 @@ impl Editor { · 25 unmodified lines | ||
| 3662 | 3647 | } |
| 3663 | 3648 | })?; |
| 3664 | 3649 | |
| 3650 | + if let Some(pre_resolve_task) = pre_resolve_task { | |
| 3651 | + pre_resolve_task.await; | |
| 3652 | + } | |
| 3653 | + | |
| 3665 | 3654 | Ok::<_, anyhow::Error>(()) |
| 3666 | 3655 | } |
| 3667 | 3656 | .log_err() |
| 3668 | 3657 | }); |
| 3658 | + | |
| 3669 | 3659 | self.completion_tasks.push((id, task)); |
| 3670 | 3660 | } |
| 3671 | 3661 | |
| 3672 | − // pub fn confirm_completion( | |
| 3673 | − // &mut self, | |
| 3674 | − // action: &ConfirmCompletion, | |
| 3675 | − // cx: &mut ViewContext<Self>, | |
| 3676 | − // ) -> Option<Task<Result<()>>> { | |
| 3677 | − // use language::ToOffset as _; | |
| 3662 | + pub fn confirm_completion( | |
| 3663 | + &mut self, | |
| 3664 | + action: &ConfirmCompletion, | |
| 3665 | + cx: &mut ViewContext<Self>, | |
| 3666 | + ) -> Option<Task<Result<()>>> { | |
| 3667 | + use language::ToOffset as _; | |
| 3678 | 3668 | |
| 3679 | − // let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? { | |
| 3680 | − // menu | |
| 3681 | − // } else { | |
| 3682 | − // return None; | |
| 3683 | − // }; | |
| 3669 | + let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? { | |
| 3670 | + menu | |
| 3671 | + } else { | |
| 3672 | + return None; | |
| 3673 | + }; | |
| 3684 | 3674 | |
| 3685 | − // let mat = completions_menu | |
| 3686 | − // .matches | |
| 3687 | − // .get(action.item_ix.unwrap_or(completions_menu.selected_item))?; | |
| 3688 | − // let buffer_handle = completions_menu.buffer; | |
| 3689 | − // let completions = completions_menu.completions.read(); | |
| 3690 | − // let completion = completions.get(mat.candidate_id)?; | |
| 3691 | − | |
| 3692 | − // let snippet; | |
| 3693 | − // let text; | |
| 3694 | − // if completion.is_snippet() { | |
| 3695 | − // snippet = Some(Snippet::parse(&completion.new_text).log_err()?); | |
| 3696 | − // text = snippet.as_ref().unwrap().text.clone(); | |
| 3697 | − // } else { | |
| 3698 | − // snippet = None; | |
| 3699 | − // text = completion.new_text.clone(); | |
| 3700 | − // }; | |
| 3701 | − // let selections = self.selections.all::<usize>(cx); | |
| 3702 | − // let buffer = buffer_handle.read(cx); | |
| 3703 | − // let old_range = completion.old_range.to_offset(buffer); | |
| 3704 | − // let old_text = buffer.text_for_range(old_range.clone()).collect::<String>(); | |
| 3705 | − | |
| 3706 | − // let newest_selection = self.selections.newest_anchor(); | |
| 3707 | − // if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) { | |
| 3708 | − // return None; | |
| 3709 | − // } | |
| 3675 | + let mat = completions_menu | |
| 3676 | + .matches | |
| 3677 | + .get(action.item_ix.unwrap_or(completions_menu.selected_item))?; | |
| 3678 | + let buffer_handle = completions_menu.buffer; | |
| 3679 | + let completions = completions_menu.completions.read(); | |
| 3680 | + let completion = completions.get(mat.candidate_id)?; | |
| 3681 | + | |
| 3682 | + let snippet; | |
| 3683 | + let text; | |
| 3684 | + if completion.is_snippet() { | |
| 3685 | + snippet = Some(Snippet::parse(&completion.new_text).log_err()?); | |
| 3686 | + text = snippet.as_ref().unwrap().text.clone(); | |
| 3687 | + } else { | |
| 3688 | + snippet = None; | |
| 3689 | + text = completion.new_text.clone(); | |
| 3690 | + }; | |
| 3691 | + let selections = self.selections.all::<usize>(cx); | |
| 3692 | + let buffer = buffer_handle.read(cx); | |
| 3693 | + let old_range = completion.old_range.to_offset(buffer); | |
| 3694 | + let old_text = buffer.text_for_range(old_range.clone()).collect::<String>(); | |
| 3710 | 3695 | |
| 3711 | − // let lookbehind = newest_selection | |
| 3712 | − // .start | |
| 3713 | − // .text_anchor | |
| 3714 | − // .to_offset(buffer) | |
| 3715 | − // .saturating_sub(old_range.start); | |
| 3716 | − // let lookahead = old_range | |
| 3717 | − // .end | |
| 3718 | − // .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); | |
| 3719 | − // let mut common_prefix_len = old_text | |
| 3720 | − // .bytes() | |
| 3721 | − // .zip(text.bytes()) | |
| 3722 | − // .take_while(|(a, b)| a == b) | |
| 3723 | − // .count(); | |
| 3724 | − | |
| 3725 | − // let snapshot = self.buffer.read(cx).snapshot(cx); | |
| 3726 | − // let mut range_to_replace: Option<Range<isize>> = None; | |
| 3727 | − // let mut ranges = Vec::new(); | |
| 3728 | − // for selection in &selections { | |
| 3729 | − // if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { | |
| 3730 | − // let start = selection.start.saturating_sub(lookbehind); | |
| 3731 | − // let end = selection.end + lookahead; | |
| 3732 | − // if selection.id == newest_selection.id { | |
| 3733 | − // range_to_replace = Some( | |
| 3734 | − // ((start + common_prefix_len) as isize - selection.start as isize) | |
| 3735 | − // ..(end as isize - selection.start as isize), | |
| 3736 | − // ); | |
| 3737 | − // } | |
| 3738 | − // ranges.push(start + common_prefix_len..end); | |
| 3739 | − // } else { | |
| 3740 | − // common_prefix_len = 0; | |
| 3741 | − // ranges.clear(); | |
| 3742 | − // ranges.extend(selections.iter().map(|s| { | |
| 3743 | − // if s.id == newest_selection.id { | |
| 3744 | − // range_to_replace = Some( | |
| 3745 | − // old_range.start.to_offset_utf16(&snapshot).0 as isize | |
| 3746 | − // - selection.start as isize | |
| 3747 | − // ..old_range.end.to_offset_utf16(&snapshot).0 as isize | |
| 3748 | − // - selection.start as isize, | |
| 3749 | − // ); | |
| 3750 | − // old_range.clone() | |
| 3751 | − // } else { | |
| 3752 | − // s.start..s.end | |
| 3753 | − // } | |
| 3754 | − // })); | |
| 3755 | − // break; | |
| 3756 | − // } | |
| 3757 | − // } | |
| 3758 | − // let text = &text[common_prefix_len..]; | |
| 3696 | + let newest_selection = self.selections.newest_anchor(); | |
| 3697 | + if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) { | |
| 3698 | + return None; | |
| 3699 | + } | |
| 3759 | 3700 | |
| 3760 | − // cx.emit(Event::InputHandled { | |
| 3761 | − // utf16_range_to_replace: range_to_replace, | |
| 3762 | − // text: text.into(), | |
| 3763 | − // }); | |
| 3701 | + let lookbehind = newest_selection | |
| 3702 | + .start | |
| 3703 | + .text_anchor | |
| 3704 | + .to_offset(buffer) | |
| 3705 | + .saturating_sub(old_range.start); | |
| 3706 | + let lookahead = old_range | |
| 3707 | + .end | |
| 3708 | + .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); | |
| 3709 | + let mut common_prefix_len = old_text | |
| 3710 | + .bytes() | |
| 3711 | + .zip(text.bytes()) | |
| 3712 | + .take_while(|(a, b)| a == b) | |
| 3713 | + .count(); | |
| 3764 | 3714 | |
| 3765 | − // self.transact(cx, |this, cx| { | |
| 3766 | − // if let Some(mut snippet) = snippet { | |
| 3767 | − // snippet.text = text.to_string(); | |
| 3768 | − // for tabstop in snippet.tabstops.iter_mut().flatten() { | |
| 3769 | − // tabstop.start -= common_prefix_len as isize; | |
| 3770 | − // tabstop.end -= common_prefix_len as isize; | |
| 3771 | − // } | |
| 3715 | + let snapshot = self.buffer.read(cx).snapshot(cx); | |
| 3716 | + let mut range_to_replace: Option<Range<isize>> = None; | |
| 3717 | + let mut ranges = Vec::new(); | |
| 3718 | + for selection in &selections { | |
| 3719 | + if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { | |
| 3720 | + let start = selection.start.saturating_sub(lookbehind); | |
| 3721 | + let end = selection.end + lookahead; | |
| 3722 | + if selection.id == newest_selection.id { | |
| 3723 | + range_to_replace = Some( | |
| 3724 | + ((start + common_prefix_len) as isize - selection.start as isize) | |
| 3725 | + ..(end as isize - selection.start as isize), | |
| 3726 | + ); | |
| 3727 | + } | |
| 3728 | + ranges.push(start + common_prefix_len..end); | |
| 3729 | + } else { | |
| 3730 | + common_prefix_len = 0; | |
| 3731 | + ranges.clear(); | |
| 3732 | + ranges.extend(selections.iter().map(|s| { | |
| 3733 | + if s.id == newest_selection.id { | |
| 3734 | + range_to_replace = Some( | |
| 3735 | + old_range.start.to_offset_utf16(&snapshot).0 as isize | |
| 3736 | + - selection.start as isize | |
| 3737 | + ..old_range.end.to_offset_utf16(&snapshot).0 as isize | |
| 3738 | + - selection.start as isize, | |
| 3739 | + ); | |
| 3740 | + old_range.clone() | |
| 3741 | + } else { | |
| 3742 | + s.start..s.end | |
| 3743 | + } | |
| 3744 | + })); | |
| 3745 | + break; | |
| 3746 | + } | |
| 3747 | + } | |
| 3748 | + let text = &text[common_prefix_len..]; | |
| 3772 | 3749 | |
| 3773 | − // this.insert_snippet(&ranges, snippet, cx).log_err(); | |
| 3774 | − // } else { | |
| 3775 | − // this.buffer.update(cx, |buffer, cx| { | |
| 3776 | − // buffer.edit( | |
| 3777 | − // ranges.iter().map(|range| (range.clone(), text)), | |
| 3778 | − // this.autoindent_mode.clone(), | |
| 3779 | − // cx, | |
| 3780 | − // ); | |
| 3781 | − // }); | |
| 3782 | − // } | |
| 3750 | + cx.emit(EditorEvent::InputHandled { | |
| 3751 | + utf16_range_to_replace: range_to_replace, | |
| 3752 | + text: text.into(), | |
| 3753 | + }); | |
| 3783 | 3754 | |
| 3784 | − // this.refresh_copilot_suggestions(true, cx); | |
| 3785 | − // }); | |
| 3755 | + self.transact(cx, |this, cx| { | |
| 3756 | + if let Some(mut snippet) = snippet { | |
| 3757 | + snippet.text = text.to_string(); | |
| 3758 | + for tabstop in snippet.tabstops.iter_mut().flatten() { | |
| 3759 | + tabstop.start -= common_prefix_len as isize; | |
| 3760 | + tabstop.end -= common_prefix_len as isize; | |
| 3761 | + } | |
| 3786 | 3762 | |
| 3787 | − // let project = self.project.clone()?; | |
| 3788 | − // let apply_edits = project.update(cx, |project, cx| { | |
| 3789 | − // project.apply_additional_edits_for_completion( | |
| 3790 | − // buffer_handle, | |
| 3791 | − // completion.clone(), | |
| 3792 | − // true, | |
| 3793 | − // cx, | |
| 3794 | − // ) | |
| 3795 | − // }); | |
| 3796 | − // Some(cx.foreground().spawn(async move { | |
| 3797 | − // apply_edits.await?; | |
| 3798 | − // Ok(()) | |
| 3799 | − // })) | |
| 3800 | − // } | |
| 3763 | + this.insert_snippet(&ranges, snippet, cx).log_err(); | |
| 3764 | + } else { | |
| 3765 | + this.buffer.update(cx, |buffer, cx| { | |
| 3766 | + buffer.edit( | |
| 3767 | + ranges.iter().map(|range| (range.clone(), text)), | |
| 3768 | + this.autoindent_mode.clone(), | |
| 3769 | + cx, | |
| 3770 | + ); | |
| 3771 | + }); | |
| 3772 | + } | |
| 3773 | + | |
| 3774 | + this.refresh_copilot_suggestions(true, cx); | |
| 3775 | + }); | |
| 3776 | + | |
| 3777 | + let project = self.project.clone()?; | |
| 3778 | + let apply_edits = project.update(cx, |project, cx| { | |
| 3779 | + project.apply_additional_edits_for_completion( | |
| 3780 | + buffer_handle, | |
| 3781 | + completion.clone(), | |
| 3782 | + true, | |
| 3783 | + cx, | |
| 3784 | + ) | |
| 3785 | + }); | |
| 3786 | + Some(cx.foreground_executor().spawn(async move { | |
| 3787 | + apply_edits.await?; | |
| 3788 | + Ok(()) | |
| 3789 | + })) | |
| 3790 | + } | |
| 3801 | 3791 | |
| 3802 | 3792 | pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) { |
| 3803 | 3793 | let mut context_menu = self.context_menu.write(); |
| @@ -4353,19 +4343,19 @@ impl Editor { · 549 unmodified lines | ||
| 4353 | 4343 | style: &EditorStyle, |
| 4354 | 4344 | is_active: bool, |
| 4355 | 4345 | cx: &mut ViewContext<Self>, |
| 4356 | − ) -> Option<AnyElement<Self>> { | |
| 4346 | + ) -> Option<IconButton> { | |
| 4357 | 4347 | if self.available_code_actions.is_some() { |
| 4358 | 4348 | Some( |
| 4359 | − IconButton::new("code_actions_indicator", ui::Icon::Bolt) | |
| 4360 | − .on_click(|editor: &mut Editor, cx| { | |
| 4349 | + IconButton::new("code_actions_indicator", ui::Icon::Bolt).on_click(cx.listener( | |
| 4350 | + |editor, e, cx| { | |
| 4361 | 4351 | editor.toggle_code_actions( |
| 4362 | 4352 | &ToggleCodeActions { |
| 4363 | 4353 | deployed_from_indicator: true, |
| 4364 | 4354 | }, |
| 4365 | 4355 | cx, |
| 4366 | 4356 | ); |
| 4367 | − }) | |
| 4368 | − .render(), | |
| 4357 | + }, | |
| 4358 | + )), | |
| 4369 | 4359 | ) |
| 4370 | 4360 | } else { |
| 4371 | 4361 | None |
| @@ -4380,7 +4370,7 @@ impl Editor { · 8 unmodified lines | ||
| 4380 | 4370 | line_height: Pixels, |
| 4381 | 4371 | gutter_margin: Pixels, |
| 4382 | 4372 | cx: &mut ViewContext<Self>, |
| 4383 | − ) -> Vec<Option<AnyElement<Self>>> { | |
| 4373 | + ) -> Vec<Option<IconButton>> { | |
| 4384 | 4374 | fold_data |
| 4385 | 4375 | .iter() |
| 4386 | 4376 | .enumerate() |
| @@ -4393,16 +4383,15 @@ impl Editor { · 6 unmodified lines | ||
| 4393 | 4383 | FoldStatus::Foldable => ui::Icon::ChevronDown, |
| 4394 | 4384 | }; |
| 4395 | 4385 | IconButton::new(ix as usize, icon) |
| 4396 | − .on_click(move |editor: &mut Editor, cx| match fold_status { | |
| 4386 | + .on_click(cx.listener(move |editor, e, cx| match fold_status { | |
| 4397 | 4387 | FoldStatus::Folded => { |
| 4398 | 4388 | editor.unfold_at(&UnfoldAt { buffer_row }, cx); |
| 4399 | 4389 | } |
| 4400 | 4390 | FoldStatus::Foldable => { |
| 4401 | 4391 | editor.fold_at(&FoldAt { buffer_row }, cx); |
| 4402 | 4392 | } |
| 4403 | − }) | |
| 4404 | − .color(ui::TextColor::Muted) | |
| 4405 | − .render() | |
| 4393 | + })) | |
| 4394 | + .color(ui::Color::Muted) | |
| 4406 | 4395 | }) |
| 4407 | 4396 | }) |
| 4408 | 4397 | .flatten() |
| @@ -4421,12 +4410,14 @@ impl Editor { · 12 unmodified lines | ||
| 4421 | 4410 | &self, |
| 4422 | 4411 | cursor_position: DisplayPoint, |
| 4423 | 4412 | style: &EditorStyle, |
| 4413 | + max_height: Pixels, | |
| 4424 | 4414 | cx: &mut ViewContext<Editor>, |
| 4425 | − ) -> Option<(DisplayPoint, AnyElement<Editor>)> { | |
| 4415 | + ) -> Option<(DisplayPoint, AnyElement)> { | |
| 4426 | 4416 | self.context_menu.read().as_ref().map(|menu| { |
| 4427 | 4417 | menu.render( |
| 4428 | 4418 | cursor_position, |
| 4429 | 4419 | style, |
| 4420 | + max_height, | |
| 4430 | 4421 | self.workspace.as_ref().map(|(w, _)| w.clone()), |
| 4431 | 4422 | cx, |
| 4432 | 4423 | ) |
| @@ -7781,7 +7772,7 @@ impl Editor { · 3348 unmodified lines | ||
| 7781 | 7772 | } |
| 7782 | 7773 | div() |
| 7783 | 7774 | .pl(cx.anchor_x) |
| 7784 | − .child(rename_editor.render_with(EditorElement::new( | |
| 7775 | + .child(EditorElement::new( | |
| 7785 | 7776 | &rename_editor, |
| 7786 | 7777 | EditorStyle { |
| 7787 | 7778 | background: cx.theme().system().transparent, |
| @@ -7789,11 +7780,13 @@ impl Editor { · 1 unmodified line | ||
| 7789 | 7780 | text: text_style, |
| 7790 | 7781 | scrollbar_width: cx.editor_style.scrollbar_width, |
| 7791 | 7782 | syntax: cx.editor_style.syntax.clone(), |
| 7792 | − diagnostic_style: | |
| 7793 | − cx.editor_style.diagnostic_style.clone(), | |
| 7783 | + diagnostic_style: cx | |
| 7784 | + .editor_style | |
| 7785 | + .diagnostic_style | |
| 7786 | + .clone(), | |
| 7794 | 7787 | }, |
| 7795 | − ))) | |
| 7796 | − .render() | |
| 7788 | + )) | |
| 7789 | + .into_any_element() | |
| 7797 | 7790 | } |
| 7798 | 7791 | }), |
| 7799 | 7792 | disposition: BlockDisposition::Below, |
| @@ -9207,6 +9200,26 @@ impl Editor { · 1407 unmodified lines | ||
| 9207 | 9200 | cx.emit(EditorEvent::Blurred); |
| 9208 | 9201 | cx.notify(); |
| 9209 | 9202 | } |
| 9203 | + | |
| 9204 | + pub fn register_action<A: Action>( | |
| 9205 | + &mut self, | |
| 9206 | + listener: impl Fn(&A, &mut WindowContext) + 'static, | |
| 9207 | + ) -> &mut Self { | |
| 9208 | + let listener = Arc::new(listener); | |
| 9209 | + | |
| 9210 | + self.editor_actions.push(Box::new(move |cx| { | |
| 9211 | + let view = cx.view().clone(); | |
| 9212 | + let cx = cx.window_context(); | |
| 9213 | + let listener = listener.clone(); | |
| 9214 | + cx.on_action(TypeId::of::<A>(), move |action, phase, cx| { | |
| 9215 | + let action = action.downcast_ref().unwrap(); | |
| 9216 | + if phase == DispatchPhase::Bubble { | |
| 9217 | + listener(action, cx) | |
| 9218 | + } | |
| 9219 | + }) | |
| 9220 | + })); | |
| 9221 | + self | |
| 9222 | + } | |
| 9210 | 9223 | } |
| 9211 | 9224 | |
| 9212 | 9225 | pub trait CollaborationHub { |
| @@ -9396,7 +9409,9 @@ impl Render for Editor { · 183 unmodified lines | ||
| 9396 | 9409 | font_weight: FontWeight::NORMAL, |
| 9397 | 9410 | font_style: FontStyle::Normal, |
| 9398 | 9411 | line_height: relative(1.).into(), |
| 9412 | + background_color: None, | |
| 9399 | 9413 | underline: None, |
| 9414 | + white_space: WhiteSpace::Normal, | |
| 9400 | 9415 | }, |
| 9401 | 9416 | |
| 9402 | 9417 | EditorMode::AutoHeight { max_lines } => todo!(), |
| @@ -9409,7 +9424,9 @@ impl Render for Editor { · 6 unmodified lines | ||
| 9409 | 9424 | font_weight: FontWeight::NORMAL, |
| 9410 | 9425 | font_style: FontStyle::Normal, |
| 9411 | 9426 | line_height: relative(settings.buffer_line_height.value()), |
| 9427 | + background_color: None, | |
| 9412 | 9428 | underline: None, |
| 9429 | + white_space: WhiteSpace::Normal, | |
| 9413 | 9430 | }, |
| 9414 | 9431 | }; |
| 9415 | 9432 | |
| @@ -9996,11 +10013,11 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend · 580 unmodified lines | ||
| 9996 | 10013 | .ml(cx.anchor_x) |
| 9997 | 10014 | })) |
| 9998 | 10015 | .cursor_pointer() |
| 9999 | − .on_click(move |_, _, cx| { | |
| 10016 | + .on_click(cx.listener(move |_, _, cx| { | |
| 10000 | 10017 | cx.write_to_clipboard(ClipboardItem::new(message.clone())); |
| 10001 | − }) | |
| 10002 | − .tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx)) | |
| 10003 | − .render() | |
| 10018 | + })) | |
| 10019 | + .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)) | |
| 10020 | + .into_any_element() | |
| 10004 | 10021 | }) |
| 10005 | 10022 | } |
| 10006 | 10023 | |
| @@ -10047,120 +10064,75 @@ pub fn diagnostic_style( · 40 unmodified lines | ||
| 10047 | 10064 | } |
| 10048 | 10065 | } |
| 10049 | 10066 | |
| 10050 | −pub fn combine_syntax_and_fuzzy_match_highlights( | |
| 10067 | +pub fn text_runs_for_highlights( | |
| 10051 | 10068 | text: &str, |
| 10052 | − default_style: HighlightStyle, | |
| 10053 | − syntax_ranges: impl Iterator<Item = (Range<usize>, HighlightStyle)>, | |
| 10054 | − match_indices: &[usize], | |
| 10055 | −) -> Vec<(Range<usize>, HighlightStyle)> { | |
| 10056 | − let mut result = Vec::new(); | |
| 10057 | − let mut match_indices = match_indices.iter().copied().peekable(); | |
| 10058 | − | |
| 10059 | − for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) | |
| 10060 | − { | |
| 10061 | − syntax_highlight.font_weight = None; | |
| 10062 | − | |
| 10063 | − // Add highlights for any fuzzy match characters before the next | |
| 10064 | − // syntax highlight range. | |
| 10065 | − while let Some(&match_index) = match_indices.peek() { | |
| 10066 | − if match_index >= range.start { | |
| 10067 | − break; | |
| 10068 | − } | |
| 10069 | − match_indices.next(); | |
| 10070 | − let end_index = char_ix_after(match_index, text); | |
| 10071 | − let mut match_style = default_style; | |
| 10072 | − match_style.font_weight = Some(FontWeight::BOLD); | |
| 10073 | − result.push((match_index..end_index, match_style)); | |
| 10074 | − } | |
| 10075 | − | |
| 10076 | − if range.start == usize::MAX { | |
| 10077 | − break; | |
| 10078 | − } | |
| 10079 | − | |
| 10080 | − // Add highlights for any fuzzy match characters within the | |
| 10081 | − // syntax highlight range. | |
| 10082 | − let mut offset = range.start; | |
| 10083 | − while let Some(&match_index) = match_indices.peek() { | |
| 10084 | − if match_index >= range.end { | |
| 10085 | − break; | |
| 10086 | − } | |
| 10087 | − | |
| 10088 | − match_indices.next(); | |
| 10089 | − if match_index > offset { | |
| 10090 | − result.push((offset..match_index, syntax_highlight)); | |
| 10091 | − } | |
| 10092 | − | |
| 10093 | − let mut end_index = char_ix_after(match_index, text); | |
| 10094 | − while let Some(&next_match_index) = match_indices.peek() { | |
| 10095 | − if next_match_index == end_index && next_match_index < range.end { | |
| 10096 | − end_index = char_ix_after(next_match_index, text); | |
| 10097 | − match_indices.next(); | |
| 10098 | − } else { | |
| 10099 | − break; | |
| 10100 | − } | |
| 10101 | − } | |
| 10102 | − | |
| 10103 | − let mut match_style = syntax_highlight; | |
| 10104 | − match_style.font_weight = Some(FontWeight::BOLD); | |
| 10105 | − result.push((match_index..end_index, match_style)); | |
| 10106 | − offset = end_index; | |
| 10107 | − } | |
| 10108 | − | |
| 10109 | − if offset < range.end { | |
| 10110 | − result.push((offset..range.end, syntax_highlight)); | |
| 10111 | − } | |
| 10069 | + default_style: &TextStyle, | |
| 10070 | + highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>, | |
| 10071 | +) -> Vec<TextRun> { | |
| 10072 | + let mut runs = Vec::new(); | |
| 10073 | + let mut ix = 0; | |
| 10074 | + for (range, highlight) in highlights { | |
| 10075 | + if ix < range.start { | |
| 10076 | + runs.push(default_style.clone().to_run(range.start - ix)); | |
| 10077 | + } | |
| 10078 | + runs.push( | |
| 10079 | + default_style | |
| 10080 | + .clone() | |
| 10081 | + .highlight(highlight) | |
| 10082 | + .to_run(range.len()), | |
| 10083 | + ); | |
| 10084 | + ix = range.end; | |
| 10112 | 10085 | } |
| 10113 | − | |
| 10114 | − fn char_ix_after(ix: usize, text: &str) -> usize { | |
| 10115 | − ix + text[ix..].chars().next().unwrap().len_utf8() | |
| 10086 | + if ix < text.len() { | |
| 10087 | + runs.push(default_style.to_run(text.len() - ix)); | |
| 10116 | 10088 | } |
| 10117 | − | |
| 10118 | − result | |
| 10089 | + runs | |
| 10119 | 10090 | } |
| 10120 | 10091 | |
| 10121 | −// pub fn styled_runs_for_code_label<'a>( | |
| 10122 | −// label: &'a CodeLabel, | |
| 10123 | −// syntax_theme: &'a theme::SyntaxTheme, | |
| 10124 | −// ) -> impl 'a + Iterator<Item = (Range<usize>, HighlightStyle)> { | |
| 10125 | −// let fade_out = HighlightStyle { | |
| 10126 | −// fade_out: Some(0.35), | |
| 10127 | −// ..Default::default() | |
| 10128 | −// }; | |
| 10092 | +pub fn styled_runs_for_code_label<'a>( | |
| 10093 | + label: &'a CodeLabel, | |
| 10094 | + syntax_theme: &'a theme::SyntaxTheme, | |
| 10095 | +) -> impl 'a + Iterator<Item = (Range<usize>, HighlightStyle)> { | |
| 10096 | + let fade_out = HighlightStyle { | |
| 10097 | + fade_out: Some(0.35), | |
| 10098 | + ..Default::default() | |
| 10099 | + }; | |
| 10100 | + | |
| 10101 | + let mut prev_end = label.filter_range.end; | |
| 10102 | + label | |
| 10103 | + .runs | |
| 10104 | + .iter() | |
| 10105 | + .enumerate() | |
| 10106 | + .flat_map(move |(ix, (range, highlight_id))| { | |
| 10107 | + let style = if let Some(style) = highlight_id.style(syntax_theme) { | |
| 10108 | + style | |
| 10109 | + } else { | |
| 10110 | + return Default::default(); | |
| 10111 | + }; | |
| 10112 | + let mut muted_style = style; | |
| 10113 | + muted_style.highlight(fade_out); | |
| 10129 | 10114 | |
| 10130 | −// let mut prev_end = label.filter_range.end; | |
| 10131 | −// label | |
| 10132 | −// .runs | |
| 10133 | −// .iter() | |
| 10134 | −// .enumerate() | |
| 10135 | −// .flat_map(move |(ix, (range, highlight_id))| { | |
| 10136 | −// let style = if let Some(style) = highlight_id.style(syntax_theme) { | |
| 10137 | −// style | |
| 10138 | −// } else { | |
| 10139 | −// return Default::default(); | |
| 10140 | −// }; | |
| 10141 | −// let mut muted_style = style; | |
| 10142 | −// muted_style.highlight(fade_out); | |
| 10143 | − | |
| 10144 | −// let mut runs = SmallVec::<[(Range<usize>, HighlightStyle); 3]>::new(); | |
| 10145 | −// if range.start >= label.filter_range.end { | |
| 10146 | −// if range.start > prev_end { | |
| 10147 | −// runs.push((prev_end..range.start, fade_out)); | |
| 10148 | −// } | |
| 10149 | −// runs.push((range.clone(), muted_style)); | |
| 10150 | −// } else if range.end <= label.filter_range.end { | |
| 10151 | −// runs.push((range.clone(), style)); | |
| 10152 | −// } else { | |
| 10153 | −// runs.push((range.start..label.filter_range.end, style)); | |
| 10154 | −// runs.push((label.filter_range.end..range.end, muted_style)); | |
| 10155 | −// } | |
| 10156 | −// prev_end = cmp::max(prev_end, range.end); | |
| 10115 | + let mut runs = SmallVec::<[(Range<usize>, HighlightStyle); 3]>::new(); | |
| 10116 | + if range.start >= label.filter_range.end { | |
| 10117 | + if range.start > prev_end { | |
| 10118 | + runs.push((prev_end..range.start, fade_out)); | |
| 10119 | + } | |
| 10120 | + runs.push((range.clone(), muted_style)); | |
| 10121 | + } else if range.end <= label.filter_range.end { | |
| 10122 | + runs.push((range.clone(), style)); | |
| 10123 | + } else { | |
| 10124 | + runs.push((range.start..label.filter_range.end, style)); | |
| 10125 | + runs.push((label.filter_range.end..range.end, muted_style)); | |
| 10126 | + } | |
| 10127 | + prev_end = cmp::max(prev_end, range.end); | |
| 10157 | 10128 | |
| 10158 | −// if ix + 1 == label.runs.len() && label.text.len() > prev_end { | |
| 10159 | −// runs.push((prev_end..label.text.len(), fade_out)); | |
| 10160 | −// } | |
| 10129 | + if ix + 1 == label.runs.len() && label.text.len() > prev_end { | |
| 10130 | + runs.push((prev_end..label.text.len(), fade_out)); | |
| 10131 | + } | |
| 10161 | 10132 | |
| 10162 | −// runs | |
| 10163 | −// }) | |
| 10133 | + runs | |
| 10134 | + }) | |
| 10135 | +} | |
| 10164 | 10136 | |
| 10165 | 10137 | pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str> + 'a { |
| 10166 | 10138 | let mut index = 0; |
| @@ -3048,7 +3048,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { · 3047 unmodified lines | ||
| 3048 | 3048 | position: snapshot.anchor_after(Point::new(2, 0)), |
| 3049 | 3049 | disposition: BlockDisposition::Below, |
| 3050 | 3050 | height: 1, |
| 3051 | − render: Arc::new(|_| div().render()), | |
| 3051 | + render: Arc::new(|_| div().into_any()), | |
| 3052 | 3052 | }], |
| 3053 | 3053 | Some(Autoscroll::fit()), |
| 3054 | 3054 | cx, |
| @@ -6740,75 +6740,6 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { · 3685 unmodified lines | ||
| 6740 | 6740 | // ); |
| 6741 | 6741 | // } |
| 6742 | 6742 | |
| 6743 | −#[test] | |
| 6744 | −fn test_combine_syntax_and_fuzzy_match_highlights() { | |
| 6745 | − let string = "abcdefghijklmnop"; | |
| 6746 | − let syntax_ranges = [ | |
| 6747 | − ( | |
| 6748 | − 0..3, | |
| 6749 | − HighlightStyle { | |
| 6750 | − color: Some(Hsla::red()), | |
| 6751 | − ..Default::default() | |
| 6752 | − }, | |
| 6753 | − ), | |
| 6754 | − ( | |
| 6755 | − 4..8, | |
| 6756 | − HighlightStyle { | |
| 6757 | − color: Some(Hsla::green()), | |
| 6758 | − ..Default::default() | |
| 6759 | − }, | |
| 6760 | − ), | |
| 6761 | − ]; | |
| 6762 | − let match_indices = [4, 6, 7, 8]; | |
| 6763 | − assert_eq!( | |
| 6764 | − combine_syntax_and_fuzzy_match_highlights( | |
| 6765 | − string, | |
| 6766 | − Default::default(), | |
| 6767 | − syntax_ranges.into_iter(), | |
| 6768 | − &match_indices, | |
| 6769 | − ), | |
| 6770 | − &[ | |
| 6771 | − ( | |
| 6772 | − 0..3, | |
| 6773 | − HighlightStyle { | |
| 6774 | − color: Some(Hsla::red()), | |
| 6775 | − ..Default::default() | |
| 6776 | − }, | |
| 6777 | − ), | |
| 6778 | − ( | |
| 6779 | − 4..5, | |
| 6780 | − HighlightStyle { | |
| 6781 | − color: Some(Hsla::green()), | |
| 6782 | − font_weight: Some(gpui::FontWeight::BOLD), | |
| 6783 | − ..Default::default() | |
| 6784 | − }, | |
| 6785 | − ), | |
| 6786 | − ( | |
| 6787 | − 5..6, | |
| 6788 | − HighlightStyle { | |
| 6789 | − color: Some(Hsla::green()), | |
| 6790 | − ..Default::default() | |
| 6791 | − }, | |
| 6792 | − ), | |
| 6793 | − ( | |
| 6794 | − 6..8, | |
| 6795 | − HighlightStyle { | |
| 6796 | − color: Some(Hsla::green()), | |
| 6797 | − font_weight: Some(gpui::FontWeight::BOLD), | |
| 6798 | − ..Default::default() | |
| 6799 | − }, | |
| 6800 | − ), | |
| 6801 | − ( | |
| 6802 | − 8..9, | |
| 6803 | − HighlightStyle { | |
| 6804 | − font_weight: Some(gpui::FontWeight::BOLD), | |
| 6805 | − ..Default::default() | |
| 6806 | − }, | |
| 6807 | − ), | |
| 6808 | − ] | |
| 6809 | − ); | |
| 6810 | −} | |
| 6811 | − | |
| 6812 | 6743 | #[gpui::test] |
| 6813 | 6744 | async fn go_to_prev_overlapping_diagnostic( |
| 6814 | 6745 | executor: BackgroundExecutor, |
| @@ -5,7 +5,9 @@ use crate::{ · 4 unmodified lines | ||
| 5 | 5 | }, |
| 6 | 6 | editor_settings::ShowScrollbar, |
| 7 | 7 | git::{diff_hunk_to_display, DisplayDiffHunk}, |
| 8 | − hover_popover::hover_at, | |
| 8 | + hover_popover::{ | |
| 9 | + self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, | |
| 10 | + }, | |
| 9 | 11 | link_go_to_definition::{ |
| 10 | 12 | go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, |
| 11 | 13 | update_inlay_link_and_hover_points, GoToDefinitionTrigger, |
| @@ -19,11 +21,11 @@ use anyhow::Result; · 7 unmodified lines | ||
| 19 | 21 | use collections::{BTreeMap, HashMap}; |
| 20 | 22 | use gpui::{ |
| 21 | 23 | div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, |
| 22 | − BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, | |
| 23 | − ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout, | |
| 24 | − MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, | |
| 25 | − ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled, | |
| 26 | − TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine, | |
| 24 | + BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, | |
| 25 | + ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, IntoElement, LineLayout, | |
| 26 | + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, | |
| 27 | + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, | |
| 28 | + TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine, | |
| 27 | 29 | }; |
| 28 | 30 | use itertools::Itertools; |
| 29 | 31 | use language::language_settings::ShowWhitespaceSetting; |
| @@ -112,18 +114,202 @@ impl SelectionLayout { · 82 unmodified lines | ||
| 112 | 114 | } |
| 113 | 115 | |
| 114 | 116 | pub struct EditorElement { |
| 115 | − editor_id: EntityId, | |
| 117 | + editor: View<Editor>, | |
| 116 | 118 | style: EditorStyle, |
| 117 | 119 | } |
| 118 | 120 | |
| 119 | 121 | impl EditorElement { |
| 120 | 122 | pub fn new(editor: &View<Editor>, style: EditorStyle) -> Self { |
| 121 | 123 | Self { |
| 122 | − editor_id: editor.entity_id(), | |
| 124 | + editor: editor.clone(), | |
| 123 | 125 | style, |
| 124 | 126 | } |
| 125 | 127 | } |
| 126 | 128 | |
| 129 | + fn register_actions(&self, cx: &mut WindowContext) { | |
| 130 | + let view = &self.editor; | |
| 131 | + self.editor.update(cx, |editor, cx| { | |
| 132 | + for action in editor.editor_actions.iter() { | |
| 133 | + (action)(cx) | |
| 134 | + } | |
| 135 | + }); | |
| 136 | + register_action(view, cx, Editor::move_left); | |
| 137 | + register_action(view, cx, Editor::move_right); | |
| 138 | + register_action(view, cx, Editor::move_down); | |
| 139 | + register_action(view, cx, Editor::move_up); | |
| 140 | + // on_action(cx, Editor::new_file); todo!() | |
| 141 | + // on_action(cx, Editor::new_file_in_direction); todo!() | |
| 142 | + register_action(view, cx, Editor::cancel); | |
| 143 | + register_action(view, cx, Editor::newline); | |
| 144 | + register_action(view, cx, Editor::newline_above); | |
| 145 | + register_action(view, cx, Editor::newline_below); | |
| 146 | + register_action(view, cx, Editor::backspace); | |
| 147 | + register_action(view, cx, Editor::delete); | |
| 148 | + register_action(view, cx, Editor::tab); | |
| 149 | + register_action(view, cx, Editor::tab_prev); | |
| 150 | + register_action(view, cx, Editor::indent); | |
| 151 | + register_action(view, cx, Editor::outdent); | |
| 152 | + register_action(view, cx, Editor::delete_line); | |
| 153 | + register_action(view, cx, Editor::join_lines); | |
| 154 | + register_action(view, cx, Editor::sort_lines_case_sensitive); | |
| 155 | + register_action(view, cx, Editor::sort_lines_case_insensitive); | |
| 156 | + register_action(view, cx, Editor::reverse_lines); | |
| 157 | + register_action(view, cx, Editor::shuffle_lines); | |
| 158 | + register_action(view, cx, Editor::convert_to_upper_case); | |
| 159 | + register_action(view, cx, Editor::convert_to_lower_case); | |
| 160 | + register_action(view, cx, Editor::convert_to_title_case); | |
| 161 | + register_action(view, cx, Editor::convert_to_snake_case); | |
| 162 | + register_action(view, cx, Editor::convert_to_kebab_case); | |
| 163 | + register_action(view, cx, Editor::convert_to_upper_camel_case); | |
| 164 | + register_action(view, cx, Editor::convert_to_lower_camel_case); | |
| 165 | + register_action(view, cx, Editor::delete_to_previous_word_start); | |
| 166 | + register_action(view, cx, Editor::delete_to_previous_subword_start); | |
| 167 | + register_action(view, cx, Editor::delete_to_next_word_end); | |
| 168 | + register_action(view, cx, Editor::delete_to_next_subword_end); | |
| 169 | + register_action(view, cx, Editor::delete_to_beginning_of_line); | |
| 170 | + register_action(view, cx, Editor::delete_to_end_of_line); | |
| 171 | + register_action(view, cx, Editor::cut_to_end_of_line); | |
| 172 | + register_action(view, cx, Editor::duplicate_line); | |
| 173 | + register_action(view, cx, Editor::move_line_up); | |
| 174 | + register_action(view, cx, Editor::move_line_down); | |
| 175 | + register_action(view, cx, Editor::transpose); | |
| 176 | + register_action(view, cx, Editor::cut); | |
| 177 | + register_action(view, cx, Editor::copy); | |
| 178 | + register_action(view, cx, Editor::paste); | |
| 179 | + register_action(view, cx, Editor::undo); | |
| 180 | + register_action(view, cx, Editor::redo); | |
| 181 | + register_action(view, cx, Editor::move_page_up); | |
| 182 | + register_action(view, cx, Editor::move_page_down); | |
| 183 | + register_action(view, cx, Editor::next_screen); | |
| 184 | + register_action(view, cx, Editor::scroll_cursor_top); | |
| 185 | + register_action(view, cx, Editor::scroll_cursor_center); | |
| 186 | + register_action(view, cx, Editor::scroll_cursor_bottom); | |
| 187 | + register_action(view, cx, |editor, _: &LineDown, cx| { | |
| 188 | + editor.scroll_screen(&ScrollAmount::Line(1.), cx) | |
| 189 | + }); | |
| 190 | + register_action(view, cx, |editor, _: &LineUp, cx| { | |
| 191 | + editor.scroll_screen(&ScrollAmount::Line(-1.), cx) | |
| 192 | + }); | |
| 193 | + register_action(view, cx, |editor, _: &HalfPageDown, cx| { | |
| 194 | + editor.scroll_screen(&ScrollAmount::Page(0.5), cx) | |
| 195 | + }); | |
| 196 | + register_action(view, cx, |editor, _: &HalfPageUp, cx| { | |
| 197 | + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) | |
| 198 | + }); | |
| 199 | + register_action(view, cx, |editor, _: &PageDown, cx| { | |
| 200 | + editor.scroll_screen(&ScrollAmount::Page(1.), cx) | |
| 201 | + }); | |
| 202 | + register_action(view, cx, |editor, _: &PageUp, cx| { | |
| 203 | + editor.scroll_screen(&ScrollAmount::Page(-1.), cx) | |
| 204 | + }); | |
| 205 | + register_action(view, cx, Editor::move_to_previous_word_start); | |
| 206 | + register_action(view, cx, Editor::move_to_previous_subword_start); | |
| 207 | + register_action(view, cx, Editor::move_to_next_word_end); | |
| 208 | + register_action(view, cx, Editor::move_to_next_subword_end); | |
| 209 | + register_action(view, cx, Editor::move_to_beginning_of_line); | |
| 210 | + register_action(view, cx, Editor::move_to_end_of_line); | |
| 211 | + register_action(view, cx, Editor::move_to_start_of_paragraph); | |
| 212 | + register_action(view, cx, Editor::move_to_end_of_paragraph); | |
| 213 | + register_action(view, cx, Editor::move_to_beginning); | |
| 214 | + register_action(view, cx, Editor::move_to_end); | |
| 215 | + register_action(view, cx, Editor::select_up); | |
| 216 | + register_action(view, cx, Editor::select_down); | |
| 217 | + register_action(view, cx, Editor::select_left); | |
| 218 | + register_action(view, cx, Editor::select_right); | |
| 219 | + register_action(view, cx, Editor::select_to_previous_word_start); | |
| 220 | + register_action(view, cx, Editor::select_to_previous_subword_start); | |
| 221 | + register_action(view, cx, Editor::select_to_next_word_end); | |
| 222 | + register_action(view, cx, Editor::select_to_next_subword_end); | |
| 223 | + register_action(view, cx, Editor::select_to_beginning_of_line); | |
| 224 | + register_action(view, cx, Editor::select_to_end_of_line); | |
| 225 | + register_action(view, cx, Editor::select_to_start_of_paragraph); | |
| 226 | + register_action(view, cx, Editor::select_to_end_of_paragraph); | |
| 227 | + register_action(view, cx, Editor::select_to_beginning); | |
| 228 | + register_action(view, cx, Editor::select_to_end); | |
| 229 | + register_action(view, cx, Editor::select_all); | |
| 230 | + register_action(view, cx, |editor, action, cx| { | |
| 231 | + editor.select_all_matches(action, cx).log_err(); | |
| 232 | + }); | |
| 233 | + register_action(view, cx, Editor::select_line); | |
| 234 | + register_action(view, cx, Editor::split_selection_into_lines); | |
| 235 | + register_action(view, cx, Editor::add_selection_above); | |
| 236 | + register_action(view, cx, Editor::add_selection_below); | |
| 237 | + register_action(view, cx, |editor, action, cx| { | |
| 238 | + editor.select_next(action, cx).log_err(); | |
| 239 | + }); | |
| 240 | + register_action(view, cx, |editor, action, cx| { | |
| 241 | + editor.select_previous(action, cx).log_err(); | |
| 242 | + }); | |
| 243 | + register_action(view, cx, Editor::toggle_comments); | |
| 244 | + register_action(view, cx, Editor::select_larger_syntax_node); | |
| 245 | + register_action(view, cx, Editor::select_smaller_syntax_node); | |
| 246 | + register_action(view, cx, Editor::move_to_enclosing_bracket); | |
| 247 | + register_action(view, cx, Editor::undo_selection); | |
| 248 | + register_action(view, cx, Editor::redo_selection); | |
| 249 | + register_action(view, cx, Editor::go_to_diagnostic); | |
| 250 | + register_action(view, cx, Editor::go_to_prev_diagnostic); | |
| 251 | + register_action(view, cx, Editor::go_to_hunk); | |
| 252 | + register_action(view, cx, Editor::go_to_prev_hunk); | |
| 253 | + register_action(view, cx, Editor::go_to_definition); | |
| 254 | + register_action(view, cx, Editor::go_to_definition_split); | |
| 255 | + register_action(view, cx, Editor::go_to_type_definition); | |
| 256 | + register_action(view, cx, Editor::go_to_type_definition_split); | |
| 257 | + register_action(view, cx, Editor::fold); | |
| 258 | + register_action(view, cx, Editor::fold_at); | |
| 259 | + register_action(view, cx, Editor::unfold_lines); | |
| 260 | + register_action(view, cx, Editor::unfold_at); | |
| 261 | + register_action(view, cx, Editor::fold_selected_ranges); | |
| 262 | + register_action(view, cx, Editor::show_completions); | |
| 263 | + register_action(view, cx, Editor::toggle_code_actions); | |
| 264 | + // on_action(cx, Editor::open_excerpts); todo!() | |
| 265 | + register_action(view, cx, Editor::toggle_soft_wrap); | |
| 266 | + register_action(view, cx, Editor::toggle_inlay_hints); | |
| 267 | + register_action(view, cx, hover_popover::hover); | |
| 268 | + register_action(view, cx, Editor::reveal_in_finder); | |
| 269 | + register_action(view, cx, Editor::copy_path); | |
| 270 | + register_action(view, cx, Editor::copy_relative_path); | |
| 271 | + register_action(view, cx, Editor::copy_highlight_json); | |
| 272 | + register_action(view, cx, |editor, action, cx| { | |
| 273 | + editor | |
| 274 | + .format(action, cx) | |
| 275 | + .map(|task| task.detach_and_log_err(cx)); | |
| 276 | + }); | |
| 277 | + register_action(view, cx, Editor::restart_language_server); | |
| 278 | + register_action(view, cx, Editor::show_character_palette); | |
| 279 | + register_action(view, cx, |editor, action, cx| { | |
| 280 | + editor | |
| 281 | + .confirm_completion(action, cx) | |
| 282 | + .map(|task| task.detach_and_log_err(cx)); | |
| 283 | + }); | |
| 284 | + register_action(view, cx, |editor, action, cx| { | |
| 285 | + editor | |
| 286 | + .confirm_code_action(action, cx) | |
| 287 | + .map(|task| task.detach_and_log_err(cx)); | |
| 288 | + }); | |
| 289 | + register_action(view, cx, |editor, action, cx| { | |
| 290 | + editor | |
| 291 | + .rename(action, cx) | |
| 292 | + .map(|task| task.detach_and_log_err(cx)); | |
| 293 | + }); | |
| 294 | + register_action(view, cx, |editor, action, cx| { | |
| 295 | + editor | |
| 296 | + .confirm_rename(action, cx) | |
| 297 | + .map(|task| task.detach_and_log_err(cx)); | |
| 298 | + }); | |
| 299 | + register_action(view, cx, |editor, action, cx| { | |
| 300 | + editor | |
| 301 | + .find_all_references(action, cx) | |
| 302 | + .map(|task| task.detach_and_log_err(cx)); | |
| 303 | + }); | |
| 304 | + register_action(view, cx, Editor::next_copilot_suggestion); | |
| 305 | + register_action(view, cx, Editor::previous_copilot_suggestion); | |
| 306 | + register_action(view, cx, Editor::copilot_suggest); | |
| 307 | + register_action(view, cx, Editor::context_menu_first); | |
| 308 | + register_action(view, cx, Editor::context_menu_prev); | |
| 309 | + register_action(view, cx, Editor::context_menu_next); | |
| 310 | + register_action(view, cx, Editor::context_menu_last); | |
| 311 | + } | |
| 312 | + | |
| 127 | 313 | fn mouse_down( |
| 128 | 314 | editor: &mut Editor, |
| 129 | 315 | event: &MouseDownEvent, |
| @@ -349,7 +535,7 @@ impl EditorElement { · 219 unmodified lines | ||
| 349 | 535 | gutter_bounds: Bounds<Pixels>, |
| 350 | 536 | text_bounds: Bounds<Pixels>, |
| 351 | 537 | layout: &LayoutState, |
| 352 | − cx: &mut ViewContext<Editor>, | |
| 538 | + cx: &mut WindowContext, | |
| 353 | 539 | ) { |
| 354 | 540 | let bounds = gutter_bounds.union(&text_bounds); |
| 355 | 541 | let scroll_top = |
| @@ -459,8 +645,7 @@ impl EditorElement { · 103 unmodified lines | ||
| 459 | 645 | &mut self, |
| 460 | 646 | bounds: Bounds<Pixels>, |
| 461 | 647 | layout: &mut LayoutState, |
| 462 | − editor: &mut Editor, | |
| 463 | − cx: &mut ViewContext<Editor>, | |
| 648 | + cx: &mut WindowContext, | |
| 464 | 649 | ) { |
| 465 | 650 | let line_height = layout.position_map.line_height; |
| 466 | 651 | |
| @@ -488,13 +673,14 @@ impl EditorElement { · 21 unmodified lines | ||
| 488 | 673 | } |
| 489 | 674 | } |
| 490 | 675 | |
| 491 | − for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() { | |
| 492 | − if let Some(fold_indicator) = fold_indicator.as_mut() { | |
| 676 | + for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() { | |
| 677 | + if let Some(mut fold_indicator) = fold_indicator { | |
| 678 | + let mut fold_indicator = fold_indicator.into_any_element(); | |
| 493 | 679 | let available_space = size( |
| 494 | 680 | AvailableSpace::MinContent, |
| 495 | 681 | AvailableSpace::Definite(line_height * 0.55), |
| 496 | 682 | ); |
| 497 | − let fold_indicator_size = fold_indicator.measure(available_space, editor, cx); | |
| 683 | + let fold_indicator_size = fold_indicator.measure(available_space, cx); | |
| 498 | 684 | |
| 499 | 685 | let position = point( |
| 500 | 686 | bounds.size.width - layout.gutter_padding, |
| @@ -505,32 +691,29 @@ impl EditorElement { · 4 unmodified lines | ||
| 505 | 691 | (line_height - fold_indicator_size.height) / 2., |
| 506 | 692 | ); |
| 507 | 693 | let origin = bounds.origin + position + centering_offset; |
| 508 | − fold_indicator.draw(origin, available_space, editor, cx); | |
| 694 | + fold_indicator.draw(origin, available_space, cx); | |
| 509 | 695 | } |
| 510 | 696 | } |
| 511 | 697 | |
| 512 | − if let Some(indicator) = layout.code_actions_indicator.as_mut() { | |
| 698 | + if let Some(indicator) = layout.code_actions_indicator.take() { | |
| 699 | + let mut button = indicator.button.into_any_element(); | |
| 513 | 700 | let available_space = size( |
| 514 | 701 | AvailableSpace::MinContent, |
| 515 | 702 | AvailableSpace::Definite(line_height), |
| 516 | 703 | ); |
| 517 | − let indicator_size = indicator.element.measure(available_space, editor, cx); | |
| 704 | + let indicator_size = button.measure(available_space, cx); | |
| 705 | + | |
| 518 | 706 | let mut x = Pixels::ZERO; |
| 519 | 707 | let mut y = indicator.row as f32 * line_height - scroll_top; |
| 520 | 708 | // Center indicator. |
| 521 | 709 | x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.; |
| 522 | 710 | y += (line_height - indicator_size.height) / 2.; |
| 523 | − indicator | |
| 524 | − .element | |
| 525 | − .draw(bounds.origin + point(x, y), available_space, editor, cx); | |
| 711 | + | |
| 712 | + button.draw(bounds.origin + point(x, y), available_space, cx); | |
| 526 | 713 | } |
| 527 | 714 | } |
| 528 | 715 | |
| 529 | − fn paint_diff_hunks( | |
| 530 | − bounds: Bounds<Pixels>, | |
| 531 | − layout: &LayoutState, | |
| 532 | − cx: &mut ViewContext<Editor>, | |
| 533 | − ) { | |
| 716 | + fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut WindowContext) { | |
| 534 | 717 | // todo!() |
| 535 | 718 | // let diff_style = &theme::current(cx).editor.diff.clone(); |
| 536 | 719 | // let line_height = layout.position_map.line_height; |
| @@ -618,14 +801,19 @@ impl EditorElement { · 81 unmodified lines | ||
| 618 | 801 | &mut self, |
| 619 | 802 | text_bounds: Bounds<Pixels>, |
| 620 | 803 | layout: &mut LayoutState, |
| 621 | − editor: &mut Editor, | |
| 622 | − cx: &mut ViewContext<Editor>, | |
| 804 | + cx: &mut WindowContext, | |
| 623 | 805 | ) { |
| 624 | 806 | let scroll_position = layout.position_map.snapshot.scroll_position(); |
| 625 | 807 | let start_row = layout.visible_display_row_range.start; |
| 626 | 808 | let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); |
| 627 | 809 | let line_end_overshoot = 0.15 * layout.position_map.line_height; |
| 628 | − let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces; | |
| 810 | + let whitespace_setting = self | |
| 811 | + .editor | |
| 812 | + .read(cx) | |
| 813 | + .buffer | |
| 814 | + .read(cx) | |
| 815 | + .settings_at(0, cx) | |
| 816 | + .show_whitespaces; | |
| 629 | 817 | |
| 630 | 818 | cx.with_content_mask( |
| 631 | 819 | Some(ContentMask { |
| @@ -674,20 +862,22 @@ impl EditorElement { · 42 unmodified lines | ||
| 674 | 862 | div() |
| 675 | 863 | .id(fold.id) |
| 676 | 864 | .size_full() |
| 677 | − .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) | |
| 678 | − .on_click(move |editor: &mut Editor, _, cx| { | |
| 679 | − editor.unfold_ranges( | |
| 680 | − [fold_range.start..fold_range.end], | |
| 681 | − true, | |
| 682 | − false, | |
| 683 | − cx, | |
| 684 | − ); | |
| 685 | − cx.stop_propagation(); | |
| 686 | − }) | |
| 865 | + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) | |
| 866 | + .on_click(cx.listener_for( | |
| 867 | + &self.editor, | |
| 868 | + move |editor: &mut Editor, _, cx| { | |
| 869 | + editor.unfold_ranges( | |
| 870 | + [fold_range.start..fold_range.end], | |
| 871 | + true, | |
| 872 | + false, | |
| 873 | + cx, | |
| 874 | + ); | |
| 875 | + cx.stop_propagation(); | |
| 876 | + }, | |
| 877 | + )) | |
| 687 | 878 | .draw( |
| 688 | 879 | fold_bounds.origin, |
| 689 | 880 | fold_bounds.size, |
| 690 | − editor, | |
| 691 | 881 | cx, |
| 692 | 882 | |fold_element_state, cx| { |
| 693 | 883 | if fold_element_state.is_active() { |
| @@ -748,7 +938,7 @@ impl EditorElement { · 54 unmodified lines | ||
| 748 | 938 | invisible_display_ranges.push(selection.range.clone()); |
| 749 | 939 | } |
| 750 | 940 | |
| 751 | − if !selection.is_local || editor.show_local_cursors(cx) { | |
| 941 | + if !selection.is_local || self.editor.read(cx).show_local_cursors(cx) { | |
| 752 | 942 | let cursor_position = selection.head; |
| 753 | 943 | if layout |
| 754 | 944 | .visible_display_row_range |
| @@ -800,12 +990,14 @@ impl EditorElement { · 45 unmodified lines | ||
| 800 | 990 | * layout.position_map.line_height |
| 801 | 991 | - layout.position_map.scroll_position.y; |
| 802 | 992 | if selection.is_newest { |
| 803 | − editor.pixel_position_of_newest_cursor = Some(point( | |
| 804 | − text_bounds.origin.x + x + block_width / 2., | |
| 805 | − text_bounds.origin.y | |
| 806 | − + y | |
| 807 | − + layout.position_map.line_height / 2., | |
| 808 | − )); | |
| 993 | + self.editor.update(cx, |editor, _| { | |
| 994 | + editor.pixel_position_of_newest_cursor = Some(point( | |
| 995 | + text_bounds.origin.x + x + block_width / 2., | |
| 996 | + text_bounds.origin.y | |
| 997 | + + y | |
| 998 | + + layout.position_map.line_height / 2., | |
| 999 | + )) | |
| 1000 | + }); | |
| 809 | 1001 | } |
| 810 | 1002 | cursors.push(Cursor { |
| 811 | 1003 | color: selection_style.cursor, |
| @@ -840,17 +1032,11 @@ impl EditorElement { · 28 unmodified lines | ||
| 840 | 1032 | } |
| 841 | 1033 | }); |
| 842 | 1034 | |
| 843 | − if let Some((position, context_menu)) = layout.context_menu.as_mut() { | |
| 844 | − cx.with_z_index(1, |cx| { | |
| 845 | − let line_height = self.style.text.line_height_in_pixels(cx.rem_size()); | |
| 846 | − let available_space = size( | |
| 847 | − AvailableSpace::MinContent, | |
| 848 | − AvailableSpace::Definite( | |
| 849 | − (12. * line_height) | |
| 850 | − .min((text_bounds.size.height - line_height) / 2.), | |
| 851 | − ), | |
| 852 | − ); | |
| 853 | − let context_menu_size = context_menu.measure(available_space, editor, cx); | |
| 1035 | + cx.with_z_index(1, |cx| { | |
| 1036 | + if let Some((position, mut context_menu)) = layout.context_menu.take() { | |
| 1037 | + let available_space = | |
| 1038 | + size(AvailableSpace::MinContent, AvailableSpace::MinContent); | |
| 1039 | + let context_menu_size = context_menu.measure(available_space, cx); | |
| 854 | 1040 | |
| 855 | 1041 | let cursor_row_layout = &layout.position_map.line_layouts |
| 856 | 1042 | [(position.row() - start_row) as usize] |
| @@ -871,84 +1057,77 @@ impl EditorElement { · 14 unmodified lines | ||
| 871 | 1057 | } |
| 872 | 1058 | |
| 873 | 1059 | if list_origin.y + list_height > text_bounds.lower_right().y { |
| 874 | − list_origin.y -= layout.position_map.line_height - list_height; | |
| 1060 | + list_origin.y -= layout.position_map.line_height + list_height; | |
| 875 | 1061 | } |
| 876 | 1062 | |
| 877 | − context_menu.draw(list_origin, available_space, editor, cx); | |
| 878 | − }) | |
| 879 | − } | |
| 1063 | + cx.break_content_mask(|cx| { | |
| 1064 | + context_menu.draw(list_origin, available_space, cx) | |
| 1065 | + }); | |
| 1066 | + } | |
| 880 | 1067 | |
| 881 | − // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { | |
| 882 | − // cx.scene().push_stacking_context(None, None); | |
| 883 | − | |
| 884 | − // // This is safe because we check on layout whether the required row is available | |
| 885 | − // let hovered_row_layout = | |
| 886 | − // &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; | |
| 887 | − | |
| 888 | − // // Minimum required size: Take the first popover, and add 1.5 times the minimum popover | |
| 889 | − // // height. This is the size we will use to decide whether to render popovers above or below | |
| 890 | − // // the hovered line. | |
| 891 | − // let first_size = hover_popovers[0].size(); | |
| 892 | − // let height_to_reserve = first_size.y | |
| 893 | − // + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height; | |
| 894 | − | |
| 895 | − // // Compute Hovered Point | |
| 896 | − // let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left; | |
| 897 | − // let y = position.row() as f32 * layout.position_map.line_height - scroll_top; | |
| 898 | − // let hovered_point = content_origin + point(x, y); | |
| 899 | − | |
| 900 | − // if hovered_point.y - height_to_reserve > 0.0 { | |
| 901 | − // // There is enough space above. Render popovers above the hovered point | |
| 902 | − // let mut current_y = hovered_point.y; | |
| 903 | − // for hover_popover in hover_popovers { | |
| 904 | − // let size = hover_popover.size(); | |
| 905 | − // let mut popover_origin = point(hovered_point.x, current_y - size.y); | |
| 906 | − | |
| 907 | − // let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x); | |
| 908 | − // if x_out_of_bounds < 0.0 { | |
| 909 | − // popover_origin.set_x(popover_origin.x + x_out_of_bounds); | |
| 910 | − // } | |
| 911 | − | |
| 912 | − // hover_popover.paint( | |
| 913 | − // popover_origin, | |
| 914 | − // Bounds::<Pixels>::from_points( | |
| 915 | − // gpui::Point::<Pixels>::zero(), | |
| 916 | − // point(f32::MAX, f32::MAX), | |
| 917 | − // ), // Let content bleed outside of editor | |
| 918 | − // editor, | |
| 919 | − // cx, | |
| 920 | − // ); | |
| 921 | − | |
| 922 | − // current_y = popover_origin.y - HOVER_POPOVER_GAP; | |
| 923 | − // } | |
| 924 | − // } else { | |
| 925 | − // // There is not enough space above. Render popovers below the hovered point | |
| 926 | − // let mut current_y = hovered_point.y + layout.position_map.line_height; | |
| 927 | − // for hover_popover in hover_popovers { | |
| 928 | − // let size = hover_popover.size(); | |
| 929 | − // let mut popover_origin = point(hovered_point.x, current_y); | |
| 930 | − | |
| 931 | − // let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x); | |
| 932 | − // if x_out_of_bounds < 0.0 { | |
| 933 | − // popover_origin.set_x(popover_origin.x + x_out_of_bounds); | |
| 934 | − // } | |
| 935 | − | |
| 936 | − // hover_popover.paint( | |
| 937 | − // popover_origin, | |
| 938 | − // Bounds::<Pixels>::from_points( | |
| 939 | − // gpui::Point::<Pixels>::zero(), | |
| 940 | − // point(f32::MAX, f32::MAX), | |
| 941 | − // ), // Let content bleed outside of editor | |
| 942 | − // editor, | |
| 943 | − // cx, | |
| 944 | − // ); | |
| 945 | − | |
| 946 | − // current_y = popover_origin.y + size.y + HOVER_POPOVER_GAP; | |
| 947 | − // } | |
| 948 | − // } | |
| 949 | − | |
| 950 | − // cx.scene().pop_stacking_context(); | |
| 951 | − // } | |
| 1068 | + if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() { | |
| 1069 | + let available_space = | |
| 1070 | + size(AvailableSpace::MinContent, AvailableSpace::MinContent); | |
| 1071 | + | |
| 1072 | + // This is safe because we check on layout whether the required row is available | |
| 1073 | + let hovered_row_layout = &layout.position_map.line_layouts | |
| 1074 | + [(position.row() - start_row) as usize] | |
| 1075 | + .line; | |
| 1076 | + | |
| 1077 | + // Minimum required size: Take the first popover, and add 1.5 times the minimum popover | |
| 1078 | + // height. This is the size we will use to decide whether to render popovers above or below | |
| 1079 | + // the hovered line. | |
| 1080 | + let first_size = hover_popovers[0].measure(available_space, cx); | |
| 1081 | + let height_to_reserve = first_size.height | |
| 1082 | + + 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height; | |
| 1083 | + | |
| 1084 | + // Compute Hovered Point | |
| 1085 | + let x = hovered_row_layout.x_for_index(position.column() as usize) | |
| 1086 | + - layout.position_map.scroll_position.x; | |
| 1087 | + let y = position.row() as f32 * layout.position_map.line_height | |
| 1088 | + - layout.position_map.scroll_position.y; | |
| 1089 | + let hovered_point = content_origin + point(x, y); | |
| 1090 | + | |
| 1091 | + if hovered_point.y - height_to_reserve > Pixels::ZERO { | |
| 1092 | + // There is enough space above. Render popovers above the hovered point | |
| 1093 | + let mut current_y = hovered_point.y; | |
| 1094 | + for mut hover_popover in hover_popovers { | |
| 1095 | + let size = hover_popover.measure(available_space, cx); | |
| 1096 | + let mut popover_origin = | |
| 1097 | + point(hovered_point.x, current_y - size.height); | |
| 1098 | + | |
| 1099 | + let x_out_of_bounds = | |
| 1100 | + text_bounds.upper_right().x - (popover_origin.x + size.width); | |
| 1101 | + if x_out_of_bounds < Pixels::ZERO { | |
| 1102 | + popover_origin.x = popover_origin.x + x_out_of_bounds; | |
| 1103 | + } | |
| 1104 | + | |
| 1105 | + cx.break_content_mask(|cx| { | |
| 1106 | + hover_popover.draw(popover_origin, available_space, cx) | |
| 1107 | + }); | |
| 1108 | + | |
| 1109 | + current_y = popover_origin.y - HOVER_POPOVER_GAP; | |
| 1110 | + } | |
| 1111 | + } else { | |
| 1112 | + // There is not enough space above. Render popovers below the hovered point | |
| 1113 | + let mut current_y = hovered_point.y + layout.position_map.line_height; | |
| 1114 | + for mut hover_popover in hover_popovers { | |
| 1115 | + let size = hover_popover.measure(available_space, cx); | |
| 1116 | + let mut popover_origin = point(hovered_point.x, current_y); | |
| 1117 | + | |
| 1118 | + let x_out_of_bounds = | |
| 1119 | + text_bounds.upper_right().x - (popover_origin.x + size.width); | |
| 1120 | + if x_out_of_bounds < Pixels::ZERO { | |
| 1121 | + popover_origin.x = popover_origin.x + x_out_of_bounds; | |
| 1122 | + } | |
| 1123 | + | |
| 1124 | + hover_popover.draw(popover_origin, available_space, cx); | |
| 1125 | + | |
| 1126 | + current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP; | |
| 1127 | + } | |
| 1128 | + } | |
| 1129 | + } | |
| 1130 | + }) | |
| 952 | 1131 | }, |
| 953 | 1132 | ) |
| 954 | 1133 | } |
| @@ -1165,7 +1344,7 @@ impl EditorElement { · 210 unmodified lines | ||
| 1165 | 1344 | layout: &LayoutState, |
| 1166 | 1345 | content_origin: gpui::Point<Pixels>, |
| 1167 | 1346 | bounds: Bounds<Pixels>, |
| 1168 | − cx: &mut ViewContext<Editor>, | |
| 1347 | + cx: &mut WindowContext, | |
| 1169 | 1348 | ) { |
| 1170 | 1349 | let start_row = layout.visible_display_row_range.start; |
| 1171 | 1350 | let end_row = layout.visible_display_row_range.end; |
| @@ -1217,14 +1396,13 @@ impl EditorElement { · 45 unmodified lines | ||
| 1217 | 1396 | &mut self, |
| 1218 | 1397 | bounds: Bounds<Pixels>, |
| 1219 | 1398 | layout: &mut LayoutState, |
| 1220 | − editor: &mut Editor, | |
| 1221 | − cx: &mut ViewContext<Editor>, | |
| 1399 | + cx: &mut WindowContext, | |
| 1222 | 1400 | ) { |
| 1223 | 1401 | let scroll_position = layout.position_map.snapshot.scroll_position(); |
| 1224 | 1402 | let scroll_left = scroll_position.x * layout.position_map.em_width; |
| 1225 | 1403 | let scroll_top = scroll_position.y * layout.position_map.line_height; |
| 1226 | 1404 | |
| 1227 | − for block in &mut layout.blocks { | |
| 1405 | + for block in layout.blocks.drain(..) { | |
| 1228 | 1406 | let mut origin = bounds.origin |
| 1229 | 1407 | + point( |
| 1230 | 1408 | Pixels::ZERO, |
| @@ -1233,13 +1411,11 @@ impl EditorElement { · 2 unmodified lines | ||
| 1233 | 1411 | if !matches!(block.style, BlockStyle::Sticky) { |
| 1234 | 1412 | origin += point(-scroll_left, Pixels::ZERO); |
| 1235 | 1413 | } |
| 1236 | − block | |
| 1237 | − .element | |
| 1238 | − .draw(origin, block.available_space, editor, cx); | |
| 1414 | + block.element.draw(origin, block.available_space, cx); | |
| 1239 | 1415 | } |
| 1240 | 1416 | } |
| 1241 | 1417 | |
| 1242 | − fn column_pixels(&self, column: usize, cx: &ViewContext<Editor>) -> Pixels { | |
| 1418 | + fn column_pixels(&self, column: usize, cx: &WindowContext) -> Pixels { | |
| 1243 | 1419 | let style = &self.style; |
| 1244 | 1420 | let font_size = style.text.font_size.to_pixels(cx.rem_size()); |
| 1245 | 1421 | let layout = cx |
| @@ -1260,7 +1436,7 @@ impl EditorElement { · 14 unmodified lines | ||
| 1260 | 1436 | layout.width |
| 1261 | 1437 | } |
| 1262 | 1438 | |
| 1263 | − fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> Pixels { | |
| 1439 | + fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &WindowContext) -> Pixels { | |
| 1264 | 1440 | let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1; |
| 1265 | 1441 | self.column_pixels(digit_count, cx) |
| 1266 | 1442 | } |
| @@ -1415,7 +1591,7 @@ impl EditorElement { · 148 unmodified lines | ||
| 1415 | 1591 | } |
| 1416 | 1592 | |
| 1417 | 1593 | fn layout_lines( |
| 1418 | − &mut self, | |
| 1594 | + &self, | |
| 1419 | 1595 | rows: Range<u32>, |
| 1420 | 1596 | line_number_layouts: &[Option<ShapedLine>], |
| 1421 | 1597 | snapshot: &EditorSnapshot, |
| @@ -1471,483 +1647,457 @@ impl EditorElement { · 49 unmodified lines | ||
| 1471 | 1647 | |
| 1472 | 1648 | fn compute_layout( |
| 1473 | 1649 | &mut self, |
| 1474 | − editor: &mut Editor, | |
| 1475 | − cx: &mut ViewContext<'_, Editor>, | |
| 1476 | 1650 | mut bounds: Bounds<Pixels>, |
| 1651 | + cx: &mut WindowContext, | |
| 1477 | 1652 | ) -> LayoutState { |
| 1478 | − // let mut size = constraint.max; | |
| 1479 | − // if size.x.is_infinite() { | |
| 1480 | − // unimplemented!("we don't yet handle an infinite width constraint on buffer elements"); | |
| 1481 | − // } | |
| 1482 | − | |
| 1483 | − let snapshot = editor.snapshot(cx); | |
| 1484 | − let style = self.style.clone(); | |
| 1653 | + self.editor.update(cx, |editor, cx| { | |
| 1654 | + // let mut size = constraint.max; | |
| 1655 | + // if size.x.is_infinite() { | |
| 1656 | + // unimplemented!("we don't yet handle an infinite width constraint on buffer elements"); | |
| 1657 | + // } | |
| 1658 | + | |
| 1659 | + let snapshot = editor.snapshot(cx); | |
| 1660 | + let style = self.style.clone(); | |
| 1661 | + | |
| 1662 | + let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); | |
| 1663 | + let font_size = style.text.font_size.to_pixels(cx.rem_size()); | |
| 1664 | + let line_height = style.text.line_height_in_pixels(cx.rem_size()); | |
| 1665 | + let em_width = cx | |
| 1666 | + .text_system() | |
| 1667 | + .typographic_bounds(font_id, font_size, 'm') | |
| 1668 | + .unwrap() | |
| 1669 | + .size | |
| 1670 | + .width; | |
| 1671 | + let em_advance = cx | |
| 1672 | + .text_system() | |
| 1673 | + .advance(font_id, font_size, 'm') | |
| 1674 | + .unwrap() | |
| 1675 | + .width; | |
| 1676 | + | |
| 1677 | + let gutter_padding; | |
| 1678 | + let gutter_width; | |
| 1679 | + let gutter_margin; | |
| 1680 | + if snapshot.show_gutter { | |
| 1681 | + let descent = cx.text_system().descent(font_id, font_size).unwrap(); | |
| 1682 | + | |
| 1683 | + let gutter_padding_factor = 3.5; | |
| 1684 | + gutter_padding = (em_width * gutter_padding_factor).round(); | |
| 1685 | + gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; | |
| 1686 | + gutter_margin = -descent; | |
| 1687 | + } else { | |
| 1688 | + gutter_padding = Pixels::ZERO; | |
| 1689 | + gutter_width = Pixels::ZERO; | |
| 1690 | + gutter_margin = Pixels::ZERO; | |
| 1691 | + }; | |
| 1485 | 1692 | |
| 1486 | − let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); | |
| 1487 | − let font_size = style.text.font_size.to_pixels(cx.rem_size()); | |
| 1488 | − let line_height = style.text.line_height_in_pixels(cx.rem_size()); | |
| 1489 | − let em_width = cx | |
| 1490 | − .text_system() | |
| 1491 | − .typographic_bounds(font_id, font_size, 'm') | |
| 1492 | − .unwrap() | |
| 1493 | − .size | |
| 1494 | − .width; | |
| 1495 | − let em_advance = cx | |
| 1496 | − .text_system() | |
| 1497 | − .advance(font_id, font_size, 'm') | |
| 1498 | − .unwrap() | |
| 1499 | − .width; | |
| 1500 | − | |
| 1501 | − let gutter_padding; | |
| 1502 | − let gutter_width; | |
| 1503 | − let gutter_margin; | |
| 1504 | − if snapshot.show_gutter { | |
| 1505 | − let descent = cx.text_system().descent(font_id, font_size).unwrap(); | |
| 1506 | − | |
| 1507 | − let gutter_padding_factor = 3.5; | |
| 1508 | − gutter_padding = (em_width * gutter_padding_factor).round(); | |
| 1509 | − gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; | |
| 1510 | − gutter_margin = -descent; | |
| 1511 | − } else { | |
| 1512 | − gutter_padding = Pixels::ZERO; | |
| 1513 | − gutter_width = Pixels::ZERO; | |
| 1514 | − gutter_margin = Pixels::ZERO; | |
| 1515 | − }; | |
| 1693 | + editor.gutter_width = gutter_width; | |
| 1694 | + let text_width = bounds.size.width - gutter_width; | |
| 1695 | + let overscroll = size(em_width, px(0.)); | |
| 1696 | + let snapshot = { | |
| 1697 | + editor.set_visible_line_count((bounds.size.height / line_height).into(), cx); | |
| 1698 | + | |
| 1699 | + let editor_width = text_width - gutter_margin - overscroll.width - em_width; | |
| 1700 | + let wrap_width = match editor.soft_wrap_mode(cx) { | |
| 1701 | + SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance, | |
| 1702 | + SoftWrap::EditorWidth => editor_width, | |
| 1703 | + SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance), | |
| 1704 | + }; | |
| 1516 | 1705 | |
| 1517 | − editor.gutter_width = gutter_width; | |
| 1518 | − let text_width = bounds.size.width - gutter_width; | |
| 1519 | − let overscroll = size(em_width, px(0.)); | |
| 1520 | − let snapshot = { | |
| 1521 | − editor.set_visible_line_count((bounds.size.height / line_height).into(), cx); | |
| 1522 | − | |
| 1523 | − let editor_width = text_width - gutter_margin - overscroll.width - em_width; | |
| 1524 | − let wrap_width = match editor.soft_wrap_mode(cx) { | |
| 1525 | − SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance, | |
| 1526 | − SoftWrap::EditorWidth => editor_width, | |
| 1527 | − SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance), | |
| 1706 | + if editor.set_wrap_width(Some(wrap_width), cx) { | |
| 1707 | + editor.snapshot(cx) | |
| 1708 | + } else { | |
| 1709 | + snapshot | |
| 1710 | + } | |
| 1528 | 1711 | }; |
| 1529 | 1712 | |
| 1530 | − if editor.set_wrap_width(Some(wrap_width), cx) { | |
| 1531 | − editor.snapshot(cx) | |
| 1713 | + let wrap_guides = editor | |
| 1714 | + .wrap_guides(cx) | |
| 1715 | + .iter() | |
| 1716 | + .map(|(guide, active)| (self.column_pixels(*guide, cx), *active)) | |
| 1717 | + .collect::<SmallVec<[_; 2]>>(); | |
| 1718 | + | |
| 1719 | + let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height; | |
| 1720 | + // todo!("this should happen during layout") | |
| 1721 | + let editor_mode = snapshot.mode; | |
| 1722 | + if let EditorMode::AutoHeight { max_lines } = editor_mode { | |
| 1723 | + todo!() | |
| 1724 | + // size.set_y( | |
| 1725 | + // scroll_height | |
| 1726 | + // .min(constraint.max_along(Axis::Vertical)) | |
| 1727 | + // .max(constraint.min_along(Axis::Vertical)) | |
| 1728 | + // .max(line_height) | |
| 1729 | + // .min(line_height * max_lines as f32), | |
| 1730 | + // ) | |
| 1731 | + } else if let EditorMode::SingleLine = editor_mode { | |
| 1732 | + bounds.size.height = line_height.min(bounds.size.height); | |
| 1733 | + } | |
| 1734 | + // todo!() | |
| 1735 | + // else if size.y.is_infinite() { | |
| 1736 | + // // size.set_y(scroll_height); | |
| 1737 | + // } | |
| 1738 | + // | |
| 1739 | + let gutter_size = size(gutter_width, bounds.size.height); | |
| 1740 | + let text_size = size(text_width, bounds.size.height); | |
| 1741 | + | |
| 1742 | + let autoscroll_horizontally = | |
| 1743 | + editor.autoscroll_vertically(bounds.size.height, line_height, cx); | |
| 1744 | + let mut snapshot = editor.snapshot(cx); | |
| 1745 | + | |
| 1746 | + let scroll_position = snapshot.scroll_position(); | |
| 1747 | + // The scroll position is a fractional point, the whole number of which represents | |
| 1748 | + // the top of the window in terms of display rows. | |
| 1749 | + let start_row = scroll_position.y as u32; | |
| 1750 | + let height_in_lines = f32::from(bounds.size.height / line_height); | |
| 1751 | + let max_row = snapshot.max_point().row(); | |
| 1752 | + | |
| 1753 | + // Add 1 to ensure selections bleed off screen | |
| 1754 | + let end_row = 1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row); | |
| 1755 | + | |
| 1756 | + let start_anchor = if start_row == 0 { | |
| 1757 | + Anchor::min() | |
| 1532 | 1758 | } else { |
| 1533 | 1759 | snapshot |
| 1534 | − } | |
| 1535 | − }; | |
| 1760 | + .buffer_snapshot | |
| 1761 | + .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left)) | |
| 1762 | + }; | |
| 1763 | + let end_anchor = if end_row > max_row { | |
| 1764 | + Anchor::max() | |
| 1765 | + } else { | |
| 1766 | + snapshot | |
| 1767 | + .buffer_snapshot | |
| 1768 | + .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) | |
| 1769 | + }; | |
| 1536 | 1770 | |
| 1537 | − let wrap_guides = editor | |
| 1538 | − .wrap_guides(cx) | |
| 1539 | − .iter() | |
| 1540 | − .map(|(guide, active)| (self.column_pixels(*guide, cx), *active)) | |
| 1541 | − .collect::<SmallVec<[_; 2]>>(); | |
| 1542 | − | |
| 1543 | − let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height; | |
| 1544 | − // todo!("this should happen during layout") | |
| 1545 | − let editor_mode = snapshot.mode; | |
| 1546 | − if let EditorMode::AutoHeight { max_lines } = editor_mode { | |
| 1547 | − todo!() | |
| 1548 | − // size.set_y( | |
| 1549 | − // scroll_height | |
| 1550 | − // .min(constraint.max_along(Axis::Vertical)) | |
| 1551 | − // .max(constraint.min_along(Axis::Vertical)) | |
| 1552 | − // .max(line_height) | |
| 1553 | − // .min(line_height * max_lines as f32), | |
| 1554 | − // ) | |
| 1555 | − } else if let EditorMode::SingleLine = editor_mode { | |
| 1556 | − bounds.size.height = line_height.min(bounds.size.height); | |
| 1557 | − } | |
| 1558 | − // todo!() | |
| 1559 | − // else if size.y.is_infinite() { | |
| 1560 | − // // size.set_y(scroll_height); | |
| 1561 | − // } | |
| 1562 | − // | |
| 1563 | − let gutter_size = size(gutter_width, bounds.size.height); | |
| 1564 | − let text_size = size(text_width, bounds.size.height); | |
| 1565 | − | |
| 1566 | − let autoscroll_horizontally = | |
| 1567 | − editor.autoscroll_vertically(bounds.size.height, line_height, cx); | |
| 1568 | − let mut snapshot = editor.snapshot(cx); | |
| 1569 | − | |
| 1570 | − let scroll_position = snapshot.scroll_position(); | |
| 1571 | − // The scroll position is a fractional point, the whole number of which represents | |
| 1572 | − // the top of the window in terms of display rows. | |
| 1573 | − let start_row = scroll_position.y as u32; | |
| 1574 | − let height_in_lines = f32::from(bounds.size.height / line_height); | |
| 1575 | − let max_row = snapshot.max_point().row(); | |
| 1576 | − | |
| 1577 | − // Add 1 to ensure selections bleed off screen | |
| 1578 | − let end_row = 1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row); | |
| 1579 | − | |
| 1580 | − let start_anchor = if start_row == 0 { | |
| 1581 | − Anchor::min() | |
| 1582 | − } else { | |
| 1583 | − snapshot | |
| 1584 | − .buffer_snapshot | |
| 1585 | − .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left)) | |
| 1586 | − }; | |
| 1587 | − let end_anchor = if end_row > max_row { | |
| 1588 | − Anchor::max() | |
| 1589 | − } else { | |
| 1590 | − snapshot | |
| 1591 | − .buffer_snapshot | |
| 1592 | − .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) | |
| 1593 | − }; | |
| 1771 | + let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new(); | |
| 1772 | + let mut active_rows = BTreeMap::new(); | |
| 1773 | + let is_singleton = editor.is_singleton(cx); | |
| 1594 | 1774 | |
| 1595 | − let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new(); | |
| 1596 | − let mut active_rows = BTreeMap::new(); | |
| 1597 | − let is_singleton = editor.is_singleton(cx); | |
| 1775 | + let highlighted_rows = editor.highlighted_rows(); | |
| 1776 | + let highlighted_ranges = editor.background_highlights_in_range( | |
| 1777 | + start_anchor..end_anchor, | |
| 1778 | + &snapshot.display_snapshot, | |
| 1779 | + cx.theme().colors(), | |
| 1780 | + ); | |
| 1598 | 1781 | |
| 1599 | − let highlighted_rows = editor.highlighted_rows(); | |
| 1600 | − let highlighted_ranges = editor.background_highlights_in_range( | |
| 1601 | − start_anchor..end_anchor, | |
| 1602 | − &snapshot.display_snapshot, | |
| 1603 | − cx.theme().colors(), | |
| 1604 | − ); | |
| 1782 | + let mut newest_selection_head = None; | |
| 1783 | + | |
| 1784 | + if editor.show_local_selections { | |
| 1785 | + let mut local_selections: Vec<Selection<Point>> = editor | |
| 1786 | + .selections | |
| 1787 | + .disjoint_in_range(start_anchor..end_anchor, cx); | |
| 1788 | + local_selections.extend(editor.selections.pending(cx)); | |
| 1789 | + let mut layouts = Vec::new(); | |
| 1790 | + let newest = editor.selections.newest(cx); | |
| 1791 | + for selection in local_selections.drain(..) { | |
| 1792 | + let is_empty = selection.start == selection.end; | |
| 1793 | + let is_newest = selection == newest; | |
| 1794 | + | |
| 1795 | + let layout = SelectionLayout::new( | |
| 1796 | + selection, | |
| 1797 | + editor.selections.line_mode, | |
| 1798 | + editor.cursor_shape, | |
| 1799 | + &snapshot.display_snapshot, | |
| 1800 | + is_newest, | |
| 1801 | + true, | |
| 1802 | + ); | |
| 1803 | + if is_newest { | |
| 1804 | + newest_selection_head = Some(layout.head); | |
| 1805 | + } | |
| 1605 | 1806 | |
| 1606 | − let mut newest_selection_head = None; | |
| 1607 | − | |
| 1608 | − if editor.show_local_selections { | |
| 1609 | − let mut local_selections: Vec<Selection<Point>> = editor | |
| 1610 | − .selections | |
| 1611 | − .disjoint_in_range(start_anchor..end_anchor, cx); | |
| 1612 | − local_selections.extend(editor.selections.pending(cx)); | |
| 1613 | − let mut layouts = Vec::new(); | |
| 1614 | − let newest = editor.selections.newest(cx); | |
| 1615 | − for selection in local_selections.drain(..) { | |
| 1616 | − let is_empty = selection.start == selection.end; | |
| 1617 | − let is_newest = selection == newest; | |
| 1618 | − | |
| 1619 | − let layout = SelectionLayout::new( | |
| 1620 | − selection, | |
| 1621 | − editor.selections.line_mode, | |
| 1622 | − editor.cursor_shape, | |
| 1623 | − &snapshot.display_snapshot, | |
| 1624 | − is_newest, | |
| 1625 | − true, | |
| 1626 | − ); | |
| 1627 | − if is_newest { | |
| 1628 | − newest_selection_head = Some(layout.head); | |
| 1807 | + for row in cmp::max(layout.active_rows.start, start_row) | |
| 1808 | + ..=cmp::min(layout.active_rows.end, end_row) | |
| 1809 | + { | |
| 1810 | + let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty); | |
| 1811 | + *contains_non_empty_selection |= !is_empty; | |
| 1812 | + } | |
| 1813 | + layouts.push(layout); | |
| 1629 | 1814 | } |
| 1630 | 1815 | |
| 1631 | − for row in cmp::max(layout.active_rows.start, start_row) | |
| 1632 | − ..=cmp::min(layout.active_rows.end, end_row) | |
| 1633 | − { | |
| 1634 | − let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty); | |
| 1635 | − *contains_non_empty_selection |= !is_empty; | |
| 1636 | − } | |
| 1637 | − layouts.push(layout); | |
| 1816 | + selections.push((style.local_player, layouts)); | |
| 1638 | 1817 | } |
| 1639 | 1818 | |
| 1640 | − selections.push((style.local_player, layouts)); | |
| 1641 | − } | |
| 1642 | − | |
| 1643 | − if let Some(collaboration_hub) = &editor.collaboration_hub { | |
| 1644 | − // When following someone, render the local selections in their color. | |
| 1645 | − if let Some(leader_id) = editor.leader_peer_id { | |
| 1646 | − if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) { | |
| 1647 | − if let Some(participant_index) = collaboration_hub | |
| 1648 | − .user_participant_indices(cx) | |
| 1649 | − .get(&collaborator.user_id) | |
| 1650 | − { | |
| 1651 | − if let Some((local_selection_style, _)) = selections.first_mut() { | |
| 1652 | − *local_selection_style = cx | |
| 1653 | − .theme() | |
| 1654 | − .players() | |
| 1655 | − .color_for_participant(participant_index.0); | |
| 1819 | + if let Some(collaboration_hub) = &editor.collaboration_hub { | |
| 1820 | + // When following someone, render the local selections in their color. | |
| 1821 | + if let Some(leader_id) = editor.leader_peer_id { | |
| 1822 | + if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) { | |
| 1823 | + if let Some(participant_index) = collaboration_hub | |
| 1824 | + .user_participant_indices(cx) | |
| 1825 | + .get(&collaborator.user_id) | |
| 1826 | + { | |
| 1827 | + if let Some((local_selection_style, _)) = selections.first_mut() { | |
| 1828 | + *local_selection_style = cx | |
| 1829 | + .theme() | |
| 1830 | + .players() | |
| 1831 | + .color_for_participant(participant_index.0); | |
| 1832 | + } | |
| 1656 | 1833 | } |
| 1657 | 1834 | } |
| 1658 | 1835 | } |
| 1659 | − } | |
| 1660 | 1836 | |
| 1661 | − let mut remote_selections = HashMap::default(); | |
| 1662 | − for selection in snapshot.remote_selections_in_range( | |
| 1663 | − &(start_anchor..end_anchor), | |
| 1664 | − collaboration_hub.as_ref(), | |
| 1665 | − cx, | |
| 1666 | − ) { | |
| 1667 | − let selection_style = if let Some(participant_index) = selection.participant_index { | |
| 1668 | − cx.theme() | |
| 1669 | − .players() | |
| 1670 | − .color_for_participant(participant_index.0) | |
| 1671 | − } else { | |
| 1672 | − cx.theme().players().absent() | |
| 1673 | − }; | |
| 1837 | + let mut remote_selections = HashMap::default(); | |
| 1838 | + for selection in snapshot.remote_selections_in_range( | |
| 1839 | + &(start_anchor..end_anchor), | |
| 1840 | + collaboration_hub.as_ref(), | |
| 1841 | + cx, | |
| 1842 | + ) { | |
| 1843 | + let selection_style = if let Some(participant_index) = selection.participant_index { | |
| 1844 | + cx.theme() | |
| 1845 | + .players() | |
| 1846 | + .color_for_participant(participant_index.0) | |
| 1847 | + } else { | |
| 1848 | + cx.theme().players().absent() | |
| 1849 | + }; | |
| 1674 | 1850 | |
| 1675 | − // Don't re-render the leader's selections, since the local selections | |
| 1676 | − // match theirs. | |
| 1677 | − if Some(selection.peer_id) == editor.leader_peer_id { | |
| 1678 | − continue; | |
| 1851 | + // Don't re-render the leader's selections, since the local selections | |
| 1852 | + // match theirs. | |
| 1853 | + if Some(selection.peer_id) == editor.leader_peer_id { | |
| 1854 | + continue; | |
| 1855 | + } | |
| 1856 | + | |
| 1857 | + remote_selections | |
| 1858 | + .entry(selection.replica_id) | |
| 1859 | + .or_insert((selection_style, Vec::new())) | |
| 1860 | + .1 | |
| 1861 | + .push(SelectionLayout::new( | |
| 1862 | + selection.selection, | |
| 1863 | + selection.line_mode, | |
| 1864 | + selection.cursor_shape, | |
| 1865 | + &snapshot.display_snapshot, | |
| 1866 | + false, | |
| 1867 | + false, | |
| 1868 | + )); | |
| 1679 | 1869 | } |
| 1680 | 1870 | |
| 1681 | − remote_selections | |
| 1682 | − .entry(selection.replica_id) | |
| 1683 | − .or_insert((selection_style, Vec::new())) | |
| 1684 | − .1 | |
| 1685 | − .push(SelectionLayout::new( | |
| 1686 | − selection.selection, | |
| 1687 | − selection.line_mode, | |
| 1688 | − selection.cursor_shape, | |
| 1689 | − &snapshot.display_snapshot, | |
| 1690 | − false, | |
| 1691 | − false, | |
| 1692 | − )); | |
| 1871 | + selections.extend(remote_selections.into_values()); | |
| 1693 | 1872 | } |
| 1694 | 1873 | |
| 1695 | − selections.extend(remote_selections.into_values()); | |
| 1696 | − } | |
| 1697 | − | |
| 1698 | − let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; | |
| 1699 | − let show_scrollbars = match scrollbar_settings.show { | |
| 1700 | − ShowScrollbar::Auto => { | |
| 1701 | − // Git | |
| 1702 | − (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs()) | |
| 1703 | − || | |
| 1704 | − // Selections | |
| 1705 | − (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty()) | |
| 1706 | − // Scrollmanager | |
| 1707 | − || editor.scroll_manager.scrollbars_visible() | |
| 1708 | − } | |
| 1709 | − ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(), | |
| 1710 | − ShowScrollbar::Always => true, | |
| 1711 | − ShowScrollbar::Never => false, | |
| 1712 | − }; | |
| 1874 | + let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; | |
| 1875 | + let show_scrollbars = match scrollbar_settings.show { | |
| 1876 | + ShowScrollbar::Auto => { | |
| 1877 | + // Git | |
| 1878 | + (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs()) | |
| 1879 | + || | |
| 1880 | + // Selections | |
| 1881 | + (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty()) | |
| 1882 | + // Scrollmanager | |
| 1883 | + || editor.scroll_manager.scrollbars_visible() | |
| 1884 | + } | |
| 1885 | + ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(), | |
| 1886 | + ShowScrollbar::Always => true, | |
| 1887 | + ShowScrollbar::Never => false, | |
| 1888 | + }; | |
| 1713 | 1889 | |
| 1714 | − let head_for_relative = newest_selection_head.unwrap_or_else(|| { | |
| 1715 | − let newest = editor.selections.newest::<Point>(cx); | |
| 1716 | − SelectionLayout::new( | |
| 1717 | − newest, | |
| 1718 | − editor.selections.line_mode, | |
| 1719 | − editor.cursor_shape, | |
| 1720 | − &snapshot.display_snapshot, | |
| 1721 | − true, | |
| 1722 | − true, | |
| 1723 | − ) | |
| 1724 | − .head | |
| 1725 | − }); | |
| 1890 | + let head_for_relative = newest_selection_head.unwrap_or_else(|| { | |
| 1891 | + let newest = editor.selections.newest::<Point>(cx); | |
| 1892 | + SelectionLayout::new( | |
| 1893 | + newest, | |
| 1894 | + editor.selections.line_mode, | |
| 1895 | + editor.cursor_shape, | |
| 1896 | + &snapshot.display_snapshot, | |
| 1897 | + true, | |
| 1898 | + true, | |
| 1899 | + ) | |
| 1900 | + .head | |
| 1901 | + }); | |
| 1726 | 1902 | |
| 1727 | − let (line_numbers, fold_statuses) = self.shape_line_numbers( | |
| 1728 | − start_row..end_row, | |
| 1729 | − &active_rows, | |
| 1730 | − head_for_relative, | |
| 1731 | − is_singleton, | |
| 1732 | − &snapshot, | |
| 1733 | − cx, | |
| 1734 | − ); | |
| 1903 | + let (line_numbers, fold_statuses) = self.shape_line_numbers( | |
| 1904 | + start_row..end_row, | |
| 1905 | + &active_rows, | |
| 1906 | + head_for_relative, | |
| 1907 | + is_singleton, | |
| 1908 | + &snapshot, | |
| 1909 | + cx, | |
| 1910 | + ); | |
| 1735 | 1911 | |
| 1736 | − let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot); | |
| 1912 | + let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot); | |
| 1737 | 1913 | |
| 1738 | − let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines); | |
| 1914 | + let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines); | |
| 1739 | 1915 | |
| 1740 | − let mut max_visible_line_width = Pixels::ZERO; | |
| 1741 | − let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx); | |
| 1742 | − for line_with_invisibles in &line_layouts { | |
| 1743 | − if line_with_invisibles.line.width > max_visible_line_width { | |
| 1744 | − max_visible_line_width = line_with_invisibles.line.width; | |
| 1916 | + let mut max_visible_line_width = Pixels::ZERO; | |
| 1917 | + let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx); | |
| 1918 | + for line_with_invisibles in &line_layouts { | |
| 1919 | + if line_with_invisibles.line.width > max_visible_line_width { | |
| 1920 | + max_visible_line_width = line_with_invisibles.line.width; | |
| 1921 | + } | |
| 1745 | 1922 | } |
| 1746 | − } | |
| 1747 | 1923 | |
| 1748 | − let longest_line_width = layout_line(snapshot.longest_row(), &snapshot, &style, cx) | |
| 1749 | − .unwrap() | |
| 1750 | − .width; | |
| 1751 | − let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; | |
| 1924 | + let longest_line_width = layout_line(snapshot.longest_row(), &snapshot, &style, cx) | |
| 1925 | + .unwrap() | |
| 1926 | + .width; | |
| 1927 | + let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; | |
| 1752 | 1928 | |
| 1753 | − let (scroll_width, blocks) = cx.with_element_id(Some("editor_blocks"), |cx| { | |
| 1754 | − self.layout_blocks( | |
| 1755 | − start_row..end_row, | |
| 1756 | − &snapshot, | |
| 1757 | − bounds.size.width, | |
| 1758 | − scroll_width, | |
| 1759 | − gutter_padding, | |
| 1760 | − gutter_width, | |
| 1761 | − em_width, | |
| 1762 | − gutter_width + gutter_margin, | |
| 1763 | − line_height, | |
| 1764 | − &style, | |
| 1765 | − &line_layouts, | |
| 1766 | − editor, | |
| 1767 | − cx, | |
| 1768 | − ) | |
| 1769 | − }); | |
| 1770 | − | |
| 1771 | − let scroll_max = point( | |
| 1772 | − f32::from((scroll_width - text_size.width) / em_width).max(0.0), | |
| 1773 | − max_row as f32, | |
| 1774 | − ); | |
| 1929 | + let (scroll_width, blocks) = cx.with_element_id(Some("editor_blocks"), |cx| { | |
| 1930 | + self.layout_blocks( | |
| 1931 | + start_row..end_row, | |
| 1932 | + &snapshot, | |
| 1933 | + bounds.size.width, | |
| 1934 | + scroll_width, | |
| 1935 | + gutter_padding, | |
| 1936 | + gutter_width, | |
| 1937 | + em_width, | |
| 1938 | + gutter_width + gutter_margin, | |
| 1939 | + line_height, | |
| 1940 | + &style, | |
| 1941 | + &line_layouts, | |
| 1942 | + editor, | |
| 1943 | + cx, | |
| 1944 | + ) | |
| 1945 | + }); | |
| 1775 | 1946 | |
| 1776 | − let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x); | |
| 1947 | + let scroll_max = point( | |
| 1948 | + f32::from((scroll_width - text_size.width) / em_width).max(0.0), | |
| 1949 | + max_row as f32, | |
| 1950 | + ); | |
| 1777 | 1951 | |
| 1778 | − let autoscrolled = if autoscroll_horizontally { | |
| 1779 | − editor.autoscroll_horizontally( | |
| 1780 | − start_row, | |
| 1781 | − text_size.width, | |
| 1782 | − scroll_width, | |
| 1783 | − em_width, | |
| 1784 | − &line_layouts, | |
| 1785 | − cx, | |
| 1786 | − ) | |
| 1787 | − } else { | |
| 1788 | − false | |
| 1789 | − }; | |
| 1952 | + let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x); | |
| 1953 | + | |
| 1954 | + let autoscrolled = if autoscroll_horizontally { | |
| 1955 | + editor.autoscroll_horizontally( | |
| 1956 | + start_row, | |
| 1957 | + text_size.width, | |
| 1958 | + scroll_width, | |
| 1959 | + em_width, | |
| 1960 | + &line_layouts, | |
| 1961 | + cx, | |
| 1962 | + ) | |
| 1963 | + } else { | |
| 1964 | + false | |
| 1965 | + }; | |
| 1790 | 1966 | |
| 1791 | − if clamped || autoscrolled { | |
| 1792 | − snapshot = editor.snapshot(cx); | |
| 1793 | − } | |
| 1967 | + if clamped || autoscrolled { | |
| 1968 | + snapshot = editor.snapshot(cx); | |
| 1969 | + } | |
| 1794 | 1970 | |
| 1795 | − let mut context_menu = None; | |
| 1796 | − let mut code_actions_indicator = None; | |
| 1797 | − if let Some(newest_selection_head) = newest_selection_head { | |
| 1798 | − if (start_row..end_row).contains(&newest_selection_head.row()) { | |
| 1799 | − if editor.context_menu_visible() { | |
| 1800 | − context_menu = | |
| 1801 | − editor.render_context_menu(newest_selection_head, &self.style, cx); | |
| 1802 | − } | |
| 1971 | + let mut context_menu = None; | |
| 1972 | + let mut code_actions_indicator = None; | |
| 1973 | + if let Some(newest_selection_head) = newest_selection_head { | |
| 1974 | + if (start_row..end_row).contains(&newest_selection_head.row()) { | |
| 1975 | + if editor.context_menu_visible() { | |
| 1976 | + let max_height = (12. * line_height).min((bounds.size.height - line_height) / 2.); | |
| 1977 | + context_menu = | |
| 1978 | + editor.render_context_menu(newest_selection_head, &self.style, max_height, cx); | |
| 1979 | + } | |
| 1803 | 1980 | |
| 1804 | − let active = matches!( | |
| 1805 | − editor.context_menu.read().as_ref(), | |
| 1806 | − Some(crate::ContextMenu::CodeActions(_)) | |
| 1807 | − ); | |
| 1981 | + let active = matches!( | |
| 1982 | + editor.context_menu.read().as_ref(), | |
| 1983 | + Some(crate::ContextMenu::CodeActions(_)) | |
| 1984 | + ); | |
| 1808 | 1985 | |
| 1809 | − code_actions_indicator = editor | |
| 1810 | − .render_code_actions_indicator(&style, active, cx) | |
| 1811 | − .map(|element| CodeActionsIndicator { | |
| 1812 | − row: newest_selection_head.row(), | |
| 1813 | − element, | |
| 1814 | − }); | |
| 1986 | + code_actions_indicator = editor | |
| 1987 | + .render_code_actions_indicator(&style, active, cx) | |
| 1988 | + .map(|element| CodeActionsIndicator { | |
| 1989 | + row: newest_selection_head.row(), | |
| 1990 | + button: element, | |
| 1991 | + }); | |
| 1992 | + } | |
| 1815 | 1993 | } |
| 1816 | − } | |
| 1817 | 1994 | |
| 1818 | − let visible_rows = start_row..start_row + line_layouts.len() as u32; | |
| 1819 | − // todo!("hover") | |
| 1820 | − // let mut hover = editor.hover_state.render( | |
| 1821 | − // &snapshot, | |
| 1822 | − // &style, | |
| 1823 | − // visible_rows, | |
| 1824 | − // editor.workspace.as_ref().map(|(w, _)| w.clone()), | |
| 1825 | − // cx, | |
| 1826 | − // ); | |
| 1827 | − // let mode = editor.mode; | |
| 1828 | − | |
| 1829 | − let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| { | |
| 1830 | − editor.render_fold_indicators( | |
| 1831 | − fold_statuses, | |
| 1995 | + let visible_rows = start_row..start_row + line_layouts.len() as u32; | |
| 1996 | + let max_size = size( | |
| 1997 | + (120. * em_width) // Default size | |
| 1998 | + .min(bounds.size.width / 2.) // Shrink to half of the editor width | |
| 1999 | + .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters | |
| 2000 | + (16. * line_height) // Default size | |
| 2001 | + .min(bounds.size.height / 2.) // Shrink to half of the editor height | |
| 2002 | + .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines | |
| 2003 | + ); | |
| 2004 | + | |
| 2005 | + let mut hover = editor.hover_state.render( | |
| 2006 | + &snapshot, | |
| 1832 | 2007 | &style, |
| 1833 | − editor.gutter_hovered, | |
| 1834 | − line_height, | |
| 1835 | − gutter_margin, | |
| 2008 | + visible_rows, | |
| 2009 | + max_size, | |
| 2010 | + editor.workspace.as_ref().map(|(w, _)| w.clone()), | |
| 1836 | 2011 | cx, |
| 1837 | − ) | |
| 1838 | − }); | |
| 1839 | − | |
| 1840 | − // todo!("context_menu") | |
| 1841 | − // if let Some((_, context_menu)) = context_menu.as_mut() { | |
| 1842 | − // context_menu.layout( | |
| 1843 | − // SizeConstraint { | |
| 1844 | − // min: gpui::Point::<Pixels>::zero(), | |
| 1845 | − // max: point( | |
| 1846 | − // cx.window_size().x * 0.7, | |
| 1847 | − // (12. * line_height).min((size.y - line_height) / 2.), | |
| 1848 | − // ), | |
| 1849 | − // }, | |
| 1850 | − // editor, | |
| 1851 | − // cx, | |
| 1852 | − // ); | |
| 1853 | − // } | |
| 1854 | − | |
| 1855 | − // todo!("hover popovers") | |
| 1856 | − // if let Some((_, hover_popovers)) = hover.as_mut() { | |
| 1857 | − // for hover_popover in hover_popovers.iter_mut() { | |
| 1858 | − // hover_popover.layout( | |
| 1859 | − // SizeConstraint { | |
| 1860 | − // min: gpui::Point::<Pixels>::zero(), | |
| 1861 | − // max: point( | |
| 1862 | − // (120. * em_width) // Default size | |
| 1863 | − // .min(size.x / 2.) // Shrink to half of the editor width | |
| 1864 | − // .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters | |
| 1865 | − // (16. * line_height) // Default size | |
| 1866 | − // .min(size.y / 2.) // Shrink to half of the editor height | |
| 1867 | − // .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines | |
| 1868 | − // ), | |
| 1869 | − // }, | |
| 1870 | − // editor, | |
| 1871 | − // cx, | |
| 1872 | − // ); | |
| 1873 | − // } | |
| 1874 | − // } | |
| 2012 | + ); | |
| 1875 | 2013 | |
| 1876 | − let invisible_symbol_font_size = font_size / 2.; | |
| 1877 | − let tab_invisible = cx | |
| 1878 | − .text_system() | |
| 1879 | − .shape_line( | |
| 1880 | − "→".into(), | |
| 1881 | − invisible_symbol_font_size, | |
| 1882 | − &[TextRun { | |
| 1883 | − len: "→".len(), | |
| 1884 | − font: self.style.text.font(), | |
| 1885 | − color: cx.theme().colors().editor_invisible, | |
| 1886 | − background_color: None, | |
| 1887 | − underline: None, | |
| 1888 | − }], | |
| 1889 | − ) | |
| 1890 | − .unwrap(); | |
| 1891 | − let space_invisible = cx | |
| 1892 | − .text_system() | |
| 1893 | − .shape_line( | |
| 1894 | − "•".into(), | |
| 1895 | − invisible_symbol_font_size, | |
| 1896 | − &[TextRun { | |
| 1897 | − len: "•".len(), | |
| 1898 | − font: self.style.text.font(), | |
| 1899 | − color: cx.theme().colors().editor_invisible, | |
| 1900 | − background_color: None, | |
| 1901 | − underline: None, | |
| 1902 | − }], | |
| 1903 | − ) | |
| 1904 | − .unwrap(); | |
| 2014 | + let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| { | |
| 2015 | + editor.render_fold_indicators( | |
| 2016 | + fold_statuses, | |
| 2017 | + &style, | |
| 2018 | + editor.gutter_hovered, | |
| 2019 | + line_height, | |
| 2020 | + gutter_margin, | |
| 2021 | + cx, | |
| 2022 | + ) | |
| 2023 | + }); | |
| 1905 | 2024 | |
| 1906 | − LayoutState { | |
| 1907 | − mode: editor_mode, | |
| 1908 | − position_map: Arc::new(PositionMap { | |
| 1909 | − size: bounds.size, | |
| 1910 | − scroll_position: point( | |
| 1911 | − scroll_position.x * em_width, | |
| 1912 | − scroll_position.y * line_height, | |
| 1913 | − ), | |
| 1914 | − scroll_max, | |
| 1915 | − line_layouts, | |
| 1916 | − line_height, | |
| 1917 | − em_width, | |
| 1918 | − em_advance, | |
| 1919 | − snapshot, | |
| 1920 | − }), | |
| 1921 | − visible_anchor_range: start_anchor..end_anchor, | |
| 1922 | − visible_display_row_range: start_row..end_row, | |
| 1923 | − wrap_guides, | |
| 1924 | − gutter_size, | |
| 1925 | − gutter_padding, | |
| 1926 | − text_size, | |
| 1927 | − scrollbar_row_range, | |
| 1928 | − show_scrollbars, | |
| 1929 | − is_singleton, | |
| 1930 | − max_row, | |
| 1931 | − gutter_margin, | |
| 1932 | − active_rows, | |
| 1933 | − highlighted_rows, | |
| 1934 | − highlighted_ranges, | |
| 1935 | − line_numbers, | |
| 1936 | − display_hunks, | |
| 1937 | − blocks, | |
| 1938 | − selections, | |
| 1939 | − context_menu, | |
| 1940 | − code_actions_indicator, | |
| 1941 | − fold_indicators, | |
| 1942 | − tab_invisible, | |
| 1943 | − space_invisible, | |
| 1944 | − // hover_popovers: hover, | |
| 1945 | − } | |
| 2025 | + let invisible_symbol_font_size = font_size / 2.; | |
| 2026 | + let tab_invisible = cx | |
| 2027 | + .text_system() | |
| 2028 | + .shape_line( | |
| 2029 | + "→".into(), | |
| 2030 | + invisible_symbol_font_size, | |
| 2031 | + &[TextRun { | |
| 2032 | + len: "→".len(), | |
| 2033 | + font: self.style.text.font(), | |
| 2034 | + color: cx.theme().colors().editor_invisible, | |
| 2035 | + background_color: None, | |
| 2036 | + underline: None, | |
| 2037 | + }], | |
| 2038 | + ) | |
| 2039 | + .unwrap(); | |
| 2040 | + let space_invisible = cx | |
| 2041 | + .text_system() | |
| 2042 | + .shape_line( | |
| 2043 | + "•".into(), | |
| 2044 | + invisible_symbol_font_size, | |
| 2045 | + &[TextRun { | |
| 2046 | + len: "•".len(), | |
| 2047 | + font: self.style.text.font(), | |
| 2048 | + color: cx.theme().colors().editor_invisible, | |
| 2049 | + background_color: None, | |
| 2050 | + underline: None, | |
| 2051 | + }], | |
| 2052 | + ) | |
| 2053 | + .unwrap(); | |
| 2054 | + | |
| 2055 | + LayoutState { | |
| 2056 | + mode: editor_mode, | |
| 2057 | + position_map: Arc::new(PositionMap { | |
| 2058 | + size: bounds.size, | |
| 2059 | + scroll_position: point( | |
| 2060 | + scroll_position.x * em_width, | |
| 2061 | + scroll_position.y * line_height, | |
| 2062 | + ), | |
| 2063 | + scroll_max, | |
| 2064 | + line_layouts, | |
| 2065 | + line_height, | |
| 2066 | + em_width, | |
| 2067 | + em_advance, | |
| 2068 | + snapshot, | |
| 2069 | + }), | |
| 2070 | + visible_anchor_range: start_anchor..end_anchor, | |
| 2071 | + visible_display_row_range: start_row..end_row, | |
| 2072 | + wrap_guides, | |
| 2073 | + gutter_size, | |
| 2074 | + gutter_padding, | |
| 2075 | + text_size, | |
| 2076 | + scrollbar_row_range, | |
| 2077 | + show_scrollbars, | |
| 2078 | + is_singleton, | |
| 2079 | + max_row, | |
| 2080 | + gutter_margin, | |
| 2081 | + active_rows, | |
| 2082 | + highlighted_rows, | |
| 2083 | + highlighted_ranges, | |
| 2084 | + line_numbers, | |
| 2085 | + display_hunks, | |
| 2086 | + blocks, | |
| 2087 | + selections, | |
| 2088 | + context_menu, | |
| 2089 | + code_actions_indicator, | |
| 2090 | + fold_indicators, | |
| 2091 | + tab_invisible, | |
| 2092 | + space_invisible, | |
| 2093 | + hover_popovers: hover, | |
| 2094 | + } | |
| 2095 | + }) | |
| 1946 | 2096 | } |
| 1947 | 2097 | |
| 1948 | 2098 | #[allow(clippy::too_many_arguments)] |
| 1949 | 2099 | fn layout_blocks( |
| 1950 | − &mut self, | |
| 2100 | + &self, | |
| 1951 | 2101 | rows: Range<u32>, |
| 1952 | 2102 | snapshot: &EditorSnapshot, |
| 1953 | 2103 | editor_width: Pixels, |
| @@ -2028,12 +2178,10 @@ impl EditorElement { · 74 unmodified lines | ||
| 2028 | 2178 | let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); |
| 2029 | 2179 | |
| 2030 | 2180 | IconButton::new(block_id, ui::Icon::ArrowUpRight) |
| 2031 | − .on_click(move |editor: &mut Editor, cx| { | |
| 2181 | + .on_click(cx.listener_for(&self.editor, move |editor, e, cx| { | |
| 2032 | 2182 | editor.jump(jump_path.clone(), jump_position, jump_anchor, cx); |
| 2033 | − }) | |
| 2034 | − .tooltip(move |_, cx| { | |
| 2035 | − Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx) | |
| 2036 | − }) | |
| 2183 | + })) | |
| 2184 | + .tooltip(|cx| Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)) | |
| 2037 | 2185 | }); |
| 2038 | 2186 | |
| 2039 | 2187 | let element = if *starts_new_buffer { |
| @@ -2043,15 +2191,20 @@ impl EditorElement { · 3 unmodified lines | ||
| 2043 | 2191 | // Can't use .and_then() because `.file_name()` and `.parent()` return references :( |
| 2044 | 2192 | if let Some(path) = path { |
| 2045 | 2193 | filename = path.file_name().map(|f| f.to_string_lossy().to_string()); |
| 2046 | − parent_path = | |
| 2047 | − path.parent().map(|p| p.to_string_lossy().to_string() + "/"); | |
| 2194 | + parent_path = path | |
| 2195 | + .parent() | |
| 2196 | + .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/")); | |
| 2048 | 2197 | } |
| 2049 | 2198 | |
| 2050 | 2199 | h_stack() |
| 2051 | 2200 | .id("path header block") |
| 2052 | 2201 | .size_full() |
| 2053 | 2202 | .bg(gpui::red()) |
| 2054 | − .child(filename.unwrap_or_else(|| "untitled".to_string())) | |
| 2203 | + .child( | |
| 2204 | + filename | |
| 2205 | + .map(SharedString::from) | |
| 2206 | + .unwrap_or_else(|| "untitled".into()), | |
| 2207 | + ) | |
| 2055 | 2208 | .children(parent_path) |
| 2056 | 2209 | .children(jump_icon) // .p_x(gutter_padding) |
| 2057 | 2210 | } else { |
| @@ -2063,11 +2216,11 @@ impl EditorElement { · 5 unmodified lines | ||
| 2063 | 2216 | .child("⋯") |
| 2064 | 2217 | .children(jump_icon) // .p_x(gutter_padding) |
| 2065 | 2218 | }; |
| 2066 | − element.render() | |
| 2219 | + element.into_any() | |
| 2067 | 2220 | } |
| 2068 | 2221 | }; |
| 2069 | 2222 | |
| 2070 | − let size = element.measure(available_space, editor, cx); | |
| 2223 | + let size = element.measure(available_space, cx); | |
| 2071 | 2224 | (element, size) |
| 2072 | 2225 | }; |
| 2073 | 2226 | |
| @@ -2126,47 +2279,61 @@ impl EditorElement { · 52 unmodified lines | ||
| 2126 | 2279 | gutter_bounds: Bounds<Pixels>, |
| 2127 | 2280 | text_bounds: Bounds<Pixels>, |
| 2128 | 2281 | layout: &LayoutState, |
| 2129 | − cx: &mut ViewContext<Editor>, | |
| 2282 | + cx: &mut WindowContext, | |
| 2130 | 2283 | ) { |
| 2131 | 2284 | let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); |
| 2132 | 2285 | |
| 2133 | 2286 | cx.on_mouse_event({ |
| 2134 | 2287 | let position_map = layout.position_map.clone(); |
| 2135 | − move |editor, event: &ScrollWheelEvent, phase, cx| { | |
| 2288 | + let editor = self.editor.clone(); | |
| 2289 | + | |
| 2290 | + move |event: &ScrollWheelEvent, phase, cx| { | |
| 2136 | 2291 | if phase != DispatchPhase::Bubble { |
| 2137 | 2292 | return; |
| 2138 | 2293 | } |
| 2139 | 2294 | |
| 2140 | − if Self::scroll(editor, event, &position_map, bounds, cx) { | |
| 2295 | + let should_cancel = editor.update(cx, |editor, cx| { | |
| 2296 | + Self::scroll(editor, event, &position_map, bounds, cx) | |
| 2297 | + }); | |
| 2298 | + if should_cancel { | |
| 2141 | 2299 | cx.stop_propagation(); |
| 2142 | 2300 | } |
| 2143 | 2301 | } |
| 2144 | 2302 | }); |
| 2303 | + | |
| 2145 | 2304 | cx.on_mouse_event({ |
| 2146 | 2305 | let position_map = layout.position_map.clone(); |
| 2147 | − move |editor, event: &MouseDownEvent, phase, cx| { | |
| 2306 | + let editor = self.editor.clone(); | |
| 2307 | + | |
| 2308 | + move |event: &MouseDownEvent, phase, cx| { | |
| 2148 | 2309 | if phase != DispatchPhase::Bubble { |
| 2149 | 2310 | return; |
| 2150 | 2311 | } |
| 2151 | 2312 | |
| 2152 | − if Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx) { | |
| 2313 | + let should_cancel = editor.update(cx, |editor, cx| { | |
| 2314 | + Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx) | |
| 2315 | + }); | |
| 2316 | + | |
| 2317 | + if should_cancel { | |
| 2153 | 2318 | cx.stop_propagation() |
| 2154 | 2319 | } |
| 2155 | 2320 | } |
| 2156 | 2321 | }); |
| 2322 | + | |
| 2157 | 2323 | cx.on_mouse_event({ |
| 2158 | 2324 | let position_map = layout.position_map.clone(); |
| 2159 | − move |editor, event: &MouseUpEvent, phase, cx| { | |
| 2160 | − if phase != DispatchPhase::Bubble { | |
| 2161 | − return; | |
| 2162 | − } | |
| 2325 | + let editor = self.editor.clone(); | |
| 2326 | + move |event: &MouseUpEvent, phase, cx| { | |
| 2327 | + let should_cancel = editor.update(cx, |editor, cx| { | |
| 2328 | + Self::mouse_up(editor, event, &position_map, text_bounds, cx) | |
| 2329 | + }); | |
| 2163 | 2330 | |
| 2164 | − if Self::mouse_up(editor, event, &position_map, text_bounds, cx) { | |
| 2331 | + if should_cancel { | |
| 2165 | 2332 | cx.stop_propagation() |
| 2166 | 2333 | } |
| 2167 | 2334 | } |
| 2168 | 2335 | }); |
| 2169 | − // todo!() | |
| 2336 | + //todo!() | |
| 2170 | 2337 | // on_down(MouseButton::Right, { |
| 2171 | 2338 | // let position_map = layout.position_map.clone(); |
| 2172 | 2339 | // move |event, editor, cx| { |
| @@ -2183,12 +2350,17 @@ impl EditorElement { · 10 unmodified lines | ||
| 2183 | 2350 | // }); |
| 2184 | 2351 | cx.on_mouse_event({ |
| 2185 | 2352 | let position_map = layout.position_map.clone(); |
| 2186 | − move |editor, event: &MouseMoveEvent, phase, cx| { | |
| 2353 | + let editor = self.editor.clone(); | |
| 2354 | + move |event: &MouseMoveEvent, phase, cx| { | |
| 2187 | 2355 | if phase != DispatchPhase::Bubble { |
| 2188 | 2356 | return; |
| 2189 | 2357 | } |
| 2190 | 2358 | |
| 2191 | − if Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx) { | |
| 2359 | + let stop_propogating = editor.update(cx, |editor, cx| { | |
| 2360 | + Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx) | |
| 2361 | + }); | |
| 2362 | + | |
| 2363 | + if stop_propogating { | |
| 2192 | 2364 | cx.stop_propagation() |
| 2193 | 2365 | } |
| 2194 | 2366 | } |
| @@ -2267,7 +2439,7 @@ impl LineWithInvisibles { · 72 unmodified lines | ||
| 2267 | 2439 | len: line_chunk.len(), |
| 2268 | 2440 | font: text_style.font(), |
| 2269 | 2441 | color: text_style.color, |
| 2270 | − background_color: None, | |
| 2442 | + background_color: text_style.background_color, | |
| 2271 | 2443 | underline: text_style.underline, |
| 2272 | 2444 | }); |
| 2273 | 2445 | |
| @@ -2317,7 +2489,7 @@ impl LineWithInvisibles { · 43 unmodified lines | ||
| 2317 | 2489 | content_origin: gpui::Point<Pixels>, |
| 2318 | 2490 | whitespace_setting: ShowWhitespaceSetting, |
| 2319 | 2491 | selection_ranges: &[Range<DisplayPoint>], |
| 2320 | − cx: &mut ViewContext<Editor>, | |
| 2492 | + cx: &mut WindowContext, | |
| 2321 | 2493 | ) { |
| 2322 | 2494 | let line_height = layout.position_map.line_height; |
| 2323 | 2495 | let line_y = line_height * row as f32 - layout.position_map.scroll_position.y; |
| @@ -2349,7 +2521,7 @@ impl LineWithInvisibles { · 25 unmodified lines | ||
| 2349 | 2521 | row: u32, |
| 2350 | 2522 | line_height: Pixels, |
| 2351 | 2523 | whitespace_setting: ShowWhitespaceSetting, |
| 2352 | − cx: &mut ViewContext<Editor>, | |
| 2524 | + cx: &mut WindowContext, | |
| 2353 | 2525 | ) { |
| 2354 | 2526 | let allowed_invisibles_regions = match whitespace_setting { |
| 2355 | 2527 | ShowWhitespaceSetting::None => return, |
| @@ -2392,41 +2564,42 @@ enum Invisible { · 36 unmodified lines | ||
| 2392 | 2564 | Whitespace { line_offset: usize }, |
| 2393 | 2565 | } |
| 2394 | 2566 | |
| 2395 | −impl Element<Editor> for EditorElement { | |
| 2396 | − type ElementState = (); | |
| 2397 | − | |
| 2398 | − fn element_id(&self) -> Option<gpui::ElementId> { | |
| 2399 | − Some(self.editor_id.into()) | |
| 2400 | − } | |
| 2567 | +impl Element for EditorElement { | |
| 2568 | + type State = (); | |
| 2401 | 2569 | |
| 2402 | 2570 | fn layout( |
| 2403 | 2571 | &mut self, |
| 2404 | − editor: &mut Editor, | |
| 2405 | − element_state: Option<Self::ElementState>, | |
| 2406 | − cx: &mut gpui::ViewContext<Editor>, | |
| 2407 | − ) -> (gpui::LayoutId, Self::ElementState) { | |
| 2408 | − editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. | |
| 2409 | − | |
| 2410 | − let rem_size = cx.rem_size(); | |
| 2411 | − let mut style = Style::default(); | |
| 2412 | − style.size.width = relative(1.).into(); | |
| 2413 | − style.size.height = match editor.mode { | |
| 2414 | − EditorMode::SingleLine => self.style.text.line_height_in_pixels(cx.rem_size()).into(), | |
| 2415 | − EditorMode::AutoHeight { .. } => todo!(), | |
| 2416 | − EditorMode::Full => relative(1.).into(), | |
| 2417 | − }; | |
| 2418 | − let layout_id = cx.request_layout(&style, None); | |
| 2419 | − (layout_id, ()) | |
| 2572 | + element_state: Option<Self::State>, | |
| 2573 | + cx: &mut gpui::WindowContext, | |
| 2574 | + ) -> (gpui::LayoutId, Self::State) { | |
| 2575 | + self.editor.update(cx, |editor, cx| { | |
| 2576 | + editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. | |
| 2577 | + | |
| 2578 | + let rem_size = cx.rem_size(); | |
| 2579 | + let mut style = Style::default(); | |
| 2580 | + style.size.width = relative(1.).into(); | |
| 2581 | + style.size.height = match editor.mode { | |
| 2582 | + EditorMode::SingleLine => { | |
| 2583 | + self.style.text.line_height_in_pixels(cx.rem_size()).into() | |
| 2584 | + } | |
| 2585 | + EditorMode::AutoHeight { .. } => todo!(), | |
| 2586 | + EditorMode::Full => relative(1.).into(), | |
| 2587 | + }; | |
| 2588 | + let layout_id = cx.request_layout(&style, None); | |
| 2589 | + | |
| 2590 | + (layout_id, ()) | |
| 2591 | + }) | |
| 2420 | 2592 | } |
| 2421 | 2593 | |
| 2422 | 2594 | fn paint( |
| 2423 | − &mut self, | |
| 2595 | + mut self, | |
| 2424 | 2596 | bounds: Bounds<gpui::Pixels>, |
| 2425 | − editor: &mut Editor, | |
| 2426 | − element_state: &mut Self::ElementState, | |
| 2427 | − cx: &mut gpui::ViewContext<Editor>, | |
| 2597 | + element_state: &mut Self::State, | |
| 2598 | + cx: &mut gpui::WindowContext, | |
| 2428 | 2599 | ) { |
| 2429 | − let mut layout = self.compute_layout(editor, cx, bounds); | |
| 2600 | + let editor = self.editor.clone(); | |
| 2601 | + | |
| 2602 | + let mut layout = self.compute_layout(bounds, cx); | |
| 2430 | 2603 | let gutter_bounds = Bounds { |
| 2431 | 2604 | origin: bounds.origin, |
| 2432 | 2605 | size: layout.gutter_size, |
| @@ -2436,43 +2609,46 @@ impl Element<Editor> for EditorElement { · 3 unmodified lines | ||
| 2436 | 2609 | size: layout.text_size, |
| 2437 | 2610 | }; |
| 2438 | 2611 | |
| 2439 | − let dispatch_context = editor.dispatch_context(cx); | |
| 2440 | − cx.with_key_dispatch( | |
| 2441 | − dispatch_context, | |
| 2442 | − Some(editor.focus_handle.clone()), | |
| 2443 | − |_, cx| { | |
| 2444 | − register_actions(cx); | |
| 2445 | − | |
| 2446 | − // We call with_z_index to establish a new stacking context. | |
| 2447 | − cx.with_z_index(0, |cx| { | |
| 2448 | − cx.with_content_mask(Some(ContentMask { bounds }), |cx| { | |
| 2449 | − // Paint mouse listeners first, so any elements we paint on top of the editor | |
| 2450 | − // take precedence. | |
| 2451 | − self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); | |
| 2452 | − let input_handler = ElementInputHandler::new(bounds, cx); | |
| 2453 | − cx.handle_input(&editor.focus_handle, input_handler); | |
| 2454 | − | |
| 2455 | − self.paint_background(gutter_bounds, text_bounds, &layout, cx); | |
| 2456 | − if layout.gutter_size.width > Pixels::ZERO { | |
| 2457 | − self.paint_gutter(gutter_bounds, &mut layout, editor, cx); | |
| 2458 | − } | |
| 2459 | − self.paint_text(text_bounds, &mut layout, editor, cx); | |
| 2612 | + let focus_handle = editor.focus_handle(cx); | |
| 2613 | + let dispatch_context = self.editor.read(cx).dispatch_context(cx); | |
| 2614 | + cx.with_key_dispatch(dispatch_context, Some(focus_handle.clone()), |_, cx| { | |
| 2615 | + self.register_actions(cx); | |
| 2616 | + | |
| 2617 | + // We call with_z_index to establish a new stacking context. | |
| 2618 | + cx.with_z_index(0, |cx| { | |
| 2619 | + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { | |
| 2620 | + // Paint mouse listeners first, so any elements we paint on top of the editor | |
| 2621 | + // take precedence. | |
| 2622 | + self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); | |
| 2623 | + let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx); | |
| 2624 | + cx.handle_input(&focus_handle, input_handler); | |
| 2625 | + | |
| 2626 | + self.paint_background(gutter_bounds, text_bounds, &layout, cx); | |
| 2627 | + if layout.gutter_size.width > Pixels::ZERO { | |
| 2628 | + self.paint_gutter(gutter_bounds, &mut layout, cx); | |
| 2629 | + } | |
| 2630 | + self.paint_text(text_bounds, &mut layout, cx); | |
| 2460 | 2631 | |
| 2461 | − if !layout.blocks.is_empty() { | |
| 2462 | − cx.with_element_id(Some("editor_blocks"), |cx| { | |
| 2463 | − self.paint_blocks(bounds, &mut layout, editor, cx); | |
| 2464 | − }) | |
| 2465 | − } | |
| 2466 | − }); | |
| 2632 | + if !layout.blocks.is_empty() { | |
| 2633 | + cx.with_element_id(Some("editor_blocks"), |cx| { | |
| 2634 | + self.paint_blocks(bounds, &mut layout, cx); | |
| 2635 | + }) | |
| 2636 | + } | |
| 2467 | 2637 | }); |
| 2468 | − }, | |
| 2469 | − ) | |
| 2638 | + }); | |
| 2639 | + }) | |
| 2470 | 2640 | } |
| 2471 | 2641 | } |
| 2472 | 2642 | |
| 2473 | −impl Component<Editor> for EditorElement { | |
| 2474 | − fn render(self) -> AnyElement<Editor> { | |
| 2475 | − AnyElement::new(self) | |
| 2643 | +impl IntoElement for EditorElement { | |
| 2644 | + type Element = Self; | |
| 2645 | + | |
| 2646 | + fn element_id(&self) -> Option<gpui::ElementId> { | |
| 2647 | + self.editor.element_id() | |
| 2648 | + } | |
| 2649 | + | |
| 2650 | + fn into_element(self) -> Self::Element { | |
| 2651 | + self | |
| 2476 | 2652 | } |
| 2477 | 2653 | } |
| 2478 | 2654 | |
| @@ -3097,17 +3273,17 @@ pub struct LayoutState { · 618 unmodified lines | ||
| 3097 | 3273 | show_scrollbars: bool, |
| 3098 | 3274 | is_singleton: bool, |
| 3099 | 3275 | max_row: u32, |
| 3100 | − context_menu: Option<(DisplayPoint, AnyElement<Editor>)>, | |
| 3276 | + context_menu: Option<(DisplayPoint, AnyElement)>, | |
| 3101 | 3277 | code_actions_indicator: Option<CodeActionsIndicator>, |
| 3102 | − // hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>, | |
| 3103 | − fold_indicators: Vec<Option<AnyElement<Editor>>>, | |
| 3278 | + hover_popovers: Option<(DisplayPoint, Vec<AnyElement>)>, | |
| 3279 | + fold_indicators: Vec<Option<IconButton>>, | |
| 3104 | 3280 | tab_invisible: ShapedLine, |
| 3105 | 3281 | space_invisible: ShapedLine, |
| 3106 | 3282 | } |
| 3107 | 3283 | |
| 3108 | 3284 | struct CodeActionsIndicator { |
| 3109 | 3285 | row: u32, |
| 3110 | − element: AnyElement<Editor>, | |
| 3286 | + button: IconButton, | |
| 3111 | 3287 | } |
| 3112 | 3288 | |
| 3113 | 3289 | struct PositionMap { |
| @@ -3192,7 +3368,7 @@ impl PositionMap { · 78 unmodified lines | ||
| 3192 | 3368 | |
| 3193 | 3369 | struct BlockLayout { |
| 3194 | 3370 | row: u32, |
| 3195 | − element: AnyElement<Editor>, | |
| 3371 | + element: AnyElement, | |
| 3196 | 3372 | available_space: Size<AvailableSpace>, |
| 3197 | 3373 | style: BlockStyle, |
| 3198 | 3374 | } |
| @@ -3897,187 +4073,18 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { · 698 unmodified lines | ||
| 3897 | 4073 | // } |
| 3898 | 4074 | // } |
| 3899 | 4075 | |
| 3900 | −fn register_actions(cx: &mut ViewContext<Editor>) { | |
| 3901 | − register_action(cx, Editor::move_left); | |
| 3902 | − register_action(cx, Editor::move_right); | |
| 3903 | − register_action(cx, Editor::move_down); | |
| 3904 | − register_action(cx, Editor::move_up); | |
| 3905 | − // on_action(cx, Editor::new_file); todo!() | |
| 3906 | − // on_action(cx, Editor::new_file_in_direction); todo!() | |
| 3907 | − register_action(cx, Editor::cancel); | |
| 3908 | − register_action(cx, Editor::newline); | |
| 3909 | − register_action(cx, Editor::newline_above); | |
| 3910 | − register_action(cx, Editor::newline_below); | |
| 3911 | − register_action(cx, Editor::backspace); | |
| 3912 | − register_action(cx, Editor::delete); | |
| 3913 | − register_action(cx, Editor::tab); | |
| 3914 | − register_action(cx, Editor::tab_prev); | |
| 3915 | − register_action(cx, Editor::indent); | |
| 3916 | − register_action(cx, Editor::outdent); | |
| 3917 | − register_action(cx, Editor::delete_line); | |
| 3918 | − register_action(cx, Editor::join_lines); | |
| 3919 | − register_action(cx, Editor::sort_lines_case_sensitive); | |
| 3920 | − register_action(cx, Editor::sort_lines_case_insensitive); | |
| 3921 | − register_action(cx, Editor::reverse_lines); | |
| 3922 | − register_action(cx, Editor::shuffle_lines); | |
| 3923 | − register_action(cx, Editor::convert_to_upper_case); | |
| 3924 | − register_action(cx, Editor::convert_to_lower_case); | |
| 3925 | − register_action(cx, Editor::convert_to_title_case); | |
| 3926 | − register_action(cx, Editor::convert_to_snake_case); | |
| 3927 | − register_action(cx, Editor::convert_to_kebab_case); | |
| 3928 | − register_action(cx, Editor::convert_to_upper_camel_case); | |
| 3929 | − register_action(cx, Editor::convert_to_lower_camel_case); | |
| 3930 | − register_action(cx, Editor::delete_to_previous_word_start); | |
| 3931 | − register_action(cx, Editor::delete_to_previous_subword_start); | |
| 3932 | − register_action(cx, Editor::delete_to_next_word_end); | |
| 3933 | − register_action(cx, Editor::delete_to_next_subword_end); | |
| 3934 | − register_action(cx, Editor::delete_to_beginning_of_line); | |
| 3935 | − register_action(cx, Editor::delete_to_end_of_line); | |
| 3936 | − register_action(cx, Editor::cut_to_end_of_line); | |
| 3937 | − register_action(cx, Editor::duplicate_line); | |
| 3938 | − register_action(cx, Editor::move_line_up); | |
| 3939 | − register_action(cx, Editor::move_line_down); | |
| 3940 | − register_action(cx, Editor::transpose); | |
| 3941 | − register_action(cx, Editor::cut); | |
| 3942 | − register_action(cx, Editor::copy); | |
| 3943 | − register_action(cx, Editor::paste); | |
| 3944 | − register_action(cx, Editor::undo); | |
| 3945 | − register_action(cx, Editor::redo); | |
| 3946 | − register_action(cx, Editor::move_page_up); | |
| 3947 | − register_action(cx, Editor::move_page_down); | |
| 3948 | − register_action(cx, Editor::next_screen); | |
| 3949 | − register_action(cx, Editor::scroll_cursor_top); | |
| 3950 | − register_action(cx, Editor::scroll_cursor_center); | |
| 3951 | − register_action(cx, Editor::scroll_cursor_bottom); | |
| 3952 | − register_action(cx, |editor, _: &LineDown, cx| { | |
| 3953 | − editor.scroll_screen(&ScrollAmount::Line(1.), cx) | |
| 3954 | − }); | |
| 3955 | − register_action(cx, |editor, _: &LineUp, cx| { | |
| 3956 | − editor.scroll_screen(&ScrollAmount::Line(-1.), cx) | |
| 3957 | − }); | |
| 3958 | − register_action(cx, |editor, _: &HalfPageDown, cx| { | |
| 3959 | − editor.scroll_screen(&ScrollAmount::Page(0.5), cx) | |
| 3960 | − }); | |
| 3961 | − register_action(cx, |editor, _: &HalfPageUp, cx| { | |
| 3962 | − editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) | |
| 3963 | − }); | |
| 3964 | − register_action(cx, |editor, _: &PageDown, cx| { | |
| 3965 | − editor.scroll_screen(&ScrollAmount::Page(1.), cx) | |
| 3966 | − }); | |
| 3967 | − register_action(cx, |editor, _: &PageUp, cx| { | |
| 3968 | − editor.scroll_screen(&ScrollAmount::Page(-1.), cx) | |
| 3969 | − }); | |
| 3970 | − register_action(cx, Editor::move_to_previous_word_start); | |
| 3971 | − register_action(cx, Editor::move_to_previous_subword_start); | |
| 3972 | − register_action(cx, Editor::move_to_next_word_end); | |
| 3973 | − register_action(cx, Editor::move_to_next_subword_end); | |
| 3974 | − register_action(cx, Editor::move_to_beginning_of_line); | |
| 3975 | − register_action(cx, Editor::move_to_end_of_line); | |
| 3976 | − register_action(cx, Editor::move_to_start_of_paragraph); | |
| 3977 | − register_action(cx, Editor::move_to_end_of_paragraph); | |
| 3978 | − register_action(cx, Editor::move_to_beginning); | |
| 3979 | − register_action(cx, Editor::move_to_end); | |
| 3980 | − register_action(cx, Editor::select_up); | |
| 3981 | − register_action(cx, Editor::select_down); | |
| 3982 | − register_action(cx, Editor::select_left); | |
| 3983 | − register_action(cx, Editor::select_right); | |
| 3984 | − register_action(cx, Editor::select_to_previous_word_start); | |
| 3985 | − register_action(cx, Editor::select_to_previous_subword_start); | |
| 3986 | − register_action(cx, Editor::select_to_next_word_end); | |
| 3987 | − register_action(cx, Editor::select_to_next_subword_end); | |
| 3988 | − register_action(cx, Editor::select_to_beginning_of_line); | |
| 3989 | − register_action(cx, Editor::select_to_end_of_line); | |
| 3990 | − register_action(cx, Editor::select_to_start_of_paragraph); | |
| 3991 | − register_action(cx, Editor::select_to_end_of_paragraph); | |
| 3992 | − register_action(cx, Editor::select_to_beginning); | |
| 3993 | − register_action(cx, Editor::select_to_end); | |
| 3994 | − register_action(cx, Editor::select_all); | |
| 3995 | − register_action(cx, |editor, action, cx| { | |
| 3996 | − editor.select_all_matches(action, cx).log_err(); | |
| 3997 | − }); | |
| 3998 | − register_action(cx, Editor::select_line); | |
| 3999 | − register_action(cx, Editor::split_selection_into_lines); | |
| 4000 | − register_action(cx, Editor::add_selection_above); | |
| 4001 | − register_action(cx, Editor::add_selection_below); | |
| 4002 | − register_action(cx, |editor, action, cx| { | |
| 4003 | − editor.select_next(action, cx).log_err(); | |
| 4004 | − }); | |
| 4005 | − register_action(cx, |editor, action, cx| { | |
| 4006 | − editor.select_previous(action, cx).log_err(); | |
| 4007 | − }); | |
| 4008 | − register_action(cx, Editor::toggle_comments); | |
| 4009 | − register_action(cx, Editor::select_larger_syntax_node); | |
| 4010 | − register_action(cx, Editor::select_smaller_syntax_node); | |
| 4011 | − register_action(cx, Editor::move_to_enclosing_bracket); | |
| 4012 | − register_action(cx, Editor::undo_selection); | |
| 4013 | − register_action(cx, Editor::redo_selection); | |
| 4014 | − register_action(cx, Editor::go_to_diagnostic); | |
| 4015 | − register_action(cx, Editor::go_to_prev_diagnostic); | |
| 4016 | − register_action(cx, Editor::go_to_hunk); | |
| 4017 | − register_action(cx, Editor::go_to_prev_hunk); | |
| 4018 | − register_action(cx, Editor::go_to_definition); | |
| 4019 | − register_action(cx, Editor::go_to_definition_split); | |
| 4020 | − register_action(cx, Editor::go_to_type_definition); | |
| 4021 | − register_action(cx, Editor::go_to_type_definition_split); | |
| 4022 | − register_action(cx, Editor::fold); | |
| 4023 | − register_action(cx, Editor::fold_at); | |
| 4024 | − register_action(cx, Editor::unfold_lines); | |
| 4025 | − register_action(cx, Editor::unfold_at); | |
| 4026 | − register_action(cx, Editor::fold_selected_ranges); | |
| 4027 | − register_action(cx, Editor::show_completions); | |
| 4028 | − register_action(cx, Editor::toggle_code_actions); | |
| 4029 | − // on_action(cx, Editor::open_excerpts); todo!() | |
| 4030 | − register_action(cx, Editor::toggle_soft_wrap); | |
| 4031 | − register_action(cx, Editor::toggle_inlay_hints); | |
| 4032 | − register_action(cx, Editor::reveal_in_finder); | |
| 4033 | − register_action(cx, Editor::copy_path); | |
| 4034 | − register_action(cx, Editor::copy_relative_path); | |
| 4035 | − register_action(cx, Editor::copy_highlight_json); | |
| 4036 | − register_action(cx, |editor, action, cx| { | |
| 4037 | − editor | |
| 4038 | − .format(action, cx) | |
| 4039 | − .map(|task| task.detach_and_log_err(cx)); | |
| 4040 | − }); | |
| 4041 | − register_action(cx, Editor::restart_language_server); | |
| 4042 | − register_action(cx, Editor::show_character_palette); | |
| 4043 | − // on_action(cx, Editor::confirm_completion); todo!() | |
| 4044 | − register_action(cx, |editor, action, cx| { | |
| 4045 | − editor | |
| 4046 | − .confirm_code_action(action, cx) | |
| 4047 | − .map(|task| task.detach_and_log_err(cx)); | |
| 4048 | − }); | |
| 4049 | − register_action(cx, |editor, action, cx| { | |
| 4050 | − editor | |
| 4051 | − .rename(action, cx) | |
| 4052 | − .map(|task| task.detach_and_log_err(cx)); | |
| 4053 | − }); | |
| 4054 | − register_action(cx, |editor, action, cx| { | |
| 4055 | − editor | |
| 4056 | − .confirm_rename(action, cx) | |
| 4057 | − .map(|task| task.detach_and_log_err(cx)); | |
| 4058 | − }); | |
| 4059 | − register_action(cx, |editor, action, cx| { | |
| 4060 | − editor | |
| 4061 | − .find_all_references(action, cx) | |
| 4062 | − .map(|task| task.detach_and_log_err(cx)); | |
| 4063 | − }); | |
| 4064 | − register_action(cx, Editor::next_copilot_suggestion); | |
| 4065 | − register_action(cx, Editor::previous_copilot_suggestion); | |
| 4066 | − register_action(cx, Editor::copilot_suggest); | |
| 4067 | − register_action(cx, Editor::context_menu_first); | |
| 4068 | − register_action(cx, Editor::context_menu_prev); | |
| 4069 | − register_action(cx, Editor::context_menu_next); | |
| 4070 | − register_action(cx, Editor::context_menu_last); | |
| 4071 | −} | |
| 4072 | − | |
| 4073 | −fn register_action<T: Action>( | |
| 4074 | − cx: &mut ViewContext<Editor>, | |
| 4076 | +pub fn register_action<T: Action>( | |
| 4077 | + view: &View<Editor>, | |
| 4078 | + cx: &mut WindowContext, | |
| 4075 | 4079 | listener: impl Fn(&mut Editor, &T, &mut ViewContext<Editor>) + 'static, |
| 4076 | 4080 | ) { |
| 4077 | − cx.on_action(TypeId::of::<T>(), move |editor, action, phase, cx| { | |
| 4081 | + let view = view.clone(); | |
| 4082 | + cx.on_action(TypeId::of::<T>(), move |action, phase, cx| { | |
| 4078 | 4083 | let action = action.downcast_ref().unwrap(); |
| 4079 | 4084 | if phase == DispatchPhase::Bubble { |
| 4080 | − listener(editor, action, cx); | |
| 4085 | + view.update(cx, |editor, cx| { | |
| 4086 | + listener(editor, action, cx); | |
| 4087 | + }) | |
| 4081 | 4088 | } |
| 4082 | 4089 | }) |
| 4083 | 4090 | } |
| @@ -1,15 +1,21 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − display_map::InlayOffset, | |
| 2 | + display_map::{InlayOffset, ToDisplayPoint}, | |
| 3 | 3 | link_go_to_definition::{InlayHighlight, RangeInEditor}, |
| 4 | 4 | Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, |
| 5 | 5 | ExcerptId, RangeToAnchorExt, |
| 6 | 6 | }; |
| 7 | 7 | use futures::FutureExt; |
| 8 | −use gpui::{AnyElement, AppContext, Model, Task, ViewContext, WeakView}; | |
| 8 | +use gpui::{ | |
| 9 | + actions, div, px, AnyElement, AppContext, CursorStyle, InteractiveElement, IntoElement, Model, | |
| 10 | + MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, | |
| 11 | + Task, ViewContext, WeakView, | |
| 12 | +}; | |
| 9 | 13 | use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown}; |
| 14 | +use lsp::DiagnosticSeverity; | |
| 10 | 15 | use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; |
| 11 | 16 | use settings::Settings; |
| 12 | 17 | use std::{ops::Range, sync::Arc, time::Duration}; |
| 18 | +use ui::Tooltip; | |
| 13 | 19 | use util::TryFutureExt; |
| 14 | 20 | use workspace::Workspace; |
| 15 | 21 | |
| @@ -17,22 +23,17 @@ pub const HOVER_DELAY_MILLIS: u64 = 350; · 1 unmodified line | ||
| 17 | 23 | pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; |
| 18 | 24 | |
| 19 | 25 | pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; |
| 20 | −pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.; | |
| 21 | −pub const HOVER_POPOVER_GAP: f32 = 10.; | |
| 26 | +pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.); | |
| 27 | +pub const HOVER_POPOVER_GAP: Pixels = px(10.); | |
| 22 | 28 | |
| 23 | −// actions!(editor, [Hover]); | |
| 29 | +actions!(Hover); | |
| 24 | 30 | |
| 25 | −pub fn init(cx: &mut AppContext) { | |
| 26 | − // cx.add_action(hover); | |
| 31 | +/// Bindable action which uses the most recent selection head to trigger a hover | |
| 32 | +pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) { | |
| 33 | + let head = editor.selections.newest_display(cx).head(); | |
| 34 | + show_hover(editor, head, true, cx); | |
| 27 | 35 | } |
| 28 | 36 | |
| 29 | −// todo!() | |
| 30 | −// /// Bindable action which uses the most recent selection head to trigger a hover | |
| 31 | −// pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) { | |
| 32 | −// let head = editor.selections.newest_display(cx).head(); | |
| 33 | −// show_hover(editor, head, true, cx); | |
| 34 | −// } | |
| 35 | − | |
| 36 | 37 | /// The internal hover action dispatches between `show_hover` or `hide_hover` |
| 37 | 38 | /// depending on whether a point to hover over is provided. |
| 38 | 39 | pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) { |
| @@ -74,64 +75,63 @@ pub fn find_hovered_hint_part( · 35 unmodified lines | ||
| 74 | 75 | } |
| 75 | 76 | |
| 76 | 77 | pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) { |
| 77 | − todo!() | |
| 78 | − // if EditorSettings::get_global(cx).hover_popover_enabled { | |
| 79 | − // if editor.pending_rename.is_some() { | |
| 80 | − // return; | |
| 81 | − // } | |
| 82 | − | |
| 83 | − // let Some(project) = editor.project.clone() else { | |
| 84 | − // return; | |
| 85 | − // }; | |
| 86 | − | |
| 87 | − // if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { | |
| 88 | − // if let RangeInEditor::Inlay(range) = symbol_range { | |
| 89 | − // if range == &inlay_hover.range { | |
| 90 | − // // Hover triggered from same location as last time. Don't show again. | |
| 91 | − // return; | |
| 92 | − // } | |
| 93 | − // } | |
| 94 | − // hide_hover(editor, cx); | |
| 95 | − // } | |
| 96 | − | |
| 97 | − // let task = cx.spawn(|this, mut cx| { | |
| 98 | − // async move { | |
| 99 | − // cx.background_executor() | |
| 100 | − // .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) | |
| 101 | − // .await; | |
| 102 | − // this.update(&mut cx, |this, _| { | |
| 103 | − // this.hover_state.diagnostic_popover = None; | |
| 104 | − // })?; | |
| 105 | − | |
| 106 | − // let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?; | |
| 107 | − // let blocks = vec![inlay_hover.tooltip]; | |
| 108 | − // let parsed_content = parse_blocks(&blocks, &language_registry, None).await; | |
| 109 | − | |
| 110 | − // let hover_popover = InfoPopover { | |
| 111 | − // project: project.clone(), | |
| 112 | − // symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), | |
| 113 | − // blocks, | |
| 114 | − // parsed_content, | |
| 115 | − // }; | |
| 116 | − | |
| 117 | − // this.update(&mut cx, |this, cx| { | |
| 118 | − // // Highlight the selected symbol using a background highlight | |
| 119 | − // this.highlight_inlay_background::<HoverState>( | |
| 120 | − // vec![inlay_hover.range], | |
| 121 | − // |theme| theme.editor.hover_popover.highlight, | |
| 122 | − // cx, | |
| 123 | − // ); | |
| 124 | − // this.hover_state.info_popover = Some(hover_popover); | |
| 125 | − // cx.notify(); | |
| 126 | − // })?; | |
| 127 | − | |
| 128 | − // anyhow::Ok(()) | |
| 129 | − // } | |
| 130 | − // .log_err() | |
| 131 | − // }); | |
| 132 | − | |
| 133 | − // editor.hover_state.info_task = Some(task); | |
| 134 | − // } | |
| 78 | + if EditorSettings::get_global(cx).hover_popover_enabled { | |
| 79 | + if editor.pending_rename.is_some() { | |
| 80 | + return; | |
| 81 | + } | |
| 82 | + | |
| 83 | + let Some(project) = editor.project.clone() else { | |
| 84 | + return; | |
| 85 | + }; | |
| 86 | + | |
| 87 | + if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { | |
| 88 | + if let RangeInEditor::Inlay(range) = symbol_range { | |
| 89 | + if range == &inlay_hover.range { | |
| 90 | + // Hover triggered from same location as last time. Don't show again. | |
| 91 | + return; | |
| 92 | + } | |
| 93 | + } | |
| 94 | + hide_hover(editor, cx); | |
| 95 | + } | |
| 96 | + | |
| 97 | + let task = cx.spawn(|this, mut cx| { | |
| 98 | + async move { | |
| 99 | + cx.background_executor() | |
| 100 | + .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) | |
| 101 | + .await; | |
| 102 | + this.update(&mut cx, |this, _| { | |
| 103 | + this.hover_state.diagnostic_popover = None; | |
| 104 | + })?; | |
| 105 | + | |
| 106 | + let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?; | |
| 107 | + let blocks = vec![inlay_hover.tooltip]; | |
| 108 | + let parsed_content = parse_blocks(&blocks, &language_registry, None).await; | |
| 109 | + | |
| 110 | + let hover_popover = InfoPopover { | |
| 111 | + project: project.clone(), | |
| 112 | + symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), | |
| 113 | + blocks, | |
| 114 | + parsed_content, | |
| 115 | + }; | |
| 116 | + | |
| 117 | + this.update(&mut cx, |this, cx| { | |
| 118 | + // Highlight the selected symbol using a background highlight | |
| 119 | + this.highlight_inlay_background::<HoverState>( | |
| 120 | + vec![inlay_hover.range], | |
| 121 | + |theme| theme.element_hover, // todo!("use a proper background here") | |
| 122 | + cx, | |
| 123 | + ); | |
| 124 | + this.hover_state.info_popover = Some(hover_popover); | |
| 125 | + cx.notify(); | |
| 126 | + })?; | |
| 127 | + | |
| 128 | + anyhow::Ok(()) | |
| 129 | + } | |
| 130 | + .log_err() | |
| 131 | + }); | |
| 132 | + | |
| 133 | + editor.hover_state.info_task = Some(task); | |
| 134 | + } | |
| 135 | 135 | } |
| 136 | 136 | |
| 137 | 137 | /// Hides the type information popup. |
| @@ -420,43 +420,42 @@ impl HoverState { · 282 unmodified lines | ||
| 420 | 420 | snapshot: &EditorSnapshot, |
| 421 | 421 | style: &EditorStyle, |
| 422 | 422 | visible_rows: Range<u32>, |
| 423 | + max_size: Size<Pixels>, | |
| 423 | 424 | workspace: Option<WeakView<Workspace>>, |
| 424 | 425 | cx: &mut ViewContext<Editor>, |
| 425 | − ) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> { | |
| 426 | − todo!("old version below") | |
| 426 | + ) -> Option<(DisplayPoint, Vec<AnyElement>)> { | |
| 427 | + // If there is a diagnostic, position the popovers based on that. | |
| 428 | + // Otherwise use the start of the hover range | |
| 429 | + let anchor = self | |
| 430 | + .diagnostic_popover | |
| 431 | + .as_ref() | |
| 432 | + .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) | |
| 433 | + .or_else(|| { | |
| 434 | + self.info_popover | |
| 435 | + .as_ref() | |
| 436 | + .map(|info_popover| match &info_popover.symbol_range { | |
| 437 | + RangeInEditor::Text(range) => &range.start, | |
| 438 | + RangeInEditor::Inlay(range) => &range.inlay_position, | |
| 439 | + }) | |
| 440 | + })?; | |
| 441 | + let point = anchor.to_display_point(&snapshot.display_snapshot); | |
| 442 | + | |
| 443 | + // Don't render if the relevant point isn't on screen | |
| 444 | + if !self.visible() || !visible_rows.contains(&point.row()) { | |
| 445 | + return None; | |
| 446 | + } | |
| 447 | + | |
| 448 | + let mut elements = Vec::new(); | |
| 449 | + | |
| 450 | + if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { | |
| 451 | + elements.push(diagnostic_popover.render(style, max_size, cx)); | |
| 452 | + } | |
| 453 | + if let Some(info_popover) = self.info_popover.as_mut() { | |
| 454 | + elements.push(info_popover.render(style, max_size, workspace, cx)); | |
| 455 | + } | |
| 456 | + | |
| 457 | + Some((point, elements)) | |
| 427 | 458 | } |
| 428 | − // // If there is a diagnostic, position the popovers based on that. | |
| 429 | − // // Otherwise use the start of the hover range | |
| 430 | − // let anchor = self | |
| 431 | − // .diagnostic_popover | |
| 432 | − // .as_ref() | |
| 433 | − // .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) | |
| 434 | − // .or_else(|| { | |
| 435 | − // self.info_popover | |
| 436 | − // .as_ref() | |
| 437 | − // .map(|info_popover| match &info_popover.symbol_range { | |
| 438 | − // RangeInEditor::Text(range) => &range.start, | |
| 439 | − // RangeInEditor::Inlay(range) => &range.inlay_position, | |
| 440 | − // }) | |
| 441 | − // })?; | |
| 442 | − // let point = anchor.to_display_point(&snapshot.display_snapshot); | |
| 443 | − | |
| 444 | − // // Don't render if the relevant point isn't on screen | |
| 445 | − // if !self.visible() || !visible_rows.contains(&point.row()) { | |
| 446 | − // return None; | |
| 447 | − // } | |
| 448 | − | |
| 449 | − // let mut elements = Vec::new(); | |
| 450 | − | |
| 451 | − // if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { | |
| 452 | − // elements.push(diagnostic_popover.render(style, cx)); | |
| 453 | − // } | |
| 454 | − // if let Some(info_popover) = self.info_popover.as_mut() { | |
| 455 | − // elements.push(info_popover.render(style, workspace, cx)); | |
| 456 | − // } | |
| 457 | − | |
| 458 | − // Some((point, elements)) | |
| 459 | − // } | |
| 460 | 459 | } |
| 461 | 460 | |
| 462 | 461 | #[derive(Debug, Clone)] |
| @@ -467,35 +466,36 @@ pub struct InfoPopover { · 4 unmodified lines | ||
| 467 | 466 | parsed_content: ParsedMarkdown, |
| 468 | 467 | } |
| 469 | 468 | |
| 470 | −// impl InfoPopover { | |
| 471 | −// pub fn render( | |
| 472 | −// &mut self, | |
| 473 | −// style: &EditorStyle, | |
| 474 | −// workspace: Option<WeakView<Workspace>>, | |
| 475 | −// cx: &mut ViewContext<Editor>, | |
| 476 | −// ) -> AnyElement<Editor> { | |
| 477 | −// MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| { | |
| 478 | −// Flex::column() | |
| 479 | −// .scrollable::<HoverBlock>(0, None, cx) | |
| 480 | −// .with_child(crate::render_parsed_markdown::<HoverBlock>( | |
| 481 | −// &self.parsed_content, | |
| 482 | −// style, | |
| 483 | −// workspace, | |
| 484 | −// cx, | |
| 485 | −// )) | |
| 486 | −// .contained() | |
| 487 | −// .with_style(style.hover_popover.container) | |
| 488 | −// }) | |
| 489 | −// .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. | |
| 490 | −// .with_cursor_style(CursorStyle::Arrow) | |
| 491 | −// .with_padding(Padding { | |
| 492 | −// bottom: HOVER_POPOVER_GAP, | |
| 493 | −// top: HOVER_POPOVER_GAP, | |
| 494 | −// ..Default::default() | |
| 495 | −// }) | |
| 496 | −// .into_any() | |
| 497 | −// } | |
| 498 | −// } | |
| 469 | +impl InfoPopover { | |
| 470 | + pub fn render( | |
| 471 | + &mut self, | |
| 472 | + style: &EditorStyle, | |
| 473 | + max_size: Size<Pixels>, | |
| 474 | + workspace: Option<WeakView<Workspace>>, | |
| 475 | + cx: &mut ViewContext<Editor>, | |
| 476 | + ) -> AnyElement { | |
| 477 | + div() | |
| 478 | + .id("info_popover") | |
| 479 | + .overflow_y_scroll() | |
| 480 | + .bg(gpui::red()) | |
| 481 | + .max_w(max_size.width) | |
| 482 | + .max_h(max_size.height) | |
| 483 | + // Prevent a mouse move on the popover from being propagated to the editor, | |
| 484 | + // because that would dismiss the popover. | |
| 485 | + .on_mouse_move(|_, cx| cx.stop_propagation()) | |
| 486 | + // Prevent a mouse down on the popover from being propagated to the editor, | |
| 487 | + // because that would move the cursor. | |
| 488 | + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) | |
| 489 | + .child(crate::render_parsed_markdown( | |
| 490 | + "content", | |
| 491 | + &self.parsed_content, | |
| 492 | + style, | |
| 493 | + workspace, | |
| 494 | + cx, | |
| 495 | + )) | |
| 496 | + .into_any_element() | |
| 497 | + } | |
| 498 | +} | |
| 499 | 499 | |
| 500 | 500 | #[derive(Debug, Clone)] |
| 501 | 501 | pub struct DiagnosticPopover { |
| @@ -504,57 +504,40 @@ pub struct DiagnosticPopover { · 2 unmodified lines | ||
| 504 | 504 | } |
| 505 | 505 | |
| 506 | 506 | impl DiagnosticPopover { |
| 507 | − pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> { | |
| 508 | − todo!() | |
| 509 | − // enum PrimaryDiagnostic {} | |
| 510 | − | |
| 511 | − // let mut text_style = style.hover_popover.prose.clone(); | |
| 512 | − // text_style.font_size = style.text.font_size; | |
| 513 | − // let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone(); | |
| 514 | − | |
| 515 | − // let text = match &self.local_diagnostic.diagnostic.source { | |
| 516 | − // Some(source) => Text::new( | |
| 517 | − // format!("{source}: {}", self.local_diagnostic.diagnostic.message), | |
| 518 | − // text_style, | |
| 519 | − // ) | |
| 520 | − // .with_highlights(vec![(0..source.len(), diagnostic_source_style)]), | |
| 521 | − | |
| 522 | − // None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style), | |
| 523 | − // }; | |
| 524 | − | |
| 525 | − // let container_style = match self.local_diagnostic.diagnostic.severity { | |
| 526 | − // DiagnosticSeverity::HINT => style.hover_popover.info_container, | |
| 527 | − // DiagnosticSeverity::INFORMATION => style.hover_popover.info_container, | |
| 528 | − // DiagnosticSeverity::WARNING => style.hover_popover.warning_container, | |
| 529 | − // DiagnosticSeverity::ERROR => style.hover_popover.error_container, | |
| 530 | − // _ => style.hover_popover.container, | |
| 531 | − // }; | |
| 532 | − | |
| 533 | − // let tooltip_style = theme::current(cx).tooltip.clone(); | |
| 534 | − | |
| 535 | − // MouseEventHandler::new::<DiagnosticPopover, _>(0, cx, |_, _| { | |
| 536 | − // text.with_soft_wrap(true) | |
| 537 | − // .contained() | |
| 538 | − // .with_style(container_style) | |
| 539 | − // }) | |
| 540 | − // .with_padding(Padding { | |
| 541 | − // top: HOVER_POPOVER_GAP, | |
| 542 | − // bottom: HOVER_POPOVER_GAP, | |
| 543 | − // ..Default::default() | |
| 544 | − // }) | |
| 545 | − // .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. | |
| 546 | − // .on_click(MouseButton::Left, |_, this, cx| { | |
| 547 | − // this.go_to_diagnostic(&Default::default(), cx) | |
| 548 | − // }) | |
| 549 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 550 | − // .with_tooltip::<PrimaryDiagnostic>( | |
| 551 | − // 0, | |
| 552 | − // "Go To Diagnostic".to_string(), | |
| 553 | − // Some(Box::new(crate::GoToDiagnostic)), | |
| 554 | − // tooltip_style, | |
| 555 | − // cx, | |
| 556 | − // ) | |
| 557 | − // .into_any() | |
| 507 | + pub fn render( | |
| 508 | + &self, | |
| 509 | + style: &EditorStyle, | |
| 510 | + max_size: Size<Pixels>, | |
| 511 | + cx: &mut ViewContext<Editor>, | |
| 512 | + ) -> AnyElement { | |
| 513 | + let text = match &self.local_diagnostic.diagnostic.source { | |
| 514 | + Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message), | |
| 515 | + None => self.local_diagnostic.diagnostic.message.clone(), | |
| 516 | + }; | |
| 517 | + | |
| 518 | + let container_bg = crate::diagnostic_style( | |
| 519 | + self.local_diagnostic.diagnostic.severity, | |
| 520 | + true, | |
| 521 | + &style.diagnostic_style, | |
| 522 | + ); | |
| 523 | + | |
| 524 | + div() | |
| 525 | + .id("diagnostic") | |
| 526 | + .overflow_y_scroll() | |
| 527 | + .bg(container_bg) | |
| 528 | + .max_w(max_size.width) | |
| 529 | + .max_h(max_size.height) | |
| 530 | + .cursor(CursorStyle::PointingHand) | |
| 531 | + .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx)) | |
| 532 | + // Prevent a mouse move on the popover from being propagated to the editor, | |
| 533 | + // because that would dismiss the popover. | |
| 534 | + .on_mouse_move(|_, cx| cx.stop_propagation()) | |
| 535 | + // Prevent a mouse down on the popover from being propagated to the editor, | |
| 536 | + // because that would move the cursor. | |
| 537 | + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) | |
| 538 | + .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx))) | |
| 539 | + .child(SharedString::from(text)) | |
| 540 | + .into_any_element() | |
| 558 | 541 | } |
| 559 | 542 | |
| 560 | 543 | pub fn activation_info(&self) -> (usize, Anchor) { |
| @@ -567,763 +550,763 @@ impl DiagnosticPopover { · 6 unmodified lines | ||
| 567 | 550 | } |
| 568 | 551 | } |
| 569 | 552 | |
| 570 | −// #[cfg(test)] | |
| 571 | −// mod tests { | |
| 572 | −// use super::*; | |
| 573 | −// use crate::{ | |
| 574 | −// editor_tests::init_test, | |
| 575 | −// element::PointForPosition, | |
| 576 | −// inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, | |
| 577 | −// link_go_to_definition::update_inlay_link_and_hover_points, | |
| 578 | −// test::editor_lsp_test_context::EditorLspTestContext, | |
| 579 | −// InlayId, | |
| 580 | −// }; | |
| 581 | −// use collections::BTreeSet; | |
| 582 | −// use gpui::fonts::{HighlightStyle, Underline, Weight}; | |
| 583 | −// use indoc::indoc; | |
| 584 | −// use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; | |
| 585 | −// use lsp::LanguageServerId; | |
| 586 | −// use project::{HoverBlock, HoverBlockKind}; | |
| 587 | −// use smol::stream::StreamExt; | |
| 588 | −// use unindent::Unindent; | |
| 589 | −// use util::test::marked_text_ranges; | |
| 590 | − | |
| 591 | −// #[gpui::test] | |
| 592 | −// async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { | |
| 593 | −// init_test(cx, |_| {}); | |
| 594 | − | |
| 595 | −// let mut cx = EditorLspTestContext::new_rust( | |
| 596 | −// lsp::ServerCapabilities { | |
| 597 | −// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), | |
| 598 | −// ..Default::default() | |
| 599 | −// }, | |
| 600 | −// cx, | |
| 601 | −// ) | |
| 602 | −// .await; | |
| 603 | − | |
| 604 | −// // Basic hover delays and then pops without moving the mouse | |
| 605 | −// cx.set_state(indoc! {" | |
| 606 | −// fn ˇtest() { println!(); } | |
| 607 | −// "}); | |
| 608 | −// let hover_point = cx.display_point(indoc! {" | |
| 609 | −// fn test() { printˇln!(); } | |
| 610 | −// "}); | |
| 611 | − | |
| 612 | −// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); | |
| 613 | −// assert!(!cx.editor(|editor, _| editor.hover_state.visible())); | |
| 614 | − | |
| 615 | −// // After delay, hover should be visible. | |
| 616 | −// let symbol_range = cx.lsp_range(indoc! {" | |
| 617 | −// fn test() { «println!»(); } | |
| 618 | −// "}); | |
| 619 | −// let mut requests = | |
| 620 | −// cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move { | |
| 621 | −// Ok(Some(lsp::Hover { | |
| 622 | −// contents: lsp::HoverContents::Markup(lsp::MarkupContent { | |
| 623 | −// kind: lsp::MarkupKind::Markdown, | |
| 624 | −// value: "some basic docs".to_string(), | |
| 625 | −// }), | |
| 626 | −// range: Some(symbol_range), | |
| 627 | −// })) | |
| 628 | −// }); | |
| 629 | −// cx.foreground() | |
| 630 | −// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); | |
| 631 | −// requests.next().await; | |
| 632 | − | |
| 633 | −// cx.editor(|editor, _| { | |
| 634 | −// assert!(editor.hover_state.visible()); | |
| 635 | −// assert_eq!( | |
| 636 | −// editor.hover_state.info_popover.clone().unwrap().blocks, | |
| 637 | −// vec![HoverBlock { | |
| 638 | −// text: "some basic docs".to_string(), | |
| 639 | −// kind: HoverBlockKind::Markdown, | |
| 640 | −// },] | |
| 641 | −// ) | |
| 642 | −// }); | |
| 643 | − | |
| 644 | −// // Mouse moved with no hover response dismisses | |
| 645 | −// let hover_point = cx.display_point(indoc! {" | |
| 646 | −// fn teˇst() { println!(); } | |
| 647 | −// "}); | |
| 648 | −// let mut request = cx | |
| 649 | −// .lsp | |
| 650 | −// .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) }); | |
| 651 | −// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); | |
| 652 | −// cx.foreground() | |
| 653 | −// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); | |
| 654 | −// request.next().await; | |
| 655 | −// cx.editor(|editor, _| { | |
| 656 | −// assert!(!editor.hover_state.visible()); | |
| 657 | −// }); | |
| 658 | −// } | |
| 659 | − | |
| 660 | −// #[gpui::test] | |
| 661 | −// async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { | |
| 662 | −// init_test(cx, |_| {}); | |
| 663 | − | |
| 664 | −// let mut cx = EditorLspTestContext::new_rust( | |
| 665 | −// lsp::ServerCapabilities { | |
| 666 | −// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), | |
| 667 | −// ..Default::default() | |
| 668 | −// }, | |
| 669 | −// cx, | |
| 670 | −// ) | |
| 671 | −// .await; | |
| 672 | − | |
| 673 | −// // Hover with keyboard has no delay | |
| 674 | −// cx.set_state(indoc! {" | |
| 675 | −// fˇn test() { println!(); } | |
| 676 | −// "}); | |
| 677 | −// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); | |
| 678 | −// let symbol_range = cx.lsp_range(indoc! {" | |
| 679 | −// «fn» test() { println!(); } | |
| 680 | −// "}); | |
| 681 | −// cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move { | |
| 682 | −// Ok(Some(lsp::Hover { | |
| 683 | −// contents: lsp::HoverContents::Markup(lsp::MarkupContent { | |
| 684 | −// kind: lsp::MarkupKind::Markdown, | |
| 685 | −// value: "some other basic docs".to_string(), | |
| 686 | −// }), | |
| 687 | −// range: Some(symbol_range), | |
| 688 | −// })) | |
| 689 | −// }) | |
| 690 | −// .next() | |
| 691 | −// .await; | |
| 692 | − | |
| 693 | −// cx.condition(|editor, _| editor.hover_state.visible()).await; | |
| 694 | −// cx.editor(|editor, _| { | |
| 695 | −// assert_eq!( | |
| 696 | −// editor.hover_state.info_popover.clone().unwrap().blocks, | |
| 697 | −// vec![HoverBlock { | |
| 698 | −// text: "some other basic docs".to_string(), | |
| 699 | −// kind: HoverBlockKind::Markdown, | |
| 700 | −// }] | |
| 701 | −// ) | |
| 702 | −// }); | |
| 703 | −// } | |
| 704 | − | |
| 705 | −// #[gpui::test] | |
| 706 | −// async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) { | |
| 707 | −// init_test(cx, |_| {}); | |
| 708 | − | |
| 709 | −// let mut cx = EditorLspTestContext::new_rust( | |
| 710 | −// lsp::ServerCapabilities { | |
| 711 | −// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), | |
| 712 | −// ..Default::default() | |
| 713 | −// }, | |
| 714 | −// cx, | |
| 715 | −// ) | |
| 716 | −// .await; | |
| 717 | − | |
| 718 | −// // Hover with keyboard has no delay | |
| 719 | −// cx.set_state(indoc! {" | |
| 720 | −// fˇn test() { println!(); } | |
| 721 | −// "}); | |
| 722 | −// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); | |
| 723 | −// let symbol_range = cx.lsp_range(indoc! {" | |
| 724 | −// «fn» test() { println!(); } | |
| 725 | −// "}); | |
| 726 | −// cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move { | |
| 727 | −// Ok(Some(lsp::Hover { | |
| 728 | −// contents: lsp::HoverContents::Array(vec![ | |
| 729 | −// lsp::MarkedString::String("regular text for hover to show".to_string()), | |
| 730 | −// lsp::MarkedString::String("".to_string()), | |
| 731 | −// lsp::MarkedString::LanguageString(lsp::LanguageString { | |
| 732 | −// language: "Rust".to_string(), | |
| 733 | −// value: "".to_string(), | |
| 734 | −// }), | |
| 735 | −// ]), | |
| 736 | −// range: Some(symbol_range), | |
| 737 | −// })) | |
| 738 | −// }) | |
| 739 | −// .next() | |
| 740 | −// .await; | |
| 741 | − | |
| 742 | −// cx.condition(|editor, _| editor.hover_state.visible()).await; | |
| 743 | −// cx.editor(|editor, _| { | |
| 744 | −// assert_eq!( | |
| 745 | −// editor.hover_state.info_popover.clone().unwrap().blocks, | |
| 746 | −// vec![HoverBlock { | |
| 747 | −// text: "regular text for hover to show".to_string(), | |
| 748 | −// kind: HoverBlockKind::Markdown, | |
| 749 | −// }], | |
| 750 | −// "No empty string hovers should be shown" | |
| 751 | −// ); | |
| 752 | −// }); | |
| 753 | −// } | |
| 754 | − | |
| 755 | −// #[gpui::test] | |
| 756 | −// async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) { | |
| 757 | −// init_test(cx, |_| {}); | |
| 758 | − | |
| 759 | −// let mut cx = EditorLspTestContext::new_rust( | |
| 760 | −// lsp::ServerCapabilities { | |
| 761 | −// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), | |
| 762 | −// ..Default::default() | |
| 763 | −// }, | |
| 764 | −// cx, | |
| 765 | −// ) | |
| 766 | −// .await; | |
| 767 | − | |
| 768 | −// // Hover with keyboard has no delay | |
| 769 | −// cx.set_state(indoc! {" | |
| 770 | −// fˇn test() { println!(); } | |
| 771 | −// "}); | |
| 772 | −// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); | |
| 773 | −// let symbol_range = cx.lsp_range(indoc! {" | |
| 774 | −// «fn» test() { println!(); } | |
| 775 | −// "}); | |
| 776 | − | |
| 777 | −// let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; | |
| 778 | −// let markdown_string = format!("\n```rust\n{code_str}```"); | |
| 779 | − | |
| 780 | −// let closure_markdown_string = markdown_string.clone(); | |
| 781 | −// cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| { | |
| 782 | −// let future_markdown_string = closure_markdown_string.clone(); | |
| 783 | −// async move { | |
| 784 | −// Ok(Some(lsp::Hover { | |
| 785 | −// contents: lsp::HoverContents::Markup(lsp::MarkupContent { | |
| 786 | −// kind: lsp::MarkupKind::Markdown, | |
| 787 | −// value: future_markdown_string, | |
| 788 | −// }), | |
| 789 | −// range: Some(symbol_range), | |
| 790 | −// })) | |
| 791 | −// } | |
| 792 | −// }) | |
| 793 | −// .next() | |
| 794 | −// .await; | |
| 795 | − | |
| 796 | −// cx.condition(|editor, _| editor.hover_state.visible()).await; | |
| 797 | −// cx.editor(|editor, _| { | |
| 798 | −// let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; | |
| 799 | −// assert_eq!( | |
| 800 | −// blocks, | |
| 801 | −// vec![HoverBlock { | |
| 802 | −// text: markdown_string, | |
| 803 | −// kind: HoverBlockKind::Markdown, | |
| 804 | −// }], | |
| 805 | −// ); | |
| 806 | − | |
| 807 | −// let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); | |
| 808 | −// assert_eq!( | |
| 809 | −// rendered.text, | |
| 810 | −// code_str.trim(), | |
| 811 | −// "Should not have extra line breaks at end of rendered hover" | |
| 812 | −// ); | |
| 813 | −// }); | |
| 814 | −// } | |
| 815 | − | |
| 816 | −// #[gpui::test] | |
| 817 | −// async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { | |
| 818 | −// init_test(cx, |_| {}); | |
| 819 | − | |
| 820 | −// let mut cx = EditorLspTestContext::new_rust( | |
| 821 | −// lsp::ServerCapabilities { | |
| 822 | −// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), | |
| 823 | −// ..Default::default() | |
| 824 | −// }, | |
| 825 | −// cx, | |
| 826 | −// ) | |
| 827 | −// .await; | |
| 828 | − | |
| 829 | −// // Hover with just diagnostic, pops DiagnosticPopover immediately and then | |
| 830 | −// // info popover once request completes | |
| 831 | −// cx.set_state(indoc! {" | |
| 832 | −// fn teˇst() { println!(); } | |
| 833 | −// "}); | |
| 834 | − | |
| 835 | −// // Send diagnostic to client | |
| 836 | −// let range = cx.text_anchor_range(indoc! {" | |
| 837 | −// fn «test»() { println!(); } | |
| 838 | −// "}); | |
| 839 | −// cx.update_buffer(|buffer, cx| { | |
| 840 | −// let snapshot = buffer.text_snapshot(); | |
| 841 | −// let set = DiagnosticSet::from_sorted_entries( | |
| 842 | −// vec![DiagnosticEntry { | |
| 843 | −// range, | |
| 844 | −// diagnostic: Diagnostic { | |
| 845 | −// message: "A test diagnostic message.".to_string(), | |
| 846 | −// ..Default::default() | |
| 847 | −// }, | |
| 848 | −// }], | |
| 849 | −// &snapshot, | |
| 850 | −// ); | |
| 851 | −// buffer.update_diagnostics(LanguageServerId(0), set, cx); | |
| 852 | −// }); | |
| 853 | − | |
| 854 | −// // Hover pops diagnostic immediately | |
| 855 | −// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); | |
| 856 | −// cx.foreground().run_until_parked(); | |
| 857 | − | |
| 858 | −// cx.editor(|Editor { hover_state, .. }, _| { | |
| 859 | −// assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) | |
| 860 | −// }); | |
| 861 | − | |
| 862 | −// // Info Popover shows after request responded to | |
| 863 | −// let range = cx.lsp_range(indoc! {" | |
| 864 | −// fn «test»() { println!(); } | |
| 865 | −// "}); | |
| 866 | −// cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move { | |
| 867 | −// Ok(Some(lsp::Hover { | |
| 868 | −// contents: lsp::HoverContents::Markup(lsp::MarkupContent { | |
| 869 | −// kind: lsp::MarkupKind::Markdown, | |
| 870 | −// value: "some new docs".to_string(), | |
| 871 | −// }), | |
| 872 | −// range: Some(range), | |
| 873 | −// })) | |
| 874 | −// }); | |
| 875 | −// cx.foreground() | |
| 876 | −// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); | |
| 877 | − | |
| 878 | −// cx.foreground().run_until_parked(); | |
| 879 | −// cx.editor(|Editor { hover_state, .. }, _| { | |
| 880 | −// hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() | |
| 881 | −// }); | |
| 882 | −// } | |
| 883 | − | |
| 884 | −// #[gpui::test] | |
| 885 | −// fn test_render_blocks(cx: &mut gpui::TestAppContext) { | |
| 886 | −// init_test(cx, |_| {}); | |
| 887 | − | |
| 888 | −// cx.add_window(|cx| { | |
| 889 | −// let editor = Editor::single_line(None, cx); | |
| 890 | −// let style = editor.style(cx); | |
| 891 | − | |
| 892 | −// struct Row { | |
| 893 | −// blocks: Vec<HoverBlock>, | |
| 894 | −// expected_marked_text: String, | |
| 895 | −// expected_styles: Vec<HighlightStyle>, | |
| 896 | −// } | |
| 897 | − | |
| 898 | −// let rows = &[ | |
| 899 | −// // Strong emphasis | |
| 900 | −// Row { | |
| 901 | −// blocks: vec![HoverBlock { | |
| 902 | −// text: "one **two** three".to_string(), | |
| 903 | −// kind: HoverBlockKind::Markdown, | |
| 904 | −// }], | |
| 905 | −// expected_marked_text: "one «two» three".to_string(), | |
| 906 | −// expected_styles: vec![HighlightStyle { | |
| 907 | −// weight: Some(Weight::BOLD), | |
| 908 | −// ..Default::default() | |
| 909 | −// }], | |
| 910 | −// }, | |
| 911 | −// // Links | |
| 912 | −// Row { | |
| 913 | −// blocks: vec three".to_string(), | |
| 915 | −// kind: HoverBlockKind::Markdown, | |
| 916 | −// }], | |
| 917 | −// expected_marked_text: "one «two» three".to_string(), | |
| 918 | −// expected_styles: vec![HighlightStyle { | |
| 919 | −// underline: Some(Underline { | |
| 920 | −// thickness: 1.0.into(), | |
| 921 | −// ..Default::default() | |
| 922 | −// }), | |
| 923 | −// ..Default::default() | |
| 924 | −// }], | |
| 925 | −// }, | |
| 926 | −// // Lists | |
| 927 | −// Row { | |
| 928 | −// blocks: vec | |
| 936 | −// - d" | |
| 937 | −// .unindent(), | |
| 938 | −// kind: HoverBlockKind::Markdown, | |
| 939 | −// }], | |
| 940 | −// expected_marked_text: " | |
| 941 | −// lists: | |
| 942 | −// - one | |
| 943 | −// - a | |
| 944 | −// - b | |
| 945 | −// - two | |
| 946 | −// - «c» | |
| 947 | −// - d" | |
| 948 | −// .unindent(), | |
| 949 | −// expected_styles: vec![HighlightStyle { | |
| 950 | −// underline: Some(Underline { | |
| 951 | −// thickness: 1.0.into(), | |
| 952 | −// ..Default::default() | |
| 953 | −// }), | |
| 954 | −// ..Default::default() | |
| 955 | −// }], | |
| 956 | −// }, | |
| 957 | −// // Multi-paragraph list items | |
| 958 | −// Row { | |
| 959 | −// blocks: vec![HoverBlock { | |
| 960 | −// text: " | |
| 961 | −// * one two | |
| 962 | −// three | |
| 963 | − | |
| 964 | −// * four five | |
| 965 | −// * six seven | |
| 966 | −// eight | |
| 967 | − | |
| 968 | −// nine | |
| 969 | −// * ten | |
| 970 | −// * six" | |
| 971 | −// .unindent(), | |
| 972 | −// kind: HoverBlockKind::Markdown, | |
| 973 | −// }], | |
| 974 | −// expected_marked_text: " | |
| 975 | −// - one two three | |
| 976 | −// - four five | |
| 977 | −// - six seven eight | |
| 978 | − | |
| 979 | −// nine | |
| 980 | −// - ten | |
| 981 | −// - six" | |
| 982 | −// .unindent(), | |
| 983 | −// expected_styles: vec![HighlightStyle { | |
| 984 | −// underline: Some(Underline { | |
| 985 | −// thickness: 1.0.into(), | |
| 986 | −// ..Default::default() | |
| 987 | −// }), | |
| 988 | −// ..Default::default() | |
| 989 | −// }], | |
| 990 | −// }, | |
| 991 | −// ]; | |
| 992 | − | |
| 993 | −// for Row { | |
| 994 | −// blocks, | |
| 995 | −// expected_marked_text, | |
| 996 | −// expected_styles, | |
| 997 | −// } in &rows[0..] | |
| 998 | −// { | |
| 999 | −// let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); | |
| 1000 | − | |
| 1001 | −// let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); | |
| 1002 | −// let expected_highlights = ranges | |
| 1003 | −// .into_iter() | |
| 1004 | −// .zip(expected_styles.iter().cloned()) | |
| 1005 | −// .collect::<Vec<_>>(); | |
| 1006 | −// assert_eq!( | |
| 1007 | −// rendered.text, expected_text, | |
| 1008 | −// "wrong text for input {blocks:?}" | |
| 1009 | −// ); | |
| 1010 | − | |
| 1011 | −// let rendered_highlights: Vec<_> = rendered | |
| 1012 | −// .highlights | |
| 1013 | −// .iter() | |
| 1014 | −// .filter_map(|(range, highlight)| { | |
| 1015 | −// let highlight = highlight.to_highlight_style(&style.syntax)?; | |
| 1016 | −// Some((range.clone(), highlight)) | |
| 1017 | −// }) | |
| 1018 | −// .collect(); | |
| 1019 | − | |
| 1020 | −// assert_eq!( | |
| 1021 | −// rendered_highlights, expected_highlights, | |
| 1022 | −// "wrong highlights for input {blocks:?}" | |
| 1023 | −// ); | |
| 1024 | −// } | |
| 1025 | − | |
| 1026 | −// editor | |
| 1027 | −// }); | |
| 1028 | −// } | |
| 1029 | − | |
| 1030 | −// #[gpui::test] | |
| 1031 | −// async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { | |
| 1032 | −// init_test(cx, |settings| { | |
| 1033 | −// settings.defaults.inlay_hints = Some(InlayHintSettings { | |
| 1034 | −// enabled: true, | |
| 1035 | −// show_type_hints: true, | |
| 1036 | −// show_parameter_hints: true, | |
| 1037 | −// show_other_hints: true, | |
| 1038 | −// }) | |
| 1039 | −// }); | |
| 1040 | − | |
| 1041 | −// let mut cx = EditorLspTestContext::new_rust( | |
| 1042 | −// lsp::ServerCapabilities { | |
| 1043 | −// inlay_hint_provider: Some(lsp::OneOf::Right( | |
| 1044 | −// lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { | |
| 1045 | −// resolve_provider: Some(true), | |
| 1046 | −// ..Default::default() | |
| 1047 | −// }), | |
| 1048 | −// )), | |
| 1049 | −// ..Default::default() | |
| 1050 | −// }, | |
| 1051 | −// cx, | |
| 1052 | −// ) | |
| 1053 | −// .await; | |
| 1054 | − | |
| 1055 | −// cx.set_state(indoc! {" | |
| 1056 | −// struct TestStruct; | |
| 1057 | − | |
| 1058 | −// // ================== | |
| 1059 | − | |
| 1060 | −// struct TestNewType<T>(T); | |
| 1061 | − | |
| 1062 | −// fn main() { | |
| 1063 | −// let variableˇ = TestNewType(TestStruct); | |
| 1064 | −// } | |
| 1065 | −// "}); | |
| 1066 | − | |
| 1067 | −// let hint_start_offset = cx.ranges(indoc! {" | |
| 1068 | −// struct TestStruct; | |
| 1069 | − | |
| 1070 | −// // ================== | |
| 1071 | − | |
| 1072 | −// struct TestNewType<T>(T); | |
| 1073 | − | |
| 1074 | −// fn main() { | |
| 1075 | −// let variableˇ = TestNewType(TestStruct); | |
| 1076 | −// } | |
| 1077 | −// "})[0] | |
| 1078 | −// .start; | |
| 1079 | −// let hint_position = cx.to_lsp(hint_start_offset); | |
| 1080 | −// let new_type_target_range = cx.lsp_range(indoc! {" | |
| 1081 | −// struct TestStruct; | |
| 1082 | − | |
| 1083 | −// // ================== | |
| 1084 | − | |
| 1085 | −// struct «TestNewType»<T>(T); | |
| 1086 | − | |
| 1087 | −// fn main() { | |
| 1088 | −// let variable = TestNewType(TestStruct); | |
| 1089 | −// } | |
| 1090 | −// "}); | |
| 1091 | −// let struct_target_range = cx.lsp_range(indoc! {" | |
| 1092 | −// struct «TestStruct»; | |
| 1093 | − | |
| 1094 | −// // ================== | |
| 1095 | − | |
| 1096 | −// struct TestNewType<T>(T); | |
| 1097 | − | |
| 1098 | −// fn main() { | |
| 1099 | −// let variable = TestNewType(TestStruct); | |
| 1100 | −// } | |
| 1101 | −// "}); | |
| 1102 | − | |
| 1103 | −// let uri = cx.buffer_lsp_url.clone(); | |
| 1104 | −// let new_type_label = "TestNewType"; | |
| 1105 | −// let struct_label = "TestStruct"; | |
| 1106 | −// let entire_hint_label = ": TestNewType<TestStruct>"; | |
| 1107 | −// let closure_uri = uri.clone(); | |
| 1108 | −// cx.lsp | |
| 1109 | −// .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| { | |
| 1110 | −// let task_uri = closure_uri.clone(); | |
| 1111 | −// async move { | |
| 1112 | −// assert_eq!(params.text_document.uri, task_uri); | |
| 1113 | −// Ok(Some(vec![lsp::InlayHint { | |
| 1114 | −// position: hint_position, | |
| 1115 | −// label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { | |
| 1116 | −// value: entire_hint_label.to_string(), | |
| 1117 | −// ..Default::default() | |
| 1118 | −// }]), | |
| 1119 | −// kind: Some(lsp::InlayHintKind::TYPE), | |
| 1120 | −// text_edits: None, | |
| 1121 | −// tooltip: None, | |
| 1122 | −// padding_left: Some(false), | |
| 1123 | −// padding_right: Some(false), | |
| 1124 | −// data: None, | |
| 1125 | −// }])) | |
| 1126 | −// } | |
| 1127 | −// }) | |
| 1128 | −// .next() | |
| 1129 | −// .await; | |
| 1130 | −// cx.foreground().run_until_parked(); | |
| 1131 | −// cx.update_editor(|editor, cx| { | |
| 1132 | −// let expected_layers = vec![entire_hint_label.to_string()]; | |
| 1133 | −// assert_eq!(expected_layers, cached_hint_labels(editor)); | |
| 1134 | −// assert_eq!(expected_layers, visible_hint_labels(editor, cx)); | |
| 1135 | −// }); | |
| 1136 | − | |
| 1137 | −// let inlay_range = cx | |
| 1138 | −// .ranges(indoc! {" | |
| 1139 | −// struct TestStruct; | |
| 1140 | − | |
| 1141 | −// // ================== | |
| 1142 | − | |
| 1143 | −// struct TestNewType<T>(T); | |
| 1144 | − | |
| 1145 | −// fn main() { | |
| 1146 | −// let variable« »= TestNewType(TestStruct); | |
| 1147 | −// } | |
| 1148 | −// "}) | |
| 1149 | −// .get(0) | |
| 1150 | −// .cloned() | |
| 1151 | −// .unwrap(); | |
| 1152 | −// let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { | |
| 1153 | −// let snapshot = editor.snapshot(cx); | |
| 1154 | −// let previous_valid = inlay_range.start.to_display_point(&snapshot); | |
| 1155 | −// let next_valid = inlay_range.end.to_display_point(&snapshot); | |
| 1156 | −// assert_eq!(previous_valid.row(), next_valid.row()); | |
| 1157 | −// assert!(previous_valid.column() < next_valid.column()); | |
| 1158 | −// let exact_unclipped = DisplayPoint::new( | |
| 1159 | −// previous_valid.row(), | |
| 1160 | −// previous_valid.column() | |
| 1161 | −// + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) | |
| 1162 | −// as u32, | |
| 1163 | −// ); | |
| 1164 | −// PointForPosition { | |
| 1165 | −// previous_valid, | |
| 1166 | −// next_valid, | |
| 1167 | −// exact_unclipped, | |
| 1168 | −// column_overshoot_after_line_end: 0, | |
| 1169 | −// } | |
| 1170 | −// }); | |
| 1171 | −// cx.update_editor(|editor, cx| { | |
| 1172 | −// update_inlay_link_and_hover_points( | |
| 1173 | −// &editor.snapshot(cx), | |
| 1174 | −// new_type_hint_part_hover_position, | |
| 1175 | −// editor, | |
| 1176 | −// true, | |
| 1177 | −// false, | |
| 1178 | −// cx, | |
| 1179 | −// ); | |
| 1180 | −// }); | |
| 1181 | − | |
| 1182 | −// let resolve_closure_uri = uri.clone(); | |
| 1183 | −// cx.lsp | |
| 1184 | −// .handle_request::<lsp::request::InlayHintResolveRequest, _, _>( | |
| 1185 | −// move |mut hint_to_resolve, _| { | |
| 1186 | −// let mut resolved_hint_positions = BTreeSet::new(); | |
| 1187 | −// let task_uri = resolve_closure_uri.clone(); | |
| 1188 | −// async move { | |
| 1189 | −// let inserted = resolved_hint_positions.insert(hint_to_resolve.position); | |
| 1190 | −// assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); | |
| 1191 | − | |
| 1192 | −// // `: TestNewType<TestStruct>` | |
| 1193 | −// hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ | |
| 1194 | −// lsp::InlayHintLabelPart { | |
| 1195 | −// value: ": ".to_string(), | |
| 1196 | −// ..Default::default() | |
| 1197 | −// }, | |
| 1198 | −// lsp::InlayHintLabelPart { | |
| 1199 | −// value: new_type_label.to_string(), | |
| 1200 | −// location: Some(lsp::Location { | |
| 1201 | −// uri: task_uri.clone(), | |
| 1202 | −// range: new_type_target_range, | |
| 1203 | −// }), | |
| 1204 | −// tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( | |
| 1205 | −// "A tooltip for `{new_type_label}`" | |
| 1206 | −// ))), | |
| 1207 | −// ..Default::default() | |
| 1208 | −// }, | |
| 1209 | −// lsp::InlayHintLabelPart { | |
| 1210 | −// value: "<".to_string(), | |
| 1211 | −// ..Default::default() | |
| 1212 | −// }, | |
| 1213 | −// lsp::InlayHintLabelPart { | |
| 1214 | −// value: struct_label.to_string(), | |
| 1215 | −// location: Some(lsp::Location { | |
| 1216 | −// uri: task_uri, | |
| 1217 | −// range: struct_target_range, | |
| 1218 | −// }), | |
| 1219 | −// tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( | |
| 1220 | −// lsp::MarkupContent { | |
| 1221 | −// kind: lsp::MarkupKind::Markdown, | |
| 1222 | −// value: format!("A tooltip for `{struct_label}`"), | |
| 1223 | −// }, | |
| 1224 | −// )), | |
| 1225 | −// ..Default::default() | |
| 1226 | −// }, | |
| 1227 | −// lsp::InlayHintLabelPart { | |
| 1228 | −// value: ">".to_string(), | |
| 1229 | −// ..Default::default() | |
| 1230 | −// }, | |
| 1231 | −// ]); | |
| 1232 | − | |
| 1233 | −// Ok(hint_to_resolve) | |
| 1234 | −// } | |
| 1235 | −// }, | |
| 1236 | −// ) | |
| 1237 | −// .next() | |
| 1238 | −// .await; | |
| 1239 | −// cx.foreground().run_until_parked(); | |
| 1240 | − | |
| 1241 | −// cx.update_editor(|editor, cx| { | |
| 1242 | −// update_inlay_link_and_hover_points( | |
| 1243 | −// &editor.snapshot(cx), | |
| 1244 | −// new_type_hint_part_hover_position, | |
| 1245 | −// editor, | |
| 1246 | −// true, | |
| 1247 | −// false, | |
| 1248 | −// cx, | |
| 1249 | −// ); | |
| 1250 | −// }); | |
| 1251 | −// cx.foreground() | |
| 1252 | −// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); | |
| 1253 | −// cx.foreground().run_until_parked(); | |
| 1254 | −// cx.update_editor(|editor, cx| { | |
| 1255 | −// let hover_state = &editor.hover_state; | |
| 1256 | −// assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); | |
| 1257 | −// let popover = hover_state.info_popover.as_ref().unwrap(); | |
| 1258 | −// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); | |
| 1259 | −// assert_eq!( | |
| 1260 | −// popover.symbol_range, | |
| 1261 | −// RangeInEditor::Inlay(InlayHighlight { | |
| 1262 | −// inlay: InlayId::Hint(0), | |
| 1263 | −// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), | |
| 1264 | −// range: ": ".len()..": ".len() + new_type_label.len(), | |
| 1265 | −// }), | |
| 1266 | −// "Popover range should match the new type label part" | |
| 1267 | −// ); | |
| 1268 | −// assert_eq!( | |
| 1269 | −// popover.parsed_content.text, | |
| 1270 | −// format!("A tooltip for `{new_type_label}`"), | |
| 1271 | −// "Rendered text should not anyhow alter backticks" | |
| 1272 | −// ); | |
| 1273 | −// }); | |
| 1274 | − | |
| 1275 | −// let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { | |
| 1276 | −// let snapshot = editor.snapshot(cx); | |
| 1277 | −// let previous_valid = inlay_range.start.to_display_point(&snapshot); | |
| 1278 | −// let next_valid = inlay_range.end.to_display_point(&snapshot); | |
| 1279 | −// assert_eq!(previous_valid.row(), next_valid.row()); | |
| 1280 | −// assert!(previous_valid.column() < next_valid.column()); | |
| 1281 | −// let exact_unclipped = DisplayPoint::new( | |
| 1282 | −// previous_valid.row(), | |
| 1283 | −// previous_valid.column() | |
| 1284 | −// + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) | |
| 1285 | −// as u32, | |
| 1286 | −// ); | |
| 1287 | −// PointForPosition { | |
| 1288 | −// previous_valid, | |
| 1289 | −// next_valid, | |
| 1290 | −// exact_unclipped, | |
| 1291 | −// column_overshoot_after_line_end: 0, | |
| 1292 | −// } | |
| 1293 | −// }); | |
| 1294 | −// cx.update_editor(|editor, cx| { | |
| 1295 | −// update_inlay_link_and_hover_points( | |
| 1296 | −// &editor.snapshot(cx), | |
| 1297 | −// struct_hint_part_hover_position, | |
| 1298 | −// editor, | |
| 1299 | −// true, | |
| 1300 | −// false, | |
| 1301 | −// cx, | |
| 1302 | −// ); | |
| 1303 | −// }); | |
| 1304 | −// cx.foreground() | |
| 1305 | −// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); | |
| 1306 | −// cx.foreground().run_until_parked(); | |
| 1307 | −// cx.update_editor(|editor, cx| { | |
| 1308 | −// let hover_state = &editor.hover_state; | |
| 1309 | −// assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); | |
| 1310 | −// let popover = hover_state.info_popover.as_ref().unwrap(); | |
| 1311 | −// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); | |
| 1312 | −// assert_eq!( | |
| 1313 | −// popover.symbol_range, | |
| 1314 | −// RangeInEditor::Inlay(InlayHighlight { | |
| 1315 | −// inlay: InlayId::Hint(0), | |
| 1316 | −// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), | |
| 1317 | −// range: ": ".len() + new_type_label.len() + "<".len() | |
| 1318 | −// ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), | |
| 1319 | −// }), | |
| 1320 | −// "Popover range should match the struct label part" | |
| 1321 | −// ); | |
| 1322 | −// assert_eq!( | |
| 1323 | −// popover.parsed_content.text, | |
| 1324 | −// format!("A tooltip for {struct_label}"), | |
| 1325 | −// "Rendered markdown element should remove backticks from text" | |
| 1326 | −// ); | |
| 1327 | −// }); | |
| 1328 | −// } | |
| 1329 | −// } | |
| 553 | +#[cfg(test)] | |
| 554 | +mod tests { | |
| 555 | + use super::*; | |
| 556 | + use crate::{ | |
| 557 | + editor_tests::init_test, | |
| 558 | + element::PointForPosition, | |
| 559 | + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, | |
| 560 | + link_go_to_definition::update_inlay_link_and_hover_points, | |
| 561 | + test::editor_lsp_test_context::EditorLspTestContext, | |
| 562 | + InlayId, | |
| 563 | + }; | |
| 564 | + use collections::BTreeSet; | |
| 565 | + use gpui::{FontWeight, HighlightStyle, UnderlineStyle}; | |
| 566 | + use indoc::indoc; | |
| 567 | + use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; | |
| 568 | + use lsp::LanguageServerId; | |
| 569 | + use project::{HoverBlock, HoverBlockKind}; | |
| 570 | + use smol::stream::StreamExt; | |
| 571 | + use unindent::Unindent; | |
| 572 | + use util::test::marked_text_ranges; | |
| 573 | + | |
| 574 | + #[gpui::test] | |
| 575 | + async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { | |
| 576 | + init_test(cx, |_| {}); | |
| 577 | + | |
| 578 | + let mut cx = EditorLspTestContext::new_rust( | |
| 579 | + lsp::ServerCapabilities { | |
| 580 | + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), | |
| 581 | + ..Default::default() | |
| 582 | + }, | |
| 583 | + cx, | |
| 584 | + ) | |
| 585 | + .await; | |
| 586 | + | |
| 587 | + // Basic hover delays and then pops without moving the mouse | |
| 588 | + cx.set_state(indoc! {" | |
| 589 | + fn ˇtest() { println!(); } | |
| 590 | + "}); | |
| 591 | + let hover_point = cx.display_point(indoc! {" | |
| 592 | + fn test() { printˇln!(); } | |
| 593 | + "}); | |
| 594 | + | |
| 595 | + cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); | |
| 596 | + assert!(!cx.editor(|editor, _| editor.hover_state.visible())); | |
| 597 | + | |
| 598 | + // After delay, hover should be visible. | |
| 599 | + let symbol_range = cx.lsp_range(indoc! {" | |
| 600 | + fn test() { «println!»(); } | |
| 601 | + "}); | |
| 602 | + let mut requests = | |
| 603 | + cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move { | |
| 604 | + Ok(Some(lsp::Hover { | |
| 605 | + contents: lsp::HoverContents::Markup(lsp::MarkupContent { | |
| 606 | + kind: lsp::MarkupKind::Markdown, | |
| 607 | + value: "some basic docs".to_string(), | |
| 608 | + }), | |
| 609 | + range: Some(symbol_range), | |
| 610 | + })) | |
| 611 | + }); | |
| 612 | + cx.background_executor | |
| 613 | + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); | |
| 614 | + requests.next().await; | |
| 615 | + | |
| 616 | + cx.editor(|editor, _| { | |
| 617 | + assert!(editor.hover_state.visible()); | |
| 618 | + assert_eq!( | |
| 619 | + editor.hover_state.info_popover.clone().unwrap().blocks, | |
| 620 | + vec![HoverBlock { | |
| 621 | + text: "some basic docs".to_string(), | |
| 622 | + kind: HoverBlockKind::Markdown, | |
| 623 | + },] | |
| 624 | + ) | |
| 625 | + }); | |
| 626 | + | |
| 627 | + // Mouse moved with no hover response dismisses | |
| 628 | + let hover_point = cx.display_point(indoc! {" | |
| 629 | + fn teˇst() { println!(); } | |
| 630 | + "}); | |
| 631 | + let mut request = cx | |
| 632 | + .lsp | |
| 633 | + .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) }); | |
| 634 | + cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); | |
| 635 | + cx.background_executor | |
| 636 | + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); | |
| 637 | + request.next().await; | |
| 638 | + cx.editor(|editor, _| { | |
| 639 | + assert!(!editor.hover_state.visible()); | |
| 640 | + }); | |
| 641 | + } | |
| 642 | + | |
| 643 | + #[gpui::test] | |
| 644 | + async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { | |
| 645 | + init_test(cx, |_| {}); | |
| 646 | + | |
| 647 | + let mut cx = EditorLspTestContext::new_rust( | |
| 648 | + lsp::ServerCapabilities { | |
| 649 | + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), | |
| 650 | + ..Default::default() | |
| 651 | + }, | |
| 652 | + cx, | |
| 653 | + ) | |
| 654 | + .await; | |
| 655 | + | |
| 656 | + // Hover with keyboard has no delay | |
| 657 | + cx.set_state(indoc! {" | |
| 658 | + fˇn test() { println!(); } | |
| 659 | + "}); | |
| 660 | + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); | |
| 661 | + let symbol_range = cx.lsp_range(indoc! {" | |
| 662 | + «fn» test() { println!(); } | |
| 663 | + "}); | |
| 664 | + cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move { | |
| 665 | + Ok(Some(lsp::Hover { | |
| 666 | + contents: lsp::HoverContents::Markup(lsp::MarkupContent { | |
| 667 | + kind: lsp::MarkupKind::Markdown, | |
| 668 | + value: "some other basic docs".to_string(), | |
| 669 | + }), | |
| 670 | + range: Some(symbol_range), | |
| 671 | + })) | |
| 672 | + }) | |
| 673 | + .next() | |
| 674 | + .await; | |
| 675 | + | |
| 676 | + cx.condition(|editor, _| editor.hover_state.visible()).await; | |
| 677 | + cx.editor(|editor, _| { | |
| 678 | + assert_eq!( | |
| 679 | + editor.hover_state.info_popover.clone().unwrap().blocks, | |
| 680 | + vec![HoverBlock { | |
| 681 | + text: "some other basic docs".to_string(), | |
| 682 | + kind: HoverBlockKind::Markdown, | |
| 683 | + }] | |
| 684 | + ) | |
| 685 | + }); | |
| 686 | + } | |
| 687 | + | |
| 688 | + #[gpui::test] | |
| 689 | + async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) { | |
| 690 | + init_test(cx, |_| {}); | |
| 691 | + | |
| 692 | + let mut cx = EditorLspTestContext::new_rust( | |
| 693 | + lsp::ServerCapabilities { | |
| 694 | + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), | |
| 695 | + ..Default::default() | |
| 696 | + }, | |
| 697 | + cx, | |
| 698 | + ) | |
| 699 | + .await; | |
| 700 | + | |
| 701 | + // Hover with keyboard has no delay | |
| 702 | + cx.set_state(indoc! {" | |
| 703 | + fˇn test() { println!(); } | |
| 704 | + "}); | |
| 705 | + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); | |
| 706 | + let symbol_range = cx.lsp_range(indoc! {" | |
| 707 | + «fn» test() { println!(); } | |
| 708 | + "}); | |
| 709 | + cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move { | |
| 710 | + Ok(Some(lsp::Hover { | |
| 711 | + contents: lsp::HoverContents::Array(vec![ | |
| 712 | + lsp::MarkedString::String("regular text for hover to show".to_string()), | |
| 713 | + lsp::MarkedString::String("".to_string()), | |
| 714 | + lsp::MarkedString::LanguageString(lsp::LanguageString { | |
| 715 | + language: "Rust".to_string(), | |
| 716 | + value: "".to_string(), | |
| 717 | + }), | |
| 718 | + ]), | |
| 719 | + range: Some(symbol_range), | |
| 720 | + })) | |
| 721 | + }) | |
| 722 | + .next() | |
| 723 | + .await; | |
| 724 | + | |
| 725 | + cx.condition(|editor, _| editor.hover_state.visible()).await; | |
| 726 | + cx.editor(|editor, _| { | |
| 727 | + assert_eq!( | |
| 728 | + editor.hover_state.info_popover.clone().unwrap().blocks, | |
| 729 | + vec![HoverBlock { | |
| 730 | + text: "regular text for hover to show".to_string(), | |
| 731 | + kind: HoverBlockKind::Markdown, | |
| 732 | + }], | |
| 733 | + "No empty string hovers should be shown" | |
| 734 | + ); | |
| 735 | + }); | |
| 736 | + } | |
| 737 | + | |
| 738 | + #[gpui::test] | |
| 739 | + async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) { | |
| 740 | + init_test(cx, |_| {}); | |
| 741 | + | |
| 742 | + let mut cx = EditorLspTestContext::new_rust( | |
| 743 | + lsp::ServerCapabilities { | |
| 744 | + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), | |
| 745 | + ..Default::default() | |
| 746 | + }, | |
| 747 | + cx, | |
| 748 | + ) | |
| 749 | + .await; | |
| 750 | + | |
| 751 | + // Hover with keyboard has no delay | |
| 752 | + cx.set_state(indoc! {" | |
| 753 | + fˇn test() { println!(); } | |
| 754 | + "}); | |
| 755 | + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); | |
| 756 | + let symbol_range = cx.lsp_range(indoc! {" | |
| 757 | + «fn» test() { println!(); } | |
| 758 | + "}); | |
| 759 | + | |
| 760 | + let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; | |
| 761 | + let markdown_string = format!("\n```rust\n{code_str}```"); | |
| 762 | + | |
| 763 | + let closure_markdown_string = markdown_string.clone(); | |
| 764 | + cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| { | |
| 765 | + let future_markdown_string = closure_markdown_string.clone(); | |
| 766 | + async move { | |
| 767 | + Ok(Some(lsp::Hover { | |
| 768 | + contents: lsp::HoverContents::Markup(lsp::MarkupContent { | |
| 769 | + kind: lsp::MarkupKind::Markdown, | |
| 770 | + value: future_markdown_string, | |
| 771 | + }), | |
| 772 | + range: Some(symbol_range), | |
| 773 | + })) | |
| 774 | + } | |
| 775 | + }) | |
| 776 | + .next() | |
| 777 | + .await; | |
| 778 | + | |
| 779 | + cx.condition(|editor, _| editor.hover_state.visible()).await; | |
| 780 | + cx.editor(|editor, _| { | |
| 781 | + let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; | |
| 782 | + assert_eq!( | |
| 783 | + blocks, | |
| 784 | + vec![HoverBlock { | |
| 785 | + text: markdown_string, | |
| 786 | + kind: HoverBlockKind::Markdown, | |
| 787 | + }], | |
| 788 | + ); | |
| 789 | + | |
| 790 | + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); | |
| 791 | + assert_eq!( | |
| 792 | + rendered.text, | |
| 793 | + code_str.trim(), | |
| 794 | + "Should not have extra line breaks at end of rendered hover" | |
| 795 | + ); | |
| 796 | + }); | |
| 797 | + } | |
| 798 | + | |
| 799 | + #[gpui::test] | |
| 800 | + async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { | |
| 801 | + init_test(cx, |_| {}); | |
| 802 | + | |
| 803 | + let mut cx = EditorLspTestContext::new_rust( | |
| 804 | + lsp::ServerCapabilities { | |
| 805 | + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), | |
| 806 | + ..Default::default() | |
| 807 | + }, | |
| 808 | + cx, | |
| 809 | + ) | |
| 810 | + .await; | |
| 811 | + | |
| 812 | + // Hover with just diagnostic, pops DiagnosticPopover immediately and then | |
| 813 | + // info popover once request completes | |
| 814 | + cx.set_state(indoc! {" | |
| 815 | + fn teˇst() { println!(); } | |
| 816 | + "}); | |
| 817 | + | |
| 818 | + // Send diagnostic to client | |
| 819 | + let range = cx.text_anchor_range(indoc! {" | |
| 820 | + fn «test»() { println!(); } | |
| 821 | + "}); | |
| 822 | + cx.update_buffer(|buffer, cx| { | |
| 823 | + let snapshot = buffer.text_snapshot(); | |
| 824 | + let set = DiagnosticSet::from_sorted_entries( | |
| 825 | + vec![DiagnosticEntry { | |
| 826 | + range, | |
| 827 | + diagnostic: Diagnostic { | |
| 828 | + message: "A test diagnostic message.".to_string(), | |
| 829 | + ..Default::default() | |
| 830 | + }, | |
| 831 | + }], | |
| 832 | + &snapshot, | |
| 833 | + ); | |
| 834 | + buffer.update_diagnostics(LanguageServerId(0), set, cx); | |
| 835 | + }); | |
| 836 | + | |
| 837 | + // Hover pops diagnostic immediately | |
| 838 | + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); | |
| 839 | + cx.background_executor.run_until_parked(); | |
| 840 | + | |
| 841 | + cx.editor(|Editor { hover_state, .. }, _| { | |
| 842 | + assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) | |
| 843 | + }); | |
| 844 | + | |
| 845 | + // Info Popover shows after request responded to | |
| 846 | + let range = cx.lsp_range(indoc! {" | |
| 847 | + fn «test»() { println!(); } | |
| 848 | + "}); | |
| 849 | + cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move { | |
| 850 | + Ok(Some(lsp::Hover { | |
| 851 | + contents: lsp::HoverContents::Markup(lsp::MarkupContent { | |
| 852 | + kind: lsp::MarkupKind::Markdown, | |
| 853 | + value: "some new docs".to_string(), | |
| 854 | + }), | |
| 855 | + range: Some(range), | |
| 856 | + })) | |
| 857 | + }); | |
| 858 | + cx.background_executor | |
| 859 | + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); | |
| 860 | + | |
| 861 | + cx.background_executor.run_until_parked(); | |
| 862 | + cx.editor(|Editor { hover_state, .. }, _| { | |
| 863 | + hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() | |
| 864 | + }); | |
| 865 | + } | |
| 866 | + | |
| 867 | + #[gpui::test] | |
| 868 | + fn test_render_blocks(cx: &mut gpui::TestAppContext) { | |
| 869 | + init_test(cx, |_| {}); | |
| 870 | + | |
| 871 | + let editor = cx.add_window(|cx| Editor::single_line(cx)); | |
| 872 | + editor | |
| 873 | + .update(cx, |editor, cx| { | |
| 874 | + let style = editor.style.clone().unwrap(); | |
| 875 | + | |
| 876 | + struct Row { | |
| 877 | + blocks: Vec<HoverBlock>, | |
| 878 | + expected_marked_text: String, | |
| 879 | + expected_styles: Vec<HighlightStyle>, | |
| 880 | + } | |
| 881 | + | |
| 882 | + let rows = &[ | |
| 883 | + // Strong emphasis | |
| 884 | + Row { | |
| 885 | + blocks: vec![HoverBlock { | |
| 886 | + text: "one **two** three".to_string(), | |
| 887 | + kind: HoverBlockKind::Markdown, | |
| 888 | + }], | |
| 889 | + expected_marked_text: "one «two» three".to_string(), | |
| 890 | + expected_styles: vec![HighlightStyle { | |
| 891 | + font_weight: Some(FontWeight::BOLD), | |
| 892 | + ..Default::default() | |
| 893 | + }], | |
| 894 | + }, | |
| 895 | + // Links | |
| 896 | + Row { | |
| 897 | + blocks: vec three".to_string(), | |
| 899 | + kind: HoverBlockKind::Markdown, | |
| 900 | + }], | |
| 901 | + expected_marked_text: "one «two» three".to_string(), | |
| 902 | + expected_styles: vec![HighlightStyle { | |
| 903 | + underline: Some(UnderlineStyle { | |
| 904 | + thickness: 1.0.into(), | |
| 905 | + ..Default::default() | |
| 906 | + }), | |
| 907 | + ..Default::default() | |
| 908 | + }], | |
| 909 | + }, | |
| 910 | + // Lists | |
| 911 | + Row { | |
| 912 | + blocks: vec | |
| 920 | + - d" | |
| 921 | + .unindent(), | |
| 922 | + kind: HoverBlockKind::Markdown, | |
| 923 | + }], | |
| 924 | + expected_marked_text: " | |
| 925 | + lists: | |
| 926 | + - one | |
| 927 | + - a | |
| 928 | + - b | |
| 929 | + - two | |
| 930 | + - «c» | |
| 931 | + - d" | |
| 932 | + .unindent(), | |
| 933 | + expected_styles: vec![HighlightStyle { | |
| 934 | + underline: Some(UnderlineStyle { | |
| 935 | + thickness: 1.0.into(), | |
| 936 | + ..Default::default() | |
| 937 | + }), | |
| 938 | + ..Default::default() | |
| 939 | + }], | |
| 940 | + }, | |
| 941 | + // Multi-paragraph list items | |
| 942 | + Row { | |
| 943 | + blocks: vec![HoverBlock { | |
| 944 | + text: " | |
| 945 | + * one two | |
| 946 | + three | |
| 947 | + | |
| 948 | + * four five | |
| 949 | + * six seven | |
| 950 | + eight | |
| 951 | + | |
| 952 | + nine | |
| 953 | + * ten | |
| 954 | + * six" | |
| 955 | + .unindent(), | |
| 956 | + kind: HoverBlockKind::Markdown, | |
| 957 | + }], | |
| 958 | + expected_marked_text: " | |
| 959 | + - one two three | |
| 960 | + - four five | |
| 961 | + - six seven eight | |
| 962 | + | |
| 963 | + nine | |
| 964 | + - ten | |
| 965 | + - six" | |
| 966 | + .unindent(), | |
| 967 | + expected_styles: vec![HighlightStyle { | |
| 968 | + underline: Some(UnderlineStyle { | |
| 969 | + thickness: 1.0.into(), | |
| 970 | + ..Default::default() | |
| 971 | + }), | |
| 972 | + ..Default::default() | |
| 973 | + }], | |
| 974 | + }, | |
| 975 | + ]; | |
| 976 | + | |
| 977 | + for Row { | |
| 978 | + blocks, | |
| 979 | + expected_marked_text, | |
| 980 | + expected_styles, | |
| 981 | + } in &rows[0..] | |
| 982 | + { | |
| 983 | + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); | |
| 984 | + | |
| 985 | + let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); | |
| 986 | + let expected_highlights = ranges | |
| 987 | + .into_iter() | |
| 988 | + .zip(expected_styles.iter().cloned()) | |
| 989 | + .collect::<Vec<_>>(); | |
| 990 | + assert_eq!( | |
| 991 | + rendered.text, expected_text, | |
| 992 | + "wrong text for input {blocks:?}" | |
| 993 | + ); | |
| 994 | + | |
| 995 | + let rendered_highlights: Vec<_> = rendered | |
| 996 | + .highlights | |
| 997 | + .iter() | |
| 998 | + .filter_map(|(range, highlight)| { | |
| 999 | + let highlight = highlight.to_highlight_style(&style.syntax)?; | |
| 1000 | + Some((range.clone(), highlight)) | |
| 1001 | + }) | |
| 1002 | + .collect(); | |
| 1003 | + | |
| 1004 | + assert_eq!( | |
| 1005 | + rendered_highlights, expected_highlights, | |
| 1006 | + "wrong highlights for input {blocks:?}" | |
| 1007 | + ); | |
| 1008 | + } | |
| 1009 | + }) | |
| 1010 | + .unwrap(); | |
| 1011 | + } | |
| 1012 | + | |
| 1013 | + #[gpui::test] | |
| 1014 | + async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { | |
| 1015 | + init_test(cx, |settings| { | |
| 1016 | + settings.defaults.inlay_hints = Some(InlayHintSettings { | |
| 1017 | + enabled: true, | |
| 1018 | + show_type_hints: true, | |
| 1019 | + show_parameter_hints: true, | |
| 1020 | + show_other_hints: true, | |
| 1021 | + }) | |
| 1022 | + }); | |
| 1023 | + | |
| 1024 | + let mut cx = EditorLspTestContext::new_rust( | |
| 1025 | + lsp::ServerCapabilities { | |
| 1026 | + inlay_hint_provider: Some(lsp::OneOf::Right( | |
| 1027 | + lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { | |
| 1028 | + resolve_provider: Some(true), | |
| 1029 | + ..Default::default() | |
| 1030 | + }), | |
| 1031 | + )), | |
| 1032 | + ..Default::default() | |
| 1033 | + }, | |
| 1034 | + cx, | |
| 1035 | + ) | |
| 1036 | + .await; | |
| 1037 | + | |
| 1038 | + cx.set_state(indoc! {" | |
| 1039 | + struct TestStruct; | |
| 1040 | + | |
| 1041 | + // ================== | |
| 1042 | + | |
| 1043 | + struct TestNewType<T>(T); | |
| 1044 | + | |
| 1045 | + fn main() { | |
| 1046 | + let variableˇ = TestNewType(TestStruct); | |
| 1047 | + } | |
| 1048 | + "}); | |
| 1049 | + | |
| 1050 | + let hint_start_offset = cx.ranges(indoc! {" | |
| 1051 | + struct TestStruct; | |
| 1052 | + | |
| 1053 | + // ================== | |
| 1054 | + | |
| 1055 | + struct TestNewType<T>(T); | |
| 1056 | + | |
| 1057 | + fn main() { | |
| 1058 | + let variableˇ = TestNewType(TestStruct); | |
| 1059 | + } | |
| 1060 | + "})[0] | |
| 1061 | + .start; | |
| 1062 | + let hint_position = cx.to_lsp(hint_start_offset); | |
| 1063 | + let new_type_target_range = cx.lsp_range(indoc! {" | |
| 1064 | + struct TestStruct; | |
| 1065 | + | |
| 1066 | + // ================== | |
| 1067 | + | |
| 1068 | + struct «TestNewType»<T>(T); | |
| 1069 | + | |
| 1070 | + fn main() { | |
| 1071 | + let variable = TestNewType(TestStruct); | |
| 1072 | + } | |
| 1073 | + "}); | |
| 1074 | + let struct_target_range = cx.lsp_range(indoc! {" | |
| 1075 | + struct «TestStruct»; | |
| 1076 | + | |
| 1077 | + // ================== | |
| 1078 | + | |
| 1079 | + struct TestNewType<T>(T); | |
| 1080 | + | |
| 1081 | + fn main() { | |
| 1082 | + let variable = TestNewType(TestStruct); | |
| 1083 | + } | |
| 1084 | + "}); | |
| 1085 | + | |
| 1086 | + let uri = cx.buffer_lsp_url.clone(); | |
| 1087 | + let new_type_label = "TestNewType"; | |
| 1088 | + let struct_label = "TestStruct"; | |
| 1089 | + let entire_hint_label = ": TestNewType<TestStruct>"; | |
| 1090 | + let closure_uri = uri.clone(); | |
| 1091 | + cx.lsp | |
| 1092 | + .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| { | |
| 1093 | + let task_uri = closure_uri.clone(); | |
| 1094 | + async move { | |
| 1095 | + assert_eq!(params.text_document.uri, task_uri); | |
| 1096 | + Ok(Some(vec![lsp::InlayHint { | |
| 1097 | + position: hint_position, | |
| 1098 | + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { | |
| 1099 | + value: entire_hint_label.to_string(), | |
| 1100 | + ..Default::default() | |
| 1101 | + }]), | |
| 1102 | + kind: Some(lsp::InlayHintKind::TYPE), | |
| 1103 | + text_edits: None, | |
| 1104 | + tooltip: None, | |
| 1105 | + padding_left: Some(false), | |
| 1106 | + padding_right: Some(false), | |
| 1107 | + data: None, | |
| 1108 | + }])) | |
| 1109 | + } | |
| 1110 | + }) | |
| 1111 | + .next() | |
| 1112 | + .await; | |
| 1113 | + cx.background_executor.run_until_parked(); | |
| 1114 | + cx.update_editor(|editor, cx| { | |
| 1115 | + let expected_layers = vec![entire_hint_label.to_string()]; | |
| 1116 | + assert_eq!(expected_layers, cached_hint_labels(editor)); | |
| 1117 | + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); | |
| 1118 | + }); | |
| 1119 | + | |
| 1120 | + let inlay_range = cx | |
| 1121 | + .ranges(indoc! {" | |
| 1122 | + struct TestStruct; | |
| 1123 | + | |
| 1124 | + // ================== | |
| 1125 | + | |
| 1126 | + struct TestNewType<T>(T); | |
| 1127 | + | |
| 1128 | + fn main() { | |
| 1129 | + let variable« »= TestNewType(TestStruct); | |
| 1130 | + } | |
| 1131 | + "}) | |
| 1132 | + .get(0) | |
| 1133 | + .cloned() | |
| 1134 | + .unwrap(); | |
| 1135 | + let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { | |
| 1136 | + let snapshot = editor.snapshot(cx); | |
| 1137 | + let previous_valid = inlay_range.start.to_display_point(&snapshot); | |
| 1138 | + let next_valid = inlay_range.end.to_display_point(&snapshot); | |
| 1139 | + assert_eq!(previous_valid.row(), next_valid.row()); | |
| 1140 | + assert!(previous_valid.column() < next_valid.column()); | |
| 1141 | + let exact_unclipped = DisplayPoint::new( | |
| 1142 | + previous_valid.row(), | |
| 1143 | + previous_valid.column() | |
| 1144 | + + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) | |
| 1145 | + as u32, | |
| 1146 | + ); | |
| 1147 | + PointForPosition { | |
| 1148 | + previous_valid, | |
| 1149 | + next_valid, | |
| 1150 | + exact_unclipped, | |
| 1151 | + column_overshoot_after_line_end: 0, | |
| 1152 | + } | |
| 1153 | + }); | |
| 1154 | + cx.update_editor(|editor, cx| { | |
| 1155 | + update_inlay_link_and_hover_points( | |
| 1156 | + &editor.snapshot(cx), | |
| 1157 | + new_type_hint_part_hover_position, | |
| 1158 | + editor, | |
| 1159 | + true, | |
| 1160 | + false, | |
| 1161 | + cx, | |
| 1162 | + ); | |
| 1163 | + }); | |
| 1164 | + | |
| 1165 | + let resolve_closure_uri = uri.clone(); | |
| 1166 | + cx.lsp | |
| 1167 | + .handle_request::<lsp::request::InlayHintResolveRequest, _, _>( | |
| 1168 | + move |mut hint_to_resolve, _| { | |
| 1169 | + let mut resolved_hint_positions = BTreeSet::new(); | |
| 1170 | + let task_uri = resolve_closure_uri.clone(); | |
| 1171 | + async move { | |
| 1172 | + let inserted = resolved_hint_positions.insert(hint_to_resolve.position); | |
| 1173 | + assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); | |
| 1174 | + | |
| 1175 | + // `: TestNewType<TestStruct>` | |
| 1176 | + hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ | |
| 1177 | + lsp::InlayHintLabelPart { | |
| 1178 | + value: ": ".to_string(), | |
| 1179 | + ..Default::default() | |
| 1180 | + }, | |
| 1181 | + lsp::InlayHintLabelPart { | |
| 1182 | + value: new_type_label.to_string(), | |
| 1183 | + location: Some(lsp::Location { | |
| 1184 | + uri: task_uri.clone(), | |
| 1185 | + range: new_type_target_range, | |
| 1186 | + }), | |
| 1187 | + tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( | |
| 1188 | + "A tooltip for `{new_type_label}`" | |
| 1189 | + ))), | |
| 1190 | + ..Default::default() | |
| 1191 | + }, | |
| 1192 | + lsp::InlayHintLabelPart { | |
| 1193 | + value: "<".to_string(), | |
| 1194 | + ..Default::default() | |
| 1195 | + }, | |
| 1196 | + lsp::InlayHintLabelPart { | |
| 1197 | + value: struct_label.to_string(), | |
| 1198 | + location: Some(lsp::Location { | |
| 1199 | + uri: task_uri, | |
| 1200 | + range: struct_target_range, | |
| 1201 | + }), | |
| 1202 | + tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( | |
| 1203 | + lsp::MarkupContent { | |
| 1204 | + kind: lsp::MarkupKind::Markdown, | |
| 1205 | + value: format!("A tooltip for `{struct_label}`"), | |
| 1206 | + }, | |
| 1207 | + )), | |
| 1208 | + ..Default::default() | |
| 1209 | + }, | |
| 1210 | + lsp::InlayHintLabelPart { | |
| 1211 | + value: ">".to_string(), | |
| 1212 | + ..Default::default() | |
| 1213 | + }, | |
| 1214 | + ]); | |
| 1215 | + | |
| 1216 | + Ok(hint_to_resolve) | |
| 1217 | + } | |
| 1218 | + }, | |
| 1219 | + ) | |
| 1220 | + .next() | |
| 1221 | + .await; | |
| 1222 | + cx.background_executor.run_until_parked(); | |
| 1223 | + | |
| 1224 | + cx.update_editor(|editor, cx| { | |
| 1225 | + update_inlay_link_and_hover_points( | |
| 1226 | + &editor.snapshot(cx), | |
| 1227 | + new_type_hint_part_hover_position, | |
| 1228 | + editor, | |
| 1229 | + true, | |
| 1230 | + false, | |
| 1231 | + cx, | |
| 1232 | + ); | |
| 1233 | + }); | |
| 1234 | + cx.background_executor | |
| 1235 | + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); | |
| 1236 | + cx.background_executor.run_until_parked(); | |
| 1237 | + cx.update_editor(|editor, cx| { | |
| 1238 | + let hover_state = &editor.hover_state; | |
| 1239 | + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); | |
| 1240 | + let popover = hover_state.info_popover.as_ref().unwrap(); | |
| 1241 | + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); | |
| 1242 | + assert_eq!( | |
| 1243 | + popover.symbol_range, | |
| 1244 | + RangeInEditor::Inlay(InlayHighlight { | |
| 1245 | + inlay: InlayId::Hint(0), | |
| 1246 | + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), | |
| 1247 | + range: ": ".len()..": ".len() + new_type_label.len(), | |
| 1248 | + }), | |
| 1249 | + "Popover range should match the new type label part" | |
| 1250 | + ); | |
| 1251 | + assert_eq!( | |
| 1252 | + popover.parsed_content.text, | |
| 1253 | + format!("A tooltip for `{new_type_label}`"), | |
| 1254 | + "Rendered text should not anyhow alter backticks" | |
| 1255 | + ); | |
| 1256 | + }); | |
| 1257 | + | |
| 1258 | + let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { | |
| 1259 | + let snapshot = editor.snapshot(cx); | |
| 1260 | + let previous_valid = inlay_range.start.to_display_point(&snapshot); | |
| 1261 | + let next_valid = inlay_range.end.to_display_point(&snapshot); | |
| 1262 | + assert_eq!(previous_valid.row(), next_valid.row()); | |
| 1263 | + assert!(previous_valid.column() < next_valid.column()); | |
| 1264 | + let exact_unclipped = DisplayPoint::new( | |
| 1265 | + previous_valid.row(), | |
| 1266 | + previous_valid.column() | |
| 1267 | + + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) | |
| 1268 | + as u32, | |
| 1269 | + ); | |
| 1270 | + PointForPosition { | |
| 1271 | + previous_valid, | |
| 1272 | + next_valid, | |
| 1273 | + exact_unclipped, | |
| 1274 | + column_overshoot_after_line_end: 0, | |
| 1275 | + } | |
| 1276 | + }); | |
| 1277 | + cx.update_editor(|editor, cx| { | |
| 1278 | + update_inlay_link_and_hover_points( | |
| 1279 | + &editor.snapshot(cx), | |
| 1280 | + struct_hint_part_hover_position, | |
| 1281 | + editor, | |
| 1282 | + true, | |
| 1283 | + false, | |
| 1284 | + cx, | |
| 1285 | + ); | |
| 1286 | + }); | |
| 1287 | + cx.background_executor | |
| 1288 | + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); | |
| 1289 | + cx.background_executor.run_until_parked(); | |
| 1290 | + cx.update_editor(|editor, cx| { | |
| 1291 | + let hover_state = &editor.hover_state; | |
| 1292 | + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); | |
| 1293 | + let popover = hover_state.info_popover.as_ref().unwrap(); | |
| 1294 | + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); | |
| 1295 | + assert_eq!( | |
| 1296 | + popover.symbol_range, | |
| 1297 | + RangeInEditor::Inlay(InlayHighlight { | |
| 1298 | + inlay: InlayId::Hint(0), | |
| 1299 | + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), | |
| 1300 | + range: ": ".len() + new_type_label.len() + "<".len() | |
| 1301 | + ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), | |
| 1302 | + }), | |
| 1303 | + "Popover range should match the struct label part" | |
| 1304 | + ); | |
| 1305 | + assert_eq!( | |
| 1306 | + popover.parsed_content.text, | |
| 1307 | + format!("A tooltip for {struct_label}"), | |
| 1308 | + "Rendered markdown element should remove backticks from text" | |
| 1309 | + ); | |
| 1310 | + }); | |
| 1311 | + } | |
| 1312 | +} | |
| @@ -861,7 +861,7 @@ async fn fetch_and_update_hints( · 860 unmodified lines | ||
| 861 | 861 | let inlay_hints_fetch_task = editor |
| 862 | 862 | .update(&mut cx, |editor, cx| { |
| 863 | 863 | if got_throttled { |
| 864 | − let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { | |
| 864 | + let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) { | |
| 865 | 865 | Some((_, _, current_visible_range)) => { |
| 866 | 866 | let visible_offset_length = current_visible_range.len(); |
| 867 | 867 | let double_visible_range = current_visible_range |
| @@ -2201,7 +2201,9 @@ pub mod tests { · 1333 unmodified lines | ||
| 2201 | 2201 | cx: &mut gpui::TestAppContext, |
| 2202 | 2202 | ) -> Range<Point> { |
| 2203 | 2203 | let ranges = editor |
| 2204 | − .update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)) | |
| 2204 | + .update(cx, |editor, cx| { | |
| 2205 | + editor.excerpts_for_inlay_hints_query(None, cx) | |
| 2206 | + }) | |
| 2205 | 2207 | .unwrap(); |
| 2206 | 2208 | assert_eq!( |
| 2207 | 2209 | ranges.len(), |
| @@ -9,8 +9,8 @@ use collections::HashSet; · 8 unmodified lines | ||
| 9 | 9 | use futures::future::try_join_all; |
| 10 | 10 | use gpui::{ |
| 11 | 11 | div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter, |
| 12 | − FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View, | |
| 13 | − ViewContext, VisualContext, WeakView, | |
| 12 | + FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View, | |
| 13 | + ViewContext, VisualContext, WeakView, WindowContext, | |
| 14 | 14 | }; |
| 15 | 15 | use language::{ |
| 16 | 16 | proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt, |
| @@ -30,7 +30,7 @@ use std::{ · 13 unmodified lines | ||
| 30 | 30 | }; |
| 31 | 31 | use text::Selection; |
| 32 | 32 | use theme::{ActiveTheme, Theme}; |
| 33 | −use ui::{Label, TextColor}; | |
| 33 | +use ui::{Color, Label}; | |
| 34 | 34 | use util::{paths::PathExt, ResultExt, TryFutureExt}; |
| 35 | 35 | use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; |
| 36 | 36 | use workspace::{ |
| @@ -584,7 +584,7 @@ impl Item for Editor { · 547 unmodified lines | ||
| 584 | 584 | Some(path.to_string_lossy().to_string().into()) |
| 585 | 585 | } |
| 586 | 586 | |
| 587 | − fn tab_content<T: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<T> { | |
| 587 | + fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement { | |
| 588 | 588 | let theme = cx.theme(); |
| 589 | 589 | |
| 590 | 590 | AnyElement::new( |
| @@ -604,7 +604,7 @@ impl Item for Editor { · 13 unmodified lines | ||
| 604 | 604 | &description, |
| 605 | 605 | MAX_TAB_TITLE_LEN, |
| 606 | 606 | )) |
| 607 | − .color(TextColor::Muted), | |
| 607 | + .color(Color::Muted), | |
| 608 | 608 | ), |
| 609 | 609 | ) |
| 610 | 610 | })), |
| @@ -761,7 +761,7 @@ impl Item for Editor { · 150 unmodified lines | ||
| 761 | 761 | } |
| 762 | 762 | |
| 763 | 763 | fn breadcrumb_location(&self) -> ToolbarItemLocation { |
| 764 | − ToolbarItemLocation::PrimaryLeft { flex: None } | |
| 764 | + ToolbarItemLocation::PrimaryLeft | |
| 765 | 765 | } |
| 766 | 766 | |
| 767 | 767 | fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> { |
| @@ -907,17 +907,15 @@ impl SearchableItem for Editor { · 139 unmodified lines | ||
| 907 | 907 | type Match = Range<Anchor>; |
| 908 | 908 | |
| 909 | 909 | fn clear_matches(&mut self, cx: &mut ViewContext<Self>) { |
| 910 | − todo!() | |
| 911 | − // self.clear_background_highlights::<BufferSearchHighlights>(cx); | |
| 910 | + self.clear_background_highlights::<BufferSearchHighlights>(cx); | |
| 912 | 911 | } |
| 913 | 912 | |
| 914 | 913 | fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) { |
| 915 | − todo!() | |
| 916 | − // self.highlight_background::<BufferSearchHighlights>( | |
| 917 | − // matches, | |
| 918 | − // |theme| theme.search.match_background, | |
| 919 | − // cx, | |
| 920 | − // ); | |
| 914 | + self.highlight_background::<BufferSearchHighlights>( | |
| 915 | + matches, | |
| 916 | + |theme| theme.title_bar_background, // todo: update theme | |
| 917 | + cx, | |
| 918 | + ); | |
| 921 | 919 | } |
| 922 | 920 | |
| 923 | 921 | fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String { |
| @@ -952,22 +950,20 @@ impl SearchableItem for Editor { · 28 unmodified lines | ||
| 952 | 950 | matches: Vec<Range<Anchor>>, |
| 953 | 951 | cx: &mut ViewContext<Self>, |
| 954 | 952 | ) { |
| 955 | − todo!() | |
| 956 | − // self.unfold_ranges([matches[index].clone()], false, true, cx); | |
| 957 | − // let range = self.range_for_match(&matches[index]); | |
| 958 | − // self.change_selections(Some(Autoscroll::fit()), cx, |s| { | |
| 959 | − // s.select_ranges([range]); | |
| 960 | − // }) | |
| 953 | + self.unfold_ranges([matches[index].clone()], false, true, cx); | |
| 954 | + let range = self.range_for_match(&matches[index]); | |
| 955 | + self.change_selections(Some(Autoscroll::fit()), cx, |s| { | |
| 956 | + s.select_ranges([range]); | |
| 957 | + }) | |
| 961 | 958 | } |
| 962 | 959 | |
| 963 | 960 | fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) { |
| 964 | − todo!() | |
| 965 | − // self.unfold_ranges(matches.clone(), false, false, cx); | |
| 966 | − // let mut ranges = Vec::new(); | |
| 967 | − // for m in &matches { | |
| 968 | − // ranges.push(self.range_for_match(&m)) | |
| 969 | − // } | |
| 970 | − // self.change_selections(None, cx, |s| s.select_ranges(ranges)); | |
| 961 | + self.unfold_ranges(matches.clone(), false, false, cx); | |
| 962 | + let mut ranges = Vec::new(); | |
| 963 | + for m in &matches { | |
| 964 | + ranges.push(self.range_for_match(&m)) | |
| 965 | + } | |
| 966 | + self.change_selections(None, cx, |s| s.select_ranges(ranges)); | |
| 971 | 967 | } |
| 972 | 968 | fn replace( |
| 973 | 969 | &mut self, |
| @@ -2,9 +2,8 @@ use collections::HashMap; · 1 unmodified line | ||
| 2 | 2 | use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; |
| 3 | 3 | use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; |
| 4 | 4 | use gpui::{ |
| 5 | − actions, div, AppContext, Component, Div, EventEmitter, FocusHandle, FocusableView, | |
| 6 | − InteractiveComponent, Manager, Model, ParentComponent, Render, Styled, Task, View, ViewContext, | |
| 7 | − VisualContext, WeakView, | |
| 5 | + actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model, | |
| 6 | + ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, | |
| 8 | 7 | }; |
| 9 | 8 | use picker::{Picker, PickerDelegate}; |
| 10 | 9 | use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; |
| @@ -16,8 +15,7 @@ use std::{ · 5 unmodified lines | ||
| 16 | 15 | }, |
| 17 | 16 | }; |
| 18 | 17 | use text::Point; |
| 19 | −use theme::ActiveTheme; | |
| 20 | −use ui::{v_stack, HighlightedLabel, StyledExt}; | |
| 18 | +use ui::{v_stack, HighlightedLabel, ListItem}; | |
| 21 | 19 | use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; |
| 22 | 20 | use workspace::Workspace; |
| 23 | 21 | |
| @@ -111,14 +109,14 @@ impl FileFinder { · 87 unmodified lines | ||
| 111 | 109 | } |
| 112 | 110 | } |
| 113 | 111 | |
| 114 | −impl EventEmitter<Manager> for FileFinder {} | |
| 112 | +impl EventEmitter<DismissEvent> for FileFinder {} | |
| 115 | 113 | impl FocusableView for FileFinder { |
| 116 | 114 | fn focus_handle(&self, cx: &AppContext) -> FocusHandle { |
| 117 | 115 | self.picker.focus_handle(cx) |
| 118 | 116 | } |
| 119 | 117 | } |
| 120 | 118 | impl Render for FileFinder { |
| 121 | − type Element = Div<Self>; | |
| 119 | + type Element = Div; | |
| 122 | 120 | |
| 123 | 121 | fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { |
| 124 | 122 | v_stack().w_96().child(self.picker.clone()) |
| @@ -530,7 +528,7 @@ impl FileFinderDelegate { · 405 unmodified lines | ||
| 530 | 528 | } |
| 531 | 529 | |
| 532 | 530 | impl PickerDelegate for FileFinderDelegate { |
| 533 | − type ListItem = Div<Picker<Self>>; | |
| 531 | + type ListItem = ListItem; | |
| 534 | 532 | |
| 535 | 533 | fn placeholder_text(&self) -> Arc<str> { |
| 536 | 534 | "Search project files...".into() |
| @@ -690,7 +688,7 @@ impl PickerDelegate for FileFinderDelegate { · 153 unmodified lines | ||
| 690 | 688 | } |
| 691 | 689 | } |
| 692 | 690 | finder |
| 693 | − .update(&mut cx, |_, cx| cx.emit(Manager::Dismiss)) | |
| 691 | + .update(&mut cx, |_, cx| cx.emit(DismissEvent::Dismiss)) | |
| 694 | 692 | .ok()?; |
| 695 | 693 | |
| 696 | 694 | Some(()) |
| @@ -702,7 +700,7 @@ impl PickerDelegate for FileFinderDelegate { · 5 unmodified lines | ||
| 702 | 700 | |
| 703 | 701 | fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) { |
| 704 | 702 | self.file_finder |
| 705 | − .update(cx, |_, cx| cx.emit(Manager::Dismiss)) | |
| 703 | + .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) | |
| 706 | 704 | .log_err(); |
| 707 | 705 | } |
| 708 | 706 | |
| @@ -711,30 +709,22 @@ impl PickerDelegate for FileFinderDelegate { · 2 unmodified lines | ||
| 711 | 709 | ix: usize, |
| 712 | 710 | selected: bool, |
| 713 | 711 | cx: &mut ViewContext<Picker<Self>>, |
| 714 | − ) -> Self::ListItem { | |
| 712 | + ) -> Option<Self::ListItem> { | |
| 715 | 713 | let path_match = self |
| 716 | 714 | .matches |
| 717 | 715 | .get(ix) |
| 718 | 716 | .expect("Invalid matches state: no element for index {ix}"); |
| 719 | − let theme = cx.theme(); | |
| 720 | − let colors = theme.colors(); | |
| 721 | 717 | |
| 722 | 718 | let (file_name, file_name_positions, full_path, full_path_positions) = |
| 723 | 719 | self.labels_for_match(path_match, cx, ix); |
| 724 | 720 | |
| 725 | − div() | |
| 726 | − .px_1() | |
| 727 | − .text_color(colors.text) | |
| 728 | − .text_ui() | |
| 729 | − .bg(colors.ghost_element_background) | |
| 730 | − .rounded_md() | |
| 731 | − .when(selected, |this| this.bg(colors.ghost_element_selected)) | |
| 732 | − .hover(|this| this.bg(colors.ghost_element_hover)) | |
| 733 | − .child( | |
| 721 | + Some( | |
| 722 | + ListItem::new(ix).selected(selected).child( | |
| 734 | 723 | v_stack() |
| 735 | 724 | .child(HighlightedLabel::new(file_name, file_name_positions)) |
| 736 | 725 | .child(HighlightedLabel::new(full_path, full_path_positions)), |
| 737 | − ) | |
| 726 | + ), | |
| 727 | + ) | |
| 738 | 728 | } |
| 739 | 729 | } |
| 740 | 730 | |
| @@ -6,6 +6,8 @@ use gpui::BackgroundExecutor; · 5 unmodified lines | ||
| 6 | 6 | use std::{ |
| 7 | 7 | borrow::Cow, |
| 8 | 8 | cmp::{self, Ordering}, |
| 9 | + iter, | |
| 10 | + ops::Range, | |
| 9 | 11 | sync::atomic::AtomicBool, |
| 10 | 12 | }; |
| 11 | 13 | |
| @@ -54,6 +56,32 @@ pub struct StringMatch { · 42 unmodified lines | ||
| 54 | 56 | pub string: String, |
| 55 | 57 | } |
| 56 | 58 | |
| 59 | +impl StringMatch { | |
| 60 | + pub fn ranges<'a>(&'a self) -> impl 'a + Iterator<Item = Range<usize>> { | |
| 61 | + let mut positions = self.positions.iter().peekable(); | |
| 62 | + iter::from_fn(move || { | |
| 63 | + while let Some(start) = positions.next().copied() { | |
| 64 | + let mut end = start + self.char_len_at_index(start); | |
| 65 | + while let Some(next_start) = positions.peek() { | |
| 66 | + if end == **next_start { | |
| 67 | + end += self.char_len_at_index(end); | |
| 68 | + positions.next(); | |
| 69 | + } else { | |
| 70 | + break; | |
| 71 | + } | |
| 72 | + } | |
| 73 | + | |
| 74 | + return Some(start..end); | |
| 75 | + } | |
| 76 | + None | |
| 77 | + }) | |
| 78 | + } | |
| 79 | + | |
| 80 | + fn char_len_at_index(&self, ix: usize) -> usize { | |
| 81 | + self.string[ix..].chars().next().unwrap().len_utf8() | |
| 82 | + } | |
| 83 | +} | |
| 84 | + | |
| 57 | 85 | impl PartialEq for StringMatch { |
| 58 | 86 | fn eq(&self, other: &Self) -> bool { |
| 59 | 87 | self.cmp(other).is_eq() |
| @@ -1,14 +1,13 @@ | ||
| 1 | 1 | use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; |
| 2 | 2 | use gpui::{ |
| 3 | − actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager, | |
| 4 | − ParentComponent, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, | |
| 3 | + actions, div, prelude::*, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, | |
| 4 | + FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, | |
| 5 | 5 | WindowContext, |
| 6 | 6 | }; |
| 7 | 7 | use text::{Bias, Point}; |
| 8 | 8 | use theme::ActiveTheme; |
| 9 | −use ui::{h_stack, v_stack, Label, StyledExt, TextColor}; | |
| 9 | +use ui::{h_stack, v_stack, Color, Label, StyledExt}; | |
| 10 | 10 | use util::paths::FILE_ROW_COLUMN_DELIMITER; |
| 11 | −use workspace::Workspace; | |
| 12 | 11 | |
| 13 | 12 | actions!(Toggle); |
| 14 | 13 | |
| @@ -26,22 +25,24 @@ pub struct GoToLine { · 11 unmodified lines | ||
| 26 | 25 | |
| 27 | 26 | impl FocusableView for GoToLine { |
| 28 | 27 | fn focus_handle(&self, cx: &AppContext) -> FocusHandle { |
| 29 | − self.active_editor.focus_handle(cx) | |
| 28 | + self.line_editor.focus_handle(cx) | |
| 30 | 29 | } |
| 31 | 30 | } |
| 32 | −impl EventEmitter<Manager> for GoToLine {} | |
| 31 | +impl EventEmitter<DismissEvent> for GoToLine {} | |
| 33 | 32 | |
| 34 | 33 | impl GoToLine { |
| 35 | − fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) { | |
| 36 | − workspace.register_action(|workspace, _: &Toggle, cx| { | |
| 37 | − let Some(editor) = workspace | |
| 38 | − .active_item(cx) | |
| 39 | − .and_then(|active_item| active_item.downcast::<Editor>()) | |
| 40 | − else { | |
| 34 | + fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) { | |
| 35 | + let handle = cx.view().downgrade(); | |
| 36 | + editor.register_action(move |_: &Toggle, cx| { | |
| 37 | + let Some(editor) = handle.upgrade() else { | |
| 41 | 38 | return; |
| 42 | 39 | }; |
| 43 | − | |
| 44 | − workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx)); | |
| 40 | + let Some(workspace) = editor.read(cx).workspace() else { | |
| 41 | + return; | |
| 42 | + }; | |
| 43 | + workspace.update(cx, |workspace, cx| { | |
| 44 | + workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx)); | |
| 45 | + }) | |
| 45 | 46 | }); |
| 46 | 47 | } |
| 47 | 48 | |
| @@ -89,7 +90,7 @@ impl GoToLine { · 41 unmodified lines | ||
| 89 | 90 | ) { |
| 90 | 91 | match event { |
| 91 | 92 | // todo!() this isn't working... |
| 92 | − editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss), | |
| 93 | + editor::EditorEvent::Blurred => cx.emit(DismissEvent::Dismiss), | |
| 93 | 94 | editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx), |
| 94 | 95 | _ => {} |
| 95 | 96 | } |
| @@ -124,7 +125,7 @@ impl GoToLine { · 28 unmodified lines | ||
| 124 | 125 | } |
| 125 | 126 | |
| 126 | 127 | fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) { |
| 127 | − cx.emit(Manager::Dismiss); | |
| 128 | + cx.emit(DismissEvent::Dismiss); | |
| 128 | 129 | } |
| 129 | 130 | |
| 130 | 131 | fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) { |
| @@ -141,19 +142,19 @@ impl GoToLine { · 10 unmodified lines | ||
| 141 | 142 | self.prev_scroll_position.take(); |
| 142 | 143 | } |
| 143 | 144 | |
| 144 | − cx.emit(Manager::Dismiss); | |
| 145 | + cx.emit(DismissEvent::Dismiss); | |
| 145 | 146 | } |
| 146 | 147 | } |
| 147 | 148 | |
| 148 | 149 | impl Render for GoToLine { |
| 149 | − type Element = Div<Self>; | |
| 150 | + type Element = Div; | |
| 150 | 151 | |
| 151 | 152 | fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { |
| 152 | 153 | div() |
| 153 | 154 | .elevation_2(cx) |
| 154 | 155 | .key_context("GoToLine") |
| 155 | − .on_action(Self::cancel) | |
| 156 | − .on_action(Self::confirm) | |
| 156 | + .on_action(cx.listener(Self::cancel)) | |
| 157 | + .on_action(cx.listener(Self::confirm)) | |
| 157 | 158 | .w_96() |
| 158 | 159 | .child( |
| 159 | 160 | v_stack() |
| @@ -177,7 +178,7 @@ impl Render for GoToLine { · 17 unmodified lines | ||
| 177 | 178 | .justify_between() |
| 178 | 179 | .px_2() |
| 179 | 180 | .py_1() |
| 180 | − .child(Label::new(self.current_text.clone()).color(TextColor::Muted)), | |
| 181 | + .child(Label::new(self.current_text.clone()).color(Color::Muted)), | |
| 181 | 182 | ), |
| 182 | 183 | ) |
| 183 | 184 | } |
| @@ -10,11 +10,12 @@ pub use entity_map::*; · 9 unmodified lines | ||
| 10 | 10 | pub use model_context::*; |
| 11 | 11 | use refineable::Refineable; |
| 12 | 12 | use smallvec::SmallVec; |
| 13 | +use smol::future::FutureExt; | |
| 13 | 14 | #[cfg(any(test, feature = "test-support"))] |
| 14 | 15 | pub use test_context::*; |
| 15 | 16 | |
| 16 | 17 | use crate::{ |
| 17 | − current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView, | |
| 18 | + current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView, | |
| 18 | 19 | AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, |
| 19 | 20 | DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, |
| 20 | 21 | ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform, |
| @@ -28,7 +29,7 @@ use futures::{channel::oneshot, future::LocalBoxFuture, Future}; · 7 unmodified lines | ||
| 28 | 29 | use parking_lot::Mutex; |
| 29 | 30 | use slotmap::SlotMap; |
| 30 | 31 | use std::{ |
| 31 | − any::{type_name, Any, TypeId}, | |
| 32 | + any::{type_name, TypeId}, | |
| 32 | 33 | cell::{Ref, RefCell, RefMut}, |
| 33 | 34 | marker::PhantomData, |
| 34 | 35 | mem, |
| @@ -194,7 +195,7 @@ pub struct AppContext { · 159 unmodified lines | ||
| 194 | 195 | asset_source: Arc<dyn AssetSource>, |
| 195 | 196 | pub(crate) image_cache: ImageCache, |
| 196 | 197 | pub(crate) text_style_stack: Vec<TextStyleRefinement>, |
| 197 | − pub(crate) globals_by_type: HashMap<TypeId, AnyBox>, | |
| 198 | + pub(crate) globals_by_type: HashMap<TypeId, Box<dyn Any>>, | |
| 198 | 199 | pub(crate) entities: EntityMap, |
| 199 | 200 | pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>, |
| 200 | 201 | pub(crate) windows: SlotMap<WindowId, Option<Window>>, |
| @@ -424,7 +425,7 @@ impl AppContext { · 223 unmodified lines | ||
| 424 | 425 | /// Opens a new window with the given option and the root view returned by the given function. |
| 425 | 426 | /// The function is invoked with a `WindowContext`, which can be used to interact with window-specific |
| 426 | 427 | /// functionality. |
| 427 | − pub fn open_window<V: Render>( | |
| 428 | + pub fn open_window<V: 'static + Render>( | |
| 428 | 429 | &mut self, |
| 429 | 430 | options: crate::WindowOptions, |
| 430 | 431 | build_root_view: impl FnOnce(&mut WindowContext) -> View<V>, |
| @@ -579,7 +580,7 @@ impl AppContext { · 148 unmodified lines | ||
| 579 | 580 | .windows |
| 580 | 581 | .iter() |
| 581 | 582 | .filter_map(|(_, window)| { |
| 582 | − let window = window.as_ref().unwrap(); | |
| 583 | + let window = window.as_ref()?; | |
| 583 | 584 | if window.dirty { |
| 584 | 585 | Some(window.handle.clone()) |
| 585 | 586 | } else { |
| @@ -983,6 +984,22 @@ impl AppContext { · 397 unmodified lines | ||
| 983 | 984 | pub fn all_action_names(&self) -> &[SharedString] { |
| 984 | 985 | self.actions.all_action_names() |
| 985 | 986 | } |
| 987 | + | |
| 988 | + pub fn on_app_quit<Fut>( | |
| 989 | + &mut self, | |
| 990 | + mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static, | |
| 991 | + ) -> Subscription | |
| 992 | + where | |
| 993 | + Fut: 'static + Future<Output = ()>, | |
| 994 | + { | |
| 995 | + self.quit_observers.insert( | |
| 996 | + (), | |
| 997 | + Box::new(move |cx| { | |
| 998 | + let future = on_quit(cx); | |
| 999 | + async move { future.await }.boxed_local() | |
| 1000 | + }), | |
| 1001 | + ) | |
| 1002 | + } | |
| 986 | 1003 | } |
| 987 | 1004 | |
| 988 | 1005 | impl Context for AppContext { |
| @@ -1032,7 +1049,9 @@ impl Context for AppContext { · 43 unmodified lines | ||
| 1032 | 1049 | let root_view = window.root_view.clone().unwrap(); |
| 1033 | 1050 | let result = update(root_view, &mut WindowContext::new(cx, &mut window)); |
| 1034 | 1051 | |
| 1035 | − if !window.removed { | |
| 1052 | + if window.removed { | |
| 1053 | + cx.windows.remove(handle.id); | |
| 1054 | + } else { | |
| 1036 | 1055 | cx.windows |
| 1037 | 1056 | .get_mut(handle.id) |
| 1038 | 1057 | .ok_or_else(|| anyhow!("window not found"))? |
| @@ -1104,12 +1123,12 @@ pub(crate) enum Effect { · 65 unmodified lines | ||
| 1104 | 1123 | |
| 1105 | 1124 | /// Wraps a global variable value during `update_global` while the value has been moved to the stack. |
| 1106 | 1125 | pub(crate) struct GlobalLease<G: 'static> { |
| 1107 | − global: AnyBox, | |
| 1126 | + global: Box<dyn Any>, | |
| 1108 | 1127 | global_type: PhantomData<G>, |
| 1109 | 1128 | } |
| 1110 | 1129 | |
| 1111 | 1130 | impl<G: 'static> GlobalLease<G> { |
| 1112 | − fn new(global: AnyBox) -> Self { | |
| 1131 | + fn new(global: Box<dyn Any>) -> Self { | |
| 1113 | 1132 | GlobalLease { |
| 1114 | 1133 | global, |
| 1115 | 1134 | global_type: PhantomData, |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView, | |
| 3 | − ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext, | |
| 4 | − VisualContext, WindowContext, WindowHandle, | |
| 2 | + AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent, | |
| 3 | + FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View, | |
| 4 | + ViewContext, VisualContext, WindowContext, WindowHandle, | |
| 5 | 5 | }; |
| 6 | 6 | use anyhow::{anyhow, Context as _}; |
| 7 | 7 | use derive_more::{Deref, DerefMut}; |
| @@ -115,7 +115,7 @@ impl AsyncAppContext { · 107 unmodified lines | ||
| 115 | 115 | build_root_view: impl FnOnce(&mut WindowContext) -> View<V>, |
| 116 | 116 | ) -> Result<WindowHandle<V>> |
| 117 | 117 | where |
| 118 | − V: Render, | |
| 118 | + V: 'static + Render, | |
| 119 | 119 | { |
| 120 | 120 | let app = self |
| 121 | 121 | .app |
| @@ -306,7 +306,7 @@ impl VisualContext for AsyncWindowContext { · 184 unmodified lines | ||
| 306 | 306 | build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, |
| 307 | 307 | ) -> Self::Result<View<V>> |
| 308 | 308 | where |
| 309 | − V: Render, | |
| 309 | + V: 'static + Render, | |
| 310 | 310 | { |
| 311 | 311 | self.window |
| 312 | 312 | .update(self, |_, cx| cx.replace_root_view(build_view)) |
| @@ -326,7 +326,7 @@ impl VisualContext for AsyncWindowContext { · 13 unmodified lines | ||
| 326 | 326 | V: crate::ManagedView, |
| 327 | 327 | { |
| 328 | 328 | self.window.update(self, |_, cx| { |
| 329 | − view.update(cx, |_, cx| cx.emit(Manager::Dismiss)) | |
| 329 | + view.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) | |
| 330 | 330 | }) |
| 331 | 331 | } |
| 332 | 332 | } |
| @@ -1,10 +1,10 @@ | ||
| 1 | −use crate::{private::Sealed, AnyBox, AppContext, Context, Entity, ModelContext}; | |
| 1 | +use crate::{private::Sealed, AppContext, Context, Entity, ModelContext}; | |
| 2 | 2 | use anyhow::{anyhow, Result}; |
| 3 | 3 | use derive_more::{Deref, DerefMut}; |
| 4 | 4 | use parking_lot::{RwLock, RwLockUpgradableReadGuard}; |
| 5 | 5 | use slotmap::{SecondaryMap, SlotMap}; |
| 6 | 6 | use std::{ |
| 7 | − any::{type_name, TypeId}, | |
| 7 | + any::{type_name, Any, TypeId}, | |
| 8 | 8 | fmt::{self, Display}, |
| 9 | 9 | hash::{Hash, Hasher}, |
| 10 | 10 | marker::PhantomData, |
| @@ -31,7 +31,7 @@ impl Display for EntityId { · 20 unmodified lines | ||
| 31 | 31 | } |
| 32 | 32 | |
| 33 | 33 | pub(crate) struct EntityMap { |
| 34 | − entities: SecondaryMap<EntityId, AnyBox>, | |
| 34 | + entities: SecondaryMap<EntityId, Box<dyn Any>>, | |
| 35 | 35 | ref_counts: Arc<RwLock<EntityRefCounts>>, |
| 36 | 36 | } |
| 37 | 37 | |
| @@ -102,7 +102,7 @@ impl EntityMap { · 64 unmodified lines | ||
| 102 | 102 | ); |
| 103 | 103 | } |
| 104 | 104 | |
| 105 | − pub fn take_dropped(&mut self) -> Vec<(EntityId, AnyBox)> { | |
| 105 | + pub fn take_dropped(&mut self) -> Vec<(EntityId, Box<dyn Any>)> { | |
| 106 | 106 | let mut ref_counts = self.ref_counts.write(); |
| 107 | 107 | let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids); |
| 108 | 108 | |
| @@ -122,7 +122,7 @@ impl EntityMap { · 13 unmodified lines | ||
| 122 | 122 | } |
| 123 | 123 | |
| 124 | 124 | pub struct Lease<'a, T> { |
| 125 | − entity: Option<AnyBox>, | |
| 125 | + entity: Option<Box<dyn Any>>, | |
| 126 | 126 | pub model: &'a Model<T>, |
| 127 | 127 | entity_type: PhantomData<T>, |
| 128 | 128 | } |
| @@ -1,8 +1,9 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | 2 | div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, |
| 3 | − BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, | |
| 4 | − Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow, | |
| 5 | − View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, | |
| 3 | + BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent, | |
| 4 | + KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, | |
| 5 | + TestPlatform, TestWindow, View, ViewContext, VisualContext, WindowContext, WindowHandle, | |
| 6 | + WindowOptions, | |
| 6 | 7 | }; |
| 7 | 8 | use anyhow::{anyhow, bail}; |
| 8 | 9 | use futures::{Stream, StreamExt}; |
| @@ -126,7 +127,7 @@ impl TestAppContext { · 117 unmodified lines | ||
| 126 | 127 | pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V> |
| 127 | 128 | where |
| 128 | 129 | F: FnOnce(&mut ViewContext<V>) -> V, |
| 129 | − V: Render, | |
| 130 | + V: 'static + Render, | |
| 130 | 131 | { |
| 131 | 132 | let mut cx = self.app.borrow_mut(); |
| 132 | 133 | cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)) |
| @@ -143,7 +144,7 @@ impl TestAppContext { · 10 unmodified lines | ||
| 143 | 144 | pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext) |
| 144 | 145 | where |
| 145 | 146 | F: FnOnce(&mut ViewContext<V>) -> V, |
| 146 | − V: Render, | |
| 147 | + V: 'static + Render, | |
| 147 | 148 | { |
| 148 | 149 | let mut cx = self.app.borrow_mut(); |
| 149 | 150 | let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)); |
| @@ -296,21 +297,19 @@ impl TestAppContext { · 146 unmodified lines | ||
| 296 | 297 | .unwrap() |
| 297 | 298 | } |
| 298 | 299 | |
| 299 | − pub fn notifications<T: 'static>(&mut self, entity: &Model<T>) -> impl Stream<Item = ()> { | |
| 300 | + pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> { | |
| 300 | 301 | let (tx, rx) = futures::channel::mpsc::unbounded(); |
| 301 | − | |
| 302 | − entity.update(self, move |_, cx: &mut ModelContext<T>| { | |
| 302 | + self.update(|cx| { | |
| 303 | 303 | cx.observe(entity, { |
| 304 | 304 | let tx = tx.clone(); |
| 305 | − move |_, _, _| { | |
| 305 | + move |_, _| { | |
| 306 | 306 | let _ = tx.unbounded_send(()); |
| 307 | 307 | } |
| 308 | 308 | }) |
| 309 | 309 | .detach(); |
| 310 | − | |
| 311 | − cx.on_release(move |_, _| tx.close_channel()).detach(); | |
| 310 | + cx.observe_release(entity, move |_, _| tx.close_channel()) | |
| 311 | + .detach() | |
| 312 | 312 | }); |
| 313 | − | |
| 314 | 313 | rx |
| 315 | 314 | } |
| 316 | 315 | |
| @@ -591,7 +590,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { · 274 unmodified lines | ||
| 591 | 590 | build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, |
| 592 | 591 | ) -> Self::Result<View<V>> |
| 593 | 592 | where |
| 594 | − V: Render, | |
| 593 | + V: 'static + Render, | |
| 595 | 594 | { |
| 596 | 595 | self.window |
| 597 | 596 | .update(self.cx, |_, cx| cx.replace_root_view(build_view)) |
| @@ -612,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { · 14 unmodified lines | ||
| 612 | 611 | { |
| 613 | 612 | self.window |
| 614 | 613 | .update(self.cx, |_, cx| { |
| 615 | − view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss)) | |
| 614 | + view.update(cx, |_, cx| cx.emit(crate::DismissEvent::Dismiss)) | |
| 616 | 615 | }) |
| 617 | 616 | .unwrap() |
| 618 | 617 | } |
| @@ -631,7 +630,7 @@ impl AnyWindowHandle { · 12 unmodified lines | ||
| 631 | 630 | pub struct EmptyView {} |
| 632 | 631 | |
| 633 | 632 | impl Render for EmptyView { |
| 634 | − type Element = Div<Self>; | |
| 633 | + type Element = Div; | |
| 635 | 634 | |
| 636 | 635 | fn render(&mut self, _cx: &mut crate::ViewContext<Self>) -> Self::Element { |
| 637 | 636 | div() |
| @@ -1,236 +1,328 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | 2 | AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext, |
| 3 | + WindowContext, | |
| 3 | 4 | }; |
| 4 | 5 | use derive_more::{Deref, DerefMut}; |
| 5 | 6 | pub(crate) use smallvec::SmallVec; |
| 6 | −use std::{any::Any, fmt::Debug, mem}; | |
| 7 | +use std::{any::Any, fmt::Debug}; | |
| 7 | 8 | |
| 8 | −pub trait Element<V: 'static> { | |
| 9 | − type ElementState: 'static; | |
| 9 | +pub trait Render: 'static + Sized { | |
| 10 | + type Element: Element + 'static; | |
| 11 | + | |
| 12 | + fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element; | |
| 13 | +} | |
| 14 | + | |
| 15 | +pub trait IntoElement: Sized { | |
| 16 | + type Element: Element + 'static; | |
| 10 | 17 | |
| 11 | 18 | fn element_id(&self) -> Option<ElementId>; |
| 12 | 19 | |
| 13 | − fn layout( | |
| 14 | − &mut self, | |
| 15 | − view_state: &mut V, | |
| 16 | − element_state: Option<Self::ElementState>, | |
| 17 | − cx: &mut ViewContext<V>, | |
| 18 | − ) -> (LayoutId, Self::ElementState); | |
| 20 | + fn into_element(self) -> Self::Element; | |
| 19 | 21 | |
| 20 | − fn paint( | |
| 21 | − &mut self, | |
| 22 | − bounds: Bounds<Pixels>, | |
| 23 | − view_state: &mut V, | |
| 24 | − element_state: &mut Self::ElementState, | |
| 25 | − cx: &mut ViewContext<V>, | |
| 26 | − ); | |
| 22 | + fn into_any_element(self) -> AnyElement { | |
| 23 | + self.into_element().into_any() | |
| 24 | + } | |
| 27 | 25 | |
| 28 | 26 | fn draw<T, R>( |
| 29 | 27 | self, |
| 30 | 28 | origin: Point<Pixels>, |
| 31 | 29 | available_space: Size<T>, |
| 32 | − view_state: &mut V, | |
| 33 | − cx: &mut ViewContext<V>, | |
| 34 | − f: impl FnOnce(&Self::ElementState, &mut ViewContext<V>) -> R, | |
| 30 | + cx: &mut WindowContext, | |
| 31 | + f: impl FnOnce(&mut <Self::Element as Element>::State, &mut WindowContext) -> R, | |
| 35 | 32 | ) -> R |
| 36 | 33 | where |
| 37 | − Self: Sized, | |
| 38 | 34 | T: Clone + Default + Debug + Into<AvailableSpace>, |
| 39 | 35 | { |
| 40 | − let mut element = RenderedElement { | |
| 41 | − element: self, | |
| 42 | − phase: ElementRenderPhase::Start, | |
| 36 | + let element = self.into_element(); | |
| 37 | + let element_id = element.element_id(); | |
| 38 | + let element = DrawableElement { | |
| 39 | + element: Some(element), | |
| 40 | + phase: ElementDrawPhase::Start, | |
| 43 | 41 | }; |
| 44 | − element.draw(origin, available_space.map(Into::into), view_state, cx); | |
| 45 | − if let ElementRenderPhase::Painted { frame_state } = &element.phase { | |
| 46 | − if let Some(frame_state) = frame_state.as_ref() { | |
| 47 | − f(&frame_state, cx) | |
| 42 | + | |
| 43 | + let frame_state = | |
| 44 | + DrawableElement::draw(element, origin, available_space.map(Into::into), cx); | |
| 45 | + | |
| 46 | + if let Some(mut frame_state) = frame_state { | |
| 47 | + f(&mut frame_state, cx) | |
| 48 | + } else { | |
| 49 | + cx.with_element_state(element_id.unwrap(), |element_state, cx| { | |
| 50 | + let mut element_state = element_state.unwrap(); | |
| 51 | + let result = f(&mut element_state, cx); | |
| 52 | + (result, element_state) | |
| 53 | + }) | |
| 54 | + } | |
| 55 | + } | |
| 56 | + | |
| 57 | + fn map<U>(self, f: impl FnOnce(Self) -> U) -> U | |
| 58 | + where | |
| 59 | + Self: Sized, | |
| 60 | + U: IntoElement, | |
| 61 | + { | |
| 62 | + f(self) | |
| 63 | + } | |
| 64 | + | |
| 65 | + fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self | |
| 66 | + where | |
| 67 | + Self: Sized, | |
| 68 | + { | |
| 69 | + self.map(|this| if condition { then(this) } else { this }) | |
| 70 | + } | |
| 71 | + | |
| 72 | + fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self | |
| 73 | + where | |
| 74 | + Self: Sized, | |
| 75 | + { | |
| 76 | + self.map(|this| { | |
| 77 | + if let Some(value) = option { | |
| 78 | + then(this, value) | |
| 48 | 79 | } else { |
| 49 | − let element_id = element | |
| 50 | − .element | |
| 51 | − .element_id() | |
| 52 | − .expect("we either have some frame_state or some element_id"); | |
| 53 | − cx.with_element_state(element_id, |element_state, cx| { | |
| 54 | − let element_state = element_state.unwrap(); | |
| 55 | − let result = f(&element_state, cx); | |
| 56 | − (result, element_state) | |
| 57 | − }) | |
| 80 | + this | |
| 58 | 81 | } |
| 59 | − } else { | |
| 60 | − unreachable!() | |
| 82 | + }) | |
| 83 | + } | |
| 84 | +} | |
| 85 | + | |
| 86 | +pub trait Element: 'static + IntoElement { | |
| 87 | + type State: 'static; | |
| 88 | + | |
| 89 | + fn layout( | |
| 90 | + &mut self, | |
| 91 | + state: Option<Self::State>, | |
| 92 | + cx: &mut WindowContext, | |
| 93 | + ) -> (LayoutId, Self::State); | |
| 94 | + | |
| 95 | + fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext); | |
| 96 | + | |
| 97 | + fn into_any(self) -> AnyElement { | |
| 98 | + AnyElement::new(self) | |
| 99 | + } | |
| 100 | +} | |
| 101 | + | |
| 102 | +pub trait RenderOnce: 'static { | |
| 103 | + type Rendered: IntoElement; | |
| 104 | + | |
| 105 | + fn render(self, cx: &mut WindowContext) -> Self::Rendered; | |
| 106 | +} | |
| 107 | + | |
| 108 | +pub struct Component<C> { | |
| 109 | + component: Option<C>, | |
| 110 | +} | |
| 111 | + | |
| 112 | +pub struct CompositeElementState<C: RenderOnce> { | |
| 113 | + rendered_element: Option<<C::Rendered as IntoElement>::Element>, | |
| 114 | + rendered_element_state: <<C::Rendered as IntoElement>::Element as Element>::State, | |
| 115 | +} | |
| 116 | + | |
| 117 | +impl<C> Component<C> { | |
| 118 | + pub fn new(component: C) -> Self { | |
| 119 | + Component { | |
| 120 | + component: Some(component), | |
| 61 | 121 | } |
| 62 | 122 | } |
| 63 | 123 | } |
| 64 | 124 | |
| 125 | +impl<C: RenderOnce> Element for Component<C> { | |
| 126 | + type State = CompositeElementState<C>; | |
| 127 | + | |
| 128 | + fn layout( | |
| 129 | + &mut self, | |
| 130 | + state: Option<Self::State>, | |
| 131 | + cx: &mut WindowContext, | |
| 132 | + ) -> (LayoutId, Self::State) { | |
| 133 | + let mut element = self.component.take().unwrap().render(cx).into_element(); | |
| 134 | + let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx); | |
| 135 | + let state = CompositeElementState { | |
| 136 | + rendered_element: Some(element), | |
| 137 | + rendered_element_state: state, | |
| 138 | + }; | |
| 139 | + (layout_id, state) | |
| 140 | + } | |
| 141 | + | |
| 142 | + fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) { | |
| 143 | + state | |
| 144 | + .rendered_element | |
| 145 | + .take() | |
| 146 | + .unwrap() | |
| 147 | + .paint(bounds, &mut state.rendered_element_state, cx); | |
| 148 | + } | |
| 149 | +} | |
| 150 | + | |
| 151 | +impl<C: RenderOnce> IntoElement for Component<C> { | |
| 152 | + type Element = Self; | |
| 153 | + | |
| 154 | + fn element_id(&self) -> Option<ElementId> { | |
| 155 | + None | |
| 156 | + } | |
| 157 | + | |
| 158 | + fn into_element(self) -> Self::Element { | |
| 159 | + self | |
| 160 | + } | |
| 161 | +} | |
| 162 | + | |
| 65 | 163 | #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] |
| 66 | 164 | pub struct GlobalElementId(SmallVec<[ElementId; 32]>); |
| 67 | 165 | |
| 68 | −pub trait ParentComponent<V: 'static> { | |
| 69 | − fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>; | |
| 166 | +pub trait ParentElement { | |
| 167 | + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; | |
| 70 | 168 | |
| 71 | − fn child(mut self, child: impl Component<V>) -> Self | |
| 169 | + fn child(mut self, child: impl IntoElement) -> Self | |
| 72 | 170 | where |
| 73 | 171 | Self: Sized, |
| 74 | 172 | { |
| 75 | − self.children_mut().push(child.render()); | |
| 173 | + self.children_mut().push(child.into_element().into_any()); | |
| 76 | 174 | self |
| 77 | 175 | } |
| 78 | 176 | |
| 79 | − fn children(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self | |
| 177 | + fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self | |
| 80 | 178 | where |
| 81 | 179 | Self: Sized, |
| 82 | 180 | { |
| 83 | 181 | self.children_mut() |
| 84 | − .extend(iter.into_iter().map(|item| item.render())); | |
| 182 | + .extend(children.into_iter().map(|child| child.into_any_element())); | |
| 85 | 183 | self |
| 86 | 184 | } |
| 87 | 185 | } |
| 88 | 186 | |
| 89 | −trait ElementObject<V> { | |
| 187 | +trait ElementObject { | |
| 90 | 188 | fn element_id(&self) -> Option<ElementId>; |
| 91 | − fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId; | |
| 92 | − fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>); | |
| 189 | + | |
| 190 | + fn layout(&mut self, cx: &mut WindowContext) -> LayoutId; | |
| 191 | + | |
| 192 | + fn paint(&mut self, cx: &mut WindowContext); | |
| 193 | + | |
| 93 | 194 | fn measure( |
| 94 | 195 | &mut self, |
| 95 | 196 | available_space: Size<AvailableSpace>, |
| 96 | − view_state: &mut V, | |
| 97 | − cx: &mut ViewContext<V>, | |
| 197 | + cx: &mut WindowContext, | |
| 98 | 198 | ) -> Size<Pixels>; |
| 199 | + | |
| 99 | 200 | fn draw( |
| 100 | 201 | &mut self, |
| 101 | 202 | origin: Point<Pixels>, |
| 102 | 203 | available_space: Size<AvailableSpace>, |
| 103 | − view_state: &mut V, | |
| 104 | − cx: &mut ViewContext<V>, | |
| 204 | + cx: &mut WindowContext, | |
| 105 | 205 | ); |
| 106 | 206 | } |
| 107 | 207 | |
| 108 | −struct RenderedElement<V: 'static, E: Element<V>> { | |
| 109 | − element: E, | |
| 110 | − phase: ElementRenderPhase<E::ElementState>, | |
| 208 | +pub struct DrawableElement<E: Element> { | |
| 209 | + element: Option<E>, | |
| 210 | + phase: ElementDrawPhase<E::State>, | |
| 111 | 211 | } |
| 112 | 212 | |
| 113 | 213 | #[derive(Default)] |
| 114 | −enum ElementRenderPhase<V> { | |
| 214 | +enum ElementDrawPhase<S> { | |
| 115 | 215 | #[default] |
| 116 | 216 | Start, |
| 117 | 217 | LayoutRequested { |
| 118 | 218 | layout_id: LayoutId, |
| 119 | − frame_state: Option<V>, | |
| 219 | + frame_state: Option<S>, | |
| 120 | 220 | }, |
| 121 | 221 | LayoutComputed { |
| 122 | 222 | layout_id: LayoutId, |
| 123 | 223 | available_space: Size<AvailableSpace>, |
| 124 | − frame_state: Option<V>, | |
| 125 | − }, | |
| 126 | − Painted { | |
| 127 | − frame_state: Option<V>, | |
| 224 | + frame_state: Option<S>, | |
| 128 | 225 | }, |
| 129 | 226 | } |
| 130 | 227 | |
| 131 | −/// Internal struct that wraps an element to store Layout and ElementState after the element is rendered. | |
| 132 | −/// It's allocated as a trait object to erase the element type and wrapped in AnyElement<E::State> for | |
| 133 | −/// improved usability. | |
| 134 | −impl<V, E: Element<V>> RenderedElement<V, E> { | |
| 228 | +/// A wrapper around an implementer of [Element] that allows it to be drawn in a window. | |
| 229 | +impl<E: Element> DrawableElement<E> { | |
| 135 | 230 | fn new(element: E) -> Self { |
| 136 | − RenderedElement { | |
| 137 | − element, | |
| 138 | − phase: ElementRenderPhase::Start, | |
| 231 | + DrawableElement { | |
| 232 | + element: Some(element), | |
| 233 | + phase: ElementDrawPhase::Start, | |
| 139 | 234 | } |
| 140 | 235 | } |
| 141 | −} | |
| 142 | 236 | |
| 143 | −impl<V, E> ElementObject<V> for RenderedElement<V, E> | |
| 144 | −where | |
| 145 | − E: Element<V>, | |
| 146 | − E::ElementState: 'static, | |
| 147 | −{ | |
| 148 | 237 | fn element_id(&self) -> Option<ElementId> { |
| 149 | − self.element.element_id() | |
| 238 | + self.element.as_ref()?.element_id() | |
| 150 | 239 | } |
| 151 | 240 | |
| 152 | − fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId { | |
| 153 | − let (layout_id, frame_state) = match mem::take(&mut self.phase) { | |
| 154 | − ElementRenderPhase::Start => { | |
| 155 | − if let Some(id) = self.element.element_id() { | |
| 156 | − let layout_id = cx.with_element_state(id, |element_state, cx| { | |
| 157 | − self.element.layout(state, element_state, cx) | |
| 158 | − }); | |
| 159 | − (layout_id, None) | |
| 160 | − } else { | |
| 161 | − let (layout_id, frame_state) = self.element.layout(state, None, cx); | |
| 162 | − (layout_id, Some(frame_state)) | |
| 163 | − } | |
| 164 | − } | |
| 165 | − ElementRenderPhase::LayoutRequested { .. } | |
| 166 | − | ElementRenderPhase::LayoutComputed { .. } | |
| 167 | − | ElementRenderPhase::Painted { .. } => { | |
| 168 | − panic!("element rendered twice") | |
| 169 | − } | |
| 241 | + fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { | |
| 242 | + let (layout_id, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id() | |
| 243 | + { | |
| 244 | + let layout_id = cx.with_element_state(id, |element_state, cx| { | |
| 245 | + self.element.as_mut().unwrap().layout(element_state, cx) | |
| 246 | + }); | |
| 247 | + (layout_id, None) | |
| 248 | + } else { | |
| 249 | + let (layout_id, frame_state) = self.element.as_mut().unwrap().layout(None, cx); | |
| 250 | + (layout_id, Some(frame_state)) | |
| 170 | 251 | }; |
| 171 | 252 | |
| 172 | − self.phase = ElementRenderPhase::LayoutRequested { | |
| 253 | + self.phase = ElementDrawPhase::LayoutRequested { | |
| 173 | 254 | layout_id, |
| 174 | 255 | frame_state, |
| 175 | 256 | }; |
| 176 | 257 | layout_id |
| 177 | 258 | } |
| 178 | 259 | |
| 179 | − fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) { | |
| 180 | − self.phase = match mem::take(&mut self.phase) { | |
| 181 | − ElementRenderPhase::LayoutRequested { | |
| 260 | + fn paint(mut self, cx: &mut WindowContext) -> Option<E::State> { | |
| 261 | + match self.phase { | |
| 262 | + ElementDrawPhase::LayoutRequested { | |
| 182 | 263 | layout_id, |
| 183 | − mut frame_state, | |
| 264 | + frame_state, | |
| 184 | 265 | } |
| 185 | − | ElementRenderPhase::LayoutComputed { | |
| 266 | + | ElementDrawPhase::LayoutComputed { | |
| 186 | 267 | layout_id, |
| 187 | − mut frame_state, | |
| 268 | + frame_state, | |
| 188 | 269 | .. |
| 189 | 270 | } => { |
| 190 | 271 | let bounds = cx.layout_bounds(layout_id); |
| 191 | − if let Some(id) = self.element.element_id() { | |
| 192 | − cx.with_element_state(id, |element_state, cx| { | |
| 272 | + | |
| 273 | + if let Some(mut frame_state) = frame_state { | |
| 274 | + self.element | |
| 275 | + .take() | |
| 276 | + .unwrap() | |
| 277 | + .paint(bounds, &mut frame_state, cx); | |
| 278 | + Some(frame_state) | |
| 279 | + } else { | |
| 280 | + let element_id = self | |
| 281 | + .element | |
| 282 | + .as_ref() | |
| 283 | + .unwrap() | |
| 284 | + .element_id() | |
| 285 | + .expect("if we don't have frame state, we should have element state"); | |
| 286 | + cx.with_element_state(element_id, |element_state, cx| { | |
| 193 | 287 | let mut element_state = element_state.unwrap(); |
| 194 | 288 | self.element |
| 195 | − .paint(bounds, view_state, &mut element_state, cx); | |
| 289 | + .take() | |
| 290 | + .unwrap() | |
| 291 | + .paint(bounds, &mut element_state, cx); | |
| 196 | 292 | ((), element_state) |
| 197 | 293 | }); |
| 198 | − } else { | |
| 199 | − self.element | |
| 200 | − .paint(bounds, view_state, frame_state.as_mut().unwrap(), cx); | |
| 294 | + None | |
| 201 | 295 | } |
| 202 | − ElementRenderPhase::Painted { frame_state } | |
| 203 | 296 | } |
| 204 | 297 | |
| 205 | 298 | _ => panic!("must call layout before paint"), |
| 206 | − }; | |
| 299 | + } | |
| 207 | 300 | } |
| 208 | 301 | |
| 209 | 302 | fn measure( |
| 210 | 303 | &mut self, |
| 211 | 304 | available_space: Size<AvailableSpace>, |
| 212 | − view_state: &mut V, | |
| 213 | − cx: &mut ViewContext<V>, | |
| 305 | + cx: &mut WindowContext, | |
| 214 | 306 | ) -> Size<Pixels> { |
| 215 | − if matches!(&self.phase, ElementRenderPhase::Start) { | |
| 216 | − self.layout(view_state, cx); | |
| 307 | + if matches!(&self.phase, ElementDrawPhase::Start) { | |
| 308 | + self.layout(cx); | |
| 217 | 309 | } |
| 218 | 310 | |
| 219 | 311 | let layout_id = match &mut self.phase { |
| 220 | − ElementRenderPhase::LayoutRequested { | |
| 312 | + ElementDrawPhase::LayoutRequested { | |
| 221 | 313 | layout_id, |
| 222 | 314 | frame_state, |
| 223 | 315 | } => { |
| 224 | 316 | cx.compute_layout(*layout_id, available_space); |
| 225 | 317 | let layout_id = *layout_id; |
| 226 | − self.phase = ElementRenderPhase::LayoutComputed { | |
| 318 | + self.phase = ElementDrawPhase::LayoutComputed { | |
| 227 | 319 | layout_id, |
| 228 | 320 | available_space, |
| 229 | 321 | frame_state: frame_state.take(), |
| 230 | 322 | }; |
| 231 | 323 | layout_id |
| 232 | 324 | } |
| 233 | − ElementRenderPhase::LayoutComputed { | |
| 325 | + ElementDrawPhase::LayoutComputed { | |
| 234 | 326 | layout_id, |
| 235 | 327 | available_space: prev_available_space, |
| 236 | 328 | .. |
| @@ -248,154 +340,203 @@ where · 11 unmodified lines | ||
| 248 | 340 | } |
| 249 | 341 | |
| 250 | 342 | fn draw( |
| 251 | − &mut self, | |
| 343 | + mut self, | |
| 252 | 344 | origin: Point<Pixels>, |
| 253 | 345 | available_space: Size<AvailableSpace>, |
| 254 | − view_state: &mut V, | |
| 255 | − cx: &mut ViewContext<V>, | |
| 256 | − ) { | |
| 257 | − self.measure(available_space, view_state, cx); | |
| 258 | − cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx)) | |
| 346 | + cx: &mut WindowContext, | |
| 347 | + ) -> Option<E::State> { | |
| 348 | + self.measure(available_space, cx); | |
| 349 | + cx.with_absolute_element_offset(origin, |cx| self.paint(cx)) | |
| 259 | 350 | } |
| 260 | 351 | } |
| 261 | 352 | |
| 262 | −pub struct AnyElement<V>(Box<dyn ElementObject<V>>); | |
| 353 | +// impl<V: 'static, E: Element> Element for DrawableElement<V, E> { | |
| 354 | +// type State = <E::Element as Element>::State; | |
| 263 | 355 | |
| 264 | −impl<V> AnyElement<V> { | |
| 265 | − pub fn new<E>(element: E) -> Self | |
| 266 | − where | |
| 267 | − V: 'static, | |
| 268 | − E: 'static + Element<V>, | |
| 269 | − E::ElementState: Any, | |
| 270 | − { | |
| 271 | − AnyElement(Box::new(RenderedElement::new(element))) | |
| 272 | − } | |
| 356 | +// fn layout( | |
| 357 | +// &mut self, | |
| 358 | +// element_state: Option<Self::State>, | |
| 359 | +// cx: &mut WindowContext, | |
| 360 | +// ) -> (LayoutId, Self::State) { | |
| 273 | 361 | |
| 274 | − pub fn element_id(&self) -> Option<ElementId> { | |
| 275 | − self.0.element_id() | |
| 362 | +// } | |
| 363 | + | |
| 364 | +// fn paint( | |
| 365 | +// self, | |
| 366 | +// bounds: Bounds<Pixels>, | |
| 367 | +// element_state: &mut Self::State, | |
| 368 | +// cx: &mut WindowContext, | |
| 369 | +// ) { | |
| 370 | +// todo!() | |
| 371 | +// } | |
| 372 | +// } | |
| 373 | + | |
| 374 | +// impl<V: 'static, E: 'static + Element> RenderOnce for DrawableElement<V, E> { | |
| 375 | +// type Element = Self; | |
| 376 | + | |
| 377 | +// fn element_id(&self) -> Option<ElementId> { | |
| 378 | +// self.element.as_ref()?.element_id() | |
| 379 | +// } | |
| 380 | + | |
| 381 | +// fn render_once(self) -> Self::Element { | |
| 382 | +// self | |
| 383 | +// } | |
| 384 | +// } | |
| 385 | + | |
| 386 | +impl<E> ElementObject for Option<DrawableElement<E>> | |
| 387 | +where | |
| 388 | + E: Element, | |
| 389 | + E::State: 'static, | |
| 390 | +{ | |
| 391 | + fn element_id(&self) -> Option<ElementId> { | |
| 392 | + self.as_ref().unwrap().element_id() | |
| 276 | 393 | } |
| 277 | 394 | |
| 278 | − pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId { | |
| 279 | − self.0.layout(view_state, cx) | |
| 395 | + fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { | |
| 396 | + DrawableElement::layout(self.as_mut().unwrap(), cx) | |
| 280 | 397 | } |
| 281 | 398 | |
| 282 | − pub fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) { | |
| 283 | − self.0.paint(view_state, cx) | |
| 399 | + fn paint(&mut self, cx: &mut WindowContext) { | |
| 400 | + DrawableElement::paint(self.take().unwrap(), cx); | |
| 284 | 401 | } |
| 285 | 402 | |
| 286 | − /// Initializes this element and performs layout within the given available space to determine its size. | |
| 287 | − pub fn measure( | |
| 403 | + fn measure( | |
| 288 | 404 | &mut self, |
| 289 | 405 | available_space: Size<AvailableSpace>, |
| 290 | − view_state: &mut V, | |
| 291 | − cx: &mut ViewContext<V>, | |
| 406 | + cx: &mut WindowContext, | |
| 292 | 407 | ) -> Size<Pixels> { |
| 293 | − self.0.measure(available_space, view_state, cx) | |
| 408 | + DrawableElement::measure(self.as_mut().unwrap(), available_space, cx) | |
| 294 | 409 | } |
| 295 | 410 | |
| 296 | − /// Initializes this element and performs layout in the available space, then paints it at the given origin. | |
| 297 | − pub fn draw( | |
| 411 | + fn draw( | |
| 298 | 412 | &mut self, |
| 299 | 413 | origin: Point<Pixels>, |
| 300 | 414 | available_space: Size<AvailableSpace>, |
| 301 | − view_state: &mut V, | |
| 302 | − cx: &mut ViewContext<V>, | |
| 415 | + cx: &mut WindowContext, | |
| 303 | 416 | ) { |
| 304 | − self.0.draw(origin, available_space, view_state, cx) | |
| 417 | + DrawableElement::draw(self.take().unwrap(), origin, available_space, cx); | |
| 305 | 418 | } |
| 306 | 419 | } |
| 307 | 420 | |
| 308 | −pub trait Component<V> { | |
| 309 | − fn render(self) -> AnyElement<V>; | |
| 421 | +pub struct AnyElement(Box<dyn ElementObject>); | |
| 310 | 422 | |
| 311 | − fn map<U>(self, f: impl FnOnce(Self) -> U) -> U | |
| 423 | +impl AnyElement { | |
| 424 | + pub fn new<E>(element: E) -> Self | |
| 312 | 425 | where |
| 313 | − Self: Sized, | |
| 314 | − U: Component<V>, | |
| 426 | + E: 'static + Element, | |
| 427 | + E::State: Any, | |
| 315 | 428 | { |
| 316 | − f(self) | |
| 429 | + AnyElement(Box::new(Some(DrawableElement::new(element))) as Box<dyn ElementObject>) | |
| 317 | 430 | } |
| 318 | 431 | |
| 319 | − fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self | |
| 320 | − where | |
| 321 | − Self: Sized, | |
| 322 | − { | |
| 323 | − self.map(|this| if condition { then(this) } else { this }) | |
| 432 | + pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { | |
| 433 | + self.0.layout(cx) | |
| 324 | 434 | } |
| 325 | 435 | |
| 326 | − fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self | |
| 327 | − where | |
| 328 | − Self: Sized, | |
| 329 | − { | |
| 330 | − self.map(|this| { | |
| 331 | − if let Some(value) = option { | |
| 332 | − then(this, value) | |
| 333 | − } else { | |
| 334 | − this | |
| 335 | − } | |
| 336 | − }) | |
| 436 | + pub fn paint(mut self, cx: &mut WindowContext) { | |
| 437 | + self.0.paint(cx) | |
| 337 | 438 | } |
| 338 | −} | |
| 339 | 439 | |
| 340 | −impl<V> Component<V> for AnyElement<V> { | |
| 341 | − fn render(self) -> AnyElement<V> { | |
| 342 | − self | |
| 440 | + /// Initializes this element and performs layout within the given available space to determine its size. | |
| 441 | + pub fn measure( | |
| 442 | + &mut self, | |
| 443 | + available_space: Size<AvailableSpace>, | |
| 444 | + cx: &mut WindowContext, | |
| 445 | + ) -> Size<Pixels> { | |
| 446 | + self.0.measure(available_space, cx) | |
| 343 | 447 | } |
| 344 | −} | |
| 345 | 448 | |
| 346 | −impl<V, E, F> Element<V> for Option<F> | |
| 347 | −where | |
| 348 | − V: 'static, | |
| 349 | − E: 'static + Component<V>, | |
| 350 | − F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, | |
| 351 | −{ | |
| 352 | − type ElementState = AnyElement<V>; | |
| 449 | + /// Initializes this element and performs layout in the available space, then paints it at the given origin. | |
| 450 | + pub fn draw( | |
| 451 | + mut self, | |
| 452 | + origin: Point<Pixels>, | |
| 453 | + available_space: Size<AvailableSpace>, | |
| 454 | + cx: &mut WindowContext, | |
| 455 | + ) { | |
| 456 | + self.0.draw(origin, available_space, cx) | |
| 457 | + } | |
| 353 | 458 | |
| 354 | − fn element_id(&self) -> Option<ElementId> { | |
| 355 | − None | |
| 459 | + /// Converts this `AnyElement` into a trait object that can be stored and manipulated. | |
| 460 | + pub fn into_any(self) -> AnyElement { | |
| 461 | + AnyElement::new(self) | |
| 356 | 462 | } |
| 357 | 463 | |
| 464 | + pub fn inner_id(&self) -> Option<ElementId> { | |
| 465 | + self.0.element_id() | |
| 466 | + } | |
| 467 | +} | |
| 468 | + | |
| 469 | +impl Element for AnyElement { | |
| 470 | + type State = (); | |
| 471 | + | |
| 358 | 472 | fn layout( |
| 359 | 473 | &mut self, |
| 360 | − view_state: &mut V, | |
| 361 | − _: Option<Self::ElementState>, | |
| 362 | − cx: &mut ViewContext<V>, | |
| 363 | − ) -> (LayoutId, Self::ElementState) { | |
| 364 | − let render = self.take().unwrap(); | |
| 365 | − let mut rendered_element = (render)(view_state, cx).render(); | |
| 366 | − let layout_id = rendered_element.layout(view_state, cx); | |
| 367 | − (layout_id, rendered_element) | |
| 474 | + _: Option<Self::State>, | |
| 475 | + cx: &mut WindowContext, | |
| 476 | + ) -> (LayoutId, Self::State) { | |
| 477 | + let layout_id = self.layout(cx); | |
| 478 | + (layout_id, ()) | |
| 368 | 479 | } |
| 369 | 480 | |
| 370 | − fn paint( | |
| 371 | − &mut self, | |
| 372 | − _bounds: Bounds<Pixels>, | |
| 373 | − view_state: &mut V, | |
| 374 | − rendered_element: &mut Self::ElementState, | |
| 375 | − cx: &mut ViewContext<V>, | |
| 376 | − ) { | |
| 377 | − rendered_element.paint(view_state, cx) | |
| 481 | + fn paint(self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) { | |
| 482 | + self.paint(cx); | |
| 378 | 483 | } |
| 379 | 484 | } |
| 380 | 485 | |
| 381 | −impl<V, E, F> Component<V> for Option<F> | |
| 382 | −where | |
| 383 | − V: 'static, | |
| 384 | − E: 'static + Component<V>, | |
| 385 | − F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, | |
| 386 | −{ | |
| 387 | − fn render(self) -> AnyElement<V> { | |
| 388 | − AnyElement::new(self) | |
| 486 | +impl IntoElement for AnyElement { | |
| 487 | + type Element = Self; | |
| 488 | + | |
| 489 | + fn element_id(&self) -> Option<ElementId> { | |
| 490 | + None | |
| 389 | 491 | } |
| 390 | −} | |
| 391 | 492 | |
| 392 | −impl<V, E, F> Component<V> for F | |
| 393 | −where | |
| 394 | − V: 'static, | |
| 395 | − E: 'static + Component<V>, | |
| 396 | − F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, | |
| 397 | −{ | |
| 398 | − fn render(self) -> AnyElement<V> { | |
| 399 | − AnyElement::new(Some(self)) | |
| 493 | + fn into_element(self) -> Self::Element { | |
| 494 | + self | |
| 400 | 495 | } |
| 401 | 496 | } |
| 497 | + | |
| 498 | +// impl<V, E, F> Element for Option<F> | |
| 499 | +// where | |
| 500 | +// V: 'static, | |
| 501 | +// E: Element, | |
| 502 | +// F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static, | |
| 503 | +// { | |
| 504 | +// type State = Option<AnyElement>; | |
| 505 | + | |
| 506 | +// fn element_id(&self) -> Option<ElementId> { | |
| 507 | +// None | |
| 508 | +// } | |
| 509 | + | |
| 510 | +// fn layout( | |
| 511 | +// &mut self, | |
| 512 | +// _: Option<Self::State>, | |
| 513 | +// cx: &mut WindowContext, | |
| 514 | +// ) -> (LayoutId, Self::State) { | |
| 515 | +// let render = self.take().unwrap(); | |
| 516 | +// let mut element = (render)(view_state, cx).into_any(); | |
| 517 | +// let layout_id = element.layout(view_state, cx); | |
| 518 | +// (layout_id, Some(element)) | |
| 519 | +// } | |
| 520 | + | |
| 521 | +// fn paint( | |
| 522 | +// self, | |
| 523 | +// _bounds: Bounds<Pixels>, | |
| 524 | +// rendered_element: &mut Self::State, | |
| 525 | +// cx: &mut WindowContext, | |
| 526 | +// ) { | |
| 527 | +// rendered_element.take().unwrap().paint(view_state, cx); | |
| 528 | +// } | |
| 529 | +// } | |
| 530 | + | |
| 531 | +// impl<V, E, F> RenderOnce for Option<F> | |
| 532 | +// where | |
| 533 | +// V: 'static, | |
| 534 | +// E: Element, | |
| 535 | +// F: FnOnce(&mut V, &mut WindowContext) -> E + 'static, | |
| 536 | +// { | |
| 537 | +// type Element = Self; | |
| 538 | + | |
| 539 | +// fn render(self) -> Self::Element { | |
| 540 | +// self | |
| 541 | +// } | |
| 542 | +// } | |
| @@ -1,9 +1,9 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | 2 | point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext, |
| 3 | − BorrowWindow, Bounds, ClickEvent, Component, DispatchPhase, Element, ElementId, FocusEvent, | |
| 4 | − FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, | |
| 5 | − MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, Point, Render, ScrollWheelEvent, | |
| 6 | − SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility, | |
| 3 | + BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle, | |
| 4 | + IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, | |
| 5 | + MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent, | |
| 6 | + SharedString, Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext, | |
| 7 | 7 | }; |
| 8 | 8 | use collections::HashMap; |
| 9 | 9 | use refineable::Refineable; |
| @@ -12,7 +12,6 @@ use std::{ · 2 unmodified lines | ||
| 12 | 12 | any::{Any, TypeId}, |
| 13 | 13 | cell::RefCell, |
| 14 | 14 | fmt::Debug, |
| 15 | − marker::PhantomData, | |
| 16 | 15 | mem, |
| 17 | 16 | rc::Rc, |
| 18 | 17 | time::Duration, |
| @@ -28,30 +27,24 @@ pub struct GroupStyle { · 9 unmodified lines | ||
| 28 | 27 | pub style: StyleRefinement, |
| 29 | 28 | } |
| 30 | 29 | |
| 31 | −pub trait InteractiveComponent<V: 'static>: Sized + Element<V> { | |
| 32 | − fn interactivity(&mut self) -> &mut Interactivity<V>; | |
| 30 | +pub trait InteractiveElement: Sized + Element { | |
| 31 | + fn interactivity(&mut self) -> &mut Interactivity; | |
| 33 | 32 | |
| 34 | 33 | fn group(mut self, group: impl Into<SharedString>) -> Self { |
| 35 | 34 | self.interactivity().group = Some(group.into()); |
| 36 | 35 | self |
| 37 | 36 | } |
| 38 | 37 | |
| 39 | − fn id(mut self, id: impl Into<ElementId>) -> Stateful<V, Self> { | |
| 38 | + fn id(mut self, id: impl Into<ElementId>) -> Stateful<Self> { | |
| 40 | 39 | self.interactivity().element_id = Some(id.into()); |
| 41 | 40 | |
| 42 | − Stateful { | |
| 43 | − element: self, | |
| 44 | − view_type: PhantomData, | |
| 45 | − } | |
| 41 | + Stateful { element: self } | |
| 46 | 42 | } |
| 47 | 43 | |
| 48 | − fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable<V, Self> { | |
| 44 | + fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable<Self> { | |
| 49 | 45 | self.interactivity().focusable = true; |
| 50 | 46 | self.interactivity().tracked_focus_handle = Some(focus_handle.clone()); |
| 51 | − Focusable { | |
| 52 | − element: self, | |
| 53 | − view_type: PhantomData, | |
| 54 | − } | |
| 47 | + Focusable { element: self } | |
| 55 | 48 | } |
| 56 | 49 | |
| 57 | 50 | fn key_context<C, E>(mut self, key_context: C) -> Self |
| @@ -85,15 +78,15 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> { · 27 unmodified lines | ||
| 85 | 78 | fn on_mouse_down( |
| 86 | 79 | mut self, |
| 87 | 80 | button: MouseButton, |
| 88 | − handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static, | |
| 81 | + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, | |
| 89 | 82 | ) -> Self { |
| 90 | 83 | self.interactivity().mouse_down_listeners.push(Box::new( |
| 91 | − move |view, event, bounds, phase, cx| { | |
| 84 | + move |event, bounds, phase, cx| { | |
| 92 | 85 | if phase == DispatchPhase::Bubble |
| 93 | 86 | && event.button == button |
| 94 | 87 | && bounds.contains_point(&event.position) |
| 95 | 88 | { |
| 96 | − handler(view, event, cx) | |
| 89 | + (listener)(event, cx) | |
| 97 | 90 | } |
| 98 | 91 | }, |
| 99 | 92 | )); |
| @@ -102,12 +95,12 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> { · 2 unmodified lines | ||
| 102 | 95 | |
| 103 | 96 | fn on_any_mouse_down( |
| 104 | 97 | mut self, |
| 105 | − handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static, | |
| 98 | + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, | |
| 106 | 99 | ) -> Self { |
| 107 | 100 | self.interactivity().mouse_down_listeners.push(Box::new( |
| 108 | − move |view, event, bounds, phase, cx| { | |
| 101 | + move |event, bounds, phase, cx| { | |
| 109 | 102 | if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { |
| 110 | − handler(view, event, cx) | |
| 103 | + (listener)(event, cx) | |
| 111 | 104 | } |
| 112 | 105 | }, |
| 113 | 106 | )); |
| @@ -117,43 +110,43 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> { · 3 unmodified lines | ||
| 117 | 110 | fn on_mouse_up( |
| 118 | 111 | mut self, |
| 119 | 112 | button: MouseButton, |
| 120 | − handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static, | |
| 113 | + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, | |
| 121 | 114 | ) -> Self { |
| 122 | − self.interactivity().mouse_up_listeners.push(Box::new( | |
| 123 | − move |view, event, bounds, phase, cx| { | |
| 115 | + self.interactivity() | |
| 116 | + .mouse_up_listeners | |
| 117 | + .push(Box::new(move |event, bounds, phase, cx| { | |
| 124 | 118 | if phase == DispatchPhase::Bubble |
| 125 | 119 | && event.button == button |
| 126 | 120 | && bounds.contains_point(&event.position) |
| 127 | 121 | { |
| 128 | − handler(view, event, cx) | |
| 122 | + (listener)(event, cx) | |
| 129 | 123 | } |
| 130 | − }, | |
| 131 | − )); | |
| 124 | + })); | |
| 132 | 125 | self |
| 133 | 126 | } |
| 134 | 127 | |
| 135 | 128 | fn on_any_mouse_up( |
| 136 | 129 | mut self, |
| 137 | − handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static, | |
| 130 | + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, | |
| 138 | 131 | ) -> Self { |
| 139 | − self.interactivity().mouse_up_listeners.push(Box::new( | |
| 140 | − move |view, event, bounds, phase, cx| { | |
| 132 | + self.interactivity() | |
| 133 | + .mouse_up_listeners | |
| 134 | + .push(Box::new(move |event, bounds, phase, cx| { | |
| 141 | 135 | if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { |
| 142 | − handler(view, event, cx) | |
| 136 | + (listener)(event, cx) | |
| 143 | 137 | } |
| 144 | − }, | |
| 145 | − )); | |
| 138 | + })); | |
| 146 | 139 | self |
| 147 | 140 | } |
| 148 | 141 | |
| 149 | 142 | fn on_mouse_down_out( |
| 150 | 143 | mut self, |
| 151 | − handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static, | |
| 144 | + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, | |
| 152 | 145 | ) -> Self { |
| 153 | 146 | self.interactivity().mouse_down_listeners.push(Box::new( |
| 154 | − move |view, event, bounds, phase, cx| { | |
| 147 | + move |event, bounds, phase, cx| { | |
| 155 | 148 | if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { |
| 156 | − handler(view, event, cx) | |
| 149 | + (listener)(event, cx) | |
| 157 | 150 | } |
| 158 | 151 | }, |
| 159 | 152 | )); |
| @@ -163,29 +156,29 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> { · 3 unmodified lines | ||
| 163 | 156 | fn on_mouse_up_out( |
| 164 | 157 | mut self, |
| 165 | 158 | button: MouseButton, |
| 166 | − handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static, | |
| 159 | + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, | |
| 167 | 160 | ) -> Self { |
| 168 | − self.interactivity().mouse_up_listeners.push(Box::new( | |
| 169 | − move |view, event, bounds, phase, cx| { | |
| 161 | + self.interactivity() | |
| 162 | + .mouse_up_listeners | |
| 163 | + .push(Box::new(move |event, bounds, phase, cx| { | |
| 170 | 164 | if phase == DispatchPhase::Capture |
| 171 | 165 | && event.button == button |
| 172 | 166 | && !bounds.contains_point(&event.position) |
| 173 | 167 | { |
| 174 | − handler(view, event, cx); | |
| 168 | + (listener)(event, cx); | |
| 175 | 169 | } |
| 176 | − }, | |
| 177 | − )); | |
| 170 | + })); | |
| 178 | 171 | self |
| 179 | 172 | } |
| 180 | 173 | |
| 181 | 174 | fn on_mouse_move( |
| 182 | 175 | mut self, |
| 183 | − handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext<V>) + 'static, | |
| 176 | + listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static, | |
| 184 | 177 | ) -> Self { |
| 185 | 178 | self.interactivity().mouse_move_listeners.push(Box::new( |
| 186 | − move |view, event, bounds, phase, cx| { | |
| 179 | + move |event, bounds, phase, cx| { | |
| 187 | 180 | if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { |
| 188 | − handler(view, event, cx); | |
| 181 | + (listener)(event, cx); | |
| 189 | 182 | } |
| 190 | 183 | }, |
| 191 | 184 | )); |
| @@ -194,29 +187,29 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> { · 2 unmodified lines | ||
| 194 | 187 | |
| 195 | 188 | fn on_scroll_wheel( |
| 196 | 189 | mut self, |
| 197 | − handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext<V>) + 'static, | |
| 190 | + listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static, | |
| 198 | 191 | ) -> Self { |
| 199 | 192 | self.interactivity().scroll_wheel_listeners.push(Box::new( |
| 200 | − move |view, event, bounds, phase, cx| { | |
| 193 | + move |event, bounds, phase, cx| { | |
| 201 | 194 | if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { |
| 202 | − handler(view, event, cx); | |
| 195 | + (listener)(event, cx); | |
| 203 | 196 | } |
| 204 | 197 | }, |
| 205 | 198 | )); |
| 206 | 199 | self |
| 207 | 200 | } |
| 208 | 201 | |
| 209 | − /// Capture the given action, fires during the capture phase | |
| 202 | + /// Capture the given action, before normal action dispatch can fire | |
| 210 | 203 | fn capture_action<A: Action>( |
| 211 | 204 | mut self, |
| 212 | − listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static, | |
| 205 | + listener: impl Fn(&A, &mut WindowContext) + 'static, | |
| 213 | 206 | ) -> Self { |
| 214 | 207 | self.interactivity().action_listeners.push(( |
| 215 | 208 | TypeId::of::<A>(), |
| 216 | − Box::new(move |view, action, phase, cx| { | |
| 209 | + Box::new(move |action, phase, cx| { | |
| 217 | 210 | let action = action.downcast_ref().unwrap(); |
| 218 | 211 | if phase == DispatchPhase::Capture { |
| 219 | − listener(view, action, cx) | |
| 212 | + (listener)(action, cx) | |
| 220 | 213 | } |
| 221 | 214 | }), |
| 222 | 215 | )); |
| @@ -224,10 +217,7 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> { · 1 unmodified line | ||
| 224 | 217 | } |
| 225 | 218 | |
| 226 | 219 | /// Add a listener for the given action, fires during the bubble event phase |
| 227 | − fn on_action<A: Action>( | |
| 228 | − mut self, | |
| 229 | − listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static, | |
| 230 | − ) -> Self { | |
| 220 | + fn on_action<A: Action>(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self { | |
| 231 | 221 | // NOTE: this debug assert has the side-effect of working around |
| 232 | 222 | // a bug where a crate consisting only of action definitions does |
| 233 | 223 | // not register the actions in debug builds: |
| @@ -244,10 +234,10 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> { · 10 unmodified lines | ||
| 244 | 234 | // ); |
| 245 | 235 | self.interactivity().action_listeners.push(( |
| 246 | 236 | TypeId::of::<A>(), |
| 247 | − Box::new(move |view, action, phase, cx| { | |
| 237 | + Box::new(move |action, phase, cx| { | |
| 248 | 238 | let action = action.downcast_ref().unwrap(); |
| 249 | 239 | if phase == DispatchPhase::Bubble { |
| 250 | − listener(view, action, cx) | |
| 240 | + (listener)(action, cx) | |
| 251 | 241 | } |
| 252 | 242 | }), |
| 253 | 243 | )); |
| @@ -256,24 +246,53 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> { · 2 unmodified lines | ||
| 256 | 246 | |
| 257 | 247 | fn on_key_down( |
| 258 | 248 | mut self, |
| 259 | − listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static, | |
| 249 | + listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, | |
| 260 | 250 | ) -> Self { |
| 261 | 251 | self.interactivity() |
| 262 | 252 | .key_down_listeners |
| 263 | − .push(Box::new(move |view, event, phase, cx| { | |
| 264 | − listener(view, event, phase, cx) | |
| 253 | + .push(Box::new(move |event, phase, cx| { | |
| 254 | + if phase == DispatchPhase::Bubble { | |
| 255 | + (listener)(event, cx) | |
| 256 | + } | |
| 265 | 257 | })); |
| 266 | 258 | self |
| 267 | 259 | } |
| 268 | 260 | |
| 269 | − fn on_key_up( | |
| 261 | + fn capture_key_down( | |
| 270 | 262 | mut self, |
| 271 | − listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static, | |
| 263 | + listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, | |
| 272 | 264 | ) -> Self { |
| 265 | + self.interactivity() | |
| 266 | + .key_down_listeners | |
| 267 | + .push(Box::new(move |event, phase, cx| { | |
| 268 | + if phase == DispatchPhase::Capture { | |
| 269 | + listener(event, cx) | |
| 270 | + } | |
| 271 | + })); | |
| 272 | + self | |
| 273 | + } | |
| 274 | + | |
| 275 | + fn on_key_up(mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) -> Self { | |
| 273 | 276 | self.interactivity() |
| 274 | 277 | .key_up_listeners |
| 275 | − .push(Box::new(move |view, event, phase, cx| { | |
| 276 | − listener(view, event, phase, cx) | |
| 278 | + .push(Box::new(move |event, phase, cx| { | |
| 279 | + if phase == DispatchPhase::Bubble { | |
| 280 | + listener(event, cx) | |
| 281 | + } | |
| 282 | + })); | |
| 283 | + self | |
| 284 | + } | |
| 285 | + | |
| 286 | + fn capture_key_up( | |
| 287 | + mut self, | |
| 288 | + listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static, | |
| 289 | + ) -> Self { | |
| 290 | + self.interactivity() | |
| 291 | + .key_up_listeners | |
| 292 | + .push(Box::new(move |event, phase, cx| { | |
| 293 | + if phase == DispatchPhase::Capture { | |
| 294 | + listener(event, cx) | |
| 295 | + } | |
| 277 | 296 | })); |
| 278 | 297 | self |
| 279 | 298 | } |
| @@ -302,25 +321,22 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> { · 22 unmodified lines | ||
| 302 | 321 | |
| 303 | 322 | fn on_drop<W: 'static>( |
| 304 | 323 | mut self, |
| 305 | − listener: impl Fn(&mut V, View<W>, &mut ViewContext<V>) + 'static, | |
| 324 | + listener: impl Fn(&View<W>, &mut WindowContext) + 'static, | |
| 306 | 325 | ) -> Self { |
| 307 | 326 | self.interactivity().drop_listeners.push(( |
| 308 | 327 | TypeId::of::<W>(), |
| 309 | − Box::new(move |view, dragged_view, cx| { | |
| 310 | − listener(view, dragged_view.downcast().unwrap(), cx); | |
| 328 | + Box::new(move |dragged_view, cx| { | |
| 329 | + listener(&dragged_view.downcast().unwrap(), cx); | |
| 311 | 330 | }), |
| 312 | 331 | )); |
| 313 | 332 | self |
| 314 | 333 | } |
| 315 | 334 | } |
| 316 | 335 | |
| 317 | −pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveComponent<V> { | |
| 318 | − fn focusable(mut self) -> Focusable<V, Self> { | |
| 336 | +pub trait StatefulInteractiveElement: InteractiveElement { | |
| 337 | + fn focusable(mut self) -> Focusable<Self> { | |
| 319 | 338 | self.interactivity().focusable = true; |
| 320 | − Focusable { | |
| 321 | − element: self, | |
| 322 | − view_type: PhantomData, | |
| 323 | − } | |
| 339 | + Focusable { element: self } | |
| 324 | 340 | } |
| 325 | 341 | |
| 326 | 342 | fn overflow_scroll(mut self) -> Self { |
| @@ -362,23 +378,17 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo · 35 unmodified lines | ||
| 362 | 378 | self |
| 363 | 379 | } |
| 364 | 380 | |
| 365 | − fn on_click( | |
| 366 | − mut self, | |
| 367 | − listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static, | |
| 368 | − ) -> Self | |
| 381 | + fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self | |
| 369 | 382 | where |
| 370 | 383 | Self: Sized, |
| 371 | 384 | { |
| 372 | 385 | self.interactivity() |
| 373 | 386 | .click_listeners |
| 374 | − .push(Box::new(move |view, event, cx| listener(view, event, cx))); | |
| 387 | + .push(Box::new(move |event, cx| listener(event, cx))); | |
| 375 | 388 | self |
| 376 | 389 | } |
| 377 | 390 | |
| 378 | − fn on_drag<W>( | |
| 379 | − mut self, | |
| 380 | − listener: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static, | |
| 381 | − ) -> Self | |
| 391 | + fn on_drag<W>(mut self, listener: impl Fn(&mut WindowContext) -> View<W> + 'static) -> Self | |
| 382 | 392 | where |
| 383 | 393 | Self: Sized, |
| 384 | 394 | W: 'static + Render, |
| @@ -387,15 +397,14 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo · 2 unmodified lines | ||
| 387 | 397 | self.interactivity().drag_listener.is_none(), |
| 388 | 398 | "calling on_drag more than once on the same element is not supported" |
| 389 | 399 | ); |
| 390 | − self.interactivity().drag_listener = | |
| 391 | − Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag { | |
| 392 | − view: listener(view_state, cx).into(), | |
| 393 | − cursor_offset, | |
| 394 | − })); | |
| 400 | + self.interactivity().drag_listener = Some(Box::new(move |cursor_offset, cx| AnyDrag { | |
| 401 | + view: listener(cx).into(), | |
| 402 | + cursor_offset, | |
| 403 | + })); | |
| 395 | 404 | self |
| 396 | 405 | } |
| 397 | 406 | |
| 398 | − fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext<V>)) -> Self | |
| 407 | + fn on_hover(mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static) -> Self | |
| 399 | 408 | where |
| 400 | 409 | Self: Sized, |
| 401 | 410 | { |
| @@ -407,10 +416,7 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo · 5 unmodified lines | ||
| 407 | 416 | self |
| 408 | 417 | } |
| 409 | 418 | |
| 410 | − fn tooltip( | |
| 411 | − mut self, | |
| 412 | − build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static, | |
| 413 | − ) -> Self | |
| 419 | + fn tooltip(mut self, build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self | |
| 414 | 420 | where |
| 415 | 421 | Self: Sized, |
| 416 | 422 | { |
| @@ -418,14 +424,13 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo · 1 unmodified line | ||
| 418 | 424 | self.interactivity().tooltip_builder.is_none(), |
| 419 | 425 | "calling tooltip more than once on the same element is not supported" |
| 420 | 426 | ); |
| 421 | − self.interactivity().tooltip_builder = | |
| 422 | − Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx))); | |
| 427 | + self.interactivity().tooltip_builder = Some(Rc::new(build_tooltip)); | |
| 423 | 428 | |
| 424 | 429 | self |
| 425 | 430 | } |
| 426 | 431 | } |
| 427 | 432 | |
| 428 | −pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> { | |
| 433 | +pub trait FocusableElement: InteractiveElement { | |
| 429 | 434 | fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self |
| 430 | 435 | where |
| 431 | 436 | Self: Sized, |
| @@ -442,49 +447,41 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> { · 10 unmodified lines | ||
| 442 | 447 | self |
| 443 | 448 | } |
| 444 | 449 | |
| 445 | − fn on_focus( | |
| 446 | − mut self, | |
| 447 | − listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static, | |
| 448 | − ) -> Self | |
| 450 | + fn on_focus(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self | |
| 449 | 451 | where |
| 450 | 452 | Self: Sized, |
| 451 | 453 | { |
| 452 | − self.interactivity().focus_listeners.push(Box::new( | |
| 453 | − move |view, focus_handle, event, cx| { | |
| 454 | + self.interactivity() | |
| 455 | + .focus_listeners | |
| 456 | + .push(Box::new(move |focus_handle, event, cx| { | |
| 454 | 457 | if event.focused.as_ref() == Some(focus_handle) { |
| 455 | − listener(view, event, cx) | |
| 458 | + listener(event, cx) | |
| 456 | 459 | } |
| 457 | − }, | |
| 458 | − )); | |
| 460 | + })); | |
| 459 | 461 | self |
| 460 | 462 | } |
| 461 | 463 | |
| 462 | − fn on_blur( | |
| 463 | − mut self, | |
| 464 | − listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static, | |
| 465 | − ) -> Self | |
| 464 | + fn on_blur(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self | |
| 466 | 465 | where |
| 467 | 466 | Self: Sized, |
| 468 | 467 | { |
| 469 | − self.interactivity().focus_listeners.push(Box::new( | |
| 470 | − move |view, focus_handle, event, cx| { | |
| 468 | + self.interactivity() | |
| 469 | + .focus_listeners | |
| 470 | + .push(Box::new(move |focus_handle, event, cx| { | |
| 471 | 471 | if event.blurred.as_ref() == Some(focus_handle) { |
| 472 | − listener(view, event, cx) | |
| 472 | + listener(event, cx) | |
| 473 | 473 | } |
| 474 | − }, | |
| 475 | − )); | |
| 474 | + })); | |
| 476 | 475 | self |
| 477 | 476 | } |
| 478 | 477 | |
| 479 | − fn on_focus_in( | |
| 480 | − mut self, | |
| 481 | − listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static, | |
| 482 | − ) -> Self | |
| 478 | + fn on_focus_in(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self | |
| 483 | 479 | where |
| 484 | 480 | Self: Sized, |
| 485 | 481 | { |
| 486 | − self.interactivity().focus_listeners.push(Box::new( | |
| 487 | − move |view, focus_handle, event, cx| { | |
| 482 | + self.interactivity() | |
| 483 | + .focus_listeners | |
| 484 | + .push(Box::new(move |focus_handle, event, cx| { | |
| 488 | 485 | let descendant_blurred = event |
| 489 | 486 | .blurred |
| 490 | 487 | .as_ref() |
| @@ -495,22 +492,19 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> { · 4 unmodified lines | ||
| 495 | 492 | .map_or(false, |focused| focus_handle.contains(focused, cx)); |
| 496 | 493 | |
| 497 | 494 | if !descendant_blurred && descendant_focused { |
| 498 | − listener(view, event, cx) | |
| 495 | + listener(event, cx) | |
| 499 | 496 | } |
| 500 | − }, | |
| 501 | − )); | |
| 497 | + })); | |
| 502 | 498 | self |
| 503 | 499 | } |
| 504 | 500 | |
| 505 | − fn on_focus_out( | |
| 506 | − mut self, | |
| 507 | − listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static, | |
| 508 | − ) -> Self | |
| 501 | + fn on_focus_out(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self | |
| 509 | 502 | where |
| 510 | 503 | Self: Sized, |
| 511 | 504 | { |
| 512 | − self.interactivity().focus_listeners.push(Box::new( | |
| 513 | − move |view, focus_handle, event, cx| { | |
| 505 | + self.interactivity() | |
| 506 | + .focus_listeners | |
| 507 | + .push(Box::new(move |focus_handle, event, cx| { | |
| 514 | 508 | let descendant_blurred = event |
| 515 | 509 | .blurred |
| 516 | 510 | .as_ref() |
| @@ -520,98 +514,80 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> { · 3 unmodified lines | ||
| 520 | 514 | .as_ref() |
| 521 | 515 | .map_or(false, |focused| focus_handle.contains(focused, cx)); |
| 522 | 516 | if descendant_blurred && !descendant_focused { |
| 523 | − listener(view, event, cx) | |
| 517 | + listener(event, cx) | |
| 524 | 518 | } |
| 525 | − }, | |
| 526 | − )); | |
| 519 | + })); | |
| 527 | 520 | self |
| 528 | 521 | } |
| 529 | 522 | } |
| 530 | 523 | |
| 531 | −pub type FocusListeners<V> = SmallVec<[FocusListener<V>; 2]>; | |
| 532 | − | |
| 533 | −pub type FocusListener<V> = | |
| 534 | − Box<dyn Fn(&mut V, &FocusHandle, &FocusEvent, &mut ViewContext<V>) + 'static>; | |
| 524 | +pub type FocusListeners = SmallVec<[FocusListener; 2]>; | |
| 535 | 525 | |
| 536 | −pub type MouseDownListener<V> = Box< | |
| 537 | − dyn Fn(&mut V, &MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static, | |
| 538 | −>; | |
| 539 | −pub type MouseUpListener<V> = Box< | |
| 540 | − dyn Fn(&mut V, &MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static, | |
| 541 | −>; | |
| 526 | +pub type FocusListener = Box<dyn Fn(&FocusHandle, &FocusEvent, &mut WindowContext) + 'static>; | |
| 542 | 527 | |
| 543 | −pub type MouseMoveListener<V> = Box< | |
| 544 | − dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static, | |
| 545 | −>; | |
| 528 | +pub type MouseDownListener = | |
| 529 | + Box<dyn Fn(&MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>; | |
| 530 | +pub type MouseUpListener = | |
| 531 | + Box<dyn Fn(&MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>; | |
| 546 | 532 | |
| 547 | −pub type ScrollWheelListener<V> = Box< | |
| 548 | − dyn Fn(&mut V, &ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) | |
| 549 | − + 'static, | |
| 550 | −>; | |
| 533 | +pub type MouseMoveListener = | |
| 534 | + Box<dyn Fn(&MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>; | |
| 551 | 535 | |
| 552 | −pub type ClickListener<V> = Box<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static>; | |
| 536 | +pub type ScrollWheelListener = | |
| 537 | + Box<dyn Fn(&ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>; | |
| 553 | 538 | |
| 554 | −pub type DragListener<V> = | |
| 555 | − Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + 'static>; | |
| 539 | +pub type ClickListener = Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>; | |
| 556 | 540 | |
| 557 | −type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static; | |
| 541 | +pub type DragListener = Box<dyn Fn(Point<Pixels>, &mut WindowContext) -> AnyDrag + 'static>; | |
| 558 | 542 | |
| 559 | −pub type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>; | |
| 543 | +type DropListener = dyn Fn(AnyView, &mut WindowContext) + 'static; | |
| 560 | 544 | |
| 561 | −pub type TooltipBuilder<V> = Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>; | |
| 545 | +pub type TooltipBuilder = Rc<dyn Fn(&mut WindowContext) -> AnyView + 'static>; | |
| 562 | 546 | |
| 563 | −pub type KeyDownListener<V> = | |
| 564 | − Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>; | |
| 547 | +pub type KeyDownListener = Box<dyn Fn(&KeyDownEvent, DispatchPhase, &mut WindowContext) + 'static>; | |
| 565 | 548 | |
| 566 | −pub type KeyUpListener<V> = | |
| 567 | − Box<dyn Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static>; | |
| 549 | +pub type KeyUpListener = Box<dyn Fn(&KeyUpEvent, DispatchPhase, &mut WindowContext) + 'static>; | |
| 568 | 550 | |
| 569 | −pub type ActionListener<V> = | |
| 570 | − Box<dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) + 'static>; | |
| 551 | +pub type ActionListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>; | |
| 571 | 552 | |
| 572 | −pub fn div<V: 'static>() -> Div<V> { | |
| 553 | +pub fn div() -> Div { | |
| 573 | 554 | Div { |
| 574 | 555 | interactivity: Interactivity::default(), |
| 575 | 556 | children: SmallVec::default(), |
| 576 | 557 | } |
| 577 | 558 | } |
| 578 | 559 | |
| 579 | −pub struct Div<V> { | |
| 580 | − interactivity: Interactivity<V>, | |
| 581 | − children: SmallVec<[AnyElement<V>; 2]>, | |
| 560 | +pub struct Div { | |
| 561 | + interactivity: Interactivity, | |
| 562 | + children: SmallVec<[AnyElement; 2]>, | |
| 582 | 563 | } |
| 583 | 564 | |
| 584 | −impl<V> Styled for Div<V> { | |
| 565 | +impl Styled for Div { | |
| 585 | 566 | fn style(&mut self) -> &mut StyleRefinement { |
| 586 | 567 | &mut self.interactivity.base_style |
| 587 | 568 | } |
| 588 | 569 | } |
| 589 | 570 | |
| 590 | −impl<V: 'static> InteractiveComponent<V> for Div<V> { | |
| 591 | − fn interactivity(&mut self) -> &mut Interactivity<V> { | |
| 571 | +impl InteractiveElement for Div { | |
| 572 | + fn interactivity(&mut self) -> &mut Interactivity { | |
| 592 | 573 | &mut self.interactivity |
| 593 | 574 | } |
| 594 | 575 | } |
| 595 | 576 | |
| 596 | −impl<V: 'static> ParentComponent<V> for Div<V> { | |
| 597 | − fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> { | |
| 577 | +impl ParentElement for Div { | |
| 578 | + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { | |
| 598 | 579 | &mut self.children |
| 599 | 580 | } |
| 600 | 581 | } |
| 601 | 582 | |
| 602 | −impl<V: 'static> Element<V> for Div<V> { | |
| 603 | − type ElementState = DivState; | |
| 604 | − | |
| 605 | − fn element_id(&self) -> Option<ElementId> { | |
| 606 | − self.interactivity.element_id.clone() | |
| 607 | − } | |
| 583 | +impl Element for Div { | |
| 584 | + type State = DivState; | |
| 608 | 585 | |
| 609 | 586 | fn layout( |
| 610 | 587 | &mut self, |
| 611 | − view_state: &mut V, | |
| 612 | − element_state: Option<Self::ElementState>, | |
| 613 | − cx: &mut ViewContext<V>, | |
| 614 | − ) -> (LayoutId, Self::ElementState) { | |
| 588 | + element_state: Option<Self::State>, | |
| 589 | + cx: &mut WindowContext, | |
| 590 | + ) -> (LayoutId, Self::State) { | |
| 615 | 591 | let mut child_layout_ids = SmallVec::new(); |
| 616 | 592 | let mut interactivity = mem::take(&mut self.interactivity); |
| 617 | 593 | let (layout_id, interactive_state) = interactivity.layout( |
| @@ -622,7 +598,7 @@ impl<V: 'static> Element<V> for Div<V> { · 4 unmodified lines | ||
| 622 | 598 | child_layout_ids = self |
| 623 | 599 | .children |
| 624 | 600 | .iter_mut() |
| 625 | − .map(|child| child.layout(view_state, cx)) | |
| 601 | + .map(|child| child.layout(cx)) | |
| 626 | 602 | .collect::<SmallVec<_>>(); |
| 627 | 603 | cx.request_layout(&style, child_layout_ids.iter().copied()) |
| 628 | 604 | }) |
| @@ -639,11 +615,10 @@ impl<V: 'static> Element<V> for Div<V> { · 10 unmodified lines | ||
| 639 | 615 | } |
| 640 | 616 | |
| 641 | 617 | fn paint( |
| 642 | − &mut self, | |
| 618 | + self, | |
| 643 | 619 | bounds: Bounds<Pixels>, |
| 644 | − view_state: &mut V, | |
| 645 | − element_state: &mut Self::ElementState, | |
| 646 | − cx: &mut ViewContext<V>, | |
| 620 | + element_state: &mut Self::State, | |
| 621 | + cx: &mut WindowContext, | |
| 647 | 622 | ) { |
| 648 | 623 | let mut child_min = point(Pixels::MAX, Pixels::MAX); |
| 649 | 624 | let mut child_max = Point::default(); |
| @@ -658,8 +633,7 @@ impl<V: 'static> Element<V> for Div<V> { · 8 unmodified lines | ||
| 658 | 633 | (child_max - child_min).into() |
| 659 | 634 | }; |
| 660 | 635 | |
| 661 | − let mut interactivity = mem::take(&mut self.interactivity); | |
| 662 | − interactivity.paint( | |
| 636 | + self.interactivity.paint( | |
| 663 | 637 | bounds, |
| 664 | 638 | content_size, |
| 665 | 639 | &mut element_state.interactive_state, |
| @@ -679,8 +653,8 @@ impl<V: 'static> Element<V> for Div<V> { · 13 unmodified lines | ||
| 679 | 653 | cx.with_text_style(style.text_style().cloned(), |cx| { |
| 680 | 654 | cx.with_content_mask(style.overflow_mask(bounds), |cx| { |
| 681 | 655 | cx.with_element_offset(scroll_offset, |cx| { |
| 682 | − for child in &mut self.children { | |
| 683 | − child.paint(view_state, cx); | |
| 656 | + for child in self.children { | |
| 657 | + child.paint(cx); | |
| 684 | 658 | } |
| 685 | 659 | }) |
| 686 | 660 | }) |
| @@ -689,13 +663,18 @@ impl<V: 'static> Element<V> for Div<V> { · 2 unmodified lines | ||
| 689 | 663 | }) |
| 690 | 664 | }, |
| 691 | 665 | ); |
| 692 | − self.interactivity = interactivity; | |
| 693 | 666 | } |
| 694 | 667 | } |
| 695 | 668 | |
| 696 | −impl<V: 'static> Component<V> for Div<V> { | |
| 697 | − fn render(self) -> AnyElement<V> { | |
| 698 | − AnyElement::new(self) | |
| 669 | +impl IntoElement for Div { | |
| 670 | + type Element = Self; | |
| 671 | + | |
| 672 | + fn element_id(&self) -> Option<ElementId> { | |
| 673 | + self.interactivity.element_id.clone() | |
| 674 | + } | |
| 675 | + | |
| 676 | + fn into_element(self) -> Self::Element { | |
| 677 | + self | |
| 699 | 678 | } |
| 700 | 679 | } |
| 701 | 680 | |
| @@ -710,12 +689,12 @@ impl DivState { · 8 unmodified lines | ||
| 710 | 689 | } |
| 711 | 690 | } |
| 712 | 691 | |
| 713 | −pub struct Interactivity<V> { | |
| 692 | +pub struct Interactivity { | |
| 714 | 693 | pub element_id: Option<ElementId>, |
| 715 | 694 | pub key_context: KeyContext, |
| 716 | 695 | pub focusable: bool, |
| 717 | 696 | pub tracked_focus_handle: Option<FocusHandle>, |
| 718 | − pub focus_listeners: FocusListeners<V>, | |
| 697 | + pub focus_listeners: FocusListeners, | |
| 719 | 698 | pub group: Option<SharedString>, |
| 720 | 699 | pub base_style: StyleRefinement, |
| 721 | 700 | pub focus_style: StyleRefinement, |
| @@ -726,29 +705,26 @@ pub struct Interactivity<V> { · 4 unmodified lines | ||
| 726 | 705 | pub group_active_style: Option<GroupStyle>, |
| 727 | 706 | pub drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, |
| 728 | 707 | pub group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, |
| 729 | − pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>, | |
| 730 | − pub mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>, | |
| 731 | − pub mouse_move_listeners: SmallVec<[MouseMoveListener<V>; 2]>, | |
| 732 | − pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener<V>; 2]>, | |
| 733 | − pub key_down_listeners: SmallVec<[KeyDownListener<V>; 2]>, | |
| 734 | − pub key_up_listeners: SmallVec<[KeyUpListener<V>; 2]>, | |
| 735 | − pub action_listeners: SmallVec<[(TypeId, ActionListener<V>); 8]>, | |
| 736 | − pub drop_listeners: SmallVec<[(TypeId, Box<DropListener<V>>); 2]>, | |
| 737 | − pub click_listeners: SmallVec<[ClickListener<V>; 2]>, | |
| 738 | − pub drag_listener: Option<DragListener<V>>, | |
| 739 | − pub hover_listener: Option<HoverListener<V>>, | |
| 740 | − pub tooltip_builder: Option<TooltipBuilder<V>>, | |
| 708 | + pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, | |
| 709 | + pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, | |
| 710 | + pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, | |
| 711 | + pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, | |
| 712 | + pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, | |
| 713 | + pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, | |
| 714 | + pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, | |
| 715 | + pub drop_listeners: SmallVec<[(TypeId, Box<DropListener>); 2]>, | |
| 716 | + pub click_listeners: SmallVec<[ClickListener; 2]>, | |
| 717 | + pub drag_listener: Option<DragListener>, | |
| 718 | + pub hover_listener: Option<Box<dyn Fn(&bool, &mut WindowContext)>>, | |
| 719 | + pub tooltip_builder: Option<TooltipBuilder>, | |
| 741 | 720 | } |
| 742 | 721 | |
| 743 | −impl<V> Interactivity<V> | |
| 744 | −where | |
| 745 | − V: 'static, | |
| 746 | −{ | |
| 722 | +impl Interactivity { | |
| 747 | 723 | pub fn layout( |
| 748 | 724 | &mut self, |
| 749 | 725 | element_state: Option<InteractiveElementState>, |
| 750 | − cx: &mut ViewContext<V>, | |
| 751 | − f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId, | |
| 726 | + cx: &mut WindowContext, | |
| 727 | + f: impl FnOnce(Style, &mut WindowContext) -> LayoutId, | |
| 752 | 728 | ) -> (LayoutId, InteractiveElementState) { |
| 753 | 729 | let mut element_state = element_state.unwrap_or_default(); |
| 754 | 730 | |
| @@ -770,12 +746,12 @@ where · 15 unmodified lines | ||
| 770 | 746 | } |
| 771 | 747 | |
| 772 | 748 | pub fn paint( |
| 773 | − &mut self, | |
| 749 | + mut self, | |
| 774 | 750 | bounds: Bounds<Pixels>, |
| 775 | 751 | content_size: Size<Pixels>, |
| 776 | 752 | element_state: &mut InteractiveElementState, |
| 777 | − cx: &mut ViewContext<V>, | |
| 778 | − f: impl FnOnce(Style, Point<Pixels>, &mut ViewContext<V>), | |
| 753 | + cx: &mut WindowContext, | |
| 754 | + f: impl FnOnce(Style, Point<Pixels>, &mut WindowContext), | |
| 779 | 755 | ) { |
| 780 | 756 | let style = self.compute_style(Some(bounds), element_state, cx); |
| 781 | 757 | |
| @@ -787,26 +763,26 @@ where · 5 unmodified lines | ||
| 787 | 763 | } |
| 788 | 764 | |
| 789 | 765 | for listener in self.mouse_down_listeners.drain(..) { |
| 790 | − cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| { | |
| 791 | − listener(state, event, &bounds, phase, cx); | |
| 766 | + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { | |
| 767 | + listener(event, &bounds, phase, cx); | |
| 792 | 768 | }) |
| 793 | 769 | } |
| 794 | 770 | |
| 795 | 771 | for listener in self.mouse_up_listeners.drain(..) { |
| 796 | − cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| { | |
| 797 | − listener(state, event, &bounds, phase, cx); | |
| 772 | + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { | |
| 773 | + listener(event, &bounds, phase, cx); | |
| 798 | 774 | }) |
| 799 | 775 | } |
| 800 | 776 | |
| 801 | 777 | for listener in self.mouse_move_listeners.drain(..) { |
| 802 | − cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| { | |
| 803 | − listener(state, event, &bounds, phase, cx); | |
| 778 | + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { | |
| 779 | + listener(event, &bounds, phase, cx); | |
| 804 | 780 | }) |
| 805 | 781 | } |
| 806 | 782 | |
| 807 | 783 | for listener in self.scroll_wheel_listeners.drain(..) { |
| 808 | − cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| { | |
| 809 | − listener(state, event, &bounds, phase, cx); | |
| 784 | + cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { | |
| 785 | + listener(event, &bounds, phase, cx); | |
| 810 | 786 | }) |
| 811 | 787 | } |
| 812 | 788 | |
| @@ -817,7 +793,7 @@ where · 4 unmodified lines | ||
| 817 | 793 | |
| 818 | 794 | if let Some(group_bounds) = hover_group_bounds { |
| 819 | 795 | let hovered = group_bounds.contains_point(&cx.mouse_position()); |
| 820 | − cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { | |
| 796 | + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { | |
| 821 | 797 | if phase == DispatchPhase::Capture { |
| 822 | 798 | if group_bounds.contains_point(&event.position) != hovered { |
| 823 | 799 | cx.notify(); |
| @@ -830,7 +806,7 @@ where · 6 unmodified lines | ||
| 830 | 806 | || (cx.active_drag.is_some() && !self.drag_over_styles.is_empty()) |
| 831 | 807 | { |
| 832 | 808 | let hovered = bounds.contains_point(&cx.mouse_position()); |
| 833 | − cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { | |
| 809 | + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { | |
| 834 | 810 | if phase == DispatchPhase::Capture { |
| 835 | 811 | if bounds.contains_point(&event.position) != hovered { |
| 836 | 812 | cx.notify(); |
| @@ -841,7 +817,7 @@ where · 4 unmodified lines | ||
| 841 | 817 | |
| 842 | 818 | if cx.active_drag.is_some() { |
| 843 | 819 | let drop_listeners = mem::take(&mut self.drop_listeners); |
| 844 | − cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| { | |
| 820 | + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { | |
| 845 | 821 | if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { |
| 846 | 822 | if let Some(drag_state_type) = |
| 847 | 823 | cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) |
| @@ -852,7 +828,7 @@ where · 4 unmodified lines | ||
| 852 | 828 | .active_drag |
| 853 | 829 | .take() |
| 854 | 830 | .expect("checked for type drag state type above"); |
| 855 | − listener(view, drag.view.clone(), cx); | |
| 831 | + listener(drag.view.clone(), cx); | |
| 856 | 832 | cx.notify(); |
| 857 | 833 | cx.stop_propagation(); |
| 858 | 834 | } |
| @@ -872,7 +848,7 @@ where · 13 unmodified lines | ||
| 872 | 848 | if let Some(drag_listener) = drag_listener { |
| 873 | 849 | let active_state = element_state.clicked_state.clone(); |
| 874 | 850 | |
| 875 | − cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { | |
| 851 | + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { | |
| 876 | 852 | if cx.active_drag.is_some() { |
| 877 | 853 | if phase == DispatchPhase::Capture { |
| 878 | 854 | cx.notify(); |
| @@ -883,7 +859,7 @@ where · 4 unmodified lines | ||
| 883 | 859 | { |
| 884 | 860 | *active_state.borrow_mut() = ElementClickedState::default(); |
| 885 | 861 | let cursor_offset = event.position - bounds.origin; |
| 886 | − let drag = drag_listener(view_state, cursor_offset, cx); | |
| 862 | + let drag = drag_listener(cursor_offset, cx); | |
| 887 | 863 | cx.active_drag = Some(drag); |
| 888 | 864 | cx.notify(); |
| 889 | 865 | cx.stop_propagation(); |
| @@ -891,21 +867,21 @@ where · 1 unmodified line | ||
| 891 | 867 | }); |
| 892 | 868 | } |
| 893 | 869 | |
| 894 | − cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| { | |
| 870 | + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { | |
| 895 | 871 | if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { |
| 896 | 872 | let mouse_click = ClickEvent { |
| 897 | 873 | down: mouse_down.clone(), |
| 898 | 874 | up: event.clone(), |
| 899 | 875 | }; |
| 900 | 876 | for listener in &click_listeners { |
| 901 | − listener(view_state, &mouse_click, cx); | |
| 877 | + listener(&mouse_click, cx); | |
| 902 | 878 | } |
| 903 | 879 | } |
| 904 | 880 | *pending_mouse_down.borrow_mut() = None; |
| 905 | 881 | cx.notify(); |
| 906 | 882 | }); |
| 907 | 883 | } else { |
| 908 | − cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| { | |
| 884 | + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { | |
| 909 | 885 | if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { |
| 910 | 886 | *pending_mouse_down.borrow_mut() = Some(event.clone()); |
| 911 | 887 | cx.notify(); |
| @@ -918,7 +894,7 @@ where · 6 unmodified lines | ||
| 918 | 894 | let was_hovered = element_state.hover_state.clone(); |
| 919 | 895 | let has_mouse_down = element_state.pending_mouse_down.clone(); |
| 920 | 896 | |
| 921 | − cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { | |
| 897 | + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { | |
| 922 | 898 | if phase != DispatchPhase::Bubble { |
| 923 | 899 | return; |
| 924 | 900 | } |
| @@ -930,7 +906,7 @@ where · 5 unmodified lines | ||
| 930 | 906 | *was_hovered = is_hovered; |
| 931 | 907 | drop(was_hovered); |
| 932 | 908 | |
| 933 | − hover_listener(view_state, is_hovered, cx); | |
| 909 | + hover_listener(&is_hovered, cx); | |
| 934 | 910 | } |
| 935 | 911 | }); |
| 936 | 912 | } |
| @@ -939,7 +915,7 @@ where · 2 unmodified lines | ||
| 939 | 915 | let active_tooltip = element_state.active_tooltip.clone(); |
| 940 | 916 | let pending_mouse_down = element_state.pending_mouse_down.clone(); |
| 941 | 917 | |
| 942 | − cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { | |
| 918 | + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { | |
| 943 | 919 | if phase != DispatchPhase::Bubble { |
| 944 | 920 | return; |
| 945 | 921 | } |
| @@ -956,12 +932,12 @@ where · 10 unmodified lines | ||
| 956 | 932 | let active_tooltip = active_tooltip.clone(); |
| 957 | 933 | let tooltip_builder = tooltip_builder.clone(); |
| 958 | 934 | |
| 959 | − move |view, mut cx| async move { | |
| 935 | + move |mut cx| async move { | |
| 960 | 936 | cx.background_executor().timer(TOOLTIP_DELAY).await; |
| 961 | − view.update(&mut cx, move |view_state, cx| { | |
| 937 | + cx.update(|_, cx| { | |
| 962 | 938 | active_tooltip.borrow_mut().replace(ActiveTooltip { |
| 963 | 939 | tooltip: Some(AnyTooltip { |
| 964 | − view: tooltip_builder(view_state, cx), | |
| 940 | + view: tooltip_builder(cx), | |
| 965 | 941 | cursor_offset: cx.mouse_position(), |
| 966 | 942 | }), |
| 967 | 943 | _task: None, |
| @@ -979,7 +955,7 @@ where · 11 unmodified lines | ||
| 979 | 955 | }); |
| 980 | 956 | |
| 981 | 957 | let active_tooltip = element_state.active_tooltip.clone(); |
| 982 | − cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| { | |
| 958 | + cx.on_mouse_event(move |_: &MouseDownEvent, _, _| { | |
| 983 | 959 | active_tooltip.borrow_mut().take(); |
| 984 | 960 | }); |
| 985 | 961 | |
| @@ -992,7 +968,7 @@ where · 6 unmodified lines | ||
| 992 | 968 | |
| 993 | 969 | let active_state = element_state.clicked_state.clone(); |
| 994 | 970 | if !active_state.borrow().is_clicked() { |
| 995 | − cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| { | |
| 971 | + cx.on_mouse_event(move |_: &MouseUpEvent, phase, cx| { | |
| 996 | 972 | if phase == DispatchPhase::Capture { |
| 997 | 973 | *active_state.borrow_mut() = ElementClickedState::default(); |
| 998 | 974 | cx.notify(); |
| @@ -1003,7 +979,7 @@ where · 4 unmodified lines | ||
| 1003 | 979 | .group_active_style |
| 1004 | 980 | .as_ref() |
| 1005 | 981 | .and_then(|group_active| GroupBounds::get(&group_active.group, cx)); |
| 1006 | − cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| { | |
| 982 | + cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| { | |
| 1007 | 983 | if phase == DispatchPhase::Bubble { |
| 1008 | 984 | let group = active_group_bounds |
| 1009 | 985 | .map_or(false, |bounds| bounds.contains_point(&down.position)); |
| @@ -1025,7 +1001,7 @@ where · 15 unmodified lines | ||
| 1025 | 1001 | let line_height = cx.line_height(); |
| 1026 | 1002 | let scroll_max = (content_size - bounds.size).max(&Size::default()); |
| 1027 | 1003 | |
| 1028 | − cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| { | |
| 1004 | + cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { | |
| 1029 | 1005 | if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { |
| 1030 | 1006 | let mut scroll_offset = scroll_offset.borrow_mut(); |
| 1031 | 1007 | let old_scroll_offset = *scroll_offset; |
| @@ -1063,27 +1039,25 @@ where · 31 unmodified lines | ||
| 1063 | 1039 | element_state.focus_handle.clone(), |
| 1064 | 1040 | |_, cx| { |
| 1065 | 1041 | for listener in self.key_down_listeners.drain(..) { |
| 1066 | − cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { | |
| 1067 | − listener(state, event, phase, cx); | |
| 1042 | + cx.on_key_event(move |event: &KeyDownEvent, phase, cx| { | |
| 1043 | + listener(event, phase, cx); | |
| 1068 | 1044 | }) |
| 1069 | 1045 | } |
| 1070 | 1046 | |
| 1071 | 1047 | for listener in self.key_up_listeners.drain(..) { |
| 1072 | − cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { | |
| 1073 | − listener(state, event, phase, cx); | |
| 1048 | + cx.on_key_event(move |event: &KeyUpEvent, phase, cx| { | |
| 1049 | + listener(event, phase, cx); | |
| 1074 | 1050 | }) |
| 1075 | 1051 | } |
| 1076 | 1052 | |
| 1077 | − for (action_type, listener) in self.action_listeners.drain(..) { | |
| 1053 | + for (action_type, listener) in self.action_listeners { | |
| 1078 | 1054 | cx.on_action(action_type, listener) |
| 1079 | 1055 | } |
| 1080 | 1056 | |
| 1081 | 1057 | if let Some(focus_handle) = element_state.focus_handle.as_ref() { |
| 1082 | − for listener in self.focus_listeners.drain(..) { | |
| 1058 | + for listener in self.focus_listeners { | |
| 1083 | 1059 | let focus_handle = focus_handle.clone(); |
| 1084 | − cx.on_focus_changed(move |view, event, cx| { | |
| 1085 | − listener(view, &focus_handle, event, cx) | |
| 1086 | − }); | |
| 1060 | + cx.on_focus_changed(move |event, cx| listener(&focus_handle, event, cx)); | |
| 1087 | 1061 | } |
| 1088 | 1062 | } |
| 1089 | 1063 | |
| @@ -1100,7 +1074,7 @@ where · 10 unmodified lines | ||
| 1100 | 1074 | &self, |
| 1101 | 1075 | bounds: Option<Bounds<Pixels>>, |
| 1102 | 1076 | element_state: &mut InteractiveElementState, |
| 1103 | − cx: &mut ViewContext<V>, | |
| 1077 | + cx: &mut WindowContext, | |
| 1104 | 1078 | ) -> Style { |
| 1105 | 1079 | let mut style = Style::default(); |
| 1106 | 1080 | style.refine(&self.base_style); |
| @@ -1171,7 +1145,7 @@ where · 64 unmodified lines | ||
| 1171 | 1145 | } |
| 1172 | 1146 | } |
| 1173 | 1147 | |
| 1174 | −impl<V: 'static> Default for Interactivity<V> { | |
| 1148 | +impl Default for Interactivity { | |
| 1175 | 1149 | fn default() -> Self { |
| 1176 | 1150 | Self { |
| 1177 | 1151 | element_id: None, |
| @@ -1259,31 +1233,25 @@ impl GroupBounds { · 81 unmodified lines | ||
| 1259 | 1233 | } |
| 1260 | 1234 | } |
| 1261 | 1235 | |
| 1262 | −pub struct Focusable<V, E> { | |
| 1236 | +pub struct Focusable<E> { | |
| 1263 | 1237 | element: E, |
| 1264 | − view_type: PhantomData<V>, | |
| 1265 | 1238 | } |
| 1266 | 1239 | |
| 1267 | −impl<V: 'static, E: InteractiveComponent<V>> FocusableComponent<V> for Focusable<V, E> {} | |
| 1240 | +impl<E: InteractiveElement> FocusableElement for Focusable<E> {} | |
| 1268 | 1241 | |
| 1269 | −impl<V, E> InteractiveComponent<V> for Focusable<V, E> | |
| 1242 | +impl<E> InteractiveElement for Focusable<E> | |
| 1270 | 1243 | where |
| 1271 | − V: 'static, | |
| 1272 | − E: InteractiveComponent<V>, | |
| 1244 | + E: InteractiveElement, | |
| 1273 | 1245 | { |
| 1274 | − fn interactivity(&mut self) -> &mut Interactivity<V> { | |
| 1246 | + fn interactivity(&mut self) -> &mut Interactivity { | |
| 1275 | 1247 | self.element.interactivity() |
| 1276 | 1248 | } |
| 1277 | 1249 | } |
| 1278 | 1250 | |
| 1279 | −impl<V: 'static, E: StatefulInteractiveComponent<V, E>> StatefulInteractiveComponent<V, E> | |
| 1280 | − for Focusable<V, E> | |
| 1281 | −{ | |
| 1282 | −} | |
| 1251 | +impl<E: StatefulInteractiveElement> StatefulInteractiveElement for Focusable<E> {} | |
| 1283 | 1252 | |
| 1284 | −impl<V, E> Styled for Focusable<V, E> | |
| 1253 | +impl<E> Styled for Focusable<E> | |
| 1285 | 1254 | where |
| 1286 | − V: 'static, | |
| 1287 | 1255 | E: Styled, |
| 1288 | 1256 | { |
| 1289 | 1257 | fn style(&mut self) -> &mut StyleRefinement { |
| @@ -1291,65 +1259,55 @@ where · 1 unmodified line | ||
| 1291 | 1259 | } |
| 1292 | 1260 | } |
| 1293 | 1261 | |
| 1294 | −impl<V, E> Element<V> for Focusable<V, E> | |
| 1262 | +impl<E> Element for Focusable<E> | |
| 1295 | 1263 | where |
| 1296 | − V: 'static, | |
| 1297 | − E: Element<V>, | |
| 1264 | + E: Element, | |
| 1298 | 1265 | { |
| 1299 | − type ElementState = E::ElementState; | |
| 1300 | − | |
| 1301 | − fn element_id(&self) -> Option<ElementId> { | |
| 1302 | − self.element.element_id() | |
| 1303 | − } | |
| 1266 | + type State = E::State; | |
| 1304 | 1267 | |
| 1305 | 1268 | fn layout( |
| 1306 | 1269 | &mut self, |
| 1307 | − view_state: &mut V, | |
| 1308 | − element_state: Option<Self::ElementState>, | |
| 1309 | − cx: &mut ViewContext<V>, | |
| 1310 | − ) -> (LayoutId, Self::ElementState) { | |
| 1311 | − self.element.layout(view_state, element_state, cx) | |
| 1270 | + state: Option<Self::State>, | |
| 1271 | + cx: &mut WindowContext, | |
| 1272 | + ) -> (LayoutId, Self::State) { | |
| 1273 | + self.element.layout(state, cx) | |
| 1312 | 1274 | } |
| 1313 | 1275 | |
| 1314 | − fn paint( | |
| 1315 | − &mut self, | |
| 1316 | − bounds: Bounds<Pixels>, | |
| 1317 | − view_state: &mut V, | |
| 1318 | − element_state: &mut Self::ElementState, | |
| 1319 | − cx: &mut ViewContext<V>, | |
| 1320 | − ) { | |
| 1321 | − self.element.paint(bounds, view_state, element_state, cx); | |
| 1276 | + fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) { | |
| 1277 | + self.element.paint(bounds, state, cx) | |
| 1322 | 1278 | } |
| 1323 | 1279 | } |
| 1324 | 1280 | |
| 1325 | −impl<V, E> Component<V> for Focusable<V, E> | |
| 1281 | +impl<E> IntoElement for Focusable<E> | |
| 1326 | 1282 | where |
| 1327 | − V: 'static, | |
| 1328 | − E: 'static + Element<V>, | |
| 1283 | + E: Element, | |
| 1329 | 1284 | { |
| 1330 | − fn render(self) -> AnyElement<V> { | |
| 1331 | − AnyElement::new(self) | |
| 1285 | + type Element = E; | |
| 1286 | + | |
| 1287 | + fn element_id(&self) -> Option<ElementId> { | |
| 1288 | + self.element.element_id() | |
| 1289 | + } | |
| 1290 | + | |
| 1291 | + fn into_element(self) -> Self::Element { | |
| 1292 | + self.element | |
| 1332 | 1293 | } |
| 1333 | 1294 | } |
| 1334 | 1295 | |
| 1335 | −impl<V, E> ParentComponent<V> for Focusable<V, E> | |
| 1296 | +impl<E> ParentElement for Focusable<E> | |
| 1336 | 1297 | where |
| 1337 | − V: 'static, | |
| 1338 | − E: ParentComponent<V>, | |
| 1298 | + E: ParentElement, | |
| 1339 | 1299 | { |
| 1340 | − fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> { | |
| 1300 | + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { | |
| 1341 | 1301 | self.element.children_mut() |
| 1342 | 1302 | } |
| 1343 | 1303 | } |
| 1344 | 1304 | |
| 1345 | −pub struct Stateful<V, E> { | |
| 1305 | +pub struct Stateful<E> { | |
| 1346 | 1306 | element: E, |
| 1347 | − view_type: PhantomData<V>, | |
| 1348 | 1307 | } |
| 1349 | 1308 | |
| 1350 | −impl<V, E> Styled for Stateful<V, E> | |
| 1309 | +impl<E> Styled for Stateful<E> | |
| 1351 | 1310 | where |
| 1352 | − V: 'static, | |
| 1353 | 1311 | E: Styled, |
| 1354 | 1312 | { |
| 1355 | 1313 | fn style(&mut self) -> &mut StyleRefinement { |
| @@ -1357,73 +1315,63 @@ where · 1 unmodified line | ||
| 1357 | 1315 | } |
| 1358 | 1316 | } |
| 1359 | 1317 | |
| 1360 | −impl<V, E> StatefulInteractiveComponent<V, E> for Stateful<V, E> | |
| 1318 | +impl<E> StatefulInteractiveElement for Stateful<E> | |
| 1361 | 1319 | where |
| 1362 | − V: 'static, | |
| 1363 | − E: Element<V>, | |
| 1364 | − Self: InteractiveComponent<V>, | |
| 1320 | + E: Element, | |
| 1321 | + Self: InteractiveElement, | |
| 1365 | 1322 | { |
| 1366 | 1323 | } |
| 1367 | 1324 | |
| 1368 | −impl<V, E> InteractiveComponent<V> for Stateful<V, E> | |
| 1325 | +impl<E> InteractiveElement for Stateful<E> | |
| 1369 | 1326 | where |
| 1370 | − V: 'static, | |
| 1371 | − E: InteractiveComponent<V>, | |
| 1327 | + E: InteractiveElement, | |
| 1372 | 1328 | { |
| 1373 | − fn interactivity(&mut self) -> &mut Interactivity<V> { | |
| 1329 | + fn interactivity(&mut self) -> &mut Interactivity { | |
| 1374 | 1330 | self.element.interactivity() |
| 1375 | 1331 | } |
| 1376 | 1332 | } |
| 1377 | 1333 | |
| 1378 | −impl<V: 'static, E: FocusableComponent<V>> FocusableComponent<V> for Stateful<V, E> {} | |
| 1334 | +impl<E: FocusableElement> FocusableElement for Stateful<E> {} | |
| 1379 | 1335 | |
| 1380 | −impl<V, E> Element<V> for Stateful<V, E> | |
| 1336 | +impl<E> Element for Stateful<E> | |
| 1381 | 1337 | where |
| 1382 | − V: 'static, | |
| 1383 | − E: Element<V>, | |
| 1338 | + E: Element, | |
| 1384 | 1339 | { |
| 1385 | − type ElementState = E::ElementState; | |
| 1386 | − | |
| 1387 | − fn element_id(&self) -> Option<ElementId> { | |
| 1388 | − self.element.element_id() | |
| 1389 | − } | |
| 1340 | + type State = E::State; | |
| 1390 | 1341 | |
| 1391 | 1342 | fn layout( |
| 1392 | 1343 | &mut self, |
| 1393 | − view_state: &mut V, | |
| 1394 | − element_state: Option<Self::ElementState>, | |
| 1395 | − cx: &mut ViewContext<V>, | |
| 1396 | − ) -> (LayoutId, Self::ElementState) { | |
| 1397 | − self.element.layout(view_state, element_state, cx) | |
| 1344 | + state: Option<Self::State>, | |
| 1345 | + cx: &mut WindowContext, | |
| 1346 | + ) -> (LayoutId, Self::State) { | |
| 1347 | + self.element.layout(state, cx) | |
| 1398 | 1348 | } |
| 1399 | 1349 | |
| 1400 | − fn paint( | |
| 1401 | − &mut self, | |
| 1402 | − bounds: Bounds<Pixels>, | |
| 1403 | − view_state: &mut V, | |
| 1404 | − element_state: &mut Self::ElementState, | |
| 1405 | − cx: &mut ViewContext<V>, | |
| 1406 | − ) { | |
| 1407 | − self.element.paint(bounds, view_state, element_state, cx) | |
| 1350 | + fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) { | |
| 1351 | + self.element.paint(bounds, state, cx) | |
| 1408 | 1352 | } |
| 1409 | 1353 | } |
| 1410 | 1354 | |
| 1411 | −impl<V, E> Component<V> for Stateful<V, E> | |
| 1355 | +impl<E> IntoElement for Stateful<E> | |
| 1412 | 1356 | where |
| 1413 | − V: 'static, | |
| 1414 | − E: 'static + Element<V>, | |
| 1357 | + E: Element, | |
| 1415 | 1358 | { |
| 1416 | − fn render(self) -> AnyElement<V> { | |
| 1417 | − AnyElement::new(self) | |
| 1359 | + type Element = Self; | |
| 1360 | + | |
| 1361 | + fn element_id(&self) -> Option<ElementId> { | |
| 1362 | + self.element.element_id() | |
| 1363 | + } | |
| 1364 | + | |
| 1365 | + fn into_element(self) -> Self::Element { | |
| 1366 | + self | |
| 1418 | 1367 | } |
| 1419 | 1368 | } |
| 1420 | 1369 | |
| 1421 | −impl<V, E> ParentComponent<V> for Stateful<V, E> | |
| 1370 | +impl<E> ParentElement for Stateful<E> | |
| 1422 | 1371 | where |
| 1423 | − V: 'static, | |
| 1424 | − E: ParentComponent<V>, | |
| 1372 | + E: ParentElement, | |
| 1425 | 1373 | { |
| 1426 | − fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> { | |
| 1374 | + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { | |
| 1427 | 1375 | self.element.children_mut() |
| 1428 | 1376 | } |
| 1429 | 1377 | } |
| @@ -1,70 +1,83 @@ | ||
| 1 | +use std::sync::Arc; | |
| 2 | + | |
| 1 | 3 | use crate::{ |
| 2 | − AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent, | |
| 3 | − InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, | |
| 4 | − Styled, ViewContext, | |
| 4 | + Bounds, Element, ImageData, InteractiveElement, InteractiveElementState, Interactivity, | |
| 5 | + IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext, | |
| 5 | 6 | }; |
| 6 | 7 | use futures::FutureExt; |
| 7 | 8 | use util::ResultExt; |
| 8 | 9 | |
| 9 | −pub struct Img<V: 'static> { | |
| 10 | − interactivity: Interactivity<V>, | |
| 11 | − uri: Option<SharedString>, | |
| 10 | +#[derive(Clone, Debug)] | |
| 11 | +pub enum ImageSource { | |
| 12 | + /// Image content will be loaded from provided URI at render time. | |
| 13 | + Uri(SharedString), | |
| 14 | + Data(Arc<ImageData>), | |
| 15 | +} | |
| 16 | + | |
| 17 | +impl From<SharedString> for ImageSource { | |
| 18 | + fn from(value: SharedString) -> Self { | |
| 19 | + Self::Uri(value) | |
| 20 | + } | |
| 21 | +} | |
| 22 | + | |
| 23 | +impl From<Arc<ImageData>> for ImageSource { | |
| 24 | + fn from(value: Arc<ImageData>) -> Self { | |
| 25 | + Self::Data(value) | |
| 26 | + } | |
| 27 | +} | |
| 28 | + | |
| 29 | +pub struct Img { | |
| 30 | + interactivity: Interactivity, | |
| 31 | + source: Option<ImageSource>, | |
| 12 | 32 | grayscale: bool, |
| 13 | 33 | } |
| 14 | 34 | |
| 15 | −pub fn img<V: 'static>() -> Img<V> { | |
| 35 | +pub fn img() -> Img { | |
| 16 | 36 | Img { |
| 17 | 37 | interactivity: Interactivity::default(), |
| 18 | − uri: None, | |
| 38 | + source: None, | |
| 19 | 39 | grayscale: false, |
| 20 | 40 | } |
| 21 | 41 | } |
| 22 | 42 | |
| 23 | −impl<V> Img<V> | |
| 24 | −where | |
| 25 | − V: 'static, | |
| 26 | −{ | |
| 43 | +impl Img { | |
| 27 | 44 | pub fn uri(mut self, uri: impl Into<SharedString>) -> Self { |
| 28 | − self.uri = Some(uri.into()); | |
| 45 | + self.source = Some(ImageSource::from(uri.into())); | |
| 46 | + self | |
| 47 | + } | |
| 48 | + pub fn data(mut self, data: Arc<ImageData>) -> Self { | |
| 49 | + self.source = Some(ImageSource::from(data)); | |
| 29 | 50 | self |
| 30 | 51 | } |
| 31 | 52 | |
| 53 | + pub fn source(mut self, source: impl Into<ImageSource>) -> Self { | |
| 54 | + self.source = Some(source.into()); | |
| 55 | + self | |
| 56 | + } | |
| 32 | 57 | pub fn grayscale(mut self, grayscale: bool) -> Self { |
| 33 | 58 | self.grayscale = grayscale; |
| 34 | 59 | self |
| 35 | 60 | } |
| 36 | 61 | } |
| 37 | 62 | |
| 38 | −impl<V> Component<V> for Img<V> { | |
| 39 | − fn render(self) -> AnyElement<V> { | |
| 40 | − AnyElement::new(self) | |
| 41 | − } | |
| 42 | −} | |
| 43 | − | |
| 44 | −impl<V> Element<V> for Img<V> { | |
| 45 | − type ElementState = InteractiveElementState; | |
| 46 | − | |
| 47 | − fn element_id(&self) -> Option<crate::ElementId> { | |
| 48 | − self.interactivity.element_id.clone() | |
| 49 | − } | |
| 63 | +impl Element for Img { | |
| 64 | + type State = InteractiveElementState; | |
| 50 | 65 | |
| 51 | 66 | fn layout( |
| 52 | 67 | &mut self, |
| 53 | − _view_state: &mut V, | |
| 54 | − element_state: Option<Self::ElementState>, | |
| 55 | − cx: &mut ViewContext<V>, | |
| 56 | − ) -> (LayoutId, Self::ElementState) { | |
| 68 | + element_state: Option<Self::State>, | |
| 69 | + cx: &mut WindowContext, | |
| 70 | + ) -> (LayoutId, Self::State) { | |
| 57 | 71 | self.interactivity.layout(element_state, cx, |style, cx| { |
| 58 | 72 | cx.request_layout(&style, None) |
| 59 | 73 | }) |
| 60 | 74 | } |
| 61 | 75 | |
| 62 | 76 | fn paint( |
| 63 | − &mut self, | |
| 77 | + self, | |
| 64 | 78 | bounds: Bounds<Pixels>, |
| 65 | − _view_state: &mut V, | |
| 66 | − element_state: &mut Self::ElementState, | |
| 67 | − cx: &mut ViewContext<V>, | |
| 79 | + element_state: &mut Self::State, | |
| 80 | + cx: &mut WindowContext, | |
| 68 | 81 | ) { |
| 69 | 82 | self.interactivity.paint( |
| 70 | 83 | bounds, |
| @@ -74,42 +87,59 @@ impl<V> Element<V> for Img<V> { · 3 unmodified lines | ||
| 74 | 87 | |style, _scroll_offset, cx| { |
| 75 | 88 | let corner_radii = style.corner_radii; |
| 76 | 89 | |
| 77 | − if let Some(uri) = self.uri.clone() { | |
| 78 | − // eprintln!(">>> image_cache.get({uri}"); | |
| 79 | − let image_future = cx.image_cache.get(uri.clone()); | |
| 80 | − // eprintln!("<<< image_cache.get({uri}"); | |
| 81 | − if let Some(data) = image_future | |
| 82 | − .clone() | |
| 83 | − .now_or_never() | |
| 84 | − .and_then(|result| result.ok()) | |
| 85 | − { | |
| 86 | − let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); | |
| 87 | − cx.with_z_index(1, |cx| { | |
| 88 | − cx.paint_image(bounds, corner_radii, data, self.grayscale) | |
| 89 | − .log_err() | |
| 90 | − }); | |
| 91 | − } else { | |
| 92 | − cx.spawn(|_, mut cx| async move { | |
| 93 | − if image_future.await.ok().is_some() { | |
| 94 | − cx.on_next_frame(|cx| cx.notify()); | |
| 90 | + if let Some(source) = self.source { | |
| 91 | + let image = match source { | |
| 92 | + ImageSource::Uri(uri) => { | |
| 93 | + let image_future = cx.image_cache.get(uri.clone()); | |
| 94 | + if let Some(data) = image_future | |
| 95 | + .clone() | |
| 96 | + .now_or_never() | |
| 97 | + .and_then(|result| result.ok()) | |
| 98 | + { | |
| 99 | + data | |
| 100 | + } else { | |
| 101 | + cx.spawn(|mut cx| async move { | |
| 102 | + if image_future.await.ok().is_some() { | |
| 103 | + cx.on_next_frame(|cx| cx.notify()); | |
| 104 | + } | |
| 105 | + }) | |
| 106 | + .detach(); | |
| 107 | + return; | |
| 95 | 108 | } |
| 96 | − }) | |
| 97 | − .detach() | |
| 98 | − } | |
| 109 | + } | |
| 110 | + ImageSource::Data(image) => image, | |
| 111 | + }; | |
| 112 | + let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); | |
| 113 | + cx.with_z_index(1, |cx| { | |
| 114 | + cx.paint_image(bounds, corner_radii, image, self.grayscale) | |
| 115 | + .log_err() | |
| 116 | + }); | |
| 99 | 117 | } |
| 100 | 118 | }, |
| 101 | 119 | ) |
| 102 | 120 | } |
| 103 | 121 | } |
| 104 | 122 | |
| 105 | −impl<V> Styled for Img<V> { | |
| 123 | +impl IntoElement for Img { | |
| 124 | + type Element = Self; | |
| 125 | + | |
| 126 | + fn element_id(&self) -> Option<crate::ElementId> { | |
| 127 | + self.interactivity.element_id.clone() | |
| 128 | + } | |
| 129 | + | |
| 130 | + fn into_element(self) -> Self::Element { | |
| 131 | + self | |
| 132 | + } | |
| 133 | +} | |
| 134 | + | |
| 135 | +impl Styled for Img { | |
| 106 | 136 | fn style(&mut self) -> &mut StyleRefinement { |
| 107 | 137 | &mut self.interactivity.base_style |
| 108 | 138 | } |
| 109 | 139 | } |
| 110 | 140 | |
| 111 | −impl<V> InteractiveComponent<V> for Img<V> { | |
| 112 | − fn interactivity(&mut self) -> &mut Interactivity<V> { | |
| 141 | +impl InteractiveElement for Img { | |
| 142 | + fn interactivity(&mut self) -> &mut Interactivity { | |
| 113 | 143 | &mut self.interactivity |
| 114 | 144 | } |
| 115 | 145 | } |
| @@ -2,16 +2,16 @@ use smallvec::SmallVec; · 1 unmodified line | ||
| 2 | 2 | use taffy::style::{Display, Position}; |
| 3 | 3 | |
| 4 | 4 | use crate::{ |
| 5 | − point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels, | |
| 6 | − Point, Size, Style, | |
| 5 | + point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels, | |
| 6 | + Point, Size, Style, WindowContext, | |
| 7 | 7 | }; |
| 8 | 8 | |
| 9 | 9 | pub struct OverlayState { |
| 10 | 10 | child_layout_ids: SmallVec<[LayoutId; 4]>, |
| 11 | 11 | } |
| 12 | 12 | |
| 13 | −pub struct Overlay<V> { | |
| 14 | − children: SmallVec<[AnyElement<V>; 2]>, | |
| 13 | +pub struct Overlay { | |
| 14 | + children: SmallVec<[AnyElement; 2]>, | |
| 15 | 15 | anchor_corner: AnchorCorner, |
| 16 | 16 | fit_mode: OverlayFitMode, |
| 17 | 17 | // todo!(); |
| @@ -21,7 +21,7 @@ pub struct Overlay<V> { · 3 unmodified lines | ||
| 21 | 21 | |
| 22 | 22 | /// overlay gives you a floating element that will avoid overflowing the window bounds. |
| 23 | 23 | /// Its children should have no margin to avoid measurement issues. |
| 24 | −pub fn overlay<V: 'static>() -> Overlay<V> { | |
| 24 | +pub fn overlay() -> Overlay { | |
| 25 | 25 | Overlay { |
| 26 | 26 | children: SmallVec::new(), |
| 27 | 27 | anchor_corner: AnchorCorner::TopLeft, |
| @@ -30,7 +30,7 @@ pub fn overlay<V: 'static>() -> Overlay<V> { · 2 unmodified lines | ||
| 30 | 30 | } |
| 31 | 31 | } |
| 32 | 32 | |
| 33 | −impl<V> Overlay<V> { | |
| 33 | +impl Overlay { | |
| 34 | 34 | /// Sets which corner of the overlay should be anchored to the current position. |
| 35 | 35 | pub fn anchor(mut self, anchor: AnchorCorner) -> Self { |
| 36 | 36 | self.anchor_corner = anchor; |
| @@ -51,35 +51,24 @@ impl<V> Overlay<V> { · 14 unmodified lines | ||
| 51 | 51 | } |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | −impl<V: 'static> ParentComponent<V> for Overlay<V> { | |
| 55 | − fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> { | |
| 54 | +impl ParentElement for Overlay { | |
| 55 | + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { | |
| 56 | 56 | &mut self.children |
| 57 | 57 | } |
| 58 | 58 | } |
| 59 | 59 | |
| 60 | −impl<V: 'static> Component<V> for Overlay<V> { | |
| 61 | − fn render(self) -> AnyElement<V> { | |
| 62 | − AnyElement::new(self) | |
| 63 | − } | |
| 64 | −} | |
| 65 | − | |
| 66 | −impl<V: 'static> Element<V> for Overlay<V> { | |
| 67 | − type ElementState = OverlayState; | |
| 68 | − | |
| 69 | − fn element_id(&self) -> Option<crate::ElementId> { | |
| 70 | − None | |
| 71 | − } | |
| 60 | +impl Element for Overlay { | |
| 61 | + type State = OverlayState; | |
| 72 | 62 | |
| 73 | 63 | fn layout( |
| 74 | 64 | &mut self, |
| 75 | − view_state: &mut V, | |
| 76 | − _: Option<Self::ElementState>, | |
| 77 | − cx: &mut crate::ViewContext<V>, | |
| 78 | − ) -> (crate::LayoutId, Self::ElementState) { | |
| 65 | + _: Option<Self::State>, | |
| 66 | + cx: &mut WindowContext, | |
| 67 | + ) -> (crate::LayoutId, Self::State) { | |
| 79 | 68 | let child_layout_ids = self |
| 80 | 69 | .children |
| 81 | 70 | .iter_mut() |
| 82 | − .map(|child| child.layout(view_state, cx)) | |
| 71 | + .map(|child| child.layout(cx)) | |
| 83 | 72 | .collect::<SmallVec<_>>(); |
| 84 | 73 | |
| 85 | 74 | let mut overlay_style = Style::default(); |
| @@ -92,11 +81,10 @@ impl<V: 'static> Element<V> for Overlay<V> { · 6 unmodified lines | ||
| 92 | 81 | } |
| 93 | 82 | |
| 94 | 83 | fn paint( |
| 95 | − &mut self, | |
| 84 | + self, | |
| 96 | 85 | bounds: crate::Bounds<crate::Pixels>, |
| 97 | − view_state: &mut V, | |
| 98 | − element_state: &mut Self::ElementState, | |
| 99 | − cx: &mut crate::ViewContext<V>, | |
| 86 | + element_state: &mut Self::State, | |
| 87 | + cx: &mut WindowContext, | |
| 100 | 88 | ) { |
| 101 | 89 | if element_state.child_layout_ids.is_empty() { |
| 102 | 90 | return; |
| @@ -117,6 +105,7 @@ impl<V: 'static> Element<V> for Overlay<V> { · 14 unmodified lines | ||
| 117 | 105 | origin: Point::zero(), |
| 118 | 106 | size: cx.viewport_size(), |
| 119 | 107 | }; |
| 108 | + dbg!(limits); | |
| 120 | 109 | |
| 121 | 110 | match self.fit_mode { |
| 122 | 111 | OverlayFitMode::SnapToWindow => { |
| @@ -156,13 +145,27 @@ impl<V: 'static> Element<V> for Overlay<V> { · 33 unmodified lines | ||
| 156 | 145 | } |
| 157 | 146 | |
| 158 | 147 | cx.with_element_offset(desired.origin - bounds.origin, |cx| { |
| 159 | − for child in &mut self.children { | |
| 160 | − child.paint(view_state, cx); | |
| 161 | − } | |
| 148 | + cx.break_content_mask(|cx| { | |
| 149 | + for child in self.children { | |
| 150 | + child.paint(cx); | |
| 151 | + } | |
| 152 | + }) | |
| 162 | 153 | }) |
| 163 | 154 | } |
| 164 | 155 | } |
| 165 | 156 | |
| 157 | +impl IntoElement for Overlay { | |
| 158 | + type Element = Self; | |
| 159 | + | |
| 160 | + fn element_id(&self) -> Option<crate::ElementId> { | |
| 161 | + None | |
| 162 | + } | |
| 163 | + | |
| 164 | + fn into_element(self) -> Self::Element { | |
| 165 | + self | |
| 166 | + } | |
| 167 | +} | |
| 168 | + | |
| 166 | 169 | enum Axis { |
| 167 | 170 | Horizontal, |
| 168 | 171 | Vertical, |
| @@ -1,60 +1,43 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent, | |
| 3 | − InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, | |
| 4 | − Styled, ViewContext, | |
| 2 | + Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity, | |
| 3 | + IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext, | |
| 5 | 4 | }; |
| 6 | 5 | use util::ResultExt; |
| 7 | 6 | |
| 8 | −pub struct Svg<V: 'static> { | |
| 9 | − interactivity: Interactivity<V>, | |
| 7 | +pub struct Svg { | |
| 8 | + interactivity: Interactivity, | |
| 10 | 9 | path: Option<SharedString>, |
| 11 | 10 | } |
| 12 | 11 | |
| 13 | −pub fn svg<V: 'static>() -> Svg<V> { | |
| 12 | +pub fn svg() -> Svg { | |
| 14 | 13 | Svg { |
| 15 | 14 | interactivity: Interactivity::default(), |
| 16 | 15 | path: None, |
| 17 | 16 | } |
| 18 | 17 | } |
| 19 | 18 | |
| 20 | −impl<V> Svg<V> { | |
| 19 | +impl Svg { | |
| 21 | 20 | pub fn path(mut self, path: impl Into<SharedString>) -> Self { |
| 22 | 21 | self.path = Some(path.into()); |
| 23 | 22 | self |
| 24 | 23 | } |
| 25 | 24 | } |
| 26 | 25 | |
| 27 | −impl<V> Component<V> for Svg<V> { | |
| 28 | − fn render(self) -> AnyElement<V> { | |
| 29 | − AnyElement::new(self) | |
| 30 | − } | |
| 31 | −} | |
| 32 | − | |
| 33 | −impl<V> Element<V> for Svg<V> { | |
| 34 | − type ElementState = InteractiveElementState; | |
| 35 | − | |
| 36 | − fn element_id(&self) -> Option<ElementId> { | |
| 37 | − self.interactivity.element_id.clone() | |
| 38 | − } | |
| 26 | +impl Element for Svg { | |
| 27 | + type State = InteractiveElementState; | |
| 39 | 28 | |
| 40 | 29 | fn layout( |
| 41 | 30 | &mut self, |
| 42 | − _view_state: &mut V, | |
| 43 | − element_state: Option<Self::ElementState>, | |
| 44 | − cx: &mut ViewContext<V>, | |
| 45 | − ) -> (LayoutId, Self::ElementState) { | |
| 31 | + element_state: Option<Self::State>, | |
| 32 | + cx: &mut WindowContext, | |
| 33 | + ) -> (LayoutId, Self::State) { | |
| 46 | 34 | self.interactivity.layout(element_state, cx, |style, cx| { |
| 47 | 35 | cx.request_layout(&style, None) |
| 48 | 36 | }) |
| 49 | 37 | } |
| 50 | 38 | |
| 51 | − fn paint( | |
| 52 | − &mut self, | |
| 53 | − bounds: Bounds<Pixels>, | |
| 54 | − _view_state: &mut V, | |
| 55 | − element_state: &mut Self::ElementState, | |
| 56 | − cx: &mut ViewContext<V>, | |
| 57 | − ) where | |
| 39 | + fn paint(self, bounds: Bounds<Pixels>, element_state: &mut Self::State, cx: &mut WindowContext) | |
| 40 | + where | |
| 58 | 41 | Self: Sized, |
| 59 | 42 | { |
| 60 | 43 | self.interactivity |
| @@ -66,14 +49,26 @@ impl<V> Element<V> for Svg<V> { · 5 unmodified lines | ||
| 66 | 49 | } |
| 67 | 50 | } |
| 68 | 51 | |
| 69 | −impl<V> Styled for Svg<V> { | |
| 52 | +impl IntoElement for Svg { | |
| 53 | + type Element = Self; | |
| 54 | + | |
| 55 | + fn element_id(&self) -> Option<ElementId> { | |
| 56 | + self.interactivity.element_id.clone() | |
| 57 | + } | |
| 58 | + | |
| 59 | + fn into_element(self) -> Self::Element { | |
| 60 | + self | |
| 61 | + } | |
| 62 | +} | |
| 63 | + | |
| 64 | +impl Styled for Svg { | |
| 70 | 65 | fn style(&mut self) -> &mut StyleRefinement { |
| 71 | 66 | &mut self.interactivity.base_style |
| 72 | 67 | } |
| 73 | 68 | } |
| 74 | 69 | |
| 75 | −impl<V> InteractiveComponent<V> for Svg<V> { | |
| 76 | − fn interactivity(&mut self) -> &mut Interactivity<V> { | |
| 70 | +impl InteractiveElement for Svg { | |
| 71 | + fn interactivity(&mut self) -> &mut Interactivity { | |
| 77 | 72 | &mut self.interactivity |
| 78 | 73 | } |
| 79 | 74 | } |
| @@ -1,74 +1,177 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels, | |
| 3 | − SharedString, Size, TextRun, ViewContext, WrappedLine, | |
| 2 | + Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent, | |
| 3 | + Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine, | |
| 4 | 4 | }; |
| 5 | +use anyhow::anyhow; | |
| 5 | 6 | use parking_lot::{Mutex, MutexGuard}; |
| 6 | 7 | use smallvec::SmallVec; |
| 7 | −use std::{cell::Cell, rc::Rc, sync::Arc}; | |
| 8 | +use std::{cell::Cell, ops::Range, rc::Rc, sync::Arc}; | |
| 8 | 9 | use util::ResultExt; |
| 9 | 10 | |
| 10 | −pub struct Text { | |
| 11 | +impl Element for &'static str { | |
| 12 | + type State = TextState; | |
| 13 | + | |
| 14 | + fn layout( | |
| 15 | + &mut self, | |
| 16 | + _: Option<Self::State>, | |
| 17 | + cx: &mut WindowContext, | |
| 18 | + ) -> (LayoutId, Self::State) { | |
| 19 | + let mut state = TextState::default(); | |
| 20 | + let layout_id = state.layout(SharedString::from(*self), None, cx); | |
| 21 | + (layout_id, state) | |
| 22 | + } | |
| 23 | + | |
| 24 | + fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) { | |
| 25 | + state.paint(bounds, self, cx) | |
| 26 | + } | |
| 27 | +} | |
| 28 | + | |
| 29 | +impl IntoElement for &'static str { | |
| 30 | + type Element = Self; | |
| 31 | + | |
| 32 | + fn element_id(&self) -> Option<ElementId> { | |
| 33 | + None | |
| 34 | + } | |
| 35 | + | |
| 36 | + fn into_element(self) -> Self::Element { | |
| 37 | + self | |
| 38 | + } | |
| 39 | +} | |
| 40 | + | |
| 41 | +impl Element for SharedString { | |
| 42 | + type State = TextState; | |
| 43 | + | |
| 44 | + fn layout( | |
| 45 | + &mut self, | |
| 46 | + _: Option<Self::State>, | |
| 47 | + cx: &mut WindowContext, | |
| 48 | + ) -> (LayoutId, Self::State) { | |
| 49 | + let mut state = TextState::default(); | |
| 50 | + let layout_id = state.layout(self.clone(), None, cx); | |
| 51 | + (layout_id, state) | |
| 52 | + } | |
| 53 | + | |
| 54 | + fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) { | |
| 55 | + let text_str: &str = self.as_ref(); | |
| 56 | + state.paint(bounds, text_str, cx) | |
| 57 | + } | |
| 58 | +} | |
| 59 | + | |
| 60 | +impl IntoElement for SharedString { | |
| 61 | + type Element = Self; | |
| 62 | + | |
| 63 | + fn element_id(&self) -> Option<ElementId> { | |
| 64 | + None | |
| 65 | + } | |
| 66 | + | |
| 67 | + fn into_element(self) -> Self::Element { | |
| 68 | + self | |
| 69 | + } | |
| 70 | +} | |
| 71 | + | |
| 72 | +/// Renders text with runs of different styles. | |
| 73 | +/// | |
| 74 | +/// Callers are responsible for setting the correct style for each run. | |
| 75 | +/// For text with a uniform style, you can usually avoid calling this constructor | |
| 76 | +/// and just pass text directly. | |
| 77 | +pub struct StyledText { | |
| 11 | 78 | text: SharedString, |
| 12 | 79 | runs: Option<Vec<TextRun>>, |
| 13 | 80 | } |
| 14 | 81 | |
| 15 | −impl Text { | |
| 16 | − /// Renders text with runs of different styles. | |
| 17 | − /// | |
| 18 | − /// Callers are responsible for setting the correct style for each run. | |
| 19 | − /// For text with a uniform style, you can usually avoid calling this constructor | |
| 20 | − /// and just pass text directly. | |
| 21 | − pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self { | |
| 22 | − Text { | |
| 23 | − text, | |
| 24 | − runs: Some(runs), | |
| 82 | +impl StyledText { | |
| 83 | + pub fn new(text: impl Into<SharedString>) -> Self { | |
| 84 | + StyledText { | |
| 85 | + text: text.into(), | |
| 86 | + runs: None, | |
| 25 | 87 | } |
| 26 | 88 | } |
| 89 | + | |
| 90 | + pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self { | |
| 91 | + self.runs = Some(runs); | |
| 92 | + self | |
| 93 | + } | |
| 27 | 94 | } |
| 28 | 95 | |
| 29 | −impl<V: 'static> Component<V> for Text { | |
| 30 | − fn render(self) -> AnyElement<V> { | |
| 31 | − AnyElement::new(self) | |
| 96 | +impl Element for StyledText { | |
| 97 | + type State = TextState; | |
| 98 | + | |
| 99 | + fn layout( | |
| 100 | + &mut self, | |
| 101 | + _: Option<Self::State>, | |
| 102 | + cx: &mut WindowContext, | |
| 103 | + ) -> (LayoutId, Self::State) { | |
| 104 | + let mut state = TextState::default(); | |
| 105 | + let layout_id = state.layout(self.text.clone(), self.runs.take(), cx); | |
| 106 | + (layout_id, state) | |
| 107 | + } | |
| 108 | + | |
| 109 | + fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) { | |
| 110 | + state.paint(bounds, &self.text, cx) | |
| 32 | 111 | } |
| 33 | 112 | } |
| 34 | 113 | |
| 35 | −impl<V: 'static> Element<V> for Text { | |
| 36 | − type ElementState = TextState; | |
| 114 | +impl IntoElement for StyledText { | |
| 115 | + type Element = Self; | |
| 37 | 116 | |
| 38 | 117 | fn element_id(&self) -> Option<crate::ElementId> { |
| 39 | 118 | None |
| 40 | 119 | } |
| 41 | 120 | |
| 121 | + fn into_element(self) -> Self::Element { | |
| 122 | + self | |
| 123 | + } | |
| 124 | +} | |
| 125 | + | |
| 126 | +#[derive(Default, Clone)] | |
| 127 | +pub struct TextState(Arc<Mutex<Option<TextStateInner>>>); | |
| 128 | + | |
| 129 | +struct TextStateInner { | |
| 130 | + lines: SmallVec<[WrappedLine; 1]>, | |
| 131 | + line_height: Pixels, | |
| 132 | + wrap_width: Option<Pixels>, | |
| 133 | + size: Option<Size<Pixels>>, | |
| 134 | +} | |
| 135 | + | |
| 136 | +impl TextState { | |
| 137 | + fn lock(&self) -> MutexGuard<Option<TextStateInner>> { | |
| 138 | + self.0.lock() | |
| 139 | + } | |
| 140 | + | |
| 42 | 141 | fn layout( |
| 43 | 142 | &mut self, |
| 44 | − _view: &mut V, | |
| 45 | − element_state: Option<Self::ElementState>, | |
| 46 | − cx: &mut ViewContext<V>, | |
| 47 | − ) -> (LayoutId, Self::ElementState) { | |
| 48 | − let element_state = element_state.unwrap_or_default(); | |
| 143 | + text: SharedString, | |
| 144 | + runs: Option<Vec<TextRun>>, | |
| 145 | + cx: &mut WindowContext, | |
| 146 | + ) -> LayoutId { | |
| 49 | 147 | let text_system = cx.text_system().clone(); |
| 50 | 148 | let text_style = cx.text_style(); |
| 51 | 149 | let font_size = text_style.font_size.to_pixels(cx.rem_size()); |
| 52 | 150 | let line_height = text_style |
| 53 | 151 | .line_height |
| 54 | 152 | .to_pixels(font_size.into(), cx.rem_size()); |
| 55 | − let text = self.text.clone(); | |
| 153 | + let text = SharedString::from(text); | |
| 56 | 154 | |
| 57 | 155 | let rem_size = cx.rem_size(); |
| 58 | 156 | |
| 59 | − let runs = if let Some(runs) = self.runs.take() { | |
| 157 | + let runs = if let Some(runs) = runs { | |
| 60 | 158 | runs |
| 61 | 159 | } else { |
| 62 | 160 | vec![text_style.to_run(text.len())] |
| 63 | 161 | }; |
| 64 | 162 | |
| 65 | 163 | let layout_id = cx.request_measured_layout(Default::default(), rem_size, { |
| 66 | − let element_state = element_state.clone(); | |
| 164 | + let element_state = self.clone(); | |
| 165 | + | |
| 67 | 166 | move |known_dimensions, available_space| { |
| 68 | − let wrap_width = known_dimensions.width.or(match available_space.width { | |
| 69 | − crate::AvailableSpace::Definite(x) => Some(x), | |
| 70 | − _ => None, | |
| 71 | − }); | |
| 167 | + let wrap_width = if text_style.white_space == WhiteSpace::Normal { | |
| 168 | + known_dimensions.width.or(match available_space.width { | |
| 169 | + crate::AvailableSpace::Definite(x) => Some(x), | |
| 170 | + _ => None, | |
| 171 | + }) | |
| 172 | + } else { | |
| 173 | + None | |
| 174 | + }; | |
| 72 | 175 | |
| 73 | 176 | if let Some(text_state) = element_state.0.lock().as_ref() { |
| 74 | 177 | if text_state.size.is_some() |
| @@ -80,10 +183,7 @@ impl<V: 'static> Element<V> for Text { · 5 unmodified lines | ||
| 80 | 183 | |
| 81 | 184 | let Some(lines) = text_system |
| 82 | 185 | .shape_text( |
| 83 | − &text, | |
| 84 | − font_size, | |
| 85 | − &runs[..], | |
| 86 | − wrap_width, // Wrap if we know the width. | |
| 186 | + &text, font_size, &runs, wrap_width, // Wrap if we know the width. | |
| 87 | 187 | ) |
| 88 | 188 | .log_err() |
| 89 | 189 | else { |
| @@ -100,7 +200,7 @@ impl<V: 'static> Element<V> for Text { · 10 unmodified lines | ||
| 100 | 200 | for line in &lines { |
| 101 | 201 | let line_size = line.size(line_height); |
| 102 | 202 | size.height += line_size.height; |
| 103 | − size.width = size.width.max(line_size.width); | |
| 203 | + size.width = size.width.max(line_size.width).ceil(); | |
| 104 | 204 | } |
| 105 | 205 | |
| 106 | 206 | element_state.lock().replace(TextStateInner { |
| @@ -114,20 +214,14 @@ impl<V: 'static> Element<V> for Text { · 7 unmodified lines | ||
| 114 | 214 | } |
| 115 | 215 | }); |
| 116 | 216 | |
| 117 | − (layout_id, element_state) | |
| 217 | + layout_id | |
| 118 | 218 | } |
| 119 | 219 | |
| 120 | − fn paint( | |
| 121 | − &mut self, | |
| 122 | − bounds: Bounds<Pixels>, | |
| 123 | − _: &mut V, | |
| 124 | − element_state: &mut Self::ElementState, | |
| 125 | − cx: &mut ViewContext<V>, | |
| 126 | − ) { | |
| 127 | − let element_state = element_state.lock(); | |
| 220 | + fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) { | |
| 221 | + let element_state = self.lock(); | |
| 128 | 222 | let element_state = element_state |
| 129 | 223 | .as_ref() |
| 130 | − .ok_or_else(|| anyhow::anyhow!("measurement has not been performed on {}", &self.text)) | |
| 224 | + .ok_or_else(|| anyhow!("measurement has not been performed on {}", text)) | |
| 131 | 225 | .unwrap(); |
| 132 | 226 | |
| 133 | 227 | let line_height = element_state.line_height; |
| @@ -137,108 +231,156 @@ impl<V: 'static> Element<V> for Text { · 3 unmodified lines | ||
| 137 | 231 | line_origin.y += line.size(line_height).height; |
| 138 | 232 | } |
| 139 | 233 | } |
| 140 | −} | |
| 141 | 234 | |
| 142 | −#[derive(Default, Clone)] | |
| 143 | −pub struct TextState(Arc<Mutex<Option<TextStateInner>>>); | |
| 235 | + fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> { | |
| 236 | + if !bounds.contains_point(&position) { | |
| 237 | + return None; | |
| 238 | + } | |
| 144 | 239 | |
| 145 | −impl TextState { | |
| 146 | − fn lock(&self) -> MutexGuard<Option<TextStateInner>> { | |
| 147 | − self.0.lock() | |
| 240 | + let element_state = self.lock(); | |
| 241 | + let element_state = element_state | |
| 242 | + .as_ref() | |
| 243 | + .expect("measurement has not been performed"); | |
| 244 | + | |
| 245 | + let line_height = element_state.line_height; | |
| 246 | + let mut line_origin = bounds.origin; | |
| 247 | + let mut line_start_ix = 0; | |
| 248 | + for line in &element_state.lines { | |
| 249 | + let line_bottom = line_origin.y + line.size(line_height).height; | |
| 250 | + if position.y > line_bottom { | |
| 251 | + line_origin.y = line_bottom; | |
| 252 | + line_start_ix += line.len() + 1; | |
| 253 | + } else { | |
| 254 | + let position_within_line = position - line_origin; | |
| 255 | + let index_within_line = | |
| 256 | + line.index_for_position(position_within_line, line_height)?; | |
| 257 | + return Some(line_start_ix + index_within_line); | |
| 258 | + } | |
| 259 | + } | |
| 260 | + | |
| 261 | + None | |
| 148 | 262 | } |
| 149 | 263 | } |
| 150 | 264 | |
| 151 | −struct TextStateInner { | |
| 152 | − lines: SmallVec<[WrappedLine; 1]>, | |
| 153 | − line_height: Pixels, | |
| 154 | − wrap_width: Option<Pixels>, | |
| 155 | − size: Option<Size<Pixels>>, | |
| 265 | +pub struct InteractiveText { | |
| 266 | + element_id: ElementId, | |
| 267 | + text: StyledText, | |
| 268 | + click_listener: Option<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>, | |
| 156 | 269 | } |
| 157 | 270 | |
| 158 | −struct InteractiveText { | |
| 159 | − id: ElementId, | |
| 160 | − text: Text, | |
| 271 | +struct InteractiveTextClickEvent { | |
| 272 | + mouse_down_index: usize, | |
| 273 | + mouse_up_index: usize, | |
| 161 | 274 | } |
| 162 | 275 | |
| 163 | −struct InteractiveTextState { | |
| 276 | +pub struct InteractiveTextState { | |
| 164 | 277 | text_state: TextState, |
| 165 | − clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>, | |
| 278 | + mouse_down_index: Rc<Cell<Option<usize>>>, | |
| 166 | 279 | } |
| 167 | 280 | |
| 168 | −impl<V: 'static> Element<V> for InteractiveText { | |
| 169 | − type ElementState = InteractiveTextState; | |
| 281 | +impl InteractiveText { | |
| 282 | + pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self { | |
| 283 | + Self { | |
| 284 | + element_id: id.into(), | |
| 285 | + text, | |
| 286 | + click_listener: None, | |
| 287 | + } | |
| 288 | + } | |
| 170 | 289 | |
| 171 | − fn element_id(&self) -> Option<ElementId> { | |
| 172 | − Some(self.id.clone()) | |
| 290 | + pub fn on_click( | |
| 291 | + mut self, | |
| 292 | + ranges: Vec<Range<usize>>, | |
| 293 | + listener: impl Fn(usize, &mut WindowContext<'_>) + 'static, | |
| 294 | + ) -> Self { | |
| 295 | + self.click_listener = Some(Box::new(move |event, cx| { | |
| 296 | + for (range_ix, range) in ranges.iter().enumerate() { | |
| 297 | + if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index) | |
| 298 | + { | |
| 299 | + listener(range_ix, cx); | |
| 300 | + } | |
| 301 | + } | |
| 302 | + })); | |
| 303 | + self | |
| 173 | 304 | } |
| 305 | +} | |
| 306 | + | |
| 307 | +impl Element for InteractiveText { | |
| 308 | + type State = InteractiveTextState; | |
| 174 | 309 | |
| 175 | 310 | fn layout( |
| 176 | 311 | &mut self, |
| 177 | − view_state: &mut V, | |
| 178 | − element_state: Option<Self::ElementState>, | |
| 179 | − cx: &mut ViewContext<V>, | |
| 180 | − ) -> (LayoutId, Self::ElementState) { | |
| 312 | + state: Option<Self::State>, | |
| 313 | + cx: &mut WindowContext, | |
| 314 | + ) -> (LayoutId, Self::State) { | |
| 181 | 315 | if let Some(InteractiveTextState { |
| 182 | − text_state, | |
| 183 | − clicked_range_ixs, | |
| 184 | − }) = element_state | |
| 316 | + mouse_down_index, .. | |
| 317 | + }) = state | |
| 185 | 318 | { |
| 186 | − let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx); | |
| 319 | + let (layout_id, text_state) = self.text.layout(None, cx); | |
| 187 | 320 | let element_state = InteractiveTextState { |
| 188 | 321 | text_state, |
| 189 | − clicked_range_ixs, | |
| 322 | + mouse_down_index, | |
| 190 | 323 | }; |
| 191 | 324 | (layout_id, element_state) |
| 192 | 325 | } else { |
| 193 | − let (layout_id, text_state) = self.text.layout(view_state, None, cx); | |
| 326 | + let (layout_id, text_state) = self.text.layout(None, cx); | |
| 194 | 327 | let element_state = InteractiveTextState { |
| 195 | 328 | text_state, |
| 196 | − clicked_range_ixs: Rc::default(), | |
| 329 | + mouse_down_index: Rc::default(), | |
| 197 | 330 | }; |
| 198 | 331 | (layout_id, element_state) |
| 199 | 332 | } |
| 200 | 333 | } |
| 201 | 334 | |
| 202 | − fn paint( | |
| 203 | − &mut self, | |
| 204 | − bounds: Bounds<Pixels>, | |
| 205 | − view_state: &mut V, | |
| 206 | − element_state: &mut Self::ElementState, | |
| 207 | − cx: &mut ViewContext<V>, | |
| 208 | − ) { | |
| 209 | − self.text | |
| 210 | − .paint(bounds, view_state, &mut element_state.text_state, cx) | |
| 211 | − } | |
| 212 | −} | |
| 335 | + fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) { | |
| 336 | + if let Some(click_listener) = self.click_listener { | |
| 337 | + let text_state = state.text_state.clone(); | |
| 338 | + let mouse_down = state.mouse_down_index.clone(); | |
| 339 | + if let Some(mouse_down_index) = mouse_down.get() { | |
| 340 | + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { | |
| 341 | + if phase == DispatchPhase::Bubble { | |
| 342 | + if let Some(mouse_up_index) = | |
| 343 | + text_state.index_for_position(bounds, event.position) | |
| 344 | + { | |
| 345 | + click_listener( | |
| 346 | + InteractiveTextClickEvent { | |
| 347 | + mouse_down_index, | |
| 348 | + mouse_up_index, | |
| 349 | + }, | |
| 350 | + cx, | |
| 351 | + ) | |
| 352 | + } | |
| 213 | 353 | |
| 214 | −impl<V: 'static> Component<V> for SharedString { | |
| 215 | − fn render(self) -> AnyElement<V> { | |
| 216 | − Text { | |
| 217 | − text: self, | |
| 218 | − runs: None, | |
| 354 | + mouse_down.take(); | |
| 355 | + cx.notify(); | |
| 356 | + } | |
| 357 | + }); | |
| 358 | + } else { | |
| 359 | + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { | |
| 360 | + if phase == DispatchPhase::Bubble { | |
| 361 | + if let Some(mouse_down_index) = | |
| 362 | + text_state.index_for_position(bounds, event.position) | |
| 363 | + { | |
| 364 | + mouse_down.set(Some(mouse_down_index)); | |
| 365 | + cx.notify(); | |
| 366 | + } | |
| 367 | + } | |
| 368 | + }); | |
| 369 | + } | |
| 219 | 370 | } |
| 220 | − .render() | |
| 371 | + | |
| 372 | + self.text.paint(bounds, &mut state.text_state, cx) | |
| 221 | 373 | } |
| 222 | 374 | } |
| 223 | 375 | |
| 224 | −impl<V: 'static> Component<V> for &'static str { | |
| 225 | − fn render(self) -> AnyElement<V> { | |
| 226 | − Text { | |
| 227 | − text: self.into(), | |
| 228 | − runs: None, | |
| 229 | − } | |
| 230 | − .render() | |
| 376 | +impl IntoElement for InteractiveText { | |
| 377 | + type Element = Self; | |
| 378 | + | |
| 379 | + fn element_id(&self) -> Option<ElementId> { | |
| 380 | + Some(self.element_id.clone()) | |
| 231 | 381 | } |
| 232 | −} | |
| 233 | 382 | |
| 234 | −// TODO: Figure out how to pass `String` to `child` without this. | |
| 235 | −// This impl doesn't exist in the `gpui2` crate. | |
| 236 | −impl<V: 'static> Component<V> for String { | |
| 237 | − fn render(self) -> AnyElement<V> { | |
| 238 | − Text { | |
| 239 | − text: self.into(), | |
| 240 | − runs: None, | |
| 241 | − } | |
| 242 | − .render() | |
| 383 | + fn into_element(self) -> Self::Element { | |
| 384 | + self | |
| 243 | 385 | } |
| 244 | 386 | } |
| @@ -1,61 +1,60 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, | |
| 3 | − ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels, | |
| 4 | − Point, Size, StyleRefinement, Styled, ViewContext, | |
| 2 | + point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element, | |
| 3 | + ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId, | |
| 4 | + Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext, | |
| 5 | 5 | }; |
| 6 | 6 | use smallvec::SmallVec; |
| 7 | −use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc}; | |
| 7 | +use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; | |
| 8 | 8 | use taffy::style::Overflow; |
| 9 | 9 | |
| 10 | 10 | /// uniform_list provides lazy rendering for a set of items that are of uniform height. |
| 11 | 11 | /// When rendered into a container with overflow-y: hidden and a fixed (or max) height, |
| 12 | −/// uniform_list will only render the visibile subset of items. | |
| 13 | −pub fn uniform_list<I, V, C>( | |
| 12 | +/// uniform_list will only render the visible subset of items. | |
| 13 | +pub fn uniform_list<I, R, V>( | |
| 14 | + view: View<V>, | |
| 14 | 15 | id: I, |
| 15 | 16 | item_count: usize, |
| 16 | − f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<C>, | |
| 17 | −) -> UniformList<V> | |
| 17 | + f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<R>, | |
| 18 | +) -> UniformList | |
| 18 | 19 | where |
| 19 | 20 | I: Into<ElementId>, |
| 20 | − V: 'static, | |
| 21 | − C: Component<V>, | |
| 21 | + R: IntoElement, | |
| 22 | + V: Render, | |
| 22 | 23 | { |
| 23 | 24 | let id = id.into(); |
| 24 | − let mut style = StyleRefinement::default(); | |
| 25 | − style.overflow.y = Some(Overflow::Hidden); | |
| 25 | + let mut base_style = StyleRefinement::default(); | |
| 26 | + base_style.overflow.y = Some(Overflow::Scroll); | |
| 27 | + | |
| 28 | + let render_range = move |range, cx: &mut WindowContext| { | |
| 29 | + view.update(cx, |this, cx| { | |
| 30 | + f(this, range, cx) | |
| 31 | + .into_iter() | |
| 32 | + .map(|component| component.into_any_element()) | |
| 33 | + .collect() | |
| 34 | + }) | |
| 35 | + }; | |
| 26 | 36 | |
| 27 | 37 | UniformList { |
| 28 | 38 | id: id.clone(), |
| 29 | − style, | |
| 30 | 39 | item_count, |
| 31 | 40 | item_to_measure_index: 0, |
| 32 | − render_items: Box::new(move |view, visible_range, cx| { | |
| 33 | − f(view, visible_range, cx) | |
| 34 | − .into_iter() | |
| 35 | − .map(|component| component.render()) | |
| 36 | − .collect() | |
| 37 | − }), | |
| 41 | + render_items: Box::new(render_range), | |
| 38 | 42 | interactivity: Interactivity { |
| 39 | 43 | element_id: Some(id.into()), |
| 44 | + base_style, | |
| 40 | 45 | ..Default::default() |
| 41 | 46 | }, |
| 42 | 47 | scroll_handle: None, |
| 43 | 48 | } |
| 44 | 49 | } |
| 45 | 50 | |
| 46 | −pub struct UniformList<V: 'static> { | |
| 51 | +pub struct UniformList { | |
| 47 | 52 | id: ElementId, |
| 48 | − style: StyleRefinement, | |
| 49 | 53 | item_count: usize, |
| 50 | 54 | item_to_measure_index: usize, |
| 51 | − render_items: Box< | |
| 52 | − dyn for<'a> Fn( | |
| 53 | − &'a mut V, | |
| 54 | − Range<usize>, | |
| 55 | − &'a mut ViewContext<V>, | |
| 56 | − ) -> SmallVec<[AnyElement<V>; 64]>, | |
| 57 | − >, | |
| 58 | − interactivity: Interactivity<V>, | |
| 55 | + render_items: | |
| 56 | + Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>, | |
| 57 | + interactivity: Interactivity, | |
| 59 | 58 | scroll_handle: Option<UniformListScrollHandle>, |
| 60 | 59 | } |
| 61 | 60 | |
| @@ -89,9 +88,9 @@ impl UniformListScrollHandle { · 27 unmodified lines | ||
| 89 | 88 | } |
| 90 | 89 | } |
| 91 | 90 | |
| 92 | −impl<V: 'static> Styled for UniformList<V> { | |
| 91 | +impl Styled for UniformList { | |
| 93 | 92 | fn style(&mut self) -> &mut StyleRefinement { |
| 94 | − &mut self.style | |
| 93 | + &mut self.interactivity.base_style | |
| 95 | 94 | } |
| 96 | 95 | } |
| 97 | 96 | |
| @@ -101,29 +100,24 @@ pub struct UniformListState { · 3 unmodified lines | ||
| 101 | 100 | item_size: Size<Pixels>, |
| 102 | 101 | } |
| 103 | 102 | |
| 104 | −impl<V: 'static> Element<V> for UniformList<V> { | |
| 105 | − type ElementState = UniformListState; | |
| 106 | − | |
| 107 | − fn element_id(&self) -> Option<crate::ElementId> { | |
| 108 | − Some(self.id.clone()) | |
| 109 | − } | |
| 103 | +impl Element for UniformList { | |
| 104 | + type State = UniformListState; | |
| 110 | 105 | |
| 111 | 106 | fn layout( |
| 112 | 107 | &mut self, |
| 113 | − view_state: &mut V, | |
| 114 | − element_state: Option<Self::ElementState>, | |
| 115 | − cx: &mut ViewContext<V>, | |
| 116 | − ) -> (LayoutId, Self::ElementState) { | |
| 108 | + state: Option<Self::State>, | |
| 109 | + cx: &mut WindowContext, | |
| 110 | + ) -> (LayoutId, Self::State) { | |
| 117 | 111 | let max_items = self.item_count; |
| 118 | 112 | let rem_size = cx.rem_size(); |
| 119 | − let item_size = element_state | |
| 113 | + let item_size = state | |
| 120 | 114 | .as_ref() |
| 121 | 115 | .map(|s| s.item_size) |
| 122 | − .unwrap_or_else(|| self.measure_item(view_state, None, cx)); | |
| 116 | + .unwrap_or_else(|| self.measure_item(None, cx)); | |
| 123 | 117 | |
| 124 | 118 | let (layout_id, interactive) = |
| 125 | 119 | self.interactivity |
| 126 | − .layout(element_state.map(|s| s.interactive), cx, |style, cx| { | |
| 120 | + .layout(state.map(|s| s.interactive), cx, |style, cx| { | |
| 127 | 121 | cx.request_measured_layout( |
| 128 | 122 | style, |
| 129 | 123 | rem_size, |
| @@ -159,11 +153,10 @@ impl<V: 'static> Element<V> for UniformList<V> { · 29 unmodified lines | ||
| 159 | 153 | } |
| 160 | 154 | |
| 161 | 155 | fn paint( |
| 162 | − &mut self, | |
| 156 | + self, | |
| 163 | 157 | bounds: Bounds<crate::Pixels>, |
| 164 | − view_state: &mut V, | |
| 165 | − element_state: &mut Self::ElementState, | |
| 166 | − cx: &mut ViewContext<V>, | |
| 158 | + element_state: &mut Self::State, | |
| 159 | + cx: &mut WindowContext, | |
| 167 | 160 | ) { |
| 168 | 161 | let style = |
| 169 | 162 | self.interactivity |
| @@ -183,14 +176,15 @@ impl<V: 'static> Element<V> for UniformList<V> { · 13 unmodified lines | ||
| 183 | 176 | height: item_size.height * self.item_count, |
| 184 | 177 | }; |
| 185 | 178 | |
| 186 | − let mut interactivity = mem::take(&mut self.interactivity); | |
| 187 | 179 | let shared_scroll_offset = element_state |
| 188 | 180 | .interactive |
| 189 | 181 | .scroll_offset |
| 190 | 182 | .get_or_insert_with(Rc::default) |
| 191 | 183 | .clone(); |
| 192 | 184 | |
| 193 | − interactivity.paint( | |
| 185 | + let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height; | |
| 186 | + | |
| 187 | + self.interactivity.paint( | |
| 194 | 188 | bounds, |
| 195 | 189 | content_size, |
| 196 | 190 | &mut element_state.interactive, |
| @@ -209,9 +203,6 @@ impl<V: 'static> Element<V> for UniformList<V> { · 12 unmodified lines | ||
| 209 | 203 | style.paint(bounds, cx); |
| 210 | 204 | |
| 211 | 205 | if self.item_count > 0 { |
| 212 | − let item_height = self | |
| 213 | − .measure_item(view_state, Some(padded_bounds.size.width), cx) | |
| 214 | − .height; | |
| 215 | 206 | if let Some(scroll_handle) = self.scroll_handle.clone() { |
| 216 | 207 | scroll_handle.0.borrow_mut().replace(ScrollHandleState { |
| 217 | 208 | item_height, |
| @@ -219,58 +210,64 @@ impl<V: 'static> Element<V> for UniformList<V> { · 1 unmodified line | ||
| 219 | 210 | scroll_offset: shared_scroll_offset, |
| 220 | 211 | }); |
| 221 | 212 | } |
| 222 | − let visible_item_count = if item_height > px(0.) { | |
| 223 | − (padded_bounds.size.height / item_height).ceil() as usize + 1 | |
| 224 | − } else { | |
| 225 | − 0 | |
| 226 | − }; | |
| 227 | 213 | |
| 228 | 214 | let first_visible_element_ix = |
| 229 | 215 | (-scroll_offset.y / item_height).floor() as usize; |
| 216 | + let last_visible_element_ix = | |
| 217 | + ((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil() | |
| 218 | + as usize; | |
| 230 | 219 | let visible_range = first_visible_element_ix |
| 231 | − ..cmp::min( | |
| 232 | − first_visible_element_ix + visible_item_count, | |
| 233 | − self.item_count, | |
| 234 | − ); | |
| 220 | + ..cmp::min(last_visible_element_ix, self.item_count); | |
| 235 | 221 | |
| 236 | − let mut items = (self.render_items)(view_state, visible_range.clone(), cx); | |
| 222 | + let items = (self.render_items)(visible_range.clone(), cx); | |
| 237 | 223 | cx.with_z_index(1, |cx| { |
| 238 | − for (item, ix) in items.iter_mut().zip(visible_range) { | |
| 239 | − let item_origin = padded_bounds.origin | |
| 240 | − + point(px(0.), item_height * ix + scroll_offset.y); | |
| 241 | − let available_space = size( | |
| 242 | − AvailableSpace::Definite(padded_bounds.size.width), | |
| 243 | − AvailableSpace::Definite(item_height), | |
| 244 | − ); | |
| 245 | − item.draw(item_origin, available_space, view_state, cx); | |
| 246 | − } | |
| 224 | + let content_mask = ContentMask { | |
| 225 | + bounds: padded_bounds, | |
| 226 | + }; | |
| 227 | + cx.with_content_mask(Some(content_mask), |cx| { | |
| 228 | + for (item, ix) in items.into_iter().zip(visible_range) { | |
| 229 | + let item_origin = padded_bounds.origin | |
| 230 | + + point(px(0.), item_height * ix + scroll_offset.y); | |
| 231 | + let available_space = size( | |
| 232 | + AvailableSpace::Definite(padded_bounds.size.width), | |
| 233 | + AvailableSpace::Definite(item_height), | |
| 234 | + ); | |
| 235 | + item.draw(item_origin, available_space, cx); | |
| 236 | + } | |
| 237 | + }); | |
| 247 | 238 | }); |
| 248 | 239 | } |
| 249 | 240 | }) |
| 250 | 241 | }, |
| 251 | 242 | ); |
| 252 | − self.interactivity = interactivity; | |
| 253 | 243 | } |
| 254 | 244 | } |
| 255 | 245 | |
| 256 | −impl<V> UniformList<V> { | |
| 246 | +impl IntoElement for UniformList { | |
| 247 | + type Element = Self; | |
| 248 | + | |
| 249 | + fn element_id(&self) -> Option<crate::ElementId> { | |
| 250 | + Some(self.id.clone()) | |
| 251 | + } | |
| 252 | + | |
| 253 | + fn into_element(self) -> Self::Element { | |
| 254 | + self | |
| 255 | + } | |
| 256 | +} | |
| 257 | + | |
| 258 | +impl UniformList { | |
| 257 | 259 | pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self { |
| 258 | 260 | self.item_to_measure_index = item_index.unwrap_or(0); |
| 259 | 261 | self |
| 260 | 262 | } |
| 261 | 263 | |
| 262 | − fn measure_item( | |
| 263 | − &self, | |
| 264 | − view_state: &mut V, | |
| 265 | − list_width: Option<Pixels>, | |
| 266 | − cx: &mut ViewContext<V>, | |
| 267 | − ) -> Size<Pixels> { | |
| 264 | + fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> { | |
| 268 | 265 | if self.item_count == 0 { |
| 269 | 266 | return Size::default(); |
| 270 | 267 | } |
| 271 | 268 | |
| 272 | 269 | let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1); |
| 273 | − let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx); | |
| 270 | + let mut items = (self.render_items)(item_ix..item_ix + 1, cx); | |
| 274 | 271 | let mut item_to_measure = items.pop().unwrap(); |
| 275 | 272 | let available_space = size( |
| 276 | 273 | list_width.map_or(AvailableSpace::MinContent, |width| { |
| @@ -278,7 +275,7 @@ impl<V> UniformList<V> { · 1 unmodified line | ||
| 278 | 275 | }), |
| 279 | 276 | AvailableSpace::MinContent, |
| 280 | 277 | ); |
| 281 | − item_to_measure.measure(available_space, view_state, cx) | |
| 278 | + item_to_measure.measure(available_space, cx) | |
| 282 | 279 | } |
| 283 | 280 | |
| 284 | 281 | pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self { |
| @@ -287,14 +284,8 @@ impl<V> UniformList<V> { · 2 unmodified lines | ||
| 287 | 284 | } |
| 288 | 285 | } |
| 289 | 286 | |
| 290 | −impl<V> InteractiveComponent<V> for UniformList<V> { | |
| 291 | − fn interactivity(&mut self) -> &mut crate::Interactivity<V> { | |
| 287 | +impl InteractiveElement for UniformList { | |
| 288 | + fn interactivity(&mut self) -> &mut crate::Interactivity { | |
| 292 | 289 | &mut self.interactivity |
| 293 | 290 | } |
| 294 | 291 | } |
| 295 | − | |
| 296 | −impl<V: 'static> Component<V> for UniformList<V> { | |
| 297 | − fn render(self) -> AnyElement<V> { | |
| 298 | − AnyElement::new(self) | |
| 299 | − } | |
| 300 | −} | |
| @@ -78,8 +78,6 @@ use std::{ · 77 unmodified lines | ||
| 78 | 78 | }; |
| 79 | 79 | use taffy::TaffyLayoutEngine; |
| 80 | 80 | |
| 81 | −type AnyBox = Box<dyn Any>; | |
| 82 | − | |
| 83 | 81 | pub trait Context { |
| 84 | 82 | type Result<T>; |
| 85 | 83 | |
| @@ -136,7 +134,7 @@ pub trait VisualContext: Context { · 50 unmodified lines | ||
| 136 | 134 | build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, |
| 137 | 135 | ) -> Self::Result<View<V>> |
| 138 | 136 | where |
| 139 | − V: Render; | |
| 137 | + V: 'static + Render; | |
| 140 | 138 | |
| 141 | 139 | fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()> |
| 142 | 140 | where |
| @@ -1,4 +1,6 @@ | ||
| 1 | −use crate::{AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext}; | |
| 1 | +use crate::{ | |
| 2 | + AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext, WindowContext, | |
| 3 | +}; | |
| 2 | 4 | use std::ops::Range; |
| 3 | 5 | |
| 4 | 6 | /// Implement this trait to allow views to handle textual input when implementing an editor, field, etc. |
| @@ -43,9 +45,9 @@ pub struct ElementInputHandler<V> { · 38 unmodified lines | ||
| 43 | 45 | impl<V: 'static> ElementInputHandler<V> { |
| 44 | 46 | /// Used in [Element::paint] with the element's bounds and a view context for its |
| 45 | 47 | /// containing view. |
| 46 | − pub fn new(element_bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) -> Self { | |
| 48 | + pub fn new(element_bounds: Bounds<Pixels>, view: View<V>, cx: &mut WindowContext) -> Self { | |
| 47 | 49 | ElementInputHandler { |
| 48 | − view: cx.view().clone(), | |
| 50 | + view, | |
| 49 | 51 | element_bounds, |
| 50 | 52 | cx: cx.to_async(), |
| 51 | 53 | } |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, | |
| 3 | − ViewContext, | |
| 2 | + div, point, Div, Element, FocusHandle, IntoElement, Keystroke, Modifiers, Pixels, Point, | |
| 3 | + Render, ViewContext, | |
| 4 | 4 | }; |
| 5 | 5 | use smallvec::SmallVec; |
| 6 | 6 | use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf}; |
| @@ -64,24 +64,24 @@ pub struct Drag<S, R, V, E> · 57 unmodified lines | ||
| 64 | 64 | where |
| 65 | 65 | R: Fn(&mut V, &mut ViewContext<V>) -> E, |
| 66 | 66 | V: 'static, |
| 67 | − E: Component<()>, | |
| 67 | + E: IntoElement, | |
| 68 | 68 | { |
| 69 | 69 | pub state: S, |
| 70 | 70 | pub render_drag_handle: R, |
| 71 | − view_type: PhantomData<V>, | |
| 71 | + view_element_types: PhantomData<(V, E)>, | |
| 72 | 72 | } |
| 73 | 73 | |
| 74 | 74 | impl<S, R, V, E> Drag<S, R, V, E> |
| 75 | 75 | where |
| 76 | 76 | R: Fn(&mut V, &mut ViewContext<V>) -> E, |
| 77 | 77 | V: 'static, |
| 78 | − E: Component<()>, | |
| 78 | + E: Element, | |
| 79 | 79 | { |
| 80 | 80 | pub fn new(state: S, render_drag_handle: R) -> Self { |
| 81 | 81 | Drag { |
| 82 | 82 | state, |
| 83 | 83 | render_drag_handle, |
| 84 | − view_type: PhantomData, | |
| 84 | + view_element_types: Default::default(), | |
| 85 | 85 | } |
| 86 | 86 | } |
| 87 | 87 | } |
| @@ -194,7 +194,7 @@ impl Deref for MouseExitEvent { · 106 unmodified lines | ||
| 194 | 194 | pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>); |
| 195 | 195 | |
| 196 | 196 | impl Render for ExternalPaths { |
| 197 | − type Element = Div<Self>; | |
| 197 | + type Element = Div; | |
| 198 | 198 | |
| 199 | 199 | fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element { |
| 200 | 200 | div() // Intentionally left empty because the platform will render icons for the dragged files |
| @@ -286,8 +286,8 @@ pub struct FocusEvent { · 85 unmodified lines | ||
| 286 | 286 | #[cfg(test)] |
| 287 | 287 | mod test { |
| 288 | 288 | use crate::{ |
| 289 | − self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding, | |
| 290 | − Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext, | |
| 289 | + self as gpui, div, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding, | |
| 290 | + Keystroke, ParentElement, Render, Stateful, TestAppContext, VisualContext, | |
| 291 | 291 | }; |
| 292 | 292 | |
| 293 | 293 | struct TestView { |
| @@ -299,20 +299,24 @@ mod test { · 5 unmodified lines | ||
| 299 | 299 | actions!(TestAction); |
| 300 | 300 | |
| 301 | 301 | impl Render for TestView { |
| 302 | − type Element = Stateful<Self, Div<Self>>; | |
| 302 | + type Element = Stateful<Div>; | |
| 303 | 303 | |
| 304 | − fn render(&mut self, _: &mut gpui::ViewContext<Self>) -> Self::Element { | |
| 304 | + fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element { | |
| 305 | 305 | div().id("testview").child( |
| 306 | 306 | div() |
| 307 | 307 | .key_context("parent") |
| 308 | − .on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true) | |
| 309 | − .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true) | |
| 310 | − .child(|this: &mut Self, _cx: &mut ViewContext<Self>| { | |
| 308 | + .on_key_down(cx.listener(|this, _, _| this.saw_key_down = true)) | |
| 309 | + .on_action( | |
| 310 | + cx.listener(|this: &mut TestView, _: &TestAction, _| { | |
| 311 | + this.saw_action = true | |
| 312 | + }), | |
| 313 | + ) | |
| 314 | + .child( | |
| 311 | 315 | div() |
| 312 | 316 | .key_context("nested") |
| 313 | − .track_focus(&this.focus_handle) | |
| 314 | − .render() | |
| 315 | − }), | |
| 317 | + .track_focus(&self.focus_handle) | |
| 318 | + .into_element(), | |
| 319 | + ), | |
| 316 | 320 | ) |
| 317 | 321 | } |
| 318 | 322 | } |
| @@ -683,6 +683,9 @@ impl Drop for MacWindow { · 682 unmodified lines | ||
| 683 | 683 | this.executor |
| 684 | 684 | .spawn(async move { |
| 685 | 685 | unsafe { |
| 686 | + // todo!() this panic()s when you click the red close button | |
| 687 | + // unless should_close returns false. | |
| 688 | + // (luckliy in zed it always returns false) | |
| 686 | 689 | window.close(); |
| 687 | 690 | } |
| 688 | 691 | }) |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | pub use crate::{ |
| 2 | − BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent, | |
| 3 | − ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext, | |
| 2 | + BorrowAppContext, BorrowWindow, Context, Element, FocusableElement, InteractiveElement, | |
| 3 | + IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, | |
| 4 | + VisualContext, | |
| 4 | 5 | }; |
| @@ -1,9 +1,12 @@ | ||
| 1 | +use std::{iter, mem, ops::Range}; | |
| 2 | + | |
| 1 | 3 | use crate::{ |
| 2 | 4 | black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, |
| 3 | 5 | Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, |
| 4 | 6 | FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, |
| 5 | − SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, | |
| 7 | + SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext, | |
| 6 | 8 | }; |
| 9 | +use collections::HashSet; | |
| 7 | 10 | use refineable::{Cascade, Refineable}; |
| 8 | 11 | use smallvec::SmallVec; |
| 9 | 12 | pub use taffy::style::{ |
| @@ -128,6 +131,13 @@ pub struct BoxShadow { · 118 unmodified lines | ||
| 128 | 131 | pub spread_radius: Pixels, |
| 129 | 132 | } |
| 130 | 133 | |
| 134 | +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] | |
| 135 | +pub enum WhiteSpace { | |
| 136 | + #[default] | |
| 137 | + Normal, | |
| 138 | + Nowrap, | |
| 139 | +} | |
| 140 | + | |
| 131 | 141 | #[derive(Refineable, Clone, Debug)] |
| 132 | 142 | #[refineable(Debug)] |
| 133 | 143 | pub struct TextStyle { |
| @@ -138,7 +148,9 @@ pub struct TextStyle { · 4 unmodified lines | ||
| 138 | 148 | pub line_height: DefiniteLength, |
| 139 | 149 | pub font_weight: FontWeight, |
| 140 | 150 | pub font_style: FontStyle, |
| 151 | + pub background_color: Option<Hsla>, | |
| 141 | 152 | pub underline: Option<UnderlineStyle>, |
| 153 | + pub white_space: WhiteSpace, | |
| 142 | 154 | } |
| 143 | 155 | |
| 144 | 156 | impl Default for TextStyle { |
| @@ -151,13 +163,16 @@ impl Default for TextStyle { · 6 unmodified lines | ||
| 151 | 163 | line_height: phi(), |
| 152 | 164 | font_weight: FontWeight::default(), |
| 153 | 165 | font_style: FontStyle::default(), |
| 166 | + background_color: None, | |
| 154 | 167 | underline: None, |
| 168 | + white_space: WhiteSpace::Normal, | |
| 155 | 169 | } |
| 156 | 170 | } |
| 157 | 171 | } |
| 158 | 172 | |
| 159 | 173 | impl TextStyle { |
| 160 | − pub fn highlight(mut self, style: HighlightStyle) -> Self { | |
| 174 | + pub fn highlight(mut self, style: impl Into<HighlightStyle>) -> Self { | |
| 175 | + let style = style.into(); | |
| 161 | 176 | if let Some(weight) = style.font_weight { |
| 162 | 177 | self.font_weight = weight; |
| 163 | 178 | } |
| @@ -173,6 +188,10 @@ impl TextStyle { · 9 unmodified lines | ||
| 173 | 188 | self.color.fade_out(factor); |
| 174 | 189 | } |
| 175 | 190 | |
| 191 | + if let Some(background_color) = style.background_color { | |
| 192 | + self.background_color = Some(background_color); | |
| 193 | + } | |
| 194 | + | |
| 176 | 195 | if let Some(underline) = style.underline { |
| 177 | 196 | self.underline = Some(underline); |
| 178 | 197 | } |
| @@ -203,7 +222,7 @@ impl TextStyle { · 24 unmodified lines | ||
| 203 | 222 | style: self.font_style, |
| 204 | 223 | }, |
| 205 | 224 | color: self.color, |
| 206 | − background_color: None, | |
| 225 | + background_color: self.background_color, | |
| 207 | 226 | underline: self.underline.clone(), |
| 208 | 227 | } |
| 209 | 228 | } |
| @@ -214,6 +233,7 @@ pub struct HighlightStyle { · 4 unmodified lines | ||
| 214 | 233 | pub color: Option<Hsla>, |
| 215 | 234 | pub font_weight: Option<FontWeight>, |
| 216 | 235 | pub font_style: Option<FontStyle>, |
| 236 | + pub background_color: Option<Hsla>, | |
| 217 | 237 | pub underline: Option<UnderlineStyle>, |
| 218 | 238 | pub fade_out: Option<f32>, |
| 219 | 239 | } |
| @@ -313,7 +333,7 @@ impl Style { · 93 unmodified lines | ||
| 313 | 333 | } |
| 314 | 334 | |
| 315 | 335 | /// Paints the background of an element styled with this style. |
| 316 | − pub fn paint<V: 'static>(&self, bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) { | |
| 336 | + pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) { | |
| 317 | 337 | let rem_size = cx.rem_size(); |
| 318 | 338 | |
| 319 | 339 | cx.with_z_index(0, |cx| { |
| @@ -432,6 +452,7 @@ impl From<&TextStyle> for HighlightStyle { · 112 unmodified lines | ||
| 432 | 452 | color: Some(other.color), |
| 433 | 453 | font_weight: Some(other.font_weight), |
| 434 | 454 | font_style: Some(other.font_style), |
| 455 | + background_color: other.background_color, | |
| 435 | 456 | underline: other.underline.clone(), |
| 436 | 457 | fade_out: None, |
| 437 | 458 | } |
| @@ -458,6 +479,10 @@ impl HighlightStyle { · 20 unmodified lines | ||
| 458 | 479 | self.font_style = other.font_style; |
| 459 | 480 | } |
| 460 | 481 | |
| 482 | + if other.background_color.is_some() { | |
| 483 | + self.background_color = other.background_color; | |
| 484 | + } | |
| 485 | + | |
| 461 | 486 | if other.underline.is_some() { |
| 462 | 487 | self.underline = other.underline; |
| 463 | 488 | } |
| @@ -481,6 +506,24 @@ impl From<Hsla> for HighlightStyle { · 17 unmodified lines | ||
| 481 | 506 | } |
| 482 | 507 | } |
| 483 | 508 | |
| 509 | +impl From<FontWeight> for HighlightStyle { | |
| 510 | + fn from(font_weight: FontWeight) -> Self { | |
| 511 | + Self { | |
| 512 | + font_weight: Some(font_weight), | |
| 513 | + ..Default::default() | |
| 514 | + } | |
| 515 | + } | |
| 516 | +} | |
| 517 | + | |
| 518 | +impl From<FontStyle> for HighlightStyle { | |
| 519 | + fn from(font_style: FontStyle) -> Self { | |
| 520 | + Self { | |
| 521 | + font_style: Some(font_style), | |
| 522 | + ..Default::default() | |
| 523 | + } | |
| 524 | + } | |
| 525 | +} | |
| 526 | + | |
| 484 | 527 | impl From<Rgba> for HighlightStyle { |
| 485 | 528 | fn from(color: Rgba) -> Self { |
| 486 | 529 | Self { |
| @@ -489,3 +532,140 @@ impl From<Rgba> for HighlightStyle { · 2 unmodified lines | ||
| 489 | 532 | } |
| 490 | 533 | } |
| 491 | 534 | } |
| 535 | + | |
| 536 | +pub fn combine_highlights( | |
| 537 | + a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>, | |
| 538 | + b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>, | |
| 539 | +) -> impl Iterator<Item = (Range<usize>, HighlightStyle)> { | |
| 540 | + let mut endpoints = Vec::new(); | |
| 541 | + let mut highlights = Vec::new(); | |
| 542 | + for (range, highlight) in a.into_iter().chain(b) { | |
| 543 | + if !range.is_empty() { | |
| 544 | + let highlight_id = highlights.len(); | |
| 545 | + endpoints.push((range.start, highlight_id, true)); | |
| 546 | + endpoints.push((range.end, highlight_id, false)); | |
| 547 | + highlights.push(highlight); | |
| 548 | + } | |
| 549 | + } | |
| 550 | + endpoints.sort_unstable_by_key(|(position, _, _)| *position); | |
| 551 | + let mut endpoints = endpoints.into_iter().peekable(); | |
| 552 | + | |
| 553 | + let mut active_styles = HashSet::default(); | |
| 554 | + let mut ix = 0; | |
| 555 | + iter::from_fn(move || { | |
| 556 | + while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() { | |
| 557 | + let prev_index = mem::replace(&mut ix, *endpoint_ix); | |
| 558 | + if ix > prev_index && !active_styles.is_empty() { | |
| 559 | + let mut current_style = HighlightStyle::default(); | |
| 560 | + for highlight_id in &active_styles { | |
| 561 | + current_style.highlight(highlights[*highlight_id]); | |
| 562 | + } | |
| 563 | + return Some((prev_index..ix, current_style)); | |
| 564 | + } | |
| 565 | + | |
| 566 | + if *is_start { | |
| 567 | + active_styles.insert(*highlight_id); | |
| 568 | + } else { | |
| 569 | + active_styles.remove(highlight_id); | |
| 570 | + } | |
| 571 | + endpoints.next(); | |
| 572 | + } | |
| 573 | + None | |
| 574 | + }) | |
| 575 | +} | |
| 576 | + | |
| 577 | +#[cfg(test)] | |
| 578 | +mod tests { | |
| 579 | + use crate::{blue, green, red, yellow}; | |
| 580 | + | |
| 581 | + use super::*; | |
| 582 | + | |
| 583 | + #[test] | |
| 584 | + fn test_combine_highlights() { | |
| 585 | + assert_eq!( | |
| 586 | + combine_highlights( | |
| 587 | + [ | |
| 588 | + (0..5, green().into()), | |
| 589 | + (4..10, FontWeight::BOLD.into()), | |
| 590 | + (15..20, yellow().into()), | |
| 591 | + ], | |
| 592 | + [ | |
| 593 | + (2..6, FontStyle::Italic.into()), | |
| 594 | + (1..3, blue().into()), | |
| 595 | + (21..23, red().into()), | |
| 596 | + ] | |
| 597 | + ) | |
| 598 | + .collect::<Vec<_>>(), | |
| 599 | + [ | |
| 600 | + ( | |
| 601 | + 0..1, | |
| 602 | + HighlightStyle { | |
| 603 | + color: Some(green()), | |
| 604 | + ..Default::default() | |
| 605 | + } | |
| 606 | + ), | |
| 607 | + ( | |
| 608 | + 1..2, | |
| 609 | + HighlightStyle { | |
| 610 | + color: Some(blue()), | |
| 611 | + ..Default::default() | |
| 612 | + } | |
| 613 | + ), | |
| 614 | + ( | |
| 615 | + 2..3, | |
| 616 | + HighlightStyle { | |
| 617 | + color: Some(blue()), | |
| 618 | + font_style: Some(FontStyle::Italic), | |
| 619 | + ..Default::default() | |
| 620 | + } | |
| 621 | + ), | |
| 622 | + ( | |
| 623 | + 3..4, | |
| 624 | + HighlightStyle { | |
| 625 | + color: Some(green()), | |
| 626 | + font_style: Some(FontStyle::Italic), | |
| 627 | + ..Default::default() | |
| 628 | + } | |
| 629 | + ), | |
| 630 | + ( | |
| 631 | + 4..5, | |
| 632 | + HighlightStyle { | |
| 633 | + color: Some(green()), | |
| 634 | + font_weight: Some(FontWeight::BOLD), | |
| 635 | + font_style: Some(FontStyle::Italic), | |
| 636 | + ..Default::default() | |
| 637 | + } | |
| 638 | + ), | |
| 639 | + ( | |
| 640 | + 5..6, | |
| 641 | + HighlightStyle { | |
| 642 | + font_weight: Some(FontWeight::BOLD), | |
| 643 | + font_style: Some(FontStyle::Italic), | |
| 644 | + ..Default::default() | |
| 645 | + } | |
| 646 | + ), | |
| 647 | + ( | |
| 648 | + 6..10, | |
| 649 | + HighlightStyle { | |
| 650 | + font_weight: Some(FontWeight::BOLD), | |
| 651 | + ..Default::default() | |
| 652 | + } | |
| 653 | + ), | |
| 654 | + ( | |
| 655 | + 15..20, | |
| 656 | + HighlightStyle { | |
| 657 | + color: Some(yellow()), | |
| 658 | + ..Default::default() | |
| 659 | + } | |
| 660 | + ), | |
| 661 | + ( | |
| 662 | + 21..23, | |
| 663 | + HighlightStyle { | |
| 664 | + color: Some(red()), | |
| 665 | + ..Default::default() | |
| 666 | + } | |
| 667 | + ) | |
| 668 | + ] | |
| 669 | + ); | |
| 670 | + } | |
| 671 | +} | |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | 2 | self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, |
| 3 | 3 | DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position, |
| 4 | − SharedString, StyleRefinement, Visibility, | |
| 4 | + SharedString, StyleRefinement, Visibility, WhiteSpace, | |
| 5 | 5 | }; |
| 6 | 6 | use crate::{BoxShadow, TextStyleRefinement}; |
| 7 | 7 | use smallvec::{smallvec, SmallVec}; |
| @@ -101,6 +101,24 @@ pub trait Styled: Sized { · 93 unmodified lines | ||
| 101 | 101 | self |
| 102 | 102 | } |
| 103 | 103 | |
| 104 | + /// Sets the whitespace of the element to `normal`. | |
| 105 | + /// [Docs](https://tailwindcss.com/docs/whitespace#normal) | |
| 106 | + fn whitespace_normal(mut self) -> Self { | |
| 107 | + self.text_style() | |
| 108 | + .get_or_insert_with(Default::default) | |
| 109 | + .white_space = Some(WhiteSpace::Normal); | |
| 110 | + self | |
| 111 | + } | |
| 112 | + | |
| 113 | + /// Sets the whitespace of the element to `nowrap`. | |
| 114 | + /// [Docs](https://tailwindcss.com/docs/whitespace#nowrap) | |
| 115 | + fn whitespace_nowrap(mut self) -> Self { | |
| 116 | + self.text_style() | |
| 117 | + .get_or_insert_with(Default::default) | |
| 118 | + .white_space = Some(WhiteSpace::Nowrap); | |
| 119 | + self | |
| 120 | + } | |
| 121 | + | |
| 104 | 122 | /// Sets the flex direction of the element to `column`. |
| 105 | 123 | /// [Docs](https://tailwindcss.com/docs/flex-direction#column) |
| 106 | 124 | fn flex_col(mut self) -> Self { |
| @@ -343,6 +361,13 @@ pub trait Styled: Sized { · 236 unmodified lines | ||
| 343 | 361 | self |
| 344 | 362 | } |
| 345 | 363 | |
| 364 | + fn text_bg(mut self, bg: impl Into<Hsla>) -> Self { | |
| 365 | + self.text_style() | |
| 366 | + .get_or_insert_with(Default::default) | |
| 367 | + .background_color = Some(bg.into()); | |
| 368 | + self | |
| 369 | + } | |
| 370 | + | |
| 346 | 371 | fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self { |
| 347 | 372 | self.text_style() |
| 348 | 373 | .get_or_insert_with(Default::default) |
| @@ -196,7 +196,10 @@ impl TextSystem { · 195 unmodified lines | ||
| 196 | 196 | let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); |
| 197 | 197 | for run in runs { |
| 198 | 198 | if let Some(last_run) = decoration_runs.last_mut() { |
| 199 | − if last_run.color == run.color && last_run.underline == run.underline { | |
| 199 | + if last_run.color == run.color | |
| 200 | + && last_run.underline == run.underline | |
| 201 | + && last_run.background_color == run.background_color | |
| 202 | + { | |
| 200 | 203 | last_run.len += run.len as u32; |
| 201 | 204 | continue; |
| 202 | 205 | } |
| @@ -204,6 +207,7 @@ impl TextSystem { · 1 unmodified line | ||
| 204 | 207 | decoration_runs.push(DecorationRun { |
| 205 | 208 | len: run.len as u32, |
| 206 | 209 | color: run.color, |
| 210 | + background_color: run.background_color, | |
| 207 | 211 | underline: run.underline.clone(), |
| 208 | 212 | }); |
| 209 | 213 | } |
| @@ -254,13 +258,16 @@ impl TextSystem { · 44 unmodified lines | ||
| 254 | 258 | } |
| 255 | 259 | |
| 256 | 260 | if decoration_runs.last().map_or(false, |last_run| { |
| 257 | − last_run.color == run.color && last_run.underline == run.underline | |
| 261 | + last_run.color == run.color | |
| 262 | + && last_run.underline == run.underline | |
| 263 | + && last_run.background_color == run.background_color | |
| 258 | 264 | }) { |
| 259 | 265 | decoration_runs.last_mut().unwrap().len += run_len_within_line as u32; |
| 260 | 266 | } else { |
| 261 | 267 | decoration_runs.push(DecorationRun { |
| 262 | 268 | len: run_len_within_line as u32, |
| 263 | 269 | color: run.color, |
| 270 | + background_color: run.background_color, | |
| 264 | 271 | underline: run.underline.clone(), |
| 265 | 272 | }); |
| 266 | 273 | } |
| @@ -283,7 +290,15 @@ impl TextSystem { · 16 unmodified lines | ||
| 283 | 290 | text: SharedString::from(line_text), |
| 284 | 291 | }); |
| 285 | 292 | |
| 286 | − line_start = line_end + 1; // Skip `\n` character. | |
| 293 | + // Skip `\n` character. | |
| 294 | + line_start = line_end + 1; | |
| 295 | + if let Some(run) = runs.peek_mut() { | |
| 296 | + run.len = run.len.saturating_sub(1); | |
| 297 | + if run.len == 0 { | |
| 298 | + runs.next(); | |
| 299 | + } | |
| 300 | + } | |
| 301 | + | |
| 287 | 302 | font_runs.clear(); |
| 288 | 303 | } |
| 289 | 304 | |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString, | |
| 3 | − UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, | |
| 2 | + black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla, | |
| 3 | + LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary, | |
| 4 | + WrappedLineLayout, | |
| 4 | 5 | }; |
| 5 | 6 | use derive_more::{Deref, DerefMut}; |
| 6 | 7 | use smallvec::SmallVec; |
| @@ -10,6 +11,7 @@ use std::sync::Arc; · 3 unmodified lines | ||
| 10 | 11 | pub struct DecorationRun { |
| 11 | 12 | pub len: u32, |
| 12 | 13 | pub color: Hsla, |
| 14 | + pub background_color: Option<Hsla>, | |
| 13 | 15 | pub underline: Option<UnderlineStyle>, |
| 14 | 16 | } |
| 15 | 17 | |
| @@ -38,7 +40,6 @@ impl ShapedLine { · 22 unmodified lines | ||
| 38 | 40 | &self.layout, |
| 39 | 41 | line_height, |
| 40 | 42 | &self.decoration_runs, |
| 41 | − None, | |
| 42 | 43 | &[], |
| 43 | 44 | cx, |
| 44 | 45 | )?; |
| @@ -72,7 +73,6 @@ impl WrappedLine { · 27 unmodified lines | ||
| 72 | 73 | &self.layout.unwrapped_layout, |
| 73 | 74 | line_height, |
| 74 | 75 | &self.decoration_runs, |
| 75 | − self.wrap_width, | |
| 76 | 76 | &self.wrap_boundaries, |
| 77 | 77 | cx, |
| 78 | 78 | )?; |
| @@ -86,7 +86,6 @@ fn paint_line( · 7 unmodified lines | ||
| 86 | 86 | layout: &LineLayout, |
| 87 | 87 | line_height: Pixels, |
| 88 | 88 | decoration_runs: &[DecorationRun], |
| 89 | − wrap_width: Option<Pixels>, | |
| 90 | 89 | wrap_boundaries: &[WrapBoundary], |
| 91 | 90 | cx: &mut WindowContext<'_>, |
| 92 | 91 | ) -> Result<()> { |
| @@ -97,6 +96,7 @@ fn paint_line( · 4 unmodified lines | ||
| 97 | 96 | let mut run_end = 0; |
| 98 | 97 | let mut color = black(); |
| 99 | 98 | let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None; |
| 99 | + let mut current_background: Option<(Point<Pixels>, Hsla)> = None; | |
| 100 | 100 | let text_system = cx.text_system().clone(); |
| 101 | 101 | let mut glyph_origin = origin; |
| 102 | 102 | let mut prev_glyph_position = Point::default(); |
| @@ -110,12 +110,28 @@ fn paint_line( · 7 unmodified lines | ||
| 110 | 110 | |
| 111 | 111 | if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { |
| 112 | 112 | wraps.next(); |
| 113 | − if let Some((underline_origin, underline_style)) = current_underline.take() { | |
| 113 | + if let Some((background_origin, background_color)) = current_background.as_mut() { | |
| 114 | + cx.paint_quad( | |
| 115 | + Bounds { | |
| 116 | + origin: *background_origin, | |
| 117 | + size: size(glyph_origin.x - background_origin.x, line_height), | |
| 118 | + }, | |
| 119 | + Corners::default(), | |
| 120 | + *background_color, | |
| 121 | + Edges::default(), | |
| 122 | + transparent_black(), | |
| 123 | + ); | |
| 124 | + background_origin.x = origin.x; | |
| 125 | + background_origin.y += line_height; | |
| 126 | + } | |
| 127 | + if let Some((underline_origin, underline_style)) = current_underline.as_mut() { | |
| 114 | 128 | cx.paint_underline( |
| 115 | − underline_origin, | |
| 129 | + *underline_origin, | |
| 116 | 130 | glyph_origin.x - underline_origin.x, |
| 117 | − &underline_style, | |
| 118 | − )?; | |
| 131 | + underline_style, | |
| 132 | + ); | |
| 133 | + underline_origin.x = origin.x; | |
| 134 | + underline_origin.y += line_height; | |
| 119 | 135 | } |
| 120 | 136 | |
| 121 | 137 | glyph_origin.x = origin.x; |
| @@ -123,9 +139,20 @@ fn paint_line( · 1 unmodified line | ||
| 123 | 139 | } |
| 124 | 140 | prev_glyph_position = glyph.position; |
| 125 | 141 | |
| 142 | + let mut finished_background: Option<(Point<Pixels>, Hsla)> = None; | |
| 126 | 143 | let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None; |
| 127 | 144 | if glyph.index >= run_end { |
| 128 | 145 | if let Some(style_run) = decoration_runs.next() { |
| 146 | + if let Some((_, background_color)) = &mut current_background { | |
| 147 | + if style_run.background_color.as_ref() != Some(background_color) { | |
| 148 | + finished_background = current_background.take(); | |
| 149 | + } | |
| 150 | + } | |
| 151 | + if let Some(run_background) = style_run.background_color { | |
| 152 | + current_background | |
| 153 | + .get_or_insert((point(glyph_origin.x, glyph_origin.y), run_background)); | |
| 154 | + } | |
| 155 | + | |
| 129 | 156 | if let Some((_, underline_style)) = &mut current_underline { |
| 130 | 157 | if style_run.underline.as_ref() != Some(underline_style) { |
| 131 | 158 | finished_underline = current_underline.take(); |
| @@ -135,7 +162,7 @@ fn paint_line( · 3 unmodified lines | ||
| 135 | 162 | current_underline.get_or_insert(( |
| 136 | 163 | point( |
| 137 | 164 | glyph_origin.x, |
| 138 | − origin.y + baseline_offset.y + (layout.descent * 0.618), | |
| 165 | + glyph_origin.y + baseline_offset.y + (layout.descent * 0.618), | |
| 139 | 166 | ), |
| 140 | 167 | UnderlineStyle { |
| 141 | 168 | color: Some(run_underline.color.unwrap_or(style_run.color)), |
| @@ -149,16 +176,30 @@ fn paint_line( · 7 unmodified lines | ||
| 149 | 176 | color = style_run.color; |
| 150 | 177 | } else { |
| 151 | 178 | run_end = layout.len; |
| 179 | + finished_background = current_background.take(); | |
| 152 | 180 | finished_underline = current_underline.take(); |
| 153 | 181 | } |
| 154 | 182 | } |
| 155 | 183 | |
| 184 | + if let Some((background_origin, background_color)) = finished_background { | |
| 185 | + cx.paint_quad( | |
| 186 | + Bounds { | |
| 187 | + origin: background_origin, | |
| 188 | + size: size(glyph_origin.x - background_origin.x, line_height), | |
| 189 | + }, | |
| 190 | + Corners::default(), | |
| 191 | + background_color, | |
| 192 | + Edges::default(), | |
| 193 | + transparent_black(), | |
| 194 | + ); | |
| 195 | + } | |
| 196 | + | |
| 156 | 197 | if let Some((underline_origin, underline_style)) = finished_underline { |
| 157 | 198 | cx.paint_underline( |
| 158 | 199 | underline_origin, |
| 159 | 200 | glyph_origin.x - underline_origin.x, |
| 160 | 201 | &underline_style, |
| 161 | − )?; | |
| 202 | + ); | |
| 162 | 203 | } |
| 163 | 204 | |
| 164 | 205 | let max_glyph_bounds = Bounds { |
| @@ -188,13 +229,32 @@ fn paint_line( · 23 unmodified lines | ||
| 188 | 229 | } |
| 189 | 230 | } |
| 190 | 231 | |
| 232 | + let mut last_line_end_x = origin.x + layout.width; | |
| 233 | + if let Some(boundary) = wrap_boundaries.last() { | |
| 234 | + let run = &layout.runs[boundary.run_ix]; | |
| 235 | + let glyph = &run.glyphs[boundary.glyph_ix]; | |
| 236 | + last_line_end_x -= glyph.position.x; | |
| 237 | + } | |
| 238 | + | |
| 239 | + if let Some((background_origin, background_color)) = current_background.take() { | |
| 240 | + cx.paint_quad( | |
| 241 | + Bounds { | |
| 242 | + origin: background_origin, | |
| 243 | + size: size(last_line_end_x - background_origin.x, line_height), | |
| 244 | + }, | |
| 245 | + Corners::default(), | |
| 246 | + background_color, | |
| 247 | + Edges::default(), | |
| 248 | + transparent_black(), | |
| 249 | + ); | |
| 250 | + } | |
| 251 | + | |
| 191 | 252 | if let Some((underline_start, underline_style)) = current_underline.take() { |
| 192 | − let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width); | |
| 193 | 253 | cx.paint_underline( |
| 194 | 254 | underline_start, |
| 195 | − line_end_x - underline_start.x, | |
| 255 | + last_line_end_x - underline_start.x, | |
| 196 | 256 | &underline_style, |
| 197 | − )?; | |
| 257 | + ); | |
| 198 | 258 | } |
| 199 | 259 | |
| 200 | 260 | Ok(()) |
| @@ -198,6 +198,41 @@ impl WrappedLineLayout { · 197 unmodified lines | ||
| 198 | 198 | pub fn runs(&self) -> &[ShapedRun] { |
| 199 | 199 | &self.unwrapped_layout.runs |
| 200 | 200 | } |
| 201 | + | |
| 202 | + pub fn index_for_position( | |
| 203 | + &self, | |
| 204 | + position: Point<Pixels>, | |
| 205 | + line_height: Pixels, | |
| 206 | + ) -> Option<usize> { | |
| 207 | + let wrapped_line_ix = (position.y / line_height) as usize; | |
| 208 | + | |
| 209 | + let wrapped_line_start_x = if wrapped_line_ix > 0 { | |
| 210 | + let wrap_boundary_ix = wrapped_line_ix - 1; | |
| 211 | + let wrap_boundary = self.wrap_boundaries[wrap_boundary_ix]; | |
| 212 | + let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix]; | |
| 213 | + run.glyphs[wrap_boundary.glyph_ix].position.x | |
| 214 | + } else { | |
| 215 | + Pixels::ZERO | |
| 216 | + }; | |
| 217 | + | |
| 218 | + let wrapped_line_end_x = if wrapped_line_ix < self.wrap_boundaries.len() { | |
| 219 | + let next_wrap_boundary_ix = wrapped_line_ix; | |
| 220 | + let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix]; | |
| 221 | + let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix]; | |
| 222 | + run.glyphs[next_wrap_boundary.glyph_ix].position.x | |
| 223 | + } else { | |
| 224 | + self.unwrapped_layout.width | |
| 225 | + }; | |
| 226 | + | |
| 227 | + let mut position_in_unwrapped_line = position; | |
| 228 | + position_in_unwrapped_line.x += wrapped_line_start_x; | |
| 229 | + if position_in_unwrapped_line.x > wrapped_line_end_x { | |
| 230 | + None | |
| 231 | + } else { | |
| 232 | + self.unwrapped_layout | |
| 233 | + .index_for_x(position_in_unwrapped_line.x) | |
| 234 | + } | |
| 235 | + } | |
| 201 | 236 | } |
| 202 | 237 | |
| 203 | 238 | pub(crate) struct LineLayoutCache { |
| @@ -1,23 +1,17 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, | |
| 3 | − BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, | |
| 4 | − FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel, | |
| 2 | + private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow, | |
| 3 | + Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement, | |
| 4 | + LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel, | |
| 5 | 5 | WindowContext, |
| 6 | 6 | }; |
| 7 | 7 | use anyhow::{Context, Result}; |
| 8 | 8 | use std::{ |
| 9 | − any::{Any, TypeId}, | |
| 9 | + any::TypeId, | |
| 10 | 10 | hash::{Hash, Hasher}, |
| 11 | 11 | }; |
| 12 | 12 | |
| 13 | −pub trait Render: 'static + Sized { | |
| 14 | − type Element: Element<Self> + 'static; | |
| 15 | − | |
| 16 | − fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element; | |
| 17 | −} | |
| 18 | − | |
| 19 | 13 | pub struct View<V> { |
| 20 | − pub(crate) model: Model<V>, | |
| 14 | + pub model: Model<V>, | |
| 21 | 15 | } |
| 22 | 16 | |
| 23 | 17 | impl<V> Sealed for View<V> {} |
| @@ -65,15 +59,15 @@ impl<V: 'static> View<V> { · 41 unmodified lines | ||
| 65 | 59 | self.model.read(cx) |
| 66 | 60 | } |
| 67 | 61 | |
| 68 | − pub fn render_with<C>(&self, component: C) -> RenderViewWith<C, V> | |
| 69 | − where | |
| 70 | − C: 'static + Component<V>, | |
| 71 | − { | |
| 72 | − RenderViewWith { | |
| 73 | − view: self.clone(), | |
| 74 | − component: Some(component), | |
| 75 | − } | |
| 76 | − } | |
| 62 | + // pub fn render_with<E>(&self, component: E) -> RenderViewWith<E, V> | |
| 63 | + // where | |
| 64 | + // E: 'static + Element, | |
| 65 | + // { | |
| 66 | + // RenderViewWith { | |
| 67 | + // view: self.clone(), | |
| 68 | + // element: Some(component), | |
| 69 | + // } | |
| 70 | + // } | |
| 77 | 71 | |
| 78 | 72 | pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle |
| 79 | 73 | where |
| @@ -83,6 +77,24 @@ impl<V: 'static> View<V> { · 3 unmodified lines | ||
| 83 | 77 | } |
| 84 | 78 | } |
| 85 | 79 | |
| 80 | +impl<V: Render> Element for View<V> { | |
| 81 | + type State = Option<AnyElement>; | |
| 82 | + | |
| 83 | + fn layout( | |
| 84 | + &mut self, | |
| 85 | + _state: Option<Self::State>, | |
| 86 | + cx: &mut WindowContext, | |
| 87 | + ) -> (LayoutId, Self::State) { | |
| 88 | + let mut element = self.update(cx, |view, cx| view.render(cx).into_any()); | |
| 89 | + let layout_id = element.layout(cx); | |
| 90 | + (layout_id, Some(element)) | |
| 91 | + } | |
| 92 | + | |
| 93 | + fn paint(self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) { | |
| 94 | + element.take().unwrap().paint(cx); | |
| 95 | + } | |
| 96 | +} | |
| 97 | + | |
| 86 | 98 | impl<V> Clone for View<V> { |
| 87 | 99 | fn clone(&self) -> Self { |
| 88 | 100 | Self { |
| @@ -105,12 +117,6 @@ impl<V> PartialEq for View<V> { · 16 unmodified lines | ||
| 105 | 117 | |
| 106 | 118 | impl<V> Eq for View<V> {} |
| 107 | 119 | |
| 108 | −impl<V: Render, ParentViewState: 'static> Component<ParentViewState> for View<V> { | |
| 109 | − fn render(self) -> AnyElement<ParentViewState> { | |
| 110 | − AnyElement::new(AnyView::from(self)) | |
| 111 | − } | |
| 112 | −} | |
| 113 | − | |
| 114 | 120 | pub struct WeakView<V> { |
| 115 | 121 | pub(crate) model: WeakModel<V>, |
| 116 | 122 | } |
| @@ -163,8 +169,8 @@ impl<V> Eq for WeakView<V> {} · 46 unmodified lines | ||
| 163 | 169 | #[derive(Clone, Debug)] |
| 164 | 170 | pub struct AnyView { |
| 165 | 171 | model: AnyModel, |
| 166 | − layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>), | |
| 167 | − paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), | |
| 172 | + layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement), | |
| 173 | + paint: fn(&AnyView, AnyElement, &mut WindowContext), | |
| 168 | 174 | } |
| 169 | 175 | |
| 170 | 176 | impl AnyView { |
| @@ -202,21 +208,15 @@ impl AnyView { · 31 unmodified lines | ||
| 202 | 208 | cx: &mut WindowContext, |
| 203 | 209 | ) { |
| 204 | 210 | cx.with_absolute_element_offset(origin, |cx| { |
| 205 | − let (layout_id, mut rendered_element) = (self.layout)(self, cx); | |
| 211 | + let (layout_id, rendered_element) = (self.layout)(self, cx); | |
| 206 | 212 | cx.window |
| 207 | 213 | .layout_engine |
| 208 | 214 | .compute_layout(layout_id, available_space); |
| 209 | − (self.paint)(self, &mut rendered_element, cx); | |
| 215 | + (self.paint)(self, rendered_element, cx); | |
| 210 | 216 | }) |
| 211 | 217 | } |
| 212 | 218 | } |
| 213 | 219 | |
| 214 | −impl<V: 'static> Component<V> for AnyView { | |
| 215 | − fn render(self) -> AnyElement<V> { | |
| 216 | − AnyElement::new(self) | |
| 217 | − } | |
| 218 | −} | |
| 219 | − | |
| 220 | 220 | impl<V: Render> From<View<V>> for AnyView { |
| 221 | 221 | fn from(value: View<V>) -> Self { |
| 222 | 222 | AnyView { |
| @@ -227,37 +227,51 @@ impl<V: Render> From<View<V>> for AnyView { · 4 unmodified lines | ||
| 227 | 227 | } |
| 228 | 228 | } |
| 229 | 229 | |
| 230 | −impl<ParentViewState: 'static> Element<ParentViewState> for AnyView { | |
| 231 | − type ElementState = Box<dyn Any>; | |
| 230 | +impl Element for AnyView { | |
| 231 | + type State = Option<AnyElement>; | |
| 232 | + | |
| 233 | + fn layout( | |
| 234 | + &mut self, | |
| 235 | + _state: Option<Self::State>, | |
| 236 | + cx: &mut WindowContext, | |
| 237 | + ) -> (LayoutId, Self::State) { | |
| 238 | + let (layout_id, state) = (self.layout)(self, cx); | |
| 239 | + (layout_id, Some(state)) | |
| 240 | + } | |
| 241 | + | |
| 242 | + fn paint(self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) { | |
| 243 | + (self.paint)(&self, state.take().unwrap(), cx) | |
| 244 | + } | |
| 245 | +} | |
| 246 | + | |
| 247 | +impl<V: 'static + Render> IntoElement for View<V> { | |
| 248 | + type Element = View<V>; | |
| 232 | 249 | |
| 233 | 250 | fn element_id(&self) -> Option<ElementId> { |
| 234 | − Some(self.model.entity_id.into()) | |
| 251 | + Some(ElementId::from_entity_id(self.model.entity_id)) | |
| 235 | 252 | } |
| 236 | 253 | |
| 237 | − fn layout( | |
| 238 | − &mut self, | |
| 239 | − _view_state: &mut ParentViewState, | |
| 240 | − _element_state: Option<Self::ElementState>, | |
| 241 | − cx: &mut ViewContext<ParentViewState>, | |
| 242 | − ) -> (LayoutId, Self::ElementState) { | |
| 243 | − (self.layout)(self, cx) | |
| 254 | + fn into_element(self) -> Self::Element { | |
| 255 | + self | |
| 244 | 256 | } |
| 257 | +} | |
| 245 | 258 | |
| 246 | − fn paint( | |
| 247 | − &mut self, | |
| 248 | − _bounds: Bounds<Pixels>, | |
| 249 | − _view_state: &mut ParentViewState, | |
| 250 | − rendered_element: &mut Self::ElementState, | |
| 251 | − cx: &mut ViewContext<ParentViewState>, | |
| 252 | − ) { | |
| 253 | − (self.paint)(self, rendered_element, cx) | |
| 259 | +impl IntoElement for AnyView { | |
| 260 | + type Element = Self; | |
| 261 | + | |
| 262 | + fn element_id(&self) -> Option<ElementId> { | |
| 263 | + Some(ElementId::from_entity_id(self.model.entity_id)) | |
| 264 | + } | |
| 265 | + | |
| 266 | + fn into_element(self) -> Self::Element { | |
| 267 | + self | |
| 254 | 268 | } |
| 255 | 269 | } |
| 256 | 270 | |
| 257 | 271 | pub struct AnyWeakView { |
| 258 | 272 | model: AnyWeakModel, |
| 259 | − layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>), | |
| 260 | − paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), | |
| 273 | + layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement), | |
| 274 | + paint: fn(&AnyView, AnyElement, &mut WindowContext), | |
| 261 | 275 | } |
| 262 | 276 | |
| 263 | 277 | impl AnyWeakView { |
| @@ -271,7 +285,7 @@ impl AnyWeakView { · 7 unmodified lines | ||
| 271 | 285 | } |
| 272 | 286 | } |
| 273 | 287 | |
| 274 | −impl<V: Render> From<WeakView<V>> for AnyWeakView { | |
| 288 | +impl<V: 'static + Render> From<WeakView<V>> for AnyWeakView { | |
| 275 | 289 | fn from(view: WeakView<V>) -> Self { |
| 276 | 290 | Self { |
| 277 | 291 | model: view.model.into(), |
| @@ -281,97 +295,36 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView { · 3 unmodified lines | ||
| 281 | 295 | } |
| 282 | 296 | } |
| 283 | 297 | |
| 284 | −// impl<T, E> Render for T | |
| 285 | −// where | |
| 286 | −// T: 'static + FnMut(&mut WindowContext) -> E, | |
| 287 | −// E: 'static + Send + Element<T>, | |
| 288 | −// { | |
| 289 | −// type Element = E; | |
| 290 | − | |
| 291 | −// fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { | |
| 292 | −// (self)(cx) | |
| 293 | −// } | |
| 294 | −// } | |
| 295 | − | |
| 296 | −pub struct RenderViewWith<C, V> { | |
| 297 | − view: View<V>, | |
| 298 | − component: Option<C>, | |
| 299 | −} | |
| 300 | − | |
| 301 | −impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderViewWith<C, ViewState> | |
| 302 | −where | |
| 303 | − C: 'static + Component<ViewState>, | |
| 304 | − ParentViewState: 'static, | |
| 305 | − ViewState: 'static, | |
| 306 | −{ | |
| 307 | − fn render(self) -> AnyElement<ParentViewState> { | |
| 308 | − AnyElement::new(self) | |
| 309 | − } | |
| 310 | −} | |
| 311 | − | |
| 312 | −impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderViewWith<C, ViewState> | |
| 298 | +impl<T, E> Render for T | |
| 313 | 299 | where |
| 314 | − C: 'static + Component<ViewState>, | |
| 315 | − ParentViewState: 'static, | |
| 316 | − ViewState: 'static, | |
| 300 | + T: 'static + FnMut(&mut WindowContext) -> E, | |
| 301 | + E: 'static + Send + Element, | |
| 317 | 302 | { |
| 318 | − type ElementState = AnyElement<ViewState>; | |
| 319 | − | |
| 320 | − fn element_id(&self) -> Option<ElementId> { | |
| 321 | − Some(self.view.entity_id().into()) | |
| 322 | − } | |
| 303 | + type Element = E; | |
| 323 | 304 | |
| 324 | − fn layout( | |
| 325 | − &mut self, | |
| 326 | − _: &mut ParentViewState, | |
| 327 | − _: Option<Self::ElementState>, | |
| 328 | − cx: &mut ViewContext<ParentViewState>, | |
| 329 | − ) -> (LayoutId, Self::ElementState) { | |
| 330 | − self.view.update(cx, |view, cx| { | |
| 331 | − let mut element = self.component.take().unwrap().render(); | |
| 332 | − let layout_id = element.layout(view, cx); | |
| 333 | − (layout_id, element) | |
| 334 | − }) | |
| 335 | − } | |
| 336 | − | |
| 337 | − fn paint( | |
| 338 | − &mut self, | |
| 339 | − _: Bounds<Pixels>, | |
| 340 | − _: &mut ParentViewState, | |
| 341 | − element: &mut Self::ElementState, | |
| 342 | − cx: &mut ViewContext<ParentViewState>, | |
| 343 | − ) { | |
| 344 | − self.view.update(cx, |view, cx| element.paint(view, cx)) | |
| 305 | + fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { | |
| 306 | + (self)(cx) | |
| 345 | 307 | } |
| 346 | 308 | } |
| 347 | 309 | |
| 348 | 310 | mod any_view { |
| 349 | − use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext}; | |
| 350 | − use std::any::Any; | |
| 311 | + use crate::{AnyElement, AnyView, Element, LayoutId, Render, WindowContext}; | |
| 351 | 312 | |
| 352 | − pub(crate) fn layout<V: Render>( | |
| 313 | + pub(crate) fn layout<V: 'static + Render>( | |
| 353 | 314 | view: &AnyView, |
| 354 | 315 | cx: &mut WindowContext, |
| 355 | − ) -> (LayoutId, Box<dyn Any>) { | |
| 356 | − cx.with_element_id(Some(view.model.entity_id), |cx| { | |
| 357 | − let view = view.clone().downcast::<V>().unwrap(); | |
| 358 | − view.update(cx, |view, cx| { | |
| 359 | − let mut element = AnyElement::new(view.render(cx)); | |
| 360 | − let layout_id = element.layout(view, cx); | |
| 361 | − (layout_id, Box::new(element) as Box<dyn Any>) | |
| 362 | − }) | |
| 363 | − }) | |
| 316 | + ) -> (LayoutId, AnyElement) { | |
| 317 | + let view = view.clone().downcast::<V>().unwrap(); | |
| 318 | + let mut element = view.update(cx, |view, cx| view.render(cx).into_any()); | |
| 319 | + let layout_id = element.layout(cx); | |
| 320 | + (layout_id, element) | |
| 364 | 321 | } |
| 365 | 322 | |
| 366 | − pub(crate) fn paint<V: Render>( | |
| 367 | − view: &AnyView, | |
| 368 | − element: &mut Box<dyn Any>, | |
| 323 | + pub(crate) fn paint<V: 'static + Render>( | |
| 324 | + _view: &AnyView, | |
| 325 | + element: AnyElement, | |
| 369 | 326 | cx: &mut WindowContext, |
| 370 | 327 | ) { |
| 371 | − cx.with_element_id(Some(view.model.entity_id), |cx| { | |
| 372 | − let view = view.clone().downcast::<V>().unwrap(); | |
| 373 | − let element = element.downcast_mut::<AnyElement<V>>().unwrap(); | |
| 374 | − view.update(cx, |view, cx| element.paint(view, cx)) | |
| 375 | − }) | |
| 328 | + element.paint(cx); | |
| 376 | 329 | } |
| 377 | 330 | } |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, | |
| 2 | + key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext, | |
| 3 | 3 | AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, |
| 4 | 4 | DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, |
| 5 | 5 | EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, |
| @@ -187,17 +187,17 @@ impl Drop for FocusHandle { · 181 unmodified lines | ||
| 187 | 187 | |
| 188 | 188 | /// FocusableView allows users of your view to easily |
| 189 | 189 | /// focus it (using cx.focus_view(view)) |
| 190 | −pub trait FocusableView: Render { | |
| 190 | +pub trait FocusableView: 'static + Render { | |
| 191 | 191 | fn focus_handle(&self, cx: &AppContext) -> FocusHandle; |
| 192 | 192 | } |
| 193 | 193 | |
| 194 | 194 | /// ManagedView is a view (like a Modal, Popover, Menu, etc.) |
| 195 | 195 | /// where the lifecycle of the view is handled by another view. |
| 196 | −pub trait ManagedView: FocusableView + EventEmitter<Manager> {} | |
| 196 | +pub trait ManagedView: FocusableView + EventEmitter<DismissEvent> {} | |
| 197 | 197 | |
| 198 | −impl<M: FocusableView + EventEmitter<Manager>> ManagedView for M {} | |
| 198 | +impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {} | |
| 199 | 199 | |
| 200 | −pub enum Manager { | |
| 200 | +pub enum DismissEvent { | |
| 201 | 201 | Dismiss, |
| 202 | 202 | } |
| 203 | 203 | |
| @@ -230,9 +230,15 @@ pub struct Window { · 26 unmodified lines | ||
| 230 | 230 | pub(crate) focus: Option<FocusId>, |
| 231 | 231 | } |
| 232 | 232 | |
| 233 | +pub(crate) struct ElementStateBox { | |
| 234 | + inner: Box<dyn Any>, | |
| 235 | + #[cfg(debug_assertions)] | |
| 236 | + type_name: &'static str, | |
| 237 | +} | |
| 238 | + | |
| 233 | 239 | // #[derive(Default)] |
| 234 | 240 | pub(crate) struct Frame { |
| 235 | − pub(crate) element_states: HashMap<GlobalElementId, AnyBox>, | |
| 241 | + pub(crate) element_states: HashMap<GlobalElementId, ElementStateBox>, | |
| 236 | 242 | mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>, |
| 237 | 243 | pub(crate) dispatch_tree: DispatchTree, |
| 238 | 244 | pub(crate) focus_listeners: Vec<AnyFocusListener>, |
| @@ -875,7 +881,7 @@ impl<'a> WindowContext<'a> { · 636 unmodified lines | ||
| 875 | 881 | origin: Point<Pixels>, |
| 876 | 882 | width: Pixels, |
| 877 | 883 | style: &UnderlineStyle, |
| 878 | − ) -> Result<()> { | |
| 884 | + ) { | |
| 879 | 885 | let scale_factor = self.scale_factor(); |
| 880 | 886 | let height = if style.wavy { |
| 881 | 887 | style.thickness * 3. |
| @@ -899,7 +905,6 @@ impl<'a> WindowContext<'a> { · 17 unmodified lines | ||
| 899 | 905 | wavy: style.wavy, |
| 900 | 906 | }, |
| 901 | 907 | ); |
| 902 | − Ok(()) | |
| 903 | 908 | } |
| 904 | 909 | |
| 905 | 910 | /// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index. |
| @@ -1436,6 +1441,89 @@ impl<'a> WindowContext<'a> { · 530 unmodified lines | ||
| 1436 | 1441 | .dispatch_tree |
| 1437 | 1442 | .bindings_for_action(action) |
| 1438 | 1443 | } |
| 1444 | + | |
| 1445 | + pub fn listener_for<V: Render, E>( | |
| 1446 | + &self, | |
| 1447 | + view: &View<V>, | |
| 1448 | + f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static, | |
| 1449 | + ) -> impl Fn(&E, &mut WindowContext) + 'static { | |
| 1450 | + let view = view.downgrade(); | |
| 1451 | + move |e: &E, cx: &mut WindowContext| { | |
| 1452 | + view.update(cx, |view, cx| f(view, e, cx)).ok(); | |
| 1453 | + } | |
| 1454 | + } | |
| 1455 | + | |
| 1456 | + pub fn constructor_for<V: Render, R>( | |
| 1457 | + &self, | |
| 1458 | + view: &View<V>, | |
| 1459 | + f: impl Fn(&mut V, &mut ViewContext<V>) -> R + 'static, | |
| 1460 | + ) -> impl Fn(&mut WindowContext) -> R + 'static { | |
| 1461 | + let view = view.clone(); | |
| 1462 | + move |cx: &mut WindowContext| view.update(cx, |view, cx| f(view, cx)) | |
| 1463 | + } | |
| 1464 | + | |
| 1465 | + //========== ELEMENT RELATED FUNCTIONS =========== | |
| 1466 | + pub fn with_key_dispatch<R>( | |
| 1467 | + &mut self, | |
| 1468 | + context: KeyContext, | |
| 1469 | + focus_handle: Option<FocusHandle>, | |
| 1470 | + f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R, | |
| 1471 | + ) -> R { | |
| 1472 | + let window = &mut self.window; | |
| 1473 | + window | |
| 1474 | + .current_frame | |
| 1475 | + .dispatch_tree | |
| 1476 | + .push_node(context.clone()); | |
| 1477 | + if let Some(focus_handle) = focus_handle.as_ref() { | |
| 1478 | + window | |
| 1479 | + .current_frame | |
| 1480 | + .dispatch_tree | |
| 1481 | + .make_focusable(focus_handle.id); | |
| 1482 | + } | |
| 1483 | + let result = f(focus_handle, self); | |
| 1484 | + | |
| 1485 | + self.window.current_frame.dispatch_tree.pop_node(); | |
| 1486 | + | |
| 1487 | + result | |
| 1488 | + } | |
| 1489 | + | |
| 1490 | + /// Register a focus listener for the current frame only. It will be cleared | |
| 1491 | + /// on the next frame render. You should use this method only from within elements, | |
| 1492 | + /// and we may want to enforce that better via a different context type. | |
| 1493 | + // todo!() Move this to `FrameContext` to emphasize its individuality? | |
| 1494 | + pub fn on_focus_changed( | |
| 1495 | + &mut self, | |
| 1496 | + listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static, | |
| 1497 | + ) { | |
| 1498 | + self.window | |
| 1499 | + .current_frame | |
| 1500 | + .focus_listeners | |
| 1501 | + .push(Box::new(move |event, cx| { | |
| 1502 | + listener(event, cx); | |
| 1503 | + })); | |
| 1504 | + } | |
| 1505 | + | |
| 1506 | + /// Set an input handler, such as [ElementInputHandler], which interfaces with the | |
| 1507 | + /// platform to receive textual input with proper integration with concerns such | |
| 1508 | + /// as IME interactions. | |
| 1509 | + pub fn handle_input( | |
| 1510 | + &mut self, | |
| 1511 | + focus_handle: &FocusHandle, | |
| 1512 | + input_handler: impl PlatformInputHandler, | |
| 1513 | + ) { | |
| 1514 | + if focus_handle.is_focused(self) { | |
| 1515 | + self.window | |
| 1516 | + .platform_window | |
| 1517 | + .set_input_handler(Box::new(input_handler)); | |
| 1518 | + } | |
| 1519 | + } | |
| 1520 | + | |
| 1521 | + pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) { | |
| 1522 | + let mut this = self.to_async(); | |
| 1523 | + self.window | |
| 1524 | + .platform_window | |
| 1525 | + .on_should_close(Box::new(move || this.update(|_, cx| f(cx)).unwrap_or(true))) | |
| 1526 | + } | |
| 1439 | 1527 | } |
| 1440 | 1528 | |
| 1441 | 1529 | impl Context for WindowContext<'_> { |
| @@ -1559,7 +1647,7 @@ impl VisualContext for WindowContext<'_> { · 117 unmodified lines | ||
| 1559 | 1647 | build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, |
| 1560 | 1648 | ) -> Self::Result<View<V>> |
| 1561 | 1649 | where |
| 1562 | − V: Render, | |
| 1650 | + V: 'static + Render, | |
| 1563 | 1651 | { |
| 1564 | 1652 | let slot = self.app.entities.reserve(); |
| 1565 | 1653 | let view = View { |
| @@ -1582,7 +1670,7 @@ impl VisualContext for WindowContext<'_> { · 16 unmodified lines | ||
| 1582 | 1670 | where |
| 1583 | 1671 | V: ManagedView, |
| 1584 | 1672 | { |
| 1585 | − self.update_view(view, |_, cx| cx.emit(Manager::Dismiss)) | |
| 1673 | + self.update_view(view, |_, cx| cx.emit(DismissEvent::Dismiss)) | |
| 1586 | 1674 | } |
| 1587 | 1675 | } |
| 1588 | 1676 | |
| @@ -1617,6 +1705,10 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> { · 28 unmodified lines | ||
| 1617 | 1705 | self.borrow_mut() |
| 1618 | 1706 | } |
| 1619 | 1707 | |
| 1708 | + fn app(&self) -> &AppContext { | |
| 1709 | + self.borrow() | |
| 1710 | + } | |
| 1711 | + | |
| 1620 | 1712 | fn window(&self) -> &Window { |
| 1621 | 1713 | self.borrow() |
| 1622 | 1714 | } |
| @@ -1667,6 +1759,24 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> { · 44 unmodified lines | ||
| 1667 | 1759 | } |
| 1668 | 1760 | } |
| 1669 | 1761 | |
| 1762 | + /// Invoke the given function with the content mask reset to that | |
| 1763 | + /// of the window. | |
| 1764 | + fn break_content_mask<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { | |
| 1765 | + let mask = ContentMask { | |
| 1766 | + bounds: Bounds { | |
| 1767 | + origin: Point::default(), | |
| 1768 | + size: self.window().viewport_size, | |
| 1769 | + }, | |
| 1770 | + }; | |
| 1771 | + self.window_mut() | |
| 1772 | + .current_frame | |
| 1773 | + .content_mask_stack | |
| 1774 | + .push(mask); | |
| 1775 | + let result = f(self); | |
| 1776 | + self.window_mut().current_frame.content_mask_stack.pop(); | |
| 1777 | + result | |
| 1778 | + } | |
| 1779 | + | |
| 1670 | 1780 | /// Update the global element offset relative to the current offset. This is used to implement |
| 1671 | 1781 | /// scrolling. |
| 1672 | 1782 | fn with_element_offset<R>( |
| @@ -1735,10 +1845,37 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> { · 62 unmodified lines | ||
| 1735 | 1845 | .remove(&global_id) |
| 1736 | 1846 | }) |
| 1737 | 1847 | { |
| 1848 | + let ElementStateBox { | |
| 1849 | + inner, | |
| 1850 | + | |
| 1851 | + #[cfg(debug_assertions)] | |
| 1852 | + type_name | |
| 1853 | + } = any; | |
| 1738 | 1854 | // Using the extra inner option to avoid needing to reallocate a new box. |
| 1739 | − let mut state_box = any | |
| 1855 | + let mut state_box = inner | |
| 1740 | 1856 | .downcast::<Option<S>>() |
| 1741 | − .expect("invalid element state type for id"); | |
| 1857 | + .map_err(|_| { | |
| 1858 | + #[cfg(debug_assertions)] | |
| 1859 | + { | |
| 1860 | + anyhow!( | |
| 1861 | + "invalid element state type for id, requested_type {:?}, actual type: {:?}", | |
| 1862 | + std::any::type_name::<S>(), | |
| 1863 | + type_name | |
| 1864 | + ) | |
| 1865 | + } | |
| 1866 | + | |
| 1867 | + #[cfg(not(debug_assertions))] | |
| 1868 | + { | |
| 1869 | + anyhow!( | |
| 1870 | + "invalid element state type for id, requested_type {:?}", | |
| 1871 | + std::any::type_name::<S>(), | |
| 1872 | + ) | |
| 1873 | + } | |
| 1874 | + }) | |
| 1875 | + .unwrap(); | |
| 1876 | + | |
| 1877 | + // Actual: Option<AnyElement> <- View | |
| 1878 | + // Requested: () <- AnyElemet | |
| 1742 | 1879 | let state = state_box |
| 1743 | 1880 | .take() |
| 1744 | 1881 | .expect("element state is already on the stack"); |
| @@ -1747,14 +1884,27 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> { · 2 unmodified lines | ||
| 1747 | 1884 | cx.window_mut() |
| 1748 | 1885 | .current_frame |
| 1749 | 1886 | .element_states |
| 1750 | − .insert(global_id, state_box); | |
| 1887 | + .insert(global_id, ElementStateBox { | |
| 1888 | + inner: state_box, | |
| 1889 | + | |
| 1890 | + #[cfg(debug_assertions)] | |
| 1891 | + type_name | |
| 1892 | + }); | |
| 1751 | 1893 | result |
| 1752 | 1894 | } else { |
| 1753 | 1895 | let (result, state) = f(None, cx); |
| 1754 | 1896 | cx.window_mut() |
| 1755 | 1897 | .current_frame |
| 1756 | 1898 | .element_states |
| 1757 | − .insert(global_id, Box::new(Some(state))); | |
| 1899 | + .insert(global_id, | |
| 1900 | + ElementStateBox { | |
| 1901 | + inner: Box::new(Some(state)), | |
| 1902 | + | |
| 1903 | + #[cfg(debug_assertions)] | |
| 1904 | + type_name: std::any::type_name::<S>() | |
| 1905 | + } | |
| 1906 | + | |
| 1907 | + ); | |
| 1758 | 1908 | result |
| 1759 | 1909 | } |
| 1760 | 1910 | }) |
| @@ -2124,49 +2274,6 @@ impl<'a, V: 'static> ViewContext<'a, V> { · 363 unmodified lines | ||
| 2124 | 2274 | ) |
| 2125 | 2275 | } |
| 2126 | 2276 | |
| 2127 | − /// Register a focus listener for the current frame only. It will be cleared | |
| 2128 | − /// on the next frame render. You should use this method only from within elements, | |
| 2129 | − /// and we may want to enforce that better via a different context type. | |
| 2130 | − // todo!() Move this to `FrameContext` to emphasize its individuality? | |
| 2131 | − pub fn on_focus_changed( | |
| 2132 | − &mut self, | |
| 2133 | − listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static, | |
| 2134 | − ) { | |
| 2135 | − let handle = self.view().downgrade(); | |
| 2136 | − self.window | |
| 2137 | − .current_frame | |
| 2138 | − .focus_listeners | |
| 2139 | − .push(Box::new(move |event, cx| { | |
| 2140 | − handle | |
| 2141 | − .update(cx, |view, cx| listener(view, event, cx)) | |
| 2142 | − .log_err(); | |
| 2143 | − })); | |
| 2144 | − } | |
| 2145 | − | |
| 2146 | − pub fn with_key_dispatch<R>( | |
| 2147 | − &mut self, | |
| 2148 | − context: KeyContext, | |
| 2149 | − focus_handle: Option<FocusHandle>, | |
| 2150 | − f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R, | |
| 2151 | − ) -> R { | |
| 2152 | − let window = &mut self.window; | |
| 2153 | − window | |
| 2154 | − .current_frame | |
| 2155 | − .dispatch_tree | |
| 2156 | − .push_node(context.clone()); | |
| 2157 | − if let Some(focus_handle) = focus_handle.as_ref() { | |
| 2158 | − window | |
| 2159 | − .current_frame | |
| 2160 | − .dispatch_tree | |
| 2161 | − .make_focusable(focus_handle.id); | |
| 2162 | − } | |
| 2163 | − let result = f(focus_handle, self); | |
| 2164 | − | |
| 2165 | − self.window.current_frame.dispatch_tree.pop_node(); | |
| 2166 | − | |
| 2167 | − result | |
| 2168 | − } | |
| 2169 | − | |
| 2170 | 2277 | pub fn spawn<Fut, R>( |
| 2171 | 2278 | &mut self, |
| 2172 | 2279 | f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut, |
| @@ -2243,21 +2350,6 @@ impl<'a, V: 'static> ViewContext<'a, V> { · 70 unmodified lines | ||
| 2243 | 2350 | }); |
| 2244 | 2351 | } |
| 2245 | 2352 | |
| 2246 | − /// Set an input handler, such as [ElementInputHandler], which interfaces with the | |
| 2247 | − /// platform to receive textual input with proper integration with concerns such | |
| 2248 | − /// as IME interactions. | |
| 2249 | − pub fn handle_input( | |
| 2250 | − &mut self, | |
| 2251 | − focus_handle: &FocusHandle, | |
| 2252 | − input_handler: impl PlatformInputHandler, | |
| 2253 | − ) { | |
| 2254 | − if focus_handle.is_focused(self) { | |
| 2255 | − self.window | |
| 2256 | − .platform_window | |
| 2257 | − .set_input_handler(Box::new(input_handler)); | |
| 2258 | − } | |
| 2259 | − } | |
| 2260 | − | |
| 2261 | 2353 | pub fn emit<Evt>(&mut self, event: Evt) |
| 2262 | 2354 | where |
| 2263 | 2355 | Evt: 'static, |
| @@ -2282,7 +2374,17 @@ impl<'a, V: 'static> ViewContext<'a, V> { · 18 unmodified lines | ||
| 2282 | 2374 | where |
| 2283 | 2375 | V: ManagedView, |
| 2284 | 2376 | { |
| 2285 | − self.defer(|_, cx| cx.emit(Manager::Dismiss)) | |
| 2377 | + self.defer(|_, cx| cx.emit(DismissEvent::Dismiss)) | |
| 2378 | + } | |
| 2379 | + | |
| 2380 | + pub fn listener<E>( | |
| 2381 | + &self, | |
| 2382 | + f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static, | |
| 2383 | + ) -> impl Fn(&E, &mut WindowContext) + 'static { | |
| 2384 | + let view = self.view().downgrade(); | |
| 2385 | + move |e: &E, cx: &mut WindowContext| { | |
| 2386 | + view.update(cx, |view, cx| f(view, e, cx)).ok(); | |
| 2387 | + } | |
| 2286 | 2388 | } |
| 2287 | 2389 | } |
| 2288 | 2390 | |
| @@ -2355,7 +2457,7 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> { · 66 unmodified lines | ||
| 2355 | 2457 | build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W, |
| 2356 | 2458 | ) -> Self::Result<View<W>> |
| 2357 | 2459 | where |
| 2358 | − W: Render, | |
| 2460 | + W: 'static + Render, | |
| 2359 | 2461 | { |
| 2360 | 2462 | self.window_cx.replace_root_view(build_view) |
| 2361 | 2463 | } |
| @@ -2567,6 +2669,12 @@ pub enum ElementId { · 205 unmodified lines | ||
| 2567 | 2669 | FocusHandle(FocusId), |
| 2568 | 2670 | } |
| 2569 | 2671 | |
| 2672 | +impl ElementId { | |
| 2673 | + pub(crate) fn from_entity_id(entity_id: EntityId) -> Self { | |
| 2674 | + ElementId::View(entity_id) | |
| 2675 | + } | |
| 2676 | +} | |
| 2677 | + | |
| 2570 | 2678 | impl TryInto<SharedString> for ElementId { |
| 2571 | 2679 | type Error = anyhow::Error; |
| 2572 | 2680 | |
| @@ -2579,12 +2687,6 @@ impl TryInto<SharedString> for ElementId { · 6 unmodified lines | ||
| 2579 | 2687 | } |
| 2580 | 2688 | } |
| 2581 | 2689 | |
| 2582 | −impl From<EntityId> for ElementId { | |
| 2583 | − fn from(id: EntityId) -> Self { | |
| 2584 | − ElementId::View(id) | |
| 2585 | − } | |
| 2586 | −} | |
| 2587 | − | |
| 2588 | 2690 | impl From<usize> for ElementId { |
| 2589 | 2691 | fn from(id: usize) -> Self { |
| 2590 | 2692 | ElementId::Integer(id) |
| @@ -1,66 +0,0 @@ | ||
| 1 | −use proc_macro::TokenStream; | |
| 2 | −use quote::quote; | |
| 3 | −use syn::{parse_macro_input, parse_quote, DeriveInput}; | |
| 4 | − | |
| 5 | −pub fn derive_component(input: TokenStream) -> TokenStream { | |
| 6 | − let ast = parse_macro_input!(input as DeriveInput); | |
| 7 | − let name = &ast.ident; | |
| 8 | − | |
| 9 | − let mut trait_generics = ast.generics.clone(); | |
| 10 | − let view_type = if let Some(view_type) = specified_view_type(&ast) { | |
| 11 | − quote! { #view_type } | |
| 12 | − } else { | |
| 13 | − if let Some(first_type_param) = ast.generics.params.iter().find_map(|param| { | |
| 14 | − if let syn::GenericParam::Type(type_param) = param { | |
| 15 | − Some(type_param.ident.clone()) | |
| 16 | − } else { | |
| 17 | − None | |
| 18 | − } | |
| 19 | − }) { | |
| 20 | − quote! { #first_type_param } | |
| 21 | − } else { | |
| 22 | − trait_generics.params.push(parse_quote! { V: 'static }); | |
| 23 | − quote! { V } | |
| 24 | − } | |
| 25 | − }; | |
| 26 | − | |
| 27 | − let (impl_generics, _, where_clause) = trait_generics.split_for_impl(); | |
| 28 | − let (_, ty_generics, _) = ast.generics.split_for_impl(); | |
| 29 | − | |
| 30 | − let expanded = quote! { | |
| 31 | − impl #impl_generics gpui::Component<#view_type> for #name #ty_generics #where_clause { | |
| 32 | − fn render(self) -> gpui::AnyElement<#view_type> { | |
| 33 | − (move |view_state: &mut #view_type, cx: &mut gpui::ViewContext<'_, #view_type>| self.render(view_state, cx)) | |
| 34 | − .render() | |
| 35 | − } | |
| 36 | − } | |
| 37 | − }; | |
| 38 | − | |
| 39 | − TokenStream::from(expanded) | |
| 40 | −} | |
| 41 | − | |
| 42 | −fn specified_view_type(ast: &DeriveInput) -> Option<proc_macro2::Ident> { | |
| 43 | − let component_attr = ast | |
| 44 | − .attrs | |
| 45 | − .iter() | |
| 46 | − .find(|attr| attr.path.is_ident("component"))?; | |
| 47 | − | |
| 48 | − if let Ok(syn::Meta::List(meta_list)) = component_attr.parse_meta() { | |
| 49 | − meta_list.nested.iter().find_map(|nested| { | |
| 50 | − if let syn::NestedMeta::Meta(syn::Meta::NameValue(nv)) = nested { | |
| 51 | − if nv.path.is_ident("view_type") { | |
| 52 | − if let syn::Lit::Str(lit_str) = &nv.lit { | |
| 53 | − return Some( | |
| 54 | − lit_str | |
| 55 | − .parse::<syn::Ident>() | |
| 56 | − .expect("Failed to parse view_type"), | |
| 57 | − ); | |
| 58 | − } | |
| 59 | − } | |
| 60 | − } | |
| 61 | − None | |
| 62 | − }) | |
| 63 | − } else { | |
| 64 | − None | |
| 65 | − } | |
| 66 | −} | |
| @@ -0,0 +1,27 @@ | ||
| 1 | +use proc_macro::TokenStream; | |
| 2 | +use quote::quote; | |
| 3 | +use syn::{parse_macro_input, DeriveInput}; | |
| 4 | + | |
| 5 | +pub fn derive_into_element(input: TokenStream) -> TokenStream { | |
| 6 | + let ast = parse_macro_input!(input as DeriveInput); | |
| 7 | + let type_name = &ast.ident; | |
| 8 | + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); | |
| 9 | + | |
| 10 | + let gen = quote! { | |
| 11 | + impl #impl_generics gpui::IntoElement for #type_name #type_generics | |
| 12 | + #where_clause | |
| 13 | + { | |
| 14 | + type Element = gpui::Component<Self>; | |
| 15 | + | |
| 16 | + fn element_id(&self) -> Option<ElementId> { | |
| 17 | + None | |
| 18 | + } | |
| 19 | + | |
| 20 | + fn into_element(self) -> Self::Element { | |
| 21 | + gpui::Component::new(self) | |
| 22 | + } | |
| 23 | + } | |
| 24 | + }; | |
| 25 | + | |
| 26 | + gen.into() | |
| 27 | +} | |
| @@ -1,16 +1,11 @@ | ||
| 1 | 1 | mod action; |
| 2 | −mod derive_component; | |
| 2 | +mod derive_into_element; | |
| 3 | 3 | mod register_action; |
| 4 | 4 | mod style_helpers; |
| 5 | 5 | mod test; |
| 6 | 6 | |
| 7 | 7 | use proc_macro::TokenStream; |
| 8 | 8 | |
| 9 | −#[proc_macro] | |
| 10 | −pub fn style_helpers(args: TokenStream) -> TokenStream { | |
| 11 | − style_helpers::style_helpers(args) | |
| 12 | −} | |
| 13 | − | |
| 14 | 9 | #[proc_macro_derive(Action)] |
| 15 | 10 | pub fn action(input: TokenStream) -> TokenStream { |
| 16 | 11 | action::action(input) |
| @@ -21,9 +16,14 @@ pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream { · 4 unmodified lines | ||
| 21 | 16 | register_action::register_action_macro(attr, item) |
| 22 | 17 | } |
| 23 | 18 | |
| 24 | −#[proc_macro_derive(Component, attributes(component))] | |
| 25 | −pub fn derive_component(input: TokenStream) -> TokenStream { | |
| 26 | − derive_component::derive_component(input) | |
| 19 | +#[proc_macro_derive(IntoElement)] | |
| 20 | +pub fn derive_into_element(input: TokenStream) -> TokenStream { | |
| 21 | + derive_into_element::derive_into_element(input) | |
| 22 | +} | |
| 23 | + | |
| 24 | +#[proc_macro] | |
| 25 | +pub fn style_helpers(input: TokenStream) -> TokenStream { | |
| 26 | + style_helpers::style_helpers(input) | |
| 27 | 27 | } |
| 28 | 28 | |
| 29 | 29 | #[proc_macro_attribute] |
| @@ -11,7 +11,7 @@ pub struct HighlightId(pub u32); · 10 unmodified lines | ||
| 11 | 11 | const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); |
| 12 | 12 | |
| 13 | 13 | impl HighlightMap { |
| 14 | − pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self { | |
| 14 | + pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self { | |
| 15 | 15 | // For each capture name in the highlight query, find the longest |
| 16 | 16 | // key in the theme's syntax styles that matches all of the |
| 17 | 17 | // dot-separated components of the capture name. |
| @@ -98,9 +98,9 @@ mod tests { · 80 unmodified lines | ||
| 98 | 98 | ); |
| 99 | 99 | |
| 100 | 100 | let capture_names = &[ |
| 101 | − "function.special".to_string(), | |
| 102 | − "function.async.rust".to_string(), | |
| 103 | − "variable.builtin.self".to_string(), | |
| 101 | + "function.special", | |
| 102 | + "function.async.rust", | |
| 103 | + "variable.builtin.self", | |
| 104 | 104 | ]; |
| 105 | 105 | |
| 106 | 106 | let map = HighlightMap::new(capture_names, &theme); |
| @@ -1383,7 +1383,7 @@ impl Language { · 1382 unmodified lines | ||
| 1383 | 1383 | let query = Query::new(self.grammar_mut().ts_language, source)?; |
| 1384 | 1384 | |
| 1385 | 1385 | let mut override_configs_by_id = HashMap::default(); |
| 1386 | − for (ix, name) in query.capture_names().iter().enumerate() { | |
| 1386 | + for (ix, name) in query.capture_names().iter().copied().enumerate() { | |
| 1387 | 1387 | if !name.starts_with('_') { |
| 1388 | 1388 | let value = self.config.overrides.remove(name).unwrap_or_default(); |
| 1389 | 1389 | for server_name in &value.opt_into_language_servers { |
| @@ -1396,7 +1396,7 @@ impl Language { · 6 unmodified lines | ||
| 1396 | 1396 | } |
| 1397 | 1397 | } |
| 1398 | 1398 | |
| 1399 | − override_configs_by_id.insert(ix as u32, (name.clone(), value)); | |
| 1399 | + override_configs_by_id.insert(ix as u32, (name.into(), value)); | |
| 1400 | 1400 | } |
| 1401 | 1401 | } |
| 1402 | 1402 | |
| @@ -1300,7 +1300,7 @@ fn assert_capture_ranges( · 1299 unmodified lines | ||
| 1300 | 1300 | .collect::<Vec<_>>(); |
| 1301 | 1301 | for capture in captures { |
| 1302 | 1302 | let name = &queries[capture.grammar_index].capture_names()[capture.index as usize]; |
| 1303 | − if highlight_query_capture_names.contains(&name.as_str()) { | |
| 1303 | + if highlight_query_capture_names.contains(&name) { | |
| 1304 | 1304 | actual_ranges.push(capture.node.byte_range()); |
| 1305 | 1305 | } |
| 1306 | 1306 | } |
| @@ -7,6 +7,7 @@ pub use crate::{ · 6 unmodified lines | ||
| 7 | 7 | use crate::{ |
| 8 | 8 | diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, |
| 9 | 9 | language_settings::{language_settings, LanguageSettings}, |
| 10 | + markdown::parse_markdown, | |
| 10 | 11 | outline::OutlineItem, |
| 11 | 12 | syntax_map::{ |
| 12 | 13 | SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, |
| @@ -155,12 +156,52 @@ pub struct Diagnostic { · 142 unmodified lines | ||
| 155 | 156 | pub is_unnecessary: bool, |
| 156 | 157 | } |
| 157 | 158 | |
| 159 | +pub async fn prepare_completion_documentation( | |
| 160 | + documentation: &lsp::Documentation, | |
| 161 | + language_registry: &Arc<LanguageRegistry>, | |
| 162 | + language: Option<Arc<Language>>, | |
| 163 | +) -> Documentation { | |
| 164 | + match documentation { | |
| 165 | + lsp::Documentation::String(text) => { | |
| 166 | + if text.lines().count() <= 1 { | |
| 167 | + Documentation::SingleLine(text.clone()) | |
| 168 | + } else { | |
| 169 | + Documentation::MultiLinePlainText(text.clone()) | |
| 170 | + } | |
| 171 | + } | |
| 172 | + | |
| 173 | + lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind { | |
| 174 | + lsp::MarkupKind::PlainText => { | |
| 175 | + if value.lines().count() <= 1 { | |
| 176 | + Documentation::SingleLine(value.clone()) | |
| 177 | + } else { | |
| 178 | + Documentation::MultiLinePlainText(value.clone()) | |
| 179 | + } | |
| 180 | + } | |
| 181 | + | |
| 182 | + lsp::MarkupKind::Markdown => { | |
| 183 | + let parsed = parse_markdown(value, language_registry, language).await; | |
| 184 | + Documentation::MultiLineMarkdown(parsed) | |
| 185 | + } | |
| 186 | + }, | |
| 187 | + } | |
| 188 | +} | |
| 189 | + | |
| 190 | +#[derive(Clone, Debug)] | |
| 191 | +pub enum Documentation { | |
| 192 | + Undocumented, | |
| 193 | + SingleLine(String), | |
| 194 | + MultiLinePlainText(String), | |
| 195 | + MultiLineMarkdown(ParsedMarkdown), | |
| 196 | +} | |
| 197 | + | |
| 158 | 198 | #[derive(Clone, Debug)] |
| 159 | 199 | pub struct Completion { |
| 160 | 200 | pub old_range: Range<Anchor>, |
| 161 | 201 | pub new_text: String, |
| 162 | 202 | pub label: CodeLabel, |
| 163 | 203 | pub server_id: LanguageServerId, |
| 204 | + pub documentation: Option<Documentation>, | |
| 164 | 205 | pub lsp_completion: lsp::CompletionItem, |
| 165 | 206 | } |
| 166 | 207 | |
| @@ -11,7 +11,7 @@ pub struct HighlightId(pub u32); · 10 unmodified lines | ||
| 11 | 11 | const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); |
| 12 | 12 | |
| 13 | 13 | impl HighlightMap { |
| 14 | − pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self { | |
| 14 | + pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self { | |
| 15 | 15 | // For each capture name in the highlight query, find the longest |
| 16 | 16 | // key in the theme's syntax styles that matches all of the |
| 17 | 17 | // dot-separated components of the capture name. |
| @@ -100,9 +100,9 @@ mod tests { · 82 unmodified lines | ||
| 100 | 100 | }; |
| 101 | 101 | |
| 102 | 102 | let capture_names = &[ |
| 103 | − "function.special".to_string(), | |
| 104 | − "function.async.rust".to_string(), | |
| 105 | − "variable.builtin.self".to_string(), | |
| 103 | + "function.special", | |
| 104 | + "function.async.rust", | |
| 105 | + "variable.builtin.self", | |
| 106 | 106 | ]; |
| 107 | 107 | |
| 108 | 108 | let map = HighlightMap::new(capture_names, &theme); |
| @@ -1391,7 +1391,7 @@ impl Language { · 1390 unmodified lines | ||
| 1391 | 1391 | let mut override_configs_by_id = HashMap::default(); |
| 1392 | 1392 | for (ix, name) in query.capture_names().iter().enumerate() { |
| 1393 | 1393 | if !name.starts_with('_') { |
| 1394 | − let value = self.config.overrides.remove(name).unwrap_or_default(); | |
| 1394 | + let value = self.config.overrides.remove(*name).unwrap_or_default(); | |
| 1395 | 1395 | for server_name in &value.opt_into_language_servers { |
| 1396 | 1396 | if !self |
| 1397 | 1397 | .config |
| @@ -1402,7 +1402,7 @@ impl Language { · 4 unmodified lines | ||
| 1402 | 1402 | } |
| 1403 | 1403 | } |
| 1404 | 1404 | |
| 1405 | − override_configs_by_id.insert(ix as u32, (name.clone(), value)); | |
| 1405 | + override_configs_by_id.insert(ix as u32, (name.to_string(), value)); | |
| 1406 | 1406 | } |
| 1407 | 1407 | } |
| 1408 | 1408 | |
| @@ -482,6 +482,7 @@ pub async fn deserialize_completion( · 481 unmodified lines | ||
| 482 | 482 | lsp_completion.filter_text.as_deref(), |
| 483 | 483 | ) |
| 484 | 484 | }), |
| 485 | + documentation: None, | |
| 485 | 486 | server_id: LanguageServerId(completion.server_id as usize), |
| 486 | 487 | lsp_completion, |
| 487 | 488 | }) |
| @@ -1300,7 +1300,7 @@ fn assert_capture_ranges( · 1299 unmodified lines | ||
| 1300 | 1300 | .collect::<Vec<_>>(); |
| 1301 | 1301 | for capture in captures { |
| 1302 | 1302 | let name = &queries[capture.grammar_index].capture_names()[capture.index as usize]; |
| 1303 | − if highlight_query_capture_names.contains(&name.as_str()) { | |
| 1303 | + if highlight_query_capture_names.contains(&name) { | |
| 1304 | 1304 | actual_ranges.push(capture.node.byte_range()); |
| 1305 | 1305 | } |
| 1306 | 1306 | } |
| @@ -61,12 +61,14 @@ fn build_bridge(swift_target: &SwiftTarget) { · 60 unmodified lines | ||
| 61 | 61 | |
| 62 | 62 | let swift_package_root = swift_package_root(); |
| 63 | 63 | let swift_target_folder = swift_target_folder(); |
| 64 | + let swift_cache_folder = swift_cache_folder(); | |
| 64 | 65 | if !Command::new("swift") |
| 65 | 66 | .arg("build") |
| 66 | 67 | .arg("--disable-automatic-resolution") |
| 67 | 68 | .args(["--configuration", &env::var("PROFILE").unwrap()]) |
| 68 | 69 | .args(["--triple", &swift_target.target.triple]) |
| 69 | 70 | .args(["--build-path".into(), swift_target_folder]) |
| 71 | + .args(["--cache-path".into(), swift_cache_folder]) | |
| 70 | 72 | .current_dir(&swift_package_root) |
| 71 | 73 | .status() |
| 72 | 74 | .unwrap() |
| @@ -133,9 +135,17 @@ fn swift_package_root() -> PathBuf { · 60 unmodified lines | ||
| 133 | 135 | } |
| 134 | 136 | |
| 135 | 137 | fn swift_target_folder() -> PathBuf { |
| 138 | + let target = env::var("TARGET").unwrap(); | |
| 136 | 139 | env::current_dir() |
| 137 | 140 | .unwrap() |
| 138 | − .join(format!("../../target/{SWIFT_PACKAGE_NAME}")) | |
| 141 | + .join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_target")) | |
| 142 | +} | |
| 143 | + | |
| 144 | +fn swift_cache_folder() -> PathBuf { | |
| 145 | + let target = env::var("TARGET").unwrap(); | |
| 146 | + env::current_dir() | |
| 147 | + .unwrap() | |
| 148 | + .join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_cache")) | |
| 139 | 149 | } |
| 140 | 150 | |
| 141 | 151 | fn copy_dir(source: &Path, destination: &Path) { |
| @@ -73,6 +73,7 @@ impl RealNodeRuntime { · 72 unmodified lines | ||
| 73 | 73 | let npm_file = node_dir.join("bin/npm"); |
| 74 | 74 | |
| 75 | 75 | let result = Command::new(&node_binary) |
| 76 | + .env_clear() | |
| 76 | 77 | .arg(npm_file) |
| 77 | 78 | .arg("--version") |
| 78 | 79 | .stdin(Stdio::null()) |
| @@ -149,6 +150,7 @@ impl NodeRuntime for RealNodeRuntime { · 70 unmodified lines | ||
| 149 | 150 | } |
| 150 | 151 | |
| 151 | 152 | let mut command = Command::new(node_binary); |
| 153 | + command.env_clear(); | |
| 152 | 154 | command.env("PATH", env_path); |
| 153 | 155 | command.arg(npm_file).arg(subcommand); |
| 154 | 156 | command.args(["--cache".into(), installation_path.join("cache")]); |
| @@ -200,11 +202,11 @@ impl NodeRuntime for RealNodeRuntime { · 45 unmodified lines | ||
| 200 | 202 | &[ |
| 201 | 203 | name, |
| 202 | 204 | "--json", |
| 203 | − "-fetch-retry-mintimeout", | |
| 205 | + "--fetch-retry-mintimeout", | |
| 204 | 206 | "2000", |
| 205 | − "-fetch-retry-maxtimeout", | |
| 207 | + "--fetch-retry-maxtimeout", | |
| 206 | 208 | "5000", |
| 207 | − "-fetch-timeout", | |
| 209 | + "--fetch-timeout", | |
| 208 | 210 | "5000", |
| 209 | 211 | ], |
| 210 | 212 | ) |
| @@ -229,11 +231,11 @@ impl NodeRuntime for RealNodeRuntime { · 18 unmodified lines | ||
| 229 | 231 | |
| 230 | 232 | let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect(); |
| 231 | 233 | arguments.extend_from_slice(&[ |
| 232 | − "-fetch-retry-mintimeout", | |
| 234 | + "--fetch-retry-mintimeout", | |
| 233 | 235 | "2000", |
| 234 | − "-fetch-retry-maxtimeout", | |
| 236 | + "--fetch-retry-maxtimeout", | |
| 235 | 237 | "5000", |
| 236 | − "-fetch-timeout", | |
| 238 | + "--fetch-timeout", | |
| 237 | 239 | "5000", |
| 238 | 240 | ]); |
| 239 | 241 | |
| @@ -1,10 +1,10 @@ | ||
| 1 | 1 | use editor::Editor; |
| 2 | 2 | use gpui::{ |
| 3 | − div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView, | |
| 4 | − MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, | |
| 3 | + div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton, | |
| 4 | + MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, | |
| 5 | 5 | }; |
| 6 | 6 | use std::{cmp, sync::Arc}; |
| 7 | −use ui::{prelude::*, v_stack, Divider, Label, TextColor}; | |
| 7 | +use ui::{prelude::*, v_stack, Color, Divider, Label}; | |
| 8 | 8 | |
| 9 | 9 | pub struct Picker<D: PickerDelegate> { |
| 10 | 10 | pub delegate: D, |
| @@ -15,7 +15,7 @@ pub struct Picker<D: PickerDelegate> { · 4 unmodified lines | ||
| 15 | 15 | } |
| 16 | 16 | |
| 17 | 17 | pub trait PickerDelegate: Sized + 'static { |
| 18 | − type ListItem: Component<Picker<Self>>; | |
| 18 | + type ListItem: IntoElement; | |
| 19 | 19 | |
| 20 | 20 | fn match_count(&self) -> usize; |
| 21 | 21 | fn selected_index(&self) -> usize; |
| @@ -32,7 +32,7 @@ pub trait PickerDelegate: Sized + 'static { · 10 unmodified lines | ||
| 32 | 32 | ix: usize, |
| 33 | 33 | selected: bool, |
| 34 | 34 | cx: &mut ViewContext<Picker<Self>>, |
| 35 | − ) -> Self::ListItem; | |
| 35 | + ) -> Option<Self::ListItem>; | |
| 36 | 36 | } |
| 37 | 37 | |
| 38 | 38 | impl<D: PickerDelegate> FocusableView for Picker<D> { |
| @@ -114,6 +114,7 @@ impl<D: PickerDelegate> Picker<D> { · 75 unmodified lines | ||
| 114 | 114 | } |
| 115 | 115 | |
| 116 | 116 | fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) { |
| 117 | + dbg!("canceling!"); | |
| 117 | 118 | self.delegate.dismissed(cx); |
| 118 | 119 | } |
| 119 | 120 | |
| @@ -181,20 +182,20 @@ impl<D: PickerDelegate> Picker<D> { · 61 unmodified lines | ||
| 181 | 182 | } |
| 182 | 183 | |
| 183 | 184 | impl<D: PickerDelegate> Render for Picker<D> { |
| 184 | − type Element = Div<Self>; | |
| 185 | + type Element = Div; | |
| 185 | 186 | |
| 186 | 187 | fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { |
| 187 | 188 | div() |
| 188 | 189 | .key_context("picker") |
| 189 | 190 | .size_full() |
| 190 | 191 | .elevation_2(cx) |
| 191 | − .on_action(Self::select_next) | |
| 192 | − .on_action(Self::select_prev) | |
| 193 | − .on_action(Self::select_first) | |
| 194 | − .on_action(Self::select_last) | |
| 195 | − .on_action(Self::cancel) | |
| 196 | − .on_action(Self::confirm) | |
| 197 | − .on_action(Self::secondary_confirm) | |
| 192 | + .on_action(cx.listener(Self::select_next)) | |
| 193 | + .on_action(cx.listener(Self::select_prev)) | |
| 194 | + .on_action(cx.listener(Self::select_first)) | |
| 195 | + .on_action(cx.listener(Self::select_last)) | |
| 196 | + .on_action(cx.listener(Self::cancel)) | |
| 197 | + .on_action(cx.listener(Self::confirm)) | |
| 198 | + .on_action(cx.listener(Self::secondary_confirm)) | |
| 198 | 199 | .child( |
| 199 | 200 | v_stack() |
| 200 | 201 | .py_0p5() |
| @@ -208,31 +209,37 @@ impl<D: PickerDelegate> Render for Picker<D> { · 7 unmodified lines | ||
| 208 | 209 | .p_1() |
| 209 | 210 | .grow() |
| 210 | 211 | .child( |
| 211 | − uniform_list("candidates", self.delegate.match_count(), { | |
| 212 | − move |this: &mut Self, visible_range, cx| { | |
| 213 | − let selected_ix = this.delegate.selected_index(); | |
| 214 | − visible_range | |
| 215 | − .map(|ix| { | |
| 216 | − div() | |
| 217 | − .on_mouse_down( | |
| 218 | − MouseButton::Left, | |
| 219 | − move |this: &mut Self, event, cx| { | |
| 220 | − this.handle_click( | |
| 221 | − ix, | |
| 222 | − event.modifiers.command, | |
| 223 | − cx, | |
| 224 | − ) | |
| 225 | − }, | |
| 226 | − ) | |
| 227 | − .child(this.delegate.render_match( | |
| 228 | − ix, | |
| 229 | − ix == selected_ix, | |
| 230 | − cx, | |
| 231 | − )) | |
| 232 | − }) | |
| 233 | − .collect() | |
| 234 | − } | |
| 235 | − }) | |
| 212 | + uniform_list( | |
| 213 | + cx.view().clone(), | |
| 214 | + "candidates", | |
| 215 | + self.delegate.match_count(), | |
| 216 | + { | |
| 217 | + let selected_index = self.delegate.selected_index(); | |
| 218 | + | |
| 219 | + move |picker, visible_range, cx| { | |
| 220 | + visible_range | |
| 221 | + .map(|ix| { | |
| 222 | + div() | |
| 223 | + .on_mouse_down( | |
| 224 | + MouseButton::Left, | |
| 225 | + cx.listener(move |this, event: &MouseDownEvent, cx| { | |
| 226 | + this.handle_click( | |
| 227 | + ix, | |
| 228 | + event.modifiers.command, | |
| 229 | + cx, | |
| 230 | + ) | |
| 231 | + }), | |
| 232 | + ) | |
| 233 | + .children(picker.delegate.render_match( | |
| 234 | + ix, | |
| 235 | + ix == selected_index, | |
| 236 | + cx, | |
| 237 | + )) | |
| 238 | + }) | |
| 239 | + .collect() | |
| 240 | + } | |
| 241 | + }, | |
| 242 | + ) | |
| 236 | 243 | .track_scroll(self.scroll_handle.clone()), |
| 237 | 244 | ) |
| 238 | 245 | .max_h_72() |
| @@ -244,7 +251,7 @@ impl<D: PickerDelegate> Render for Picker<D> { · 5 unmodified lines | ||
| 244 | 251 | v_stack().p_1().grow().child( |
| 245 | 252 | div() |
| 246 | 253 | .px_1() |
| 247 | − .child(Label::new("No matches").color(TextColor::Muted)), | |
| 254 | + .child(Label::new("No matches").color(Color::Muted)), | |
| 248 | 255 | ), |
| 249 | 256 | ) |
| 250 | 257 | }) |
| @@ -13,7 +13,7 @@ mod worktree_tests; · 12 unmodified lines | ||
| 13 | 13 | use anyhow::{anyhow, Context, Result}; |
| 14 | 14 | use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; |
| 15 | 15 | use clock::ReplicaId; |
| 16 | −use collections::{hash_map, BTreeMap, HashMap, HashSet}; | |
| 16 | +use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; | |
| 17 | 17 | use copilot::Copilot; |
| 18 | 18 | use futures::{ |
| 19 | 19 | channel::{ |
| @@ -62,7 +62,10 @@ use serde::Serialize; · 42 unmodified lines | ||
| 62 | 62 | use settings::SettingsStore; |
| 63 | 63 | use sha2::{Digest, Sha256}; |
| 64 | 64 | use similar::{ChangeTag, TextDiff}; |
| 65 | −use smol::channel::{Receiver, Sender}; | |
| 65 | +use smol::{ | |
| 66 | + channel::{Receiver, Sender}, | |
| 67 | + lock::Semaphore, | |
| 68 | +}; | |
| 66 | 69 | use std::{ |
| 67 | 70 | cmp::{self, Ordering}, |
| 68 | 71 | convert::TryInto, |
| @@ -557,6 +560,7 @@ enum SearchMatchCandidate { · 488 unmodified lines | ||
| 557 | 560 | }, |
| 558 | 561 | Path { |
| 559 | 562 | worktree_id: WorktreeId, |
| 563 | + is_ignored: bool, | |
| 560 | 564 | path: Arc<Path>, |
| 561 | 565 | }, |
| 562 | 566 | } |
| @@ -5742,13 +5746,18 @@ impl Project { · 5179 unmodified lines | ||
| 5742 | 5746 | .await |
| 5743 | 5747 | .log_err(); |
| 5744 | 5748 | } |
| 5749 | + | |
| 5745 | 5750 | background |
| 5746 | 5751 | .scoped(|scope| { |
| 5752 | + let max_concurrent_workers = Arc::new(Semaphore::new(workers)); | |
| 5753 | + | |
| 5747 | 5754 | for worker_ix in 0..workers { |
| 5748 | 5755 | let worker_start_ix = worker_ix * paths_per_worker; |
| 5749 | 5756 | let worker_end_ix = worker_start_ix + paths_per_worker; |
| 5750 | 5757 | let unnamed_buffers = opened_buffers.clone(); |
| 5758 | + let limiter = Arc::clone(&max_concurrent_workers); | |
| 5751 | 5759 | scope.spawn(async move { |
| 5760 | + let _guard = limiter.acquire().await; | |
| 5752 | 5761 | let mut snapshot_start_ix = 0; |
| 5753 | 5762 | let mut abs_path = PathBuf::new(); |
| 5754 | 5763 | for snapshot in snapshots { |
| @@ -5797,6 +5806,7 @@ impl Project { · 42 unmodified lines | ||
| 5797 | 5806 | let project_path = SearchMatchCandidate::Path { |
| 5798 | 5807 | worktree_id: snapshot.id(), |
| 5799 | 5808 | path: entry.path.clone(), |
| 5809 | + is_ignored: entry.is_ignored, | |
| 5800 | 5810 | }; |
| 5801 | 5811 | if matching_paths_tx.send(project_path).await.is_err() { |
| 5802 | 5812 | break; |
| @@ -5809,6 +5819,94 @@ impl Project { · 6 unmodified lines | ||
| 5809 | 5819 | } |
| 5810 | 5820 | }); |
| 5811 | 5821 | } |
| 5822 | + | |
| 5823 | + if query.include_ignored() { | |
| 5824 | + for snapshot in snapshots { | |
| 5825 | + for ignored_entry in snapshot | |
| 5826 | + .entries(query.include_ignored()) | |
| 5827 | + .filter(|e| e.is_ignored) | |
| 5828 | + { | |
| 5829 | + let limiter = Arc::clone(&max_concurrent_workers); | |
| 5830 | + scope.spawn(async move { | |
| 5831 | + let _guard = limiter.acquire().await; | |
| 5832 | + let mut ignored_paths_to_process = | |
| 5833 | + VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]); | |
| 5834 | + while let Some(ignored_abs_path) = | |
| 5835 | + ignored_paths_to_process.pop_front() | |
| 5836 | + { | |
| 5837 | + if !query.file_matches(Some(&ignored_abs_path)) | |
| 5838 | + || snapshot.is_path_excluded(&ignored_abs_path) | |
| 5839 | + { | |
| 5840 | + continue; | |
| 5841 | + } | |
| 5842 | + if let Some(fs_metadata) = fs | |
| 5843 | + .metadata(&ignored_abs_path) | |
| 5844 | + .await | |
| 5845 | + .with_context(|| { | |
| 5846 | + format!("fetching fs metadata for {ignored_abs_path:?}") | |
| 5847 | + }) | |
| 5848 | + .log_err() | |
| 5849 | + .flatten() | |
| 5850 | + { | |
| 5851 | + if fs_metadata.is_dir { | |
| 5852 | + if let Some(mut subfiles) = fs | |
| 5853 | + .read_dir(&ignored_abs_path) | |
| 5854 | + .await | |
| 5855 | + .with_context(|| { | |
| 5856 | + format!( | |
| 5857 | + "listing ignored path {ignored_abs_path:?}" | |
| 5858 | + ) | |
| 5859 | + }) | |
| 5860 | + .log_err() | |
| 5861 | + { | |
| 5862 | + while let Some(subfile) = subfiles.next().await { | |
| 5863 | + if let Some(subfile) = subfile.log_err() { | |
| 5864 | + ignored_paths_to_process.push_back(subfile); | |
| 5865 | + } | |
| 5866 | + } | |
| 5867 | + } | |
| 5868 | + } else if !fs_metadata.is_symlink { | |
| 5869 | + let matches = if let Some(file) = fs | |
| 5870 | + .open_sync(&ignored_abs_path) | |
| 5871 | + .await | |
| 5872 | + .with_context(|| { | |
| 5873 | + format!( | |
| 5874 | + "Opening ignored path {ignored_abs_path:?}" | |
| 5875 | + ) | |
| 5876 | + }) | |
| 5877 | + .log_err() | |
| 5878 | + { | |
| 5879 | + query.detect(file).unwrap_or(false) | |
| 5880 | + } else { | |
| 5881 | + false | |
| 5882 | + }; | |
| 5883 | + if matches { | |
| 5884 | + let project_path = SearchMatchCandidate::Path { | |
| 5885 | + worktree_id: snapshot.id(), | |
| 5886 | + path: Arc::from( | |
| 5887 | + ignored_abs_path | |
| 5888 | + .strip_prefix(snapshot.abs_path()) | |
| 5889 | + .expect( | |
| 5890 | + "scanning worktree-related files", | |
| 5891 | + ), | |
| 5892 | + ), | |
| 5893 | + is_ignored: true, | |
| 5894 | + }; | |
| 5895 | + if matching_paths_tx | |
| 5896 | + .send(project_path) | |
| 5897 | + .await | |
| 5898 | + .is_err() | |
| 5899 | + { | |
| 5900 | + return; | |
| 5901 | + } | |
| 5902 | + } | |
| 5903 | + } | |
| 5904 | + } | |
| 5905 | + } | |
| 5906 | + }); | |
| 5907 | + } | |
| 5908 | + } | |
| 5909 | + } | |
| 5812 | 5910 | }) |
| 5813 | 5911 | .await; |
| 5814 | 5912 | } |
| @@ -5917,11 +6015,24 @@ impl Project { · 102 unmodified lines | ||
| 5917 | 6015 | let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); |
| 5918 | 6016 | let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); |
| 5919 | 6017 | cx.spawn(|this, cx| async move { |
| 5920 | − let mut buffers = vec![]; | |
| 6018 | + let mut buffers = Vec::new(); | |
| 6019 | + let mut ignored_buffers = Vec::new(); | |
| 5921 | 6020 | while let Some(entry) = matching_paths_rx.next().await { |
| 5922 | − buffers.push(entry); | |
| 6021 | + if matches!( | |
| 6022 | + entry, | |
| 6023 | + SearchMatchCandidate::Path { | |
| 6024 | + is_ignored: true, | |
| 6025 | + .. | |
| 6026 | + } | |
| 6027 | + ) { | |
| 6028 | + ignored_buffers.push(entry); | |
| 6029 | + } else { | |
| 6030 | + buffers.push(entry); | |
| 6031 | + } | |
| 5923 | 6032 | } |
| 5924 | 6033 | buffers.sort_by_key(|candidate| candidate.path()); |
| 6034 | + ignored_buffers.sort_by_key(|candidate| candidate.path()); | |
| 6035 | + buffers.extend(ignored_buffers); | |
| 5925 | 6036 | let matching_paths = buffers.clone(); |
| 5926 | 6037 | let _ = sorted_buffers_tx.send(buffers); |
| 5927 | 6038 | for (index, candidate) in matching_paths.into_iter().enumerate() { |
| @@ -5933,7 +6044,9 @@ impl Project { · 5 unmodified lines | ||
| 5933 | 6044 | cx.spawn(|mut cx| async move { |
| 5934 | 6045 | let buffer = match candidate { |
| 5935 | 6046 | SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), |
| 5936 | − SearchMatchCandidate::Path { worktree_id, path } => this | |
| 6047 | + SearchMatchCandidate::Path { | |
| 6048 | + worktree_id, path, .. | |
| 6049 | + } => this | |
| 5937 | 6050 | .update(&mut cx, |this, cx| { |
| 5938 | 6051 | this.open_buffer((worktree_id, path), cx) |
| 5939 | 6052 | }) |
| @@ -2226,7 +2226,7 @@ impl LocalSnapshot { · 2225 unmodified lines | ||
| 2226 | 2226 | paths |
| 2227 | 2227 | } |
| 2228 | 2228 | |
| 2229 | − fn is_abs_path_excluded(&self, abs_path: &Path) -> bool { | |
| 2229 | + pub fn is_path_excluded(&self, abs_path: &Path) -> bool { | |
| 2230 | 2230 | self.file_scan_exclusions |
| 2231 | 2231 | .iter() |
| 2232 | 2232 | .any(|exclude_matcher| exclude_matcher.is_match(abs_path)) |
| @@ -2399,26 +2399,9 @@ impl BackgroundScannerState { · 166 unmodified lines | ||
| 2399 | 2399 | self.snapshot.check_invariants(false); |
| 2400 | 2400 | } |
| 2401 | 2401 | |
| 2402 | − fn reload_repositories(&mut self, changed_paths: &[Arc<Path>], fs: &dyn Fs) { | |
| 2402 | + fn reload_repositories(&mut self, dot_git_dirs_to_reload: &HashSet<PathBuf>, fs: &dyn Fs) { | |
| 2403 | 2403 | let scan_id = self.snapshot.scan_id; |
| 2404 | − | |
| 2405 | − // Find each of the .git directories that contain any of the given paths. | |
| 2406 | − let mut prev_dot_git_dir = None; | |
| 2407 | − for changed_path in changed_paths { | |
| 2408 | − let Some(dot_git_dir) = changed_path | |
| 2409 | − .ancestors() | |
| 2410 | − .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) | |
| 2411 | − else { | |
| 2412 | − continue; | |
| 2413 | − }; | |
| 2414 | − | |
| 2415 | − // Avoid processing the same repository multiple times, if multiple paths | |
| 2416 | − // within it have changed. | |
| 2417 | − if prev_dot_git_dir == Some(dot_git_dir) { | |
| 2418 | − continue; | |
| 2419 | − } | |
| 2420 | − prev_dot_git_dir = Some(dot_git_dir); | |
| 2421 | − | |
| 2404 | + for dot_git_dir in dot_git_dirs_to_reload { | |
| 2422 | 2405 | // If there is already a repository for this .git directory, reload |
| 2423 | 2406 | // the status for all of its files. |
| 2424 | 2407 | let repository = self |
| @@ -2430,7 +2413,7 @@ impl BackgroundScannerState { · 5 unmodified lines | ||
| 2430 | 2413 | }); |
| 2431 | 2414 | match repository { |
| 2432 | 2415 | None => { |
| 2433 | − self.build_git_repository(dot_git_dir.into(), fs); | |
| 2416 | + self.build_git_repository(Arc::from(dot_git_dir.as_path()), fs); | |
| 2434 | 2417 | } |
| 2435 | 2418 | Some((entry_id, repository)) => { |
| 2436 | 2419 | if repository.git_dir_scan_id == scan_id { |
| @@ -2444,7 +2427,7 @@ impl BackgroundScannerState { · 7 unmodified lines | ||
| 2444 | 2427 | continue; |
| 2445 | 2428 | }; |
| 2446 | 2429 | |
| 2447 | − log::info!("reload git repository {:?}", dot_git_dir); | |
| 2430 | + log::info!("reload git repository {dot_git_dir:?}"); | |
| 2448 | 2431 | let repository = repository.repo_ptr.lock(); |
| 2449 | 2432 | let branch = repository.branch_name(); |
| 2450 | 2433 | repository.reload_index(); |
| @@ -2475,7 +2458,9 @@ impl BackgroundScannerState { · 24 unmodified lines | ||
| 2475 | 2458 | ids_to_preserve.insert(work_directory_id); |
| 2476 | 2459 | } else { |
| 2477 | 2460 | let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); |
| 2478 | − if snapshot.is_abs_path_excluded(&git_dir_abs_path) | |
| 2461 | + let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path) | |
| 2462 | + || snapshot.is_path_excluded(&git_dir_abs_path); | |
| 2463 | + if git_dir_excluded | |
| 2479 | 2464 | && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) |
| 2480 | 2465 | { |
| 2481 | 2466 | ids_to_preserve.insert(work_directory_id); |
| @@ -3314,11 +3299,26 @@ impl BackgroundScanner { · 832 unmodified lines | ||
| 3314 | 3299 | }; |
| 3315 | 3300 | |
| 3316 | 3301 | let mut relative_paths = Vec::with_capacity(abs_paths.len()); |
| 3302 | + let mut dot_git_paths_to_reload = HashSet::default(); | |
| 3317 | 3303 | abs_paths.sort_unstable(); |
| 3318 | 3304 | abs_paths.dedup_by(|a, b| a.starts_with(&b)); |
| 3319 | 3305 | abs_paths.retain(|abs_path| { |
| 3320 | 3306 | let snapshot = &self.state.lock().snapshot; |
| 3321 | 3307 | { |
| 3308 | + let mut is_git_related = false; | |
| 3309 | + if let Some(dot_git_dir) = abs_path | |
| 3310 | + .ancestors() | |
| 3311 | + .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) | |
| 3312 | + { | |
| 3313 | + let dot_git_path = dot_git_dir | |
| 3314 | + .strip_prefix(&root_canonical_path) | |
| 3315 | + .ok() | |
| 3316 | + .map(|path| path.to_path_buf()) | |
| 3317 | + .unwrap_or_else(|| dot_git_dir.to_path_buf()); | |
| 3318 | + dot_git_paths_to_reload.insert(dot_git_path.to_path_buf()); | |
| 3319 | + is_git_related = true; | |
| 3320 | + } | |
| 3321 | + | |
| 3322 | 3322 | let relative_path: Arc<Path> = |
| 3323 | 3323 | if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { |
| 3324 | 3324 | path.into() |
| @@ -3328,23 +3328,30 @@ impl BackgroundScanner { · 3 unmodified lines | ||
| 3328 | 3328 | ); |
| 3329 | 3329 | return false; |
| 3330 | 3330 | }; |
| 3331 | + let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { | |
| 3332 | + snapshot | |
| 3333 | + .entry_for_path(parent) | |
| 3334 | + .map_or(false, |entry| entry.kind == EntryKind::Dir) | |
| 3335 | + }); | |
| 3336 | + if !parent_dir_is_loaded { | |
| 3337 | + log::debug!("ignoring event {relative_path:?} within unloaded directory"); | |
| 3338 | + return false; | |
| 3339 | + } | |
| 3331 | 3340 | |
| 3332 | − if !is_git_related(&abs_path) { | |
| 3333 | − let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { | |
| 3334 | − snapshot | |
| 3335 | − .entry_for_path(parent) | |
| 3336 | − .map_or(false, |entry| entry.kind == EntryKind::Dir) | |
| 3337 | − }); | |
| 3338 | − if !parent_dir_is_loaded { | |
| 3339 | − log::debug!("ignoring event {relative_path:?} within unloaded directory"); | |
| 3340 | − return false; | |
| 3341 | + // FS events may come for files which parent directory is excluded, need to check ignore those. | |
| 3342 | + let mut path_to_test = abs_path.clone(); | |
| 3343 | + let mut excluded_file_event = snapshot.is_path_excluded(abs_path) | |
| 3344 | + || snapshot.is_path_excluded(&relative_path); | |
| 3345 | + while !excluded_file_event && path_to_test.pop() { | |
| 3346 | + if snapshot.is_path_excluded(&path_to_test) { | |
| 3347 | + excluded_file_event = true; | |
| 3341 | 3348 | } |
| 3342 | − if snapshot.is_abs_path_excluded(abs_path) { | |
| 3343 | − log::debug!( | |
| 3344 | − "ignoring FS event for path {relative_path:?} within excluded directory" | |
| 3345 | − ); | |
| 3346 | − return false; | |
| 3349 | + } | |
| 3350 | + if excluded_file_event { | |
| 3351 | + if !is_git_related { | |
| 3352 | + log::debug!("ignoring FS event for excluded path {relative_path:?}"); | |
| 3347 | 3353 | } |
| 3354 | + return false; | |
| 3348 | 3355 | } |
| 3349 | 3356 | |
| 3350 | 3357 | relative_paths.push(relative_path); |
| @@ -3352,31 +3359,39 @@ impl BackgroundScanner { · 1 unmodified line | ||
| 3352 | 3359 | } |
| 3353 | 3360 | }); |
| 3354 | 3361 | |
| 3355 | − if relative_paths.is_empty() { | |
| 3362 | + if dot_git_paths_to_reload.is_empty() && relative_paths.is_empty() { | |
| 3356 | 3363 | return; |
| 3357 | 3364 | } |
| 3358 | 3365 | |
| 3359 | − log::debug!("received fs events {:?}", relative_paths); | |
| 3366 | + if !relative_paths.is_empty() { | |
| 3367 | + log::debug!("received fs events {:?}", relative_paths); | |
| 3360 | 3368 | |
| 3361 | − let (scan_job_tx, scan_job_rx) = channel::unbounded(); | |
| 3362 | − self.reload_entries_for_paths( | |
| 3363 | − root_path, | |
| 3364 | − root_canonical_path, | |
| 3365 | − &relative_paths, | |
| 3366 | − abs_paths, | |
| 3367 | − Some(scan_job_tx.clone()), | |
| 3368 | − ) | |
| 3369 | − .await; | |
| 3370 | − drop(scan_job_tx); | |
| 3371 | − self.scan_dirs(false, scan_job_rx).await; | |
| 3369 | + let (scan_job_tx, scan_job_rx) = channel::unbounded(); | |
| 3370 | + self.reload_entries_for_paths( | |
| 3371 | + root_path, | |
| 3372 | + root_canonical_path, | |
| 3373 | + &relative_paths, | |
| 3374 | + abs_paths, | |
| 3375 | + Some(scan_job_tx.clone()), | |
| 3376 | + ) | |
| 3377 | + .await; | |
| 3378 | + drop(scan_job_tx); | |
| 3379 | + self.scan_dirs(false, scan_job_rx).await; | |
| 3372 | 3380 | |
| 3373 | − let (scan_job_tx, scan_job_rx) = channel::unbounded(); | |
| 3374 | − self.update_ignore_statuses(scan_job_tx).await; | |
| 3375 | − self.scan_dirs(false, scan_job_rx).await; | |
| 3381 | + let (scan_job_tx, scan_job_rx) = channel::unbounded(); | |
| 3382 | + self.update_ignore_statuses(scan_job_tx).await; | |
| 3383 | + self.scan_dirs(false, scan_job_rx).await; | |
| 3384 | + } | |
| 3376 | 3385 | |
| 3377 | 3386 | { |
| 3378 | 3387 | let mut state = self.state.lock(); |
| 3379 | − state.reload_repositories(&relative_paths, self.fs.as_ref()); | |
| 3388 | + if !dot_git_paths_to_reload.is_empty() { | |
| 3389 | + if relative_paths.is_empty() { | |
| 3390 | + state.snapshot.scan_id += 1; | |
| 3391 | + } | |
| 3392 | + log::debug!("reloading repositories: {dot_git_paths_to_reload:?}"); | |
| 3393 | + state.reload_repositories(&dot_git_paths_to_reload, self.fs.as_ref()); | |
| 3394 | + } | |
| 3380 | 3395 | state.snapshot.completed_scan_id = state.snapshot.scan_id; |
| 3381 | 3396 | for (_, entry_id) in mem::take(&mut state.removed_entry_ids) { |
| 3382 | 3397 | state.scanned_dirs.remove(&entry_id); |
| @@ -3516,7 +3531,7 @@ impl BackgroundScanner { · 133 unmodified lines | ||
| 3516 | 3531 | let state = self.state.lock(); |
| 3517 | 3532 | let snapshot = &state.snapshot; |
| 3518 | 3533 | root_abs_path = snapshot.abs_path().clone(); |
| 3519 | − if snapshot.is_abs_path_excluded(&job.abs_path) { | |
| 3534 | + if snapshot.is_path_excluded(&job.abs_path) { | |
| 3520 | 3535 | log::error!("skipping excluded directory {:?}", job.path); |
| 3521 | 3536 | return Ok(()); |
| 3522 | 3537 | } |
| @@ -3588,7 +3603,7 @@ impl BackgroundScanner { · 65 unmodified lines | ||
| 3588 | 3603 | |
| 3589 | 3604 | { |
| 3590 | 3605 | let mut state = self.state.lock(); |
| 3591 | − if state.snapshot.is_abs_path_excluded(&child_abs_path) { | |
| 3606 | + if state.snapshot.is_path_excluded(&child_abs_path) { | |
| 3592 | 3607 | let relative_path = job.path.join(child_name); |
| 3593 | 3608 | log::debug!("skipping excluded child entry {relative_path:?}"); |
| 3594 | 3609 | state.remove_path(&relative_path); |
| @@ -4130,12 +4145,6 @@ impl BackgroundScanner { · 535 unmodified lines | ||
| 4130 | 4145 | } |
| 4131 | 4146 | } |
| 4132 | 4147 | |
| 4133 | −fn is_git_related(abs_path: &Path) -> bool { | |
| 4134 | − abs_path | |
| 4135 | − .components() | |
| 4136 | − .any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE) | |
| 4137 | −} | |
| 4138 | − | |
| 4139 | 4148 | fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { |
| 4140 | 4149 | let mut result = root_char_bag; |
| 4141 | 4150 | result.extend( |
| @@ -990,6 +990,145 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { · 989 unmodified lines | ||
| 990 | 990 | }); |
| 991 | 991 | } |
| 992 | 992 | |
| 993 | +#[gpui::test] | |
| 994 | +async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { | |
| 995 | + init_test(cx); | |
| 996 | + let dir = temp_tree(json!({ | |
| 997 | + ".git": { | |
| 998 | + "HEAD": "ref: refs/heads/main\n", | |
| 999 | + "foo": "bar", | |
| 1000 | + }, | |
| 1001 | + ".gitignore": "**/target\n/node_modules\ntest_output\n", | |
| 1002 | + "target": { | |
| 1003 | + "index": "blah2" | |
| 1004 | + }, | |
| 1005 | + "node_modules": { | |
| 1006 | + ".DS_Store": "", | |
| 1007 | + "prettier": { | |
| 1008 | + "package.json": "{}", | |
| 1009 | + }, | |
| 1010 | + }, | |
| 1011 | + "src": { | |
| 1012 | + ".DS_Store": "", | |
| 1013 | + "foo": { | |
| 1014 | + "foo.rs": "mod another;\n", | |
| 1015 | + "another.rs": "// another", | |
| 1016 | + }, | |
| 1017 | + "bar": { | |
| 1018 | + "bar.rs": "// bar", | |
| 1019 | + }, | |
| 1020 | + "lib.rs": "mod foo;\nmod bar;\n", | |
| 1021 | + }, | |
| 1022 | + ".DS_Store": "", | |
| 1023 | + })); | |
| 1024 | + cx.update(|cx| { | |
| 1025 | + cx.update_global::<SettingsStore, _, _>(|store, cx| { | |
| 1026 | + store.update_user_settings::<ProjectSettings>(cx, |project_settings| { | |
| 1027 | + project_settings.file_scan_exclusions = Some(vec![ | |
| 1028 | + "**/.git".to_string(), | |
| 1029 | + "node_modules/".to_string(), | |
| 1030 | + "build_output".to_string(), | |
| 1031 | + ]); | |
| 1032 | + }); | |
| 1033 | + }); | |
| 1034 | + }); | |
| 1035 | + | |
| 1036 | + let tree = Worktree::local( | |
| 1037 | + build_client(cx), | |
| 1038 | + dir.path(), | |
| 1039 | + true, | |
| 1040 | + Arc::new(RealFs), | |
| 1041 | + Default::default(), | |
| 1042 | + &mut cx.to_async(), | |
| 1043 | + ) | |
| 1044 | + .await | |
| 1045 | + .unwrap(); | |
| 1046 | + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) | |
| 1047 | + .await; | |
| 1048 | + tree.flush_fs_events(cx).await; | |
| 1049 | + tree.read_with(cx, |tree, _| { | |
| 1050 | + check_worktree_entries( | |
| 1051 | + tree, | |
| 1052 | + &[ | |
| 1053 | + ".git/HEAD", | |
| 1054 | + ".git/foo", | |
| 1055 | + "node_modules/.DS_Store", | |
| 1056 | + "node_modules/prettier", | |
| 1057 | + "node_modules/prettier/package.json", | |
| 1058 | + ], | |
| 1059 | + &["target", "node_modules"], | |
| 1060 | + &[ | |
| 1061 | + ".DS_Store", | |
| 1062 | + "src/.DS_Store", | |
| 1063 | + "src/lib.rs", | |
| 1064 | + "src/foo/foo.rs", | |
| 1065 | + "src/foo/another.rs", | |
| 1066 | + "src/bar/bar.rs", | |
| 1067 | + ".gitignore", | |
| 1068 | + ], | |
| 1069 | + ) | |
| 1070 | + }); | |
| 1071 | + | |
| 1072 | + let new_excluded_dir = dir.path().join("build_output"); | |
| 1073 | + let new_ignored_dir = dir.path().join("test_output"); | |
| 1074 | + std::fs::create_dir_all(&new_excluded_dir) | |
| 1075 | + .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}")); | |
| 1076 | + std::fs::create_dir_all(&new_ignored_dir) | |
| 1077 | + .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}")); | |
| 1078 | + let node_modules_dir = dir.path().join("node_modules"); | |
| 1079 | + let dot_git_dir = dir.path().join(".git"); | |
| 1080 | + let src_dir = dir.path().join("src"); | |
| 1081 | + for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] { | |
| 1082 | + assert!( | |
| 1083 | + existing_dir.is_dir(), | |
| 1084 | + "Expect {existing_dir:?} to be present in the FS already" | |
| 1085 | + ); | |
| 1086 | + } | |
| 1087 | + | |
| 1088 | + for directory_for_new_file in [ | |
| 1089 | + new_excluded_dir, | |
| 1090 | + new_ignored_dir, | |
| 1091 | + node_modules_dir, | |
| 1092 | + dot_git_dir, | |
| 1093 | + src_dir, | |
| 1094 | + ] { | |
| 1095 | + std::fs::write(directory_for_new_file.join("new_file"), "new file contents") | |
| 1096 | + .unwrap_or_else(|e| { | |
| 1097 | + panic!("Failed to create in {directory_for_new_file:?} a new file: {e}") | |
| 1098 | + }); | |
| 1099 | + } | |
| 1100 | + tree.flush_fs_events(cx).await; | |
| 1101 | + | |
| 1102 | + tree.read_with(cx, |tree, _| { | |
| 1103 | + check_worktree_entries( | |
| 1104 | + tree, | |
| 1105 | + &[ | |
| 1106 | + ".git/HEAD", | |
| 1107 | + ".git/foo", | |
| 1108 | + ".git/new_file", | |
| 1109 | + "node_modules/.DS_Store", | |
| 1110 | + "node_modules/prettier", | |
| 1111 | + "node_modules/prettier/package.json", | |
| 1112 | + "node_modules/new_file", | |
| 1113 | + "build_output", | |
| 1114 | + "build_output/new_file", | |
| 1115 | + "test_output/new_file", | |
| 1116 | + ], | |
| 1117 | + &["target", "node_modules", "test_output"], | |
| 1118 | + &[ | |
| 1119 | + ".DS_Store", | |
| 1120 | + "src/.DS_Store", | |
| 1121 | + "src/lib.rs", | |
| 1122 | + "src/foo/foo.rs", | |
| 1123 | + "src/foo/another.rs", | |
| 1124 | + "src/bar/bar.rs", | |
| 1125 | + "src/new_file", | |
| 1126 | + ".gitignore", | |
| 1127 | + ], | |
| 1128 | + ) | |
| 1129 | + }); | |
| 1130 | +} | |
| 1131 | + | |
| 993 | 1132 | #[gpui::test(iterations = 30)] |
| 994 | 1133 | async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { |
| 995 | 1134 | init_test(cx); |
| @@ -10,7 +10,7 @@ use futures::future; · 9 unmodified lines | ||
| 10 | 10 | use gpui::{AppContext, AsyncAppContext, Model}; |
| 11 | 11 | use language::{ |
| 12 | 12 | language_settings::{language_settings, InlayHintKind}, |
| 13 | − point_from_lsp, point_to_lsp, | |
| 13 | + point_from_lsp, point_to_lsp, prepare_completion_documentation, | |
| 14 | 14 | proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, |
| 15 | 15 | range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, |
| 16 | 16 | CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, |
| @@ -1339,7 +1339,7 @@ impl LspCommand for GetCompletions { · 1322 unmodified lines | ||
| 1339 | 1339 | async fn response_from_lsp( |
| 1340 | 1340 | self, |
| 1341 | 1341 | completions: Option<lsp::CompletionResponse>, |
| 1342 | − _: Model<Project>, | |
| 1342 | + project: Model<Project>, | |
| 1343 | 1343 | buffer: Model<Buffer>, |
| 1344 | 1344 | server_id: LanguageServerId, |
| 1345 | 1345 | mut cx: AsyncAppContext, |
| @@ -1359,7 +1359,8 @@ impl LspCommand for GetCompletions { · 13 unmodified lines | ||
| 1359 | 1359 | Default::default() |
| 1360 | 1360 | }; |
| 1361 | 1361 | |
| 1362 | − let completions = buffer.update(&mut cx, |buffer, _| { | |
| 1362 | + let completions = buffer.update(&mut cx, |buffer, cx| { | |
| 1363 | + let language_registry = project.read(cx).languages().clone(); | |
| 1363 | 1364 | let language = buffer.language().cloned(); |
| 1364 | 1365 | let snapshot = buffer.snapshot(); |
| 1365 | 1366 | let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); |
| @@ -1443,14 +1444,29 @@ impl LspCommand for GetCompletions { · 77 unmodified lines | ||
| 1443 | 1444 | } |
| 1444 | 1445 | }; |
| 1445 | 1446 | |
| 1447 | + let language_registry = language_registry.clone(); | |
| 1446 | 1448 | let language = language.clone(); |
| 1447 | 1449 | LineEnding::normalize(&mut new_text); |
| 1448 | 1450 | Some(async move { |
| 1449 | 1451 | let mut label = None; |
| 1450 | − if let Some(language) = language { | |
| 1452 | + if let Some(language) = language.as_ref() { | |
| 1451 | 1453 | language.process_completion(&mut lsp_completion).await; |
| 1452 | 1454 | label = language.label_for_completion(&lsp_completion).await; |
| 1453 | 1455 | } |
| 1456 | + | |
| 1457 | + let documentation = if let Some(lsp_docs) = &lsp_completion.documentation { | |
| 1458 | + Some( | |
| 1459 | + prepare_completion_documentation( | |
| 1460 | + lsp_docs, | |
| 1461 | + &language_registry, | |
| 1462 | + language.clone(), | |
| 1463 | + ) | |
| 1464 | + .await, | |
| 1465 | + ) | |
| 1466 | + } else { | |
| 1467 | + None | |
| 1468 | + }; | |
| 1469 | + | |
| 1454 | 1470 | Completion { |
| 1455 | 1471 | old_range, |
| 1456 | 1472 | new_text, |
| @@ -1460,6 +1476,7 @@ impl LspCommand for GetCompletions { · 3 unmodified lines | ||
| 1460 | 1476 | lsp_completion.filter_text.as_deref(), |
| 1461 | 1477 | ) |
| 1462 | 1478 | }), |
| 1479 | + documentation, | |
| 1463 | 1480 | server_id, |
| 1464 | 1481 | lsp_completion, |
| 1465 | 1482 | } |
| @@ -13,7 +13,7 @@ mod worktree_tests; · 12 unmodified lines | ||
| 13 | 13 | use anyhow::{anyhow, Context as _, Result}; |
| 14 | 14 | use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; |
| 15 | 15 | use clock::ReplicaId; |
| 16 | −use collections::{hash_map, BTreeMap, HashMap, HashSet}; | |
| 16 | +use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; | |
| 17 | 17 | use copilot::Copilot; |
| 18 | 18 | use futures::{ |
| 19 | 19 | channel::{ |
| @@ -63,6 +63,7 @@ use settings::{Settings, SettingsStore}; · 43 unmodified lines | ||
| 63 | 63 | use sha2::{Digest, Sha256}; |
| 64 | 64 | use similar::{ChangeTag, TextDiff}; |
| 65 | 65 | use smol::channel::{Receiver, Sender}; |
| 66 | +use smol::lock::Semaphore; | |
| 66 | 67 | use std::{ |
| 67 | 68 | cmp::{self, Ordering}, |
| 68 | 69 | convert::TryInto, |
| @@ -557,6 +558,7 @@ enum SearchMatchCandidate { · 488 unmodified lines | ||
| 557 | 558 | }, |
| 558 | 559 | Path { |
| 559 | 560 | worktree_id: WorktreeId, |
| 561 | + is_ignored: bool, | |
| 560 | 562 | path: Arc<Path>, |
| 561 | 563 | }, |
| 562 | 564 | } |
| @@ -5815,11 +5817,15 @@ impl Project { · 5252 unmodified lines | ||
| 5815 | 5817 | } |
| 5816 | 5818 | executor |
| 5817 | 5819 | .scoped(|scope| { |
| 5820 | + let max_concurrent_workers = Arc::new(Semaphore::new(workers)); | |
| 5821 | + | |
| 5818 | 5822 | for worker_ix in 0..workers { |
| 5819 | 5823 | let worker_start_ix = worker_ix * paths_per_worker; |
| 5820 | 5824 | let worker_end_ix = worker_start_ix + paths_per_worker; |
| 5821 | 5825 | let unnamed_buffers = opened_buffers.clone(); |
| 5826 | + let limiter = Arc::clone(&max_concurrent_workers); | |
| 5822 | 5827 | scope.spawn(async move { |
| 5828 | + let _guard = limiter.acquire().await; | |
| 5823 | 5829 | let mut snapshot_start_ix = 0; |
| 5824 | 5830 | let mut abs_path = PathBuf::new(); |
| 5825 | 5831 | for snapshot in snapshots { |
| @@ -5868,6 +5874,7 @@ impl Project { · 42 unmodified lines | ||
| 5868 | 5874 | let project_path = SearchMatchCandidate::Path { |
| 5869 | 5875 | worktree_id: snapshot.id(), |
| 5870 | 5876 | path: entry.path.clone(), |
| 5877 | + is_ignored: entry.is_ignored, | |
| 5871 | 5878 | }; |
| 5872 | 5879 | if matching_paths_tx.send(project_path).await.is_err() { |
| 5873 | 5880 | break; |
| @@ -5880,6 +5887,94 @@ impl Project { · 6 unmodified lines | ||
| 5880 | 5887 | } |
| 5881 | 5888 | }); |
| 5882 | 5889 | } |
| 5890 | + | |
| 5891 | + if query.include_ignored() { | |
| 5892 | + for snapshot in snapshots { | |
| 5893 | + for ignored_entry in snapshot | |
| 5894 | + .entries(query.include_ignored()) | |
| 5895 | + .filter(|e| e.is_ignored) | |
| 5896 | + { | |
| 5897 | + let limiter = Arc::clone(&max_concurrent_workers); | |
| 5898 | + scope.spawn(async move { | |
| 5899 | + let _guard = limiter.acquire().await; | |
| 5900 | + let mut ignored_paths_to_process = | |
| 5901 | + VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]); | |
| 5902 | + while let Some(ignored_abs_path) = | |
| 5903 | + ignored_paths_to_process.pop_front() | |
| 5904 | + { | |
| 5905 | + if !query.file_matches(Some(&ignored_abs_path)) | |
| 5906 | + || snapshot.is_path_excluded(&ignored_abs_path) | |
| 5907 | + { | |
| 5908 | + continue; | |
| 5909 | + } | |
| 5910 | + if let Some(fs_metadata) = fs | |
| 5911 | + .metadata(&ignored_abs_path) | |
| 5912 | + .await | |
| 5913 | + .with_context(|| { | |
| 5914 | + format!("fetching fs metadata for {ignored_abs_path:?}") | |
| 5915 | + }) | |
| 5916 | + .log_err() | |
| 5917 | + .flatten() | |
| 5918 | + { | |
| 5919 | + if fs_metadata.is_dir { | |
| 5920 | + if let Some(mut subfiles) = fs | |
| 5921 | + .read_dir(&ignored_abs_path) | |
| 5922 | + .await | |
| 5923 | + .with_context(|| { | |
| 5924 | + format!( | |
| 5925 | + "listing ignored path {ignored_abs_path:?}" | |
| 5926 | + ) | |
| 5927 | + }) | |
| 5928 | + .log_err() | |
| 5929 | + { | |
| 5930 | + while let Some(subfile) = subfiles.next().await { | |
| 5931 | + if let Some(subfile) = subfile.log_err() { | |
| 5932 | + ignored_paths_to_process.push_back(subfile); | |
| 5933 | + } | |
| 5934 | + } | |
| 5935 | + } | |
| 5936 | + } else if !fs_metadata.is_symlink { | |
| 5937 | + let matches = if let Some(file) = fs | |
| 5938 | + .open_sync(&ignored_abs_path) | |
| 5939 | + .await | |
| 5940 | + .with_context(|| { | |
| 5941 | + format!( | |
| 5942 | + "Opening ignored path {ignored_abs_path:?}" | |
| 5943 | + ) | |
| 5944 | + }) | |
| 5945 | + .log_err() | |
| 5946 | + { | |
| 5947 | + query.detect(file).unwrap_or(false) | |
| 5948 | + } else { | |
| 5949 | + false | |
| 5950 | + }; | |
| 5951 | + if matches { | |
| 5952 | + let project_path = SearchMatchCandidate::Path { | |
| 5953 | + worktree_id: snapshot.id(), | |
| 5954 | + path: Arc::from( | |
| 5955 | + ignored_abs_path | |
| 5956 | + .strip_prefix(snapshot.abs_path()) | |
| 5957 | + .expect( | |
| 5958 | + "scanning worktree-related files", | |
| 5959 | + ), | |
| 5960 | + ), | |
| 5961 | + is_ignored: true, | |
| 5962 | + }; | |
| 5963 | + if matching_paths_tx | |
| 5964 | + .send(project_path) | |
| 5965 | + .await | |
| 5966 | + .is_err() | |
| 5967 | + { | |
| 5968 | + return; | |
| 5969 | + } | |
| 5970 | + } | |
| 5971 | + } | |
| 5972 | + } | |
| 5973 | + } | |
| 5974 | + }); | |
| 5975 | + } | |
| 5976 | + } | |
| 5977 | + } | |
| 5883 | 5978 | }) |
| 5884 | 5979 | .await; |
| 5885 | 5980 | } |
| @@ -5986,11 +6081,24 @@ impl Project { · 100 unmodified lines | ||
| 5986 | 6081 | let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); |
| 5987 | 6082 | let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); |
| 5988 | 6083 | cx.spawn(move |this, cx| async move { |
| 5989 | − let mut buffers = vec![]; | |
| 6084 | + let mut buffers = Vec::new(); | |
| 6085 | + let mut ignored_buffers = Vec::new(); | |
| 5990 | 6086 | while let Some(entry) = matching_paths_rx.next().await { |
| 5991 | − buffers.push(entry); | |
| 6087 | + if matches!( | |
| 6088 | + entry, | |
| 6089 | + SearchMatchCandidate::Path { | |
| 6090 | + is_ignored: true, | |
| 6091 | + .. | |
| 6092 | + } | |
| 6093 | + ) { | |
| 6094 | + ignored_buffers.push(entry); | |
| 6095 | + } else { | |
| 6096 | + buffers.push(entry); | |
| 6097 | + } | |
| 5992 | 6098 | } |
| 5993 | 6099 | buffers.sort_by_key(|candidate| candidate.path()); |
| 6100 | + ignored_buffers.sort_by_key(|candidate| candidate.path()); | |
| 6101 | + buffers.extend(ignored_buffers); | |
| 5994 | 6102 | let matching_paths = buffers.clone(); |
| 5995 | 6103 | let _ = sorted_buffers_tx.send(buffers); |
| 5996 | 6104 | for (index, candidate) in matching_paths.into_iter().enumerate() { |
| @@ -6002,7 +6110,9 @@ impl Project { · 5 unmodified lines | ||
| 6002 | 6110 | cx.spawn(move |mut cx| async move { |
| 6003 | 6111 | let buffer = match candidate { |
| 6004 | 6112 | SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), |
| 6005 | − SearchMatchCandidate::Path { worktree_id, path } => this | |
| 6113 | + SearchMatchCandidate::Path { | |
| 6114 | + worktree_id, path, .. | |
| 6115 | + } => this | |
| 6006 | 6116 | .update(&mut cx, |this, cx| { |
| 6007 | 6117 | this.open_buffer((worktree_id, path), cx) |
| 6008 | 6118 | })? |
| @@ -2222,7 +2222,7 @@ impl LocalSnapshot { · 2221 unmodified lines | ||
| 2222 | 2222 | paths |
| 2223 | 2223 | } |
| 2224 | 2224 | |
| 2225 | − fn is_abs_path_excluded(&self, abs_path: &Path) -> bool { | |
| 2225 | + pub fn is_path_excluded(&self, abs_path: &Path) -> bool { | |
| 2226 | 2226 | self.file_scan_exclusions |
| 2227 | 2227 | .iter() |
| 2228 | 2228 | .any(|exclude_matcher| exclude_matcher.is_match(abs_path)) |
| @@ -2395,26 +2395,10 @@ impl BackgroundScannerState { · 166 unmodified lines | ||
| 2395 | 2395 | self.snapshot.check_invariants(false); |
| 2396 | 2396 | } |
| 2397 | 2397 | |
| 2398 | − fn reload_repositories(&mut self, changed_paths: &[Arc<Path>], fs: &dyn Fs) { | |
| 2398 | + fn reload_repositories(&mut self, dot_git_dirs_to_reload: &HashSet<PathBuf>, fs: &dyn Fs) { | |
| 2399 | 2399 | let scan_id = self.snapshot.scan_id; |
| 2400 | 2400 | |
| 2401 | − // Find each of the .git directories that contain any of the given paths. | |
| 2402 | − let mut prev_dot_git_dir = None; | |
| 2403 | − for changed_path in changed_paths { | |
| 2404 | − let Some(dot_git_dir) = changed_path | |
| 2405 | − .ancestors() | |
| 2406 | − .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) | |
| 2407 | − else { | |
| 2408 | − continue; | |
| 2409 | − }; | |
| 2410 | − | |
| 2411 | − // Avoid processing the same repository multiple times, if multiple paths | |
| 2412 | − // within it have changed. | |
| 2413 | − if prev_dot_git_dir == Some(dot_git_dir) { | |
| 2414 | − continue; | |
| 2415 | − } | |
| 2416 | − prev_dot_git_dir = Some(dot_git_dir); | |
| 2417 | − | |
| 2401 | + for dot_git_dir in dot_git_dirs_to_reload { | |
| 2418 | 2402 | // If there is already a repository for this .git directory, reload |
| 2419 | 2403 | // the status for all of its files. |
| 2420 | 2404 | let repository = self |
| @@ -2426,7 +2410,7 @@ impl BackgroundScannerState { · 5 unmodified lines | ||
| 2426 | 2410 | }); |
| 2427 | 2411 | match repository { |
| 2428 | 2412 | None => { |
| 2429 | − self.build_git_repository(dot_git_dir.into(), fs); | |
| 2413 | + self.build_git_repository(Arc::from(dot_git_dir.as_path()), fs); | |
| 2430 | 2414 | } |
| 2431 | 2415 | Some((entry_id, repository)) => { |
| 2432 | 2416 | if repository.git_dir_scan_id == scan_id { |
| @@ -2440,7 +2424,7 @@ impl BackgroundScannerState { · 7 unmodified lines | ||
| 2440 | 2424 | continue; |
| 2441 | 2425 | }; |
| 2442 | 2426 | |
| 2443 | − log::info!("reload git repository {:?}", dot_git_dir); | |
| 2427 | + log::info!("reload git repository {dot_git_dir:?}"); | |
| 2444 | 2428 | let repository = repository.repo_ptr.lock(); |
| 2445 | 2429 | let branch = repository.branch_name(); |
| 2446 | 2430 | repository.reload_index(); |
| @@ -2471,7 +2455,9 @@ impl BackgroundScannerState { · 24 unmodified lines | ||
| 2471 | 2455 | ids_to_preserve.insert(work_directory_id); |
| 2472 | 2456 | } else { |
| 2473 | 2457 | let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); |
| 2474 | − if snapshot.is_abs_path_excluded(&git_dir_abs_path) | |
| 2458 | + let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path) | |
| 2459 | + || snapshot.is_path_excluded(&git_dir_abs_path); | |
| 2460 | + if git_dir_excluded | |
| 2475 | 2461 | && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) |
| 2476 | 2462 | { |
| 2477 | 2463 | ids_to_preserve.insert(work_directory_id); |
| @@ -3303,11 +3289,26 @@ impl BackgroundScanner { · 825 unmodified lines | ||
| 3303 | 3289 | }; |
| 3304 | 3290 | |
| 3305 | 3291 | let mut relative_paths = Vec::with_capacity(abs_paths.len()); |
| 3292 | + let mut dot_git_paths_to_reload = HashSet::default(); | |
| 3306 | 3293 | abs_paths.sort_unstable(); |
| 3307 | 3294 | abs_paths.dedup_by(|a, b| a.starts_with(&b)); |
| 3308 | 3295 | abs_paths.retain(|abs_path| { |
| 3309 | 3296 | let snapshot = &self.state.lock().snapshot; |
| 3310 | 3297 | { |
| 3298 | + let mut is_git_related = false; | |
| 3299 | + if let Some(dot_git_dir) = abs_path | |
| 3300 | + .ancestors() | |
| 3301 | + .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) | |
| 3302 | + { | |
| 3303 | + let dot_git_path = dot_git_dir | |
| 3304 | + .strip_prefix(&root_canonical_path) | |
| 3305 | + .ok() | |
| 3306 | + .map(|path| path.to_path_buf()) | |
| 3307 | + .unwrap_or_else(|| dot_git_dir.to_path_buf()); | |
| 3308 | + dot_git_paths_to_reload.insert(dot_git_path.to_path_buf()); | |
| 3309 | + is_git_related = true; | |
| 3310 | + } | |
| 3311 | + | |
| 3311 | 3312 | let relative_path: Arc<Path> = |
| 3312 | 3313 | if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { |
| 3313 | 3314 | path.into() |
| @@ -3318,22 +3319,30 @@ impl BackgroundScanner { · 4 unmodified lines | ||
| 3318 | 3319 | return false; |
| 3319 | 3320 | }; |
| 3320 | 3321 | |
| 3321 | − if !is_git_related(&abs_path) { | |
| 3322 | − let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { | |
| 3323 | − snapshot | |
| 3324 | − .entry_for_path(parent) | |
| 3325 | − .map_or(false, |entry| entry.kind == EntryKind::Dir) | |
| 3326 | − }); | |
| 3327 | − if !parent_dir_is_loaded { | |
| 3328 | − log::debug!("ignoring event {relative_path:?} within unloaded directory"); | |
| 3329 | − return false; | |
| 3322 | + let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { | |
| 3323 | + snapshot | |
| 3324 | + .entry_for_path(parent) | |
| 3325 | + .map_or(false, |entry| entry.kind == EntryKind::Dir) | |
| 3326 | + }); | |
| 3327 | + if !parent_dir_is_loaded { | |
| 3328 | + log::debug!("ignoring event {relative_path:?} within unloaded directory"); | |
| 3329 | + return false; | |
| 3330 | + } | |
| 3331 | + | |
| 3332 | + // FS events may come for files which parent directory is excluded, need to check ignore those. | |
| 3333 | + let mut path_to_test = abs_path.clone(); | |
| 3334 | + let mut excluded_file_event = snapshot.is_path_excluded(abs_path) | |
| 3335 | + || snapshot.is_path_excluded(&relative_path); | |
| 3336 | + while !excluded_file_event && path_to_test.pop() { | |
| 3337 | + if snapshot.is_path_excluded(&path_to_test) { | |
| 3338 | + excluded_file_event = true; | |
| 3330 | 3339 | } |
| 3331 | − if snapshot.is_abs_path_excluded(abs_path) { | |
| 3332 | − log::debug!( | |
| 3333 | − "ignoring FS event for path {relative_path:?} within excluded directory" | |
| 3334 | − ); | |
| 3335 | − return false; | |
| 3340 | + } | |
| 3341 | + if excluded_file_event { | |
| 3342 | + if !is_git_related { | |
| 3343 | + log::debug!("ignoring FS event for excluded path {relative_path:?}"); | |
| 3336 | 3344 | } |
| 3345 | + return false; | |
| 3337 | 3346 | } |
| 3338 | 3347 | |
| 3339 | 3348 | relative_paths.push(relative_path); |
| @@ -3341,31 +3350,39 @@ impl BackgroundScanner { · 1 unmodified line | ||
| 3341 | 3350 | } |
| 3342 | 3351 | }); |
| 3343 | 3352 | |
| 3344 | − if relative_paths.is_empty() { | |
| 3353 | + if dot_git_paths_to_reload.is_empty() && relative_paths.is_empty() { | |
| 3345 | 3354 | return; |
| 3346 | 3355 | } |
| 3347 | 3356 | |
| 3348 | − log::debug!("received fs events {:?}", relative_paths); | |
| 3357 | + if !relative_paths.is_empty() { | |
| 3358 | + log::debug!("received fs events {:?}", relative_paths); | |
| 3349 | 3359 | |
| 3350 | − let (scan_job_tx, scan_job_rx) = channel::unbounded(); | |
| 3351 | − self.reload_entries_for_paths( | |
| 3352 | − root_path, | |
| 3353 | − root_canonical_path, | |
| 3354 | − &relative_paths, | |
| 3355 | − abs_paths, | |
| 3356 | − Some(scan_job_tx.clone()), | |
| 3357 | − ) | |
| 3358 | − .await; | |
| 3359 | − drop(scan_job_tx); | |
| 3360 | − self.scan_dirs(false, scan_job_rx).await; | |
| 3360 | + let (scan_job_tx, scan_job_rx) = channel::unbounded(); | |
| 3361 | + self.reload_entries_for_paths( | |
| 3362 | + root_path, | |
| 3363 | + root_canonical_path, | |
| 3364 | + &relative_paths, | |
| 3365 | + abs_paths, | |
| 3366 | + Some(scan_job_tx.clone()), | |
| 3367 | + ) | |
| 3368 | + .await; | |
| 3369 | + drop(scan_job_tx); | |
| 3370 | + self.scan_dirs(false, scan_job_rx).await; | |
| 3361 | 3371 | |
| 3362 | − let (scan_job_tx, scan_job_rx) = channel::unbounded(); | |
| 3363 | − self.update_ignore_statuses(scan_job_tx).await; | |
| 3364 | − self.scan_dirs(false, scan_job_rx).await; | |
| 3372 | + let (scan_job_tx, scan_job_rx) = channel::unbounded(); | |
| 3373 | + self.update_ignore_statuses(scan_job_tx).await; | |
| 3374 | + self.scan_dirs(false, scan_job_rx).await; | |
| 3375 | + } | |
| 3365 | 3376 | |
| 3366 | 3377 | { |
| 3367 | 3378 | let mut state = self.state.lock(); |
| 3368 | − state.reload_repositories(&relative_paths, self.fs.as_ref()); | |
| 3379 | + if !dot_git_paths_to_reload.is_empty() { | |
| 3380 | + if relative_paths.is_empty() { | |
| 3381 | + state.snapshot.scan_id += 1; | |
| 3382 | + } | |
| 3383 | + log::debug!("reloading repositories: {dot_git_paths_to_reload:?}"); | |
| 3384 | + state.reload_repositories(&dot_git_paths_to_reload, self.fs.as_ref()); | |
| 3385 | + } | |
| 3369 | 3386 | state.snapshot.completed_scan_id = state.snapshot.scan_id; |
| 3370 | 3387 | for (_, entry_id) in mem::take(&mut state.removed_entry_ids) { |
| 3371 | 3388 | state.scanned_dirs.remove(&entry_id); |
| @@ -3505,7 +3522,7 @@ impl BackgroundScanner { · 133 unmodified lines | ||
| 3505 | 3522 | let state = self.state.lock(); |
| 3506 | 3523 | let snapshot = &state.snapshot; |
| 3507 | 3524 | root_abs_path = snapshot.abs_path().clone(); |
| 3508 | − if snapshot.is_abs_path_excluded(&job.abs_path) { | |
| 3525 | + if snapshot.is_path_excluded(&job.abs_path) { | |
| 3509 | 3526 | log::error!("skipping excluded directory {:?}", job.path); |
| 3510 | 3527 | return Ok(()); |
| 3511 | 3528 | } |
| @@ -3577,7 +3594,7 @@ impl BackgroundScanner { · 65 unmodified lines | ||
| 3577 | 3594 | |
| 3578 | 3595 | { |
| 3579 | 3596 | let mut state = self.state.lock(); |
| 3580 | − if state.snapshot.is_abs_path_excluded(&child_abs_path) { | |
| 3597 | + if state.snapshot.is_path_excluded(&child_abs_path) { | |
| 3581 | 3598 | let relative_path = job.path.join(child_name); |
| 3582 | 3599 | log::debug!("skipping excluded child entry {relative_path:?}"); |
| 3583 | 3600 | state.remove_path(&relative_path); |
| @@ -4119,12 +4136,6 @@ impl BackgroundScanner { · 535 unmodified lines | ||
| 4119 | 4136 | } |
| 4120 | 4137 | } |
| 4121 | 4138 | |
| 4122 | −fn is_git_related(abs_path: &Path) -> bool { | |
| 4123 | − abs_path | |
| 4124 | − .components() | |
| 4125 | − .any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE) | |
| 4126 | −} | |
| 4127 | − | |
| 4128 | 4139 | fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { |
| 4129 | 4140 | let mut result = root_char_bag; |
| 4130 | 4141 | result.extend( |
| @@ -992,6 +992,146 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { · 991 unmodified lines | ||
| 992 | 992 | }); |
| 993 | 993 | } |
| 994 | 994 | |
| 995 | +#[gpui::test] | |
| 996 | +async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { | |
| 997 | + init_test(cx); | |
| 998 | + cx.executor().allow_parking(); | |
| 999 | + let dir = temp_tree(json!({ | |
| 1000 | + ".git": { | |
| 1001 | + "HEAD": "ref: refs/heads/main\n", | |
| 1002 | + "foo": "bar", | |
| 1003 | + }, | |
| 1004 | + ".gitignore": "**/target\n/node_modules\ntest_output\n", | |
| 1005 | + "target": { | |
| 1006 | + "index": "blah2" | |
| 1007 | + }, | |
| 1008 | + "node_modules": { | |
| 1009 | + ".DS_Store": "", | |
| 1010 | + "prettier": { | |
| 1011 | + "package.json": "{}", | |
| 1012 | + }, | |
| 1013 | + }, | |
| 1014 | + "src": { | |
| 1015 | + ".DS_Store": "", | |
| 1016 | + "foo": { | |
| 1017 | + "foo.rs": "mod another;\n", | |
| 1018 | + "another.rs": "// another", | |
| 1019 | + }, | |
| 1020 | + "bar": { | |
| 1021 | + "bar.rs": "// bar", | |
| 1022 | + }, | |
| 1023 | + "lib.rs": "mod foo;\nmod bar;\n", | |
| 1024 | + }, | |
| 1025 | + ".DS_Store": "", | |
| 1026 | + })); | |
| 1027 | + cx.update(|cx| { | |
| 1028 | + cx.update_global::<SettingsStore, _>(|store, cx| { | |
| 1029 | + store.update_user_settings::<ProjectSettings>(cx, |project_settings| { | |
| 1030 | + project_settings.file_scan_exclusions = Some(vec![ | |
| 1031 | + "**/.git".to_string(), | |
| 1032 | + "node_modules/".to_string(), | |
| 1033 | + "build_output".to_string(), | |
| 1034 | + ]); | |
| 1035 | + }); | |
| 1036 | + }); | |
| 1037 | + }); | |
| 1038 | + | |
| 1039 | + let tree = Worktree::local( | |
| 1040 | + build_client(cx), | |
| 1041 | + dir.path(), | |
| 1042 | + true, | |
| 1043 | + Arc::new(RealFs), | |
| 1044 | + Default::default(), | |
| 1045 | + &mut cx.to_async(), | |
| 1046 | + ) | |
| 1047 | + .await | |
| 1048 | + .unwrap(); | |
| 1049 | + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) | |
| 1050 | + .await; | |
| 1051 | + tree.flush_fs_events(cx).await; | |
| 1052 | + tree.read_with(cx, |tree, _| { | |
| 1053 | + check_worktree_entries( | |
| 1054 | + tree, | |
| 1055 | + &[ | |
| 1056 | + ".git/HEAD", | |
| 1057 | + ".git/foo", | |
| 1058 | + "node_modules/.DS_Store", | |
| 1059 | + "node_modules/prettier", | |
| 1060 | + "node_modules/prettier/package.json", | |
| 1061 | + ], | |
| 1062 | + &["target", "node_modules"], | |
| 1063 | + &[ | |
| 1064 | + ".DS_Store", | |
| 1065 | + "src/.DS_Store", | |
| 1066 | + "src/lib.rs", | |
| 1067 | + "src/foo/foo.rs", | |
| 1068 | + "src/foo/another.rs", | |
| 1069 | + "src/bar/bar.rs", | |
| 1070 | + ".gitignore", | |
| 1071 | + ], | |
| 1072 | + ) | |
| 1073 | + }); | |
| 1074 | + | |
| 1075 | + let new_excluded_dir = dir.path().join("build_output"); | |
| 1076 | + let new_ignored_dir = dir.path().join("test_output"); | |
| 1077 | + std::fs::create_dir_all(&new_excluded_dir) | |
| 1078 | + .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}")); | |
| 1079 | + std::fs::create_dir_all(&new_ignored_dir) | |
| 1080 | + .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}")); | |
| 1081 | + let node_modules_dir = dir.path().join("node_modules"); | |
| 1082 | + let dot_git_dir = dir.path().join(".git"); | |
| 1083 | + let src_dir = dir.path().join("src"); | |
| 1084 | + for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] { | |
| 1085 | + assert!( | |
| 1086 | + existing_dir.is_dir(), | |
| 1087 | + "Expect {existing_dir:?} to be present in the FS already" | |
| 1088 | + ); | |
| 1089 | + } | |
| 1090 | + | |
| 1091 | + for directory_for_new_file in [ | |
| 1092 | + new_excluded_dir, | |
| 1093 | + new_ignored_dir, | |
| 1094 | + node_modules_dir, | |
| 1095 | + dot_git_dir, | |
| 1096 | + src_dir, | |
| 1097 | + ] { | |
| 1098 | + std::fs::write(directory_for_new_file.join("new_file"), "new file contents") | |
| 1099 | + .unwrap_or_else(|e| { | |
| 1100 | + panic!("Failed to create in {directory_for_new_file:?} a new file: {e}") | |
| 1101 | + }); | |
| 1102 | + } | |
| 1103 | + tree.flush_fs_events(cx).await; | |
| 1104 | + | |
| 1105 | + tree.read_with(cx, |tree, _| { | |
| 1106 | + check_worktree_entries( | |
| 1107 | + tree, | |
| 1108 | + &[ | |
| 1109 | + ".git/HEAD", | |
| 1110 | + ".git/foo", | |
| 1111 | + ".git/new_file", | |
| 1112 | + "node_modules/.DS_Store", | |
| 1113 | + "node_modules/prettier", | |
| 1114 | + "node_modules/prettier/package.json", | |
| 1115 | + "node_modules/new_file", | |
| 1116 | + "build_output", | |
| 1117 | + "build_output/new_file", | |
| 1118 | + "test_output/new_file", | |
| 1119 | + ], | |
| 1120 | + &["target", "node_modules", "test_output"], | |
| 1121 | + &[ | |
| 1122 | + ".DS_Store", | |
| 1123 | + "src/.DS_Store", | |
| 1124 | + "src/lib.rs", | |
| 1125 | + "src/foo/foo.rs", | |
| 1126 | + "src/foo/another.rs", | |
| 1127 | + "src/bar/bar.rs", | |
| 1128 | + "src/new_file", | |
| 1129 | + ".gitignore", | |
| 1130 | + ], | |
| 1131 | + ) | |
| 1132 | + }); | |
| 1133 | +} | |
| 1134 | + | |
| 995 | 1135 | #[gpui::test(iterations = 30)] |
| 996 | 1136 | async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { |
| 997 | 1137 | init_test(cx); |
| @@ -1056,7 +1196,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { · 58 unmodified lines | ||
| 1056 | 1196 | async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { |
| 1057 | 1197 | init_test(cx); |
| 1058 | 1198 | cx.executor().allow_parking(); |
| 1059 | − let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); | |
| 1199 | + let client_fake = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); | |
| 1060 | 1200 | |
| 1061 | 1201 | let fs_fake = FakeFs::new(cx.background_executor.clone()); |
| 1062 | 1202 | fs_fake |
| @@ -1096,7 +1236,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { · 33 unmodified lines | ||
| 1096 | 1236 | assert!(tree.entry_for_path("a/b/").unwrap().is_dir()); |
| 1097 | 1237 | }); |
| 1098 | 1238 | |
| 1099 | − let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); | |
| 1239 | + let client_real = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); | |
| 1100 | 1240 | |
| 1101 | 1241 | let fs_real = Arc::new(RealFs); |
| 1102 | 1242 | let temp_root = temp_tree(json!({ |
| @@ -2181,7 +2321,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { · 1078 unmodified lines | ||
| 2181 | 2321 | |
| 2182 | 2322 | fn build_client(cx: &mut TestAppContext) -> Arc<Client> { |
| 2183 | 2323 | let http_client = FakeHttpClient::with_404_response(); |
| 2184 | − cx.read(|cx| Client::new(http_client, cx)) | |
| 2324 | + cx.update(|cx| Client::new(http_client, cx)) | |
| 2185 | 2325 | } |
| 2186 | 2326 | |
| 2187 | 2327 | #[track_caller] |
| @@ -9,7 +9,6 @@ path = "src/project_panel.rs" · 8 unmodified lines | ||
| 9 | 9 | doctest = false |
| 10 | 10 | |
| 11 | 11 | [dependencies] |
| 12 | −context_menu = { path = "../context_menu" } | |
| 13 | 12 | collections = { path = "../collections" } |
| 14 | 13 | db = { path = "../db2", package = "db2" } |
| 15 | 14 | editor = { path = "../editor2", package = "editor2" } |
| @@ -9,10 +9,9 @@ use file_associations::FileAssociations; · 8 unmodified lines | ||
| 9 | 9 | use anyhow::{anyhow, Result}; |
| 10 | 10 | use gpui::{ |
| 11 | 11 | actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, |
| 12 | − ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView, | |
| 13 | − InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, | |
| 14 | − Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, | |
| 15 | − ViewContext, VisualContext as _, WeakView, WindowContext, | |
| 12 | + ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, | |
| 13 | + Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled, | |
| 14 | + Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, | |
| 16 | 15 | }; |
| 17 | 16 | use menu::{Confirm, SelectNext, SelectPrev}; |
| 18 | 17 | use project::{ |
| @@ -30,7 +29,7 @@ use std::{ · 11 unmodified lines | ||
| 30 | 29 | sync::Arc, |
| 31 | 30 | }; |
| 32 | 31 | use theme::ActiveTheme as _; |
| 33 | −use ui::{h_stack, v_stack, IconElement, Label}; | |
| 32 | +use ui::{v_stack, IconElement, Label, ListItem}; | |
| 34 | 33 | use unicase::UniCase; |
| 35 | 34 | use util::{maybe, ResultExt, TryFutureExt}; |
| 36 | 35 | use workspace::{ |
| @@ -247,7 +246,6 @@ impl ProjectPanel { · 210 unmodified lines | ||
| 247 | 246 | let mut old_dock_position = this.position(cx); |
| 248 | 247 | ProjectPanelSettings::register(cx); |
| 249 | 248 | cx.observe_global::<SettingsStore>(move |this, cx| { |
| 250 | − dbg!("OLA!"); | |
| 251 | 249 | let new_dock_position = this.position(cx); |
| 252 | 250 | if new_dock_position != old_dock_position { |
| 253 | 251 | old_dock_position = new_dock_position; |
| @@ -372,7 +370,7 @@ impl ProjectPanel { · 118 unmodified lines | ||
| 372 | 370 | _entry_id: ProjectEntryId, |
| 373 | 371 | _cx: &mut ViewContext<Self>, |
| 374 | 372 | ) { |
| 375 | − todo!() | |
| 373 | + // todo!() | |
| 376 | 374 | // let project = self.project.read(cx); |
| 377 | 375 | |
| 378 | 376 | // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { |
| @@ -645,6 +643,7 @@ impl ProjectPanel { · 266 unmodified lines | ||
| 645 | 643 | } |
| 646 | 644 | |
| 647 | 645 | fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { |
| 646 | + dbg!("odd"); | |
| 648 | 647 | self.edit_state = None; |
| 649 | 648 | self.update_visible_entries(None, cx); |
| 650 | 649 | cx.focus(&self.focus_handle); |
| @@ -1335,13 +1334,19 @@ impl ProjectPanel { · 684 unmodified lines | ||
| 1335 | 1334 | } |
| 1336 | 1335 | } |
| 1337 | 1336 | |
| 1338 | − fn render_entry_visual_element( | |
| 1339 | − details: &EntryDetails, | |
| 1340 | − editor: Option<&View<Editor>>, | |
| 1341 | − padding: Pixels, | |
| 1337 | + fn render_entry( | |
| 1338 | + &self, | |
| 1339 | + entry_id: ProjectEntryId, | |
| 1340 | + details: EntryDetails, | |
| 1341 | + // dragged_entry_destination: &mut Option<Arc<Path>>, | |
| 1342 | 1342 | cx: &mut ViewContext<Self>, |
| 1343 | − ) -> Div<Self> { | |
| 1343 | + ) -> ListItem { | |
| 1344 | + let kind = details.kind; | |
| 1345 | + let settings = ProjectPanelSettings::get_global(cx); | |
| 1344 | 1346 | let show_editor = details.is_editing && !details.is_processing; |
| 1347 | + let is_selected = self | |
| 1348 | + .selection | |
| 1349 | + .map_or(false, |selection| selection.entry_id == entry_id); | |
| 1345 | 1350 | |
| 1346 | 1351 | let theme = cx.theme(); |
| 1347 | 1352 | let filename_text_color = details |
| @@ -1354,14 +1359,17 @@ impl ProjectPanel { · 6 unmodified lines | ||
| 1354 | 1359 | }) |
| 1355 | 1360 | .unwrap_or(theme.status().info); |
| 1356 | 1361 | |
| 1357 | − h_stack() | |
| 1362 | + ListItem::new(entry_id.to_proto() as usize) | |
| 1363 | + .indent_level(details.depth) | |
| 1364 | + .indent_step_size(px(settings.indent_size)) | |
| 1365 | + .selected(is_selected) | |
| 1358 | 1366 | .child(if let Some(icon) = &details.icon { |
| 1359 | 1367 | div().child(IconElement::from_path(icon.to_string())) |
| 1360 | 1368 | } else { |
| 1361 | 1369 | div() |
| 1362 | 1370 | }) |
| 1363 | 1371 | .child( |
| 1364 | − if let (Some(editor), true) = (editor, show_editor) { | |
| 1372 | + if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { | |
| 1365 | 1373 | div().w_full().child(editor.clone()) |
| 1366 | 1374 | } else { |
| 1367 | 1375 | div() |
| @@ -1370,34 +1378,7 @@ impl ProjectPanel { · 2 unmodified lines | ||
| 1370 | 1378 | } |
| 1371 | 1379 | .ml_1(), |
| 1372 | 1380 | ) |
| 1373 | − .pl(padding) | |
| 1374 | − } | |
| 1375 | − | |
| 1376 | − fn render_entry( | |
| 1377 | − &self, | |
| 1378 | − entry_id: ProjectEntryId, | |
| 1379 | − details: EntryDetails, | |
| 1380 | − // dragged_entry_destination: &mut Option<Arc<Path>>, | |
| 1381 | − cx: &mut ViewContext<Self>, | |
| 1382 | − ) -> Stateful<Self, Div<Self>> { | |
| 1383 | − let kind = details.kind; | |
| 1384 | − let settings = ProjectPanelSettings::get_global(cx); | |
| 1385 | − const INDENT_SIZE: Pixels = px(16.0); | |
| 1386 | − let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size); | |
| 1387 | − let show_editor = details.is_editing && !details.is_processing; | |
| 1388 | − let is_selected = self | |
| 1389 | − .selection | |
| 1390 | − .map_or(false, |selection| selection.entry_id == entry_id); | |
| 1391 | − | |
| 1392 | − Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx) | |
| 1393 | − .id(entry_id.to_proto() as usize) | |
| 1394 | − .w_full() | |
| 1395 | − .cursor_pointer() | |
| 1396 | − .when(is_selected, |this| { | |
| 1397 | − this.bg(cx.theme().colors().element_selected) | |
| 1398 | − }) | |
| 1399 | − .hover(|style| style.bg(cx.theme().colors().element_hover)) | |
| 1400 | − .on_click(move |this, event, cx| { | |
| 1381 | + .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { | |
| 1401 | 1382 | if !show_editor { |
| 1402 | 1383 | if kind.is_dir() { |
| 1403 | 1384 | this.toggle_expanded(entry_id, cx); |
| @@ -1409,10 +1390,10 @@ impl ProjectPanel { · 5 unmodified lines | ||
| 1409 | 1390 | } |
| 1410 | 1391 | } |
| 1411 | 1392 | } |
| 1412 | − }) | |
| 1413 | − .on_mouse_down(MouseButton::Right, move |this, event, cx| { | |
| 1393 | + })) | |
| 1394 | + .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| { | |
| 1414 | 1395 | this.deploy_context_menu(event.position, entry_id, cx); |
| 1415 | − }) | |
| 1396 | + })) | |
| 1416 | 1397 | // .on_drop::<ProjectEntryId>(|this, event, cx| { |
| 1417 | 1398 | // this.move_entry( |
| 1418 | 1399 | // *dragged_entry, |
| @@ -1425,9 +1406,9 @@ impl ProjectPanel { · 6 unmodified lines | ||
| 1425 | 1406 | } |
| 1426 | 1407 | |
| 1427 | 1408 | impl Render for ProjectPanel { |
| 1428 | − type Element = Focusable<Self, Stateful<Self, Div<Self>>>; | |
| 1409 | + type Element = Focusable<Stateful<Div>>; | |
| 1429 | 1410 | |
| 1430 | − fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element { | |
| 1411 | + fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element { | |
| 1431 | 1412 | let has_worktree = self.visible_entries.len() != 0; |
| 1432 | 1413 | |
| 1433 | 1414 | if has_worktree { |
| @@ -1435,40 +1416,43 @@ impl Render for ProjectPanel { · 1 unmodified line | ||
| 1435 | 1416 | .id("project-panel") |
| 1436 | 1417 | .size_full() |
| 1437 | 1418 | .key_context("ProjectPanel") |
| 1438 | − .on_action(Self::select_next) | |
| 1439 | − .on_action(Self::select_prev) | |
| 1440 | − .on_action(Self::expand_selected_entry) | |
| 1441 | − .on_action(Self::collapse_selected_entry) | |
| 1442 | − .on_action(Self::collapse_all_entries) | |
| 1443 | − .on_action(Self::new_file) | |
| 1444 | − .on_action(Self::new_directory) | |
| 1445 | − .on_action(Self::rename) | |
| 1446 | − .on_action(Self::delete) | |
| 1447 | − .on_action(Self::confirm) | |
| 1448 | − .on_action(Self::open_file) | |
| 1449 | − .on_action(Self::cancel) | |
| 1450 | − .on_action(Self::cut) | |
| 1451 | − .on_action(Self::copy) | |
| 1452 | − .on_action(Self::copy_path) | |
| 1453 | − .on_action(Self::copy_relative_path) | |
| 1454 | − .on_action(Self::paste) | |
| 1455 | − .on_action(Self::reveal_in_finder) | |
| 1456 | − .on_action(Self::open_in_terminal) | |
| 1457 | − .on_action(Self::new_search_in_directory) | |
| 1419 | + .on_action(cx.listener(Self::select_next)) | |
| 1420 | + .on_action(cx.listener(Self::select_prev)) | |
| 1421 | + .on_action(cx.listener(Self::expand_selected_entry)) | |
| 1422 | + .on_action(cx.listener(Self::collapse_selected_entry)) | |
| 1423 | + .on_action(cx.listener(Self::collapse_all_entries)) | |
| 1424 | + .on_action(cx.listener(Self::new_file)) | |
| 1425 | + .on_action(cx.listener(Self::new_directory)) | |
| 1426 | + .on_action(cx.listener(Self::rename)) | |
| 1427 | + .on_action(cx.listener(Self::delete)) | |
| 1428 | + .on_action(cx.listener(Self::confirm)) | |
| 1429 | + .on_action(cx.listener(Self::open_file)) | |
| 1430 | + .on_action(cx.listener(Self::cancel)) | |
| 1431 | + .on_action(cx.listener(Self::cut)) | |
| 1432 | + .on_action(cx.listener(Self::copy)) | |
| 1433 | + .on_action(cx.listener(Self::copy_path)) | |
| 1434 | + .on_action(cx.listener(Self::copy_relative_path)) | |
| 1435 | + .on_action(cx.listener(Self::paste)) | |
| 1436 | + .on_action(cx.listener(Self::reveal_in_finder)) | |
| 1437 | + .on_action(cx.listener(Self::open_in_terminal)) | |
| 1438 | + .on_action(cx.listener(Self::new_search_in_directory)) | |
| 1458 | 1439 | .track_focus(&self.focus_handle) |
| 1459 | 1440 | .child( |
| 1460 | 1441 | uniform_list( |
| 1442 | + cx.view().clone(), | |
| 1461 | 1443 | "entries", |
| 1462 | 1444 | self.visible_entries |
| 1463 | 1445 | .iter() |
| 1464 | 1446 | .map(|(_, worktree_entries)| worktree_entries.len()) |
| 1465 | 1447 | .sum(), |
| 1466 | − |this: &mut Self, range, cx| { | |
| 1467 | − let mut items = Vec::new(); | |
| 1468 | − this.for_each_visible_entry(range, cx, |id, details, cx| { | |
| 1469 | − items.push(this.render_entry(id, details, cx)); | |
| 1470 | − }); | |
| 1471 | − items | |
| 1448 | + { | |
| 1449 | + |this, range, cx| { | |
| 1450 | + let mut items = Vec::new(); | |
| 1451 | + this.for_each_visible_entry(range, cx, |id, details, cx| { | |
| 1452 | + items.push(this.render_entry(id, details, cx)); | |
| 1453 | + }); | |
| 1454 | + items | |
| 1455 | + } | |
| 1472 | 1456 | }, |
| 1473 | 1457 | ) |
| 1474 | 1458 | .size_full() |
| @@ -56,12 +56,12 @@ pub struct Mention { · 55 unmodified lines | ||
| 56 | 56 | } |
| 57 | 57 | |
| 58 | 58 | impl RichText { |
| 59 | − pub fn element<V: 'static>( | |
| 59 | + pub fn element( | |
| 60 | 60 | &self, |
| 61 | 61 | // syntax: Arc<SyntaxTheme>, |
| 62 | 62 | // style: RichTextStyle, |
| 63 | 63 | // cx: &mut ViewContext<V>, |
| 64 | − ) -> AnyElement<V> { | |
| 64 | + ) -> AnyElement { | |
| 65 | 65 | todo!(); |
| 66 | 66 | |
| 67 | 67 | // let mut region_id = 0; |
| @@ -1767,16 +1767,13 @@ impl View for ProjectSearchBar { · 1766 unmodified lines | ||
| 1767 | 1767 | render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) |
| 1768 | 1768 | }); |
| 1769 | 1769 | |
| 1770 | − let mut include_ignored = is_semantic_disabled.then(|| { | |
| 1770 | + let include_ignored = is_semantic_disabled.then(|| { | |
| 1771 | 1771 | render_option_button_icon( |
| 1772 | − // TODO proper icon | |
| 1773 | − "icons/case_insensitive.svg", | |
| 1772 | + "icons/file_icons/git.svg", | |
| 1774 | 1773 | SearchOptions::INCLUDE_IGNORED, |
| 1775 | 1774 | cx, |
| 1776 | 1775 | ) |
| 1777 | 1776 | }); |
| 1778 | − // TODO not implemented yet | |
| 1779 | − let _ = include_ignored.take(); | |
| 1780 | 1777 | |
| 1781 | 1778 | let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| { |
| 1782 | 1779 | let is_active = if let Some(search) = self.active_project_search.as_ref() { |
| @@ -0,0 +1,40 @@ | ||
| 1 | +[package] | |
| 2 | +name = "search2" | |
| 3 | +version = "0.1.0" | |
| 4 | +edition = "2021" | |
| 5 | +publish = false | |
| 6 | + | |
| 7 | +[lib] | |
| 8 | +path = "src/search.rs" | |
| 9 | +doctest = false | |
| 10 | + | |
| 11 | +[dependencies] | |
| 12 | +bitflags = "1" | |
| 13 | +collections = { path = "../collections" } | |
| 14 | +editor = { package = "editor2", path = "../editor2" } | |
| 15 | +gpui = { package = "gpui2", path = "../gpui2" } | |
| 16 | +language = { package = "language2", path = "../language2" } | |
| 17 | +menu = { package = "menu2", path = "../menu2" } | |
| 18 | +project = { package = "project2", path = "../project2" } | |
| 19 | +settings = { package = "settings2", path = "../settings2" } | |
| 20 | +theme = { package = "theme2", path = "../theme2" } | |
| 21 | +util = { path = "../util" } | |
| 22 | +ui = {package = "ui2", path = "../ui2"} | |
| 23 | +workspace = { package = "workspace2", path = "../workspace2" } | |
| 24 | +#semantic_index = { path = "../semantic_index" } | |
| 25 | +anyhow.workspace = true | |
| 26 | +futures.workspace = true | |
| 27 | +log.workspace = true | |
| 28 | +postage.workspace = true | |
| 29 | +serde.workspace = true | |
| 30 | +serde_derive.workspace = true | |
| 31 | +smallvec.workspace = true | |
| 32 | +smol.workspace = true | |
| 33 | +serde_json.workspace = true | |
| 34 | +[dev-dependencies] | |
| 35 | +client = { package = "client2", path = "../client2", features = ["test-support"] } | |
| 36 | +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } | |
| 37 | +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } | |
| 38 | + | |
| 39 | +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } | |
| 40 | +unindent.workspace = true | |
| @@ -0,0 +1,1747 @@ | ||
| 1 | +use crate::{ | |
| 2 | + history::SearchHistory, | |
| 3 | + mode::{next_mode, SearchMode}, | |
| 4 | + search_bar::{render_nav_button, render_search_mode_button}, | |
| 5 | + ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, | |
| 6 | + ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, | |
| 7 | + ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, | |
| 8 | +}; | |
| 9 | +use collections::HashMap; | |
| 10 | +use editor::{Editor, EditorMode}; | |
| 11 | +use futures::channel::oneshot; | |
| 12 | +use gpui::{ | |
| 13 | + actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement, | |
| 14 | + ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _, | |
| 15 | + WeakView, WindowContext, | |
| 16 | +}; | |
| 17 | +use project::search::SearchQuery; | |
| 18 | +use serde::Deserialize; | |
| 19 | +use std::{any::Any, sync::Arc}; | |
| 20 | + | |
| 21 | +use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement}; | |
| 22 | +use util::ResultExt; | |
| 23 | +use workspace::{ | |
| 24 | + item::ItemHandle, | |
| 25 | + searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, | |
| 26 | + ToolbarItemLocation, ToolbarItemView, | |
| 27 | +}; | |
| 28 | + | |
| 29 | +#[derive(PartialEq, Clone, Deserialize, Default, Action)] | |
| 30 | +pub struct Deploy { | |
| 31 | + pub focus: bool, | |
| 32 | +} | |
| 33 | + | |
| 34 | +actions!(Dismiss, FocusEditor); | |
| 35 | + | |
| 36 | +pub enum Event { | |
| 37 | + UpdateLocation, | |
| 38 | +} | |
| 39 | + | |
| 40 | +pub fn init(cx: &mut AppContext) { | |
| 41 | + cx.observe_new_views(|editor: &mut Editor, cx| BufferSearchBar::register(editor, cx)) | |
| 42 | + .detach(); | |
| 43 | +} | |
| 44 | + | |
| 45 | +pub struct BufferSearchBar { | |
| 46 | + query_editor: View<Editor>, | |
| 47 | + replacement_editor: View<Editor>, | |
| 48 | + active_searchable_item: Option<Box<dyn SearchableItemHandle>>, | |
| 49 | + active_match_index: Option<usize>, | |
| 50 | + active_searchable_item_subscription: Option<Subscription>, | |
| 51 | + active_search: Option<Arc<SearchQuery>>, | |
| 52 | + searchable_items_with_matches: | |
| 53 | + HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>, | |
| 54 | + pending_search: Option<Task<()>>, | |
| 55 | + search_options: SearchOptions, | |
| 56 | + default_options: SearchOptions, | |
| 57 | + query_contains_error: bool, | |
| 58 | + dismissed: bool, | |
| 59 | + search_history: SearchHistory, | |
| 60 | + current_mode: SearchMode, | |
| 61 | + replace_enabled: bool, | |
| 62 | +} | |
| 63 | + | |
| 64 | +impl EventEmitter<Event> for BufferSearchBar {} | |
| 65 | +impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {} | |
| 66 | +impl Render for BufferSearchBar { | |
| 67 | + type Element = Div; | |
| 68 | + fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { | |
| 69 | + // let query_container_style = if self.query_contains_error { | |
| 70 | + // theme.search.invalid_editor | |
| 71 | + // } else { | |
| 72 | + // theme.search.editor.input.container | |
| 73 | + // }; | |
| 74 | + if self.dismissed { | |
| 75 | + return div(); | |
| 76 | + } | |
| 77 | + let supported_options = self.supported_options(); | |
| 78 | + | |
| 79 | + let previous_query_keystrokes = cx | |
| 80 | + .bindings_for_action(&PreviousHistoryQuery {}) | |
| 81 | + .into_iter() | |
| 82 | + .next() | |
| 83 | + .map(|binding| { | |
| 84 | + binding | |
| 85 | + .keystrokes() | |
| 86 | + .iter() | |
| 87 | + .map(|k| k.to_string()) | |
| 88 | + .collect::<Vec<_>>() | |
| 89 | + }); | |
| 90 | + let next_query_keystrokes = cx | |
| 91 | + .bindings_for_action(&NextHistoryQuery {}) | |
| 92 | + .into_iter() | |
| 93 | + .next() | |
| 94 | + .map(|binding| { | |
| 95 | + binding | |
| 96 | + .keystrokes() | |
| 97 | + .iter() | |
| 98 | + .map(|k| k.to_string()) | |
| 99 | + .collect::<Vec<_>>() | |
| 100 | + }); | |
| 101 | + let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { | |
| 102 | + (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { | |
| 103 | + format!( | |
| 104 | + "Search ({}/{} for previous/next query)", | |
| 105 | + previous_query_keystrokes.join(" "), | |
| 106 | + next_query_keystrokes.join(" ") | |
| 107 | + ) | |
| 108 | + } | |
| 109 | + (None, Some(next_query_keystrokes)) => { | |
| 110 | + format!( | |
| 111 | + "Search ({} for next query)", | |
| 112 | + next_query_keystrokes.join(" ") | |
| 113 | + ) | |
| 114 | + } | |
| 115 | + (Some(previous_query_keystrokes), None) => { | |
| 116 | + format!( | |
| 117 | + "Search ({} for previous query)", | |
| 118 | + previous_query_keystrokes.join(" ") | |
| 119 | + ) | |
| 120 | + } | |
| 121 | + (None, None) => String::new(), | |
| 122 | + }; | |
| 123 | + let new_placeholder_text = Arc::from(new_placeholder_text); | |
| 124 | + self.query_editor.update(cx, |editor, cx| { | |
| 125 | + editor.set_placeholder_text(new_placeholder_text, cx); | |
| 126 | + }); | |
| 127 | + self.replacement_editor.update(cx, |editor, cx| { | |
| 128 | + editor.set_placeholder_text("Replace with...", cx); | |
| 129 | + }); | |
| 130 | + | |
| 131 | + let search_button_for_mode = |mode| { | |
| 132 | + let is_active = self.current_mode == mode; | |
| 133 | + | |
| 134 | + render_search_mode_button( | |
| 135 | + mode, | |
| 136 | + is_active, | |
| 137 | + cx.listener(move |this, _, cx| { | |
| 138 | + this.activate_search_mode(mode, cx); | |
| 139 | + }), | |
| 140 | + ) | |
| 141 | + }; | |
| 142 | + let search_option_button = |option| { | |
| 143 | + let is_active = self.search_options.contains(option); | |
| 144 | + option.as_button(is_active) | |
| 145 | + }; | |
| 146 | + let match_count = self | |
| 147 | + .active_searchable_item | |
| 148 | + .as_ref() | |
| 149 | + .and_then(|searchable_item| { | |
| 150 | + if self.query(cx).is_empty() { | |
| 151 | + return None; | |
| 152 | + } | |
| 153 | + let matches = self | |
| 154 | + .searchable_items_with_matches | |
| 155 | + .get(&searchable_item.downgrade())?; | |
| 156 | + let message = if let Some(match_ix) = self.active_match_index { | |
| 157 | + format!("{}/{}", match_ix + 1, matches.len()) | |
| 158 | + } else { | |
| 159 | + "No matches".to_string() | |
| 160 | + }; | |
| 161 | + | |
| 162 | + Some(ui::Label::new(message)) | |
| 163 | + }); | |
| 164 | + let nav_button_for_direction = |icon, direction| { | |
| 165 | + render_nav_button( | |
| 166 | + icon, | |
| 167 | + self.active_match_index.is_some(), | |
| 168 | + cx.listener(move |this, _, cx| match direction { | |
| 169 | + Direction::Prev => this.select_prev_match(&Default::default(), cx), | |
| 170 | + Direction::Next => this.select_next_match(&Default::default(), cx), | |
| 171 | + }), | |
| 172 | + ) | |
| 173 | + }; | |
| 174 | + let should_show_replace_input = self.replace_enabled && supported_options.replacement; | |
| 175 | + let replace_all = should_show_replace_input | |
| 176 | + .then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll)); | |
| 177 | + let replace_next = should_show_replace_input | |
| 178 | + .then(|| super::render_replace_button(ReplaceNext, ui::Icon::Replace)); | |
| 179 | + let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx); | |
| 180 | + | |
| 181 | + h_stack() | |
| 182 | + .key_context("BufferSearchBar") | |
| 183 | + .when(in_replace, |this| { | |
| 184 | + this.key_context("in_replace") | |
| 185 | + .on_action(cx.listener(Self::replace_next)) | |
| 186 | + .on_action(cx.listener(Self::replace_all)) | |
| 187 | + }) | |
| 188 | + .on_action(cx.listener(Self::previous_history_query)) | |
| 189 | + .on_action(cx.listener(Self::next_history_query)) | |
| 190 | + .on_action(cx.listener(Self::dismiss)) | |
| 191 | + .w_full() | |
| 192 | + .p_1() | |
| 193 | + .child( | |
| 194 | + div() | |
| 195 | + .flex() | |
| 196 | + .flex_1() | |
| 197 | + .border_1() | |
| 198 | + .border_color(red()) | |
| 199 | + .rounded_md() | |
| 200 | + .items_center() | |
| 201 | + .child(IconElement::new(Icon::MagnifyingGlass)) | |
| 202 | + .child(self.query_editor.clone()) | |
| 203 | + .children( | |
| 204 | + supported_options | |
| 205 | + .case | |
| 206 | + .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)), | |
| 207 | + ) | |
| 208 | + .children( | |
| 209 | + supported_options | |
| 210 | + .word | |
| 211 | + .then(|| search_option_button(SearchOptions::WHOLE_WORD)), | |
| 212 | + ), | |
| 213 | + ) | |
| 214 | + .child( | |
| 215 | + h_stack() | |
| 216 | + .flex_none() | |
| 217 | + .child(ButtonGroup::new(vec![ | |
| 218 | + search_button_for_mode(SearchMode::Text), | |
| 219 | + search_button_for_mode(SearchMode::Regex), | |
| 220 | + ])) | |
| 221 | + .when(supported_options.replacement, |this| { | |
| 222 | + this.child(super::toggle_replace_button(self.replace_enabled)) | |
| 223 | + }), | |
| 224 | + ) | |
| 225 | + .child( | |
| 226 | + h_stack() | |
| 227 | + .gap_0p5() | |
| 228 | + .flex_1() | |
| 229 | + .when(self.replace_enabled, |this| { | |
| 230 | + this.child(self.replacement_editor.clone()) | |
| 231 | + .children(replace_next) | |
| 232 | + .children(replace_all) | |
| 233 | + }), | |
| 234 | + ) | |
| 235 | + .child( | |
| 236 | + h_stack() | |
| 237 | + .gap_0p5() | |
| 238 | + .flex_none() | |
| 239 | + .child(self.render_action_button()) | |
| 240 | + .children(match_count) | |
| 241 | + .child(nav_button_for_direction( | |
| 242 | + ui::Icon::ChevronLeft, | |
| 243 | + Direction::Prev, | |
| 244 | + )) | |
| 245 | + .child(nav_button_for_direction( | |
| 246 | + ui::Icon::ChevronRight, | |
| 247 | + Direction::Next, | |
| 248 | + )), | |
| 249 | + ) | |
| 250 | + } | |
| 251 | +} | |
| 252 | + | |
| 253 | +impl ToolbarItemView for BufferSearchBar { | |
| 254 | + fn set_active_pane_item( | |
| 255 | + &mut self, | |
| 256 | + item: Option<&dyn ItemHandle>, | |
| 257 | + cx: &mut ViewContext<Self>, | |
| 258 | + ) -> ToolbarItemLocation { | |
| 259 | + cx.notify(); | |
| 260 | + self.active_searchable_item_subscription.take(); | |
| 261 | + self.active_searchable_item.take(); | |
| 262 | + | |
| 263 | + self.pending_search.take(); | |
| 264 | + | |
| 265 | + if let Some(searchable_item_handle) = | |
| 266 | + item.and_then(|item| item.to_searchable_item_handle(cx)) | |
| 267 | + { | |
| 268 | + let this = cx.view().downgrade(); | |
| 269 | + | |
| 270 | + searchable_item_handle | |
| 271 | + .subscribe_to_search_events( | |
| 272 | + cx, | |
| 273 | + Box::new(move |search_event, cx| { | |
| 274 | + if let Some(this) = this.upgrade() { | |
| 275 | + this.update(cx, |this, cx| { | |
| 276 | + this.on_active_searchable_item_event(search_event, cx) | |
| 277 | + }); | |
| 278 | + } | |
| 279 | + }), | |
| 280 | + ) | |
| 281 | + .detach(); | |
| 282 | + | |
| 283 | + self.active_searchable_item = Some(searchable_item_handle); | |
| 284 | + let _ = self.update_matches(cx); | |
| 285 | + if !self.dismissed { | |
| 286 | + return ToolbarItemLocation::Secondary; | |
| 287 | + } | |
| 288 | + } | |
| 289 | + ToolbarItemLocation::Hidden | |
| 290 | + } | |
| 291 | + | |
| 292 | + fn row_count(&self, _: &WindowContext<'_>) -> usize { | |
| 293 | + 1 | |
| 294 | + } | |
| 295 | +} | |
| 296 | + | |
| 297 | +impl BufferSearchBar { | |
| 298 | + pub fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) { | |
| 299 | + if editor.mode() != EditorMode::Full { | |
| 300 | + return; | |
| 301 | + }; | |
| 302 | + | |
| 303 | + let handle = cx.view().downgrade(); | |
| 304 | + | |
| 305 | + editor.register_action(move |a: &Deploy, cx| { | |
| 306 | + let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx)) else { | |
| 307 | + return; | |
| 308 | + }; | |
| 309 | + | |
| 310 | + pane.update(cx, |this, cx| { | |
| 311 | + this.toolbar().update(cx, |this, cx| { | |
| 312 | + if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() { | |
| 313 | + search_bar.update(cx, |this, cx| { | |
| 314 | + if this.is_dismissed() { | |
| 315 | + this.show(cx); | |
| 316 | + } else { | |
| 317 | + this.dismiss(&Dismiss, cx); | |
| 318 | + } | |
| 319 | + }); | |
| 320 | + return; | |
| 321 | + } | |
| 322 | + let view = cx.build_view(|cx| BufferSearchBar::new(cx)); | |
| 323 | + this.add_item(view.clone(), cx); | |
| 324 | + view.update(cx, |this, cx| this.deploy(a, cx)); | |
| 325 | + cx.notify(); | |
| 326 | + }) | |
| 327 | + }); | |
| 328 | + }); | |
| 329 | + fn register_action<A: Action>( | |
| 330 | + editor: &mut Editor, | |
| 331 | + handle: WeakView<Editor>, | |
| 332 | + update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>), | |
| 333 | + ) { | |
| 334 | + editor.register_action(move |action: &A, cx| { | |
| 335 | + let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx)) | |
| 336 | + else { | |
| 337 | + return; | |
| 338 | + }; | |
| 339 | + pane.update(cx, move |this, cx| { | |
| 340 | + this.toolbar().update(cx, move |this, cx| { | |
| 341 | + if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() { | |
| 342 | + search_bar.update(cx, move |this, cx| update(this, action, cx)); | |
| 343 | + cx.notify(); | |
| 344 | + } | |
| 345 | + }) | |
| 346 | + }); | |
| 347 | + }); | |
| 348 | + } | |
| 349 | + | |
| 350 | + let handle = cx.view().downgrade(); | |
| 351 | + register_action( | |
| 352 | + editor, | |
| 353 | + handle.clone(), | |
| 354 | + |this, action: &ToggleCaseSensitive, cx| { | |
| 355 | + if this.supported_options().case { | |
| 356 | + this.toggle_case_sensitive(action, cx); | |
| 357 | + } | |
| 358 | + }, | |
| 359 | + ); | |
| 360 | + register_action( | |
| 361 | + editor, | |
| 362 | + handle.clone(), | |
| 363 | + |this, action: &ToggleWholeWord, cx| { | |
| 364 | + if this.supported_options().word { | |
| 365 | + this.toggle_whole_word(action, cx); | |
| 366 | + } | |
| 367 | + }, | |
| 368 | + ); | |
| 369 | + register_action( | |
| 370 | + editor, | |
| 371 | + handle.clone(), | |
| 372 | + |this, action: &ToggleReplace, cx| { | |
| 373 | + if this.supported_options().replacement { | |
| 374 | + this.toggle_replace(action, cx); | |
| 375 | + } | |
| 376 | + }, | |
| 377 | + ); | |
| 378 | + register_action(editor, handle.clone(), |this, _: &ActivateRegexMode, cx| { | |
| 379 | + if this.supported_options().regex { | |
| 380 | + this.activate_search_mode(SearchMode::Regex, cx); | |
| 381 | + } | |
| 382 | + }); | |
| 383 | + register_action(editor, handle.clone(), |this, _: &ActivateTextMode, cx| { | |
| 384 | + this.activate_search_mode(SearchMode::Text, cx); | |
| 385 | + }); | |
| 386 | + register_action(editor, handle.clone(), |this, action: &CycleMode, cx| { | |
| 387 | + if this.supported_options().regex { | |
| 388 | + // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting | |
| 389 | + // cycling. | |
| 390 | + this.cycle_mode(action, cx) | |
| 391 | + } | |
| 392 | + }); | |
| 393 | + register_action( | |
| 394 | + editor, | |
| 395 | + handle.clone(), | |
| 396 | + |this, action: &SelectNextMatch, cx| { | |
| 397 | + this.select_next_match(action, cx); | |
| 398 | + }, | |
| 399 | + ); | |
| 400 | + register_action( | |
| 401 | + editor, | |
| 402 | + handle.clone(), | |
| 403 | + |this, action: &SelectPrevMatch, cx| { | |
| 404 | + this.select_prev_match(action, cx); | |
| 405 | + }, | |
| 406 | + ); | |
| 407 | + register_action( | |
| 408 | + editor, | |
| 409 | + handle.clone(), | |
| 410 | + |this, action: &SelectAllMatches, cx| { | |
| 411 | + this.select_all_matches(action, cx); | |
| 412 | + }, | |
| 413 | + ); | |
| 414 | + register_action(editor, handle.clone(), |this, _: &editor::Cancel, cx| { | |
| 415 | + if !this.dismissed { | |
| 416 | + this.dismiss(&Dismiss, cx); | |
| 417 | + return; | |
| 418 | + } | |
| 419 | + cx.propagate(); | |
| 420 | + }); | |
| 421 | + } | |
| 422 | + pub fn new(cx: &mut ViewContext<Self>) -> Self { | |
| 423 | + let query_editor = cx.build_view(|cx| Editor::single_line(cx)); | |
| 424 | + cx.subscribe(&query_editor, Self::on_query_editor_event) | |
| 425 | + .detach(); | |
| 426 | + let replacement_editor = cx.build_view(|cx| Editor::single_line(cx)); | |
| 427 | + cx.subscribe(&replacement_editor, Self::on_query_editor_event) | |
| 428 | + .detach(); | |
| 429 | + Self { | |
| 430 | + query_editor, | |
| 431 | + replacement_editor, | |
| 432 | + active_searchable_item: None, | |
| 433 | + active_searchable_item_subscription: None, | |
| 434 | + active_match_index: None, | |
| 435 | + searchable_items_with_matches: Default::default(), | |
| 436 | + default_options: SearchOptions::NONE, | |
| 437 | + search_options: SearchOptions::NONE, | |
| 438 | + pending_search: None, | |
| 439 | + query_contains_error: false, | |
| 440 | + dismissed: true, | |
| 441 | + search_history: SearchHistory::default(), | |
| 442 | + current_mode: SearchMode::default(), | |
| 443 | + active_search: None, | |
| 444 | + replace_enabled: false, | |
| 445 | + } | |
| 446 | + } | |
| 447 | + | |
| 448 | + pub fn is_dismissed(&self) -> bool { | |
| 449 | + self.dismissed | |
| 450 | + } | |
| 451 | + | |
| 452 | + pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) { | |
| 453 | + self.dismissed = true; | |
| 454 | + for searchable_item in self.searchable_items_with_matches.keys() { | |
| 455 | + if let Some(searchable_item) = | |
| 456 | + WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) | |
| 457 | + { | |
| 458 | + searchable_item.clear_matches(cx); | |
| 459 | + } | |
| 460 | + } | |
| 461 | + if let Some(active_editor) = self.active_searchable_item.as_ref() { | |
| 462 | + let handle = active_editor.focus_handle(cx); | |
| 463 | + cx.focus(&handle); | |
| 464 | + } | |
| 465 | + cx.emit(Event::UpdateLocation); | |
| 466 | + cx.notify(); | |
| 467 | + } | |
| 468 | + | |
| 469 | + pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool { | |
| 470 | + if self.show(cx) { | |
| 471 | + self.search_suggested(cx); | |
| 472 | + if deploy.focus { | |
| 473 | + self.select_query(cx); | |
| 474 | + let handle = cx.focus_handle(); | |
| 475 | + cx.focus(&handle); | |
| 476 | + } | |
| 477 | + return true; | |
| 478 | + } | |
| 479 | + | |
| 480 | + false | |
| 481 | + } | |
| 482 | + | |
| 483 | + pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool { | |
| 484 | + if self.active_searchable_item.is_none() { | |
| 485 | + return false; | |
| 486 | + } | |
| 487 | + self.dismissed = false; | |
| 488 | + cx.notify(); | |
| 489 | + cx.emit(Event::UpdateLocation); | |
| 490 | + true | |
| 491 | + } | |
| 492 | + | |
| 493 | + fn supported_options(&self) -> workspace::searchable::SearchOptions { | |
| 494 | + self.active_searchable_item | |
| 495 | + .as_deref() | |
| 496 | + .map(SearchableItemHandle::supported_options) | |
| 497 | + .unwrap_or_default() | |
| 498 | + } | |
| 499 | + pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) { | |
| 500 | + let search = self | |
| 501 | + .query_suggestion(cx) | |
| 502 | + .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx)); | |
| 503 | + | |
| 504 | + if let Some(search) = search { | |
| 505 | + cx.spawn(|this, mut cx| async move { | |
| 506 | + search.await?; | |
| 507 | + this.update(&mut cx, |this, cx| this.activate_current_match(cx)) | |
| 508 | + }) | |
| 509 | + .detach_and_log_err(cx); | |
| 510 | + } | |
| 511 | + } | |
| 512 | + | |
| 513 | + pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) { | |
| 514 | + if let Some(match_ix) = self.active_match_index { | |
| 515 | + if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { | |
| 516 | + if let Some(matches) = self | |
| 517 | + .searchable_items_with_matches | |
| 518 | + .get(&active_searchable_item.downgrade()) | |
| 519 | + { | |
| 520 | + active_searchable_item.activate_match(match_ix, matches, cx) | |
| 521 | + } | |
| 522 | + } | |
| 523 | + } | |
| 524 | + } | |
| 525 | + | |
| 526 | + pub fn select_query(&mut self, cx: &mut ViewContext<Self>) { | |
| 527 | + self.query_editor.update(cx, |query_editor, cx| { | |
| 528 | + query_editor.select_all(&Default::default(), cx); | |
| 529 | + }); | |
| 530 | + } | |
| 531 | + | |
| 532 | + pub fn query(&self, cx: &WindowContext) -> String { | |
| 533 | + self.query_editor.read(cx).text(cx) | |
| 534 | + } | |
| 535 | + pub fn replacement(&self, cx: &WindowContext) -> String { | |
| 536 | + self.replacement_editor.read(cx).text(cx) | |
| 537 | + } | |
| 538 | + pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> { | |
| 539 | + self.active_searchable_item | |
| 540 | + .as_ref() | |
| 541 | + .map(|searchable_item| searchable_item.query_suggestion(cx)) | |
| 542 | + .filter(|suggestion| !suggestion.is_empty()) | |
| 543 | + } | |
| 544 | + | |
| 545 | + pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) { | |
| 546 | + if replacement.is_none() { | |
| 547 | + self.replace_enabled = false; | |
| 548 | + return; | |
| 549 | + } | |
| 550 | + self.replace_enabled = true; | |
| 551 | + self.replacement_editor | |
| 552 | + .update(cx, |replacement_editor, cx| { | |
| 553 | + replacement_editor | |
| 554 | + .buffer() | |
| 555 | + .update(cx, |replacement_buffer, cx| { | |
| 556 | + let len = replacement_buffer.len(cx); | |
| 557 | + replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx); | |
| 558 | + }); | |
| 559 | + }); | |
| 560 | + } | |
| 561 | + | |
| 562 | + pub fn search( | |
| 563 | + &mut self, | |
| 564 | + query: &str, | |
| 565 | + options: Option<SearchOptions>, | |
| 566 | + cx: &mut ViewContext<Self>, | |
| 567 | + ) -> oneshot::Receiver<()> { | |
| 568 | + let options = options.unwrap_or(self.default_options); | |
| 569 | + if query != self.query(cx) || self.search_options != options { | |
| 570 | + self.query_editor.update(cx, |query_editor, cx| { | |
| 571 | + query_editor.buffer().update(cx, |query_buffer, cx| { | |
| 572 | + let len = query_buffer.len(cx); | |
| 573 | + query_buffer.edit([(0..len, query)], None, cx); | |
| 574 | + }); | |
| 575 | + }); | |
| 576 | + self.search_options = options; | |
| 577 | + self.query_contains_error = false; | |
| 578 | + self.clear_matches(cx); | |
| 579 | + cx.notify(); | |
| 580 | + } | |
| 581 | + self.update_matches(cx) | |
| 582 | + } | |
| 583 | + | |
| 584 | + fn render_action_button(&self) -> impl IntoElement { | |
| 585 | + // let tooltip_style = theme.tooltip.clone(); | |
| 586 | + | |
| 587 | + // let style = theme.search.action_button.clone(); | |
| 588 | + | |
| 589 | + IconButton::new(0, ui::Icon::SelectAll) | |
| 590 | + .on_click(|_, cx| cx.dispatch_action(Box::new(SelectAllMatches))) | |
| 591 | + } | |
| 592 | + | |
| 593 | + pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) { | |
| 594 | + assert_ne!( | |
| 595 | + mode, | |
| 596 | + SearchMode::Semantic, | |
| 597 | + "Semantic search is not supported in buffer search" | |
| 598 | + ); | |
| 599 | + if mode == self.current_mode { | |
| 600 | + return; | |
| 601 | + } | |
| 602 | + self.current_mode = mode; | |
| 603 | + let _ = self.update_matches(cx); | |
| 604 | + cx.notify(); | |
| 605 | + } | |
| 606 | + | |
| 607 | + pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) { | |
| 608 | + if let Some(active_editor) = self.active_searchable_item.as_ref() { | |
| 609 | + let handle = active_editor.focus_handle(cx); | |
| 610 | + cx.focus(&handle); | |
| 611 | + } | |
| 612 | + } | |
| 613 | + | |
| 614 | + fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) { | |
| 615 | + self.search_options.toggle(search_option); | |
| 616 | + self.default_options = self.search_options; | |
| 617 | + let _ = self.update_matches(cx); | |
| 618 | + cx.notify(); | |
| 619 | + } | |
| 620 | + | |
| 621 | + pub fn set_search_options( | |
| 622 | + &mut self, | |
| 623 | + search_options: SearchOptions, | |
| 624 | + cx: &mut ViewContext<Self>, | |
| 625 | + ) { | |
| 626 | + self.search_options = search_options; | |
| 627 | + cx.notify(); | |
| 628 | + } | |
| 629 | + | |
| 630 | + fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) { | |
| 631 | + self.select_match(Direction::Next, 1, cx); | |
| 632 | + } | |
| 633 | + | |
| 634 | + fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) { | |
| 635 | + self.select_match(Direction::Prev, 1, cx); | |
| 636 | + } | |
| 637 | + | |
| 638 | + fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) { | |
| 639 | + if !self.dismissed && self.active_match_index.is_some() { | |
| 640 | + if let Some(searchable_item) = self.active_searchable_item.as_ref() { | |
| 641 | + if let Some(matches) = self | |
| 642 | + .searchable_items_with_matches | |
| 643 | + .get(&searchable_item.downgrade()) | |
| 644 | + { | |
| 645 | + searchable_item.select_matches(matches, cx); | |
| 646 | + self.focus_editor(&FocusEditor, cx); | |
| 647 | + } | |
| 648 | + } | |
| 649 | + } | |
| 650 | + } | |
| 651 | + | |
| 652 | + pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) { | |
| 653 | + if let Some(index) = self.active_match_index { | |
| 654 | + if let Some(searchable_item) = self.active_searchable_item.as_ref() { | |
| 655 | + if let Some(matches) = self | |
| 656 | + .searchable_items_with_matches | |
| 657 | + .get(&searchable_item.downgrade()) | |
| 658 | + { | |
| 659 | + let new_match_index = searchable_item | |
| 660 | + .match_index_for_direction(matches, index, direction, count, cx); | |
| 661 | + | |
| 662 | + searchable_item.update_matches(matches, cx); | |
| 663 | + searchable_item.activate_match(new_match_index, matches, cx); | |
| 664 | + } | |
| 665 | + } | |
| 666 | + } | |
| 667 | + } | |
| 668 | + | |
| 669 | + pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) { | |
| 670 | + if let Some(searchable_item) = self.active_searchable_item.as_ref() { | |
| 671 | + if let Some(matches) = self | |
| 672 | + .searchable_items_with_matches | |
| 673 | + .get(&searchable_item.downgrade()) | |
| 674 | + { | |
| 675 | + if matches.len() == 0 { | |
| 676 | + return; | |
| 677 | + } | |
| 678 | + let new_match_index = matches.len() - 1; | |
| 679 | + searchable_item.update_matches(matches, cx); | |
| 680 | + searchable_item.activate_match(new_match_index, matches, cx); | |
| 681 | + } | |
| 682 | + } | |
| 683 | + } | |
| 684 | + | |
| 685 | + fn on_query_editor_event( | |
| 686 | + &mut self, | |
| 687 | + _: View<Editor>, | |
| 688 | + event: &editor::EditorEvent, | |
| 689 | + cx: &mut ViewContext<Self>, | |
| 690 | + ) { | |
| 691 | + if let editor::EditorEvent::Edited { .. } = event { | |
| 692 | + self.query_contains_error = false; | |
| 693 | + self.clear_matches(cx); | |
| 694 | + let search = self.update_matches(cx); | |
| 695 | + cx.spawn(|this, mut cx| async move { | |
| 696 | + search.await?; | |
| 697 | + this.update(&mut cx, |this, cx| this.activate_current_match(cx)) | |
| 698 | + }) | |
| 699 | + .detach_and_log_err(cx); | |
| 700 | + } | |
| 701 | + } | |
| 702 | + | |
| 703 | + fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) { | |
| 704 | + match event { | |
| 705 | + SearchEvent::MatchesInvalidated => { | |
| 706 | + let _ = self.update_matches(cx); | |
| 707 | + } | |
| 708 | + SearchEvent::ActiveMatchChanged => self.update_match_index(cx), | |
| 709 | + } | |
| 710 | + } | |
| 711 | + | |
| 712 | + fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) { | |
| 713 | + self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx) | |
| 714 | + } | |
| 715 | + fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) { | |
| 716 | + self.toggle_search_option(SearchOptions::WHOLE_WORD, cx) | |
| 717 | + } | |
| 718 | + fn clear_matches(&mut self, cx: &mut ViewContext<Self>) { | |
| 719 | + let mut active_item_matches = None; | |
| 720 | + for (searchable_item, matches) in self.searchable_items_with_matches.drain() { | |
| 721 | + if let Some(searchable_item) = | |
| 722 | + WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) | |
| 723 | + { | |
| 724 | + if Some(&searchable_item) == self.active_searchable_item.as_ref() { | |
| 725 | + active_item_matches = Some((searchable_item.downgrade(), matches)); | |
| 726 | + } else { | |
| 727 | + searchable_item.clear_matches(cx); | |
| 728 | + } | |
| 729 | + } | |
| 730 | + } | |
| 731 | + | |
| 732 | + self.searchable_items_with_matches | |
| 733 | + .extend(active_item_matches); | |
| 734 | + } | |
| 735 | + | |
| 736 | + fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> { | |
| 737 | + let (done_tx, done_rx) = oneshot::channel(); | |
| 738 | + let query = self.query(cx); | |
| 739 | + self.pending_search.take(); | |
| 740 | + | |
| 741 | + if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { | |
| 742 | + if query.is_empty() { | |
| 743 | + self.active_match_index.take(); | |
| 744 | + active_searchable_item.clear_matches(cx); | |
| 745 | + let _ = done_tx.send(()); | |
| 746 | + cx.notify(); | |
| 747 | + } else { | |
| 748 | + let query: Arc<_> = if self.current_mode == SearchMode::Regex { | |
| 749 | + match SearchQuery::regex( | |
| 750 | + query, | |
| 751 | + self.search_options.contains(SearchOptions::WHOLE_WORD), | |
| 752 | + self.search_options.contains(SearchOptions::CASE_SENSITIVE), | |
| 753 | + false, | |
| 754 | + Vec::new(), | |
| 755 | + Vec::new(), | |
| 756 | + ) { | |
| 757 | + Ok(query) => query.with_replacement(self.replacement(cx)), | |
| 758 | + Err(_) => { | |
| 759 | + self.query_contains_error = true; | |
| 760 | + cx.notify(); | |
| 761 | + return done_rx; | |
| 762 | + } | |
| 763 | + } | |
| 764 | + } else { | |
| 765 | + match SearchQuery::text( | |
| 766 | + query, | |
| 767 | + self.search_options.contains(SearchOptions::WHOLE_WORD), | |
| 768 | + self.search_options.contains(SearchOptions::CASE_SENSITIVE), | |
| 769 | + false, | |
| 770 | + Vec::new(), | |
| 771 | + Vec::new(), | |
| 772 | + ) { | |
| 773 | + Ok(query) => query.with_replacement(self.replacement(cx)), | |
| 774 | + Err(_) => { | |
| 775 | + self.query_contains_error = true; | |
| 776 | + cx.notify(); | |
| 777 | + return done_rx; | |
| 778 | + } | |
| 779 | + } | |
| 780 | + } | |
| 781 | + .into(); | |
| 782 | + self.active_search = Some(query.clone()); | |
| 783 | + let query_text = query.as_str().to_string(); | |
| 784 | + | |
| 785 | + let matches = active_searchable_item.find_matches(query, cx); | |
| 786 | + | |
| 787 | + let active_searchable_item = active_searchable_item.downgrade(); | |
| 788 | + self.pending_search = Some(cx.spawn(|this, mut cx| async move { | |
| 789 | + let matches = matches.await; | |
| 790 | + | |
| 791 | + this.update(&mut cx, |this, cx| { | |
| 792 | + if let Some(active_searchable_item) = | |
| 793 | + WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx) | |
| 794 | + { | |
| 795 | + this.searchable_items_with_matches | |
| 796 | + .insert(active_searchable_item.downgrade(), matches); | |
| 797 | + | |
| 798 | + this.update_match_index(cx); | |
| 799 | + this.search_history.add(query_text); | |
| 800 | + if !this.dismissed { | |
| 801 | + let matches = this | |
| 802 | + .searchable_items_with_matches | |
| 803 | + .get(&active_searchable_item.downgrade()) | |
| 804 | + .unwrap(); | |
| 805 | + active_searchable_item.update_matches(matches, cx); | |
| 806 | + let _ = done_tx.send(()); | |
| 807 | + } | |
| 808 | + cx.notify(); | |
| 809 | + } | |
| 810 | + }) | |
| 811 | + .log_err(); | |
| 812 | + })); | |
| 813 | + } | |
| 814 | + } | |
| 815 | + done_rx | |
| 816 | + } | |
| 817 | + | |
| 818 | + fn update_match_index(&mut self, cx: &mut ViewContext<Self>) { | |
| 819 | + let new_index = self | |
| 820 | + .active_searchable_item | |
| 821 | + .as_ref() | |
| 822 | + .and_then(|searchable_item| { | |
| 823 | + let matches = self | |
| 824 | + .searchable_items_with_matches | |
| 825 | + .get(&searchable_item.downgrade())?; | |
| 826 | + searchable_item.active_match_index(matches, cx) | |
| 827 | + }); | |
| 828 | + if new_index != self.active_match_index { | |
| 829 | + self.active_match_index = new_index; | |
| 830 | + cx.notify(); | |
| 831 | + } | |
| 832 | + } | |
| 833 | + | |
| 834 | + fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) { | |
| 835 | + if let Some(new_query) = self.search_history.next().map(str::to_string) { | |
| 836 | + let _ = self.search(&new_query, Some(self.search_options), cx); | |
| 837 | + } else { | |
| 838 | + self.search_history.reset_selection(); | |
| 839 | + let _ = self.search("", Some(self.search_options), cx); | |
| 840 | + } | |
| 841 | + } | |
| 842 | + | |
| 843 | + fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) { | |
| 844 | + if self.query(cx).is_empty() { | |
| 845 | + if let Some(new_query) = self.search_history.current().map(str::to_string) { | |
| 846 | + let _ = self.search(&new_query, Some(self.search_options), cx); | |
| 847 | + return; | |
| 848 | + } | |
| 849 | + } | |
| 850 | + | |
| 851 | + if let Some(new_query) = self.search_history.previous().map(str::to_string) { | |
| 852 | + let _ = self.search(&new_query, Some(self.search_options), cx); | |
| 853 | + } | |
| 854 | + } | |
| 855 | + fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) { | |
| 856 | + self.activate_search_mode(next_mode(&self.current_mode, false), cx); | |
| 857 | + } | |
| 858 | + fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) { | |
| 859 | + if let Some(_) = &self.active_searchable_item { | |
| 860 | + self.replace_enabled = !self.replace_enabled; | |
| 861 | + if !self.replace_enabled { | |
| 862 | + let handle = self.query_editor.focus_handle(cx); | |
| 863 | + cx.focus(&handle); | |
| 864 | + } | |
| 865 | + cx.notify(); | |
| 866 | + } | |
| 867 | + } | |
| 868 | + fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) { | |
| 869 | + let mut should_propagate = true; | |
| 870 | + if !self.dismissed && self.active_search.is_some() { | |
| 871 | + if let Some(searchable_item) = self.active_searchable_item.as_ref() { | |
| 872 | + if let Some(query) = self.active_search.as_ref() { | |
| 873 | + if let Some(matches) = self | |
| 874 | + .searchable_items_with_matches | |
| 875 | + .get(&searchable_item.downgrade()) | |
| 876 | + { | |
| 877 | + if let Some(active_index) = self.active_match_index { | |
| 878 | + let query = query | |
| 879 | + .as_ref() | |
| 880 | + .clone() | |
| 881 | + .with_replacement(self.replacement(cx)); | |
| 882 | + searchable_item.replace(&matches[active_index], &query, cx); | |
| 883 | + self.select_next_match(&SelectNextMatch, cx); | |
| 884 | + } | |
| 885 | + should_propagate = false; | |
| 886 | + self.focus_editor(&FocusEditor, cx); | |
| 887 | + } | |
| 888 | + } | |
| 889 | + } | |
| 890 | + } | |
| 891 | + if !should_propagate { | |
| 892 | + cx.stop_propagation(); | |
| 893 | + } | |
| 894 | + } | |
| 895 | + pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) { | |
| 896 | + if !self.dismissed && self.active_search.is_some() { | |
| 897 | + if let Some(searchable_item) = self.active_searchable_item.as_ref() { | |
| 898 | + if let Some(query) = self.active_search.as_ref() { | |
| 899 | + if let Some(matches) = self | |
| 900 | + .searchable_items_with_matches | |
| 901 | + .get(&searchable_item.downgrade()) | |
| 902 | + { | |
| 903 | + let query = query | |
| 904 | + .as_ref() | |
| 905 | + .clone() | |
| 906 | + .with_replacement(self.replacement(cx)); | |
| 907 | + for m in matches { | |
| 908 | + searchable_item.replace(m, &query, cx); | |
| 909 | + } | |
| 910 | + } | |
| 911 | + } | |
| 912 | + } | |
| 913 | + } | |
| 914 | + } | |
| 915 | +} | |
| 916 | + | |
| 917 | +#[cfg(test)] | |
| 918 | +mod tests { | |
| 919 | + use std::ops::Range; | |
| 920 | + | |
| 921 | + use super::*; | |
| 922 | + use editor::{DisplayPoint, Editor}; | |
| 923 | + use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext}; | |
| 924 | + use language::Buffer; | |
| 925 | + use smol::stream::StreamExt as _; | |
| 926 | + use unindent::Unindent as _; | |
| 927 | + | |
| 928 | + fn init_globals(cx: &mut TestAppContext) { | |
| 929 | + cx.update(|cx| { | |
| 930 | + let store = settings::SettingsStore::test(cx); | |
| 931 | + cx.set_global(store); | |
| 932 | + editor::init(cx); | |
| 933 | + | |
| 934 | + language::init(cx); | |
| 935 | + theme::init(theme::LoadThemes::JustBase, cx); | |
| 936 | + }); | |
| 937 | + } | |
| 938 | + fn init_test( | |
| 939 | + cx: &mut TestAppContext, | |
| 940 | + ) -> ( | |
| 941 | + View<Editor>, | |
| 942 | + View<BufferSearchBar>, | |
| 943 | + &mut VisualTestContext<'_>, | |
| 944 | + ) { | |
| 945 | + init_globals(cx); | |
| 946 | + let buffer = cx.build_model(|cx| { | |
| 947 | + Buffer::new( | |
| 948 | + 0, | |
| 949 | + cx.entity_id().as_u64(), | |
| 950 | + r#" | |
| 951 | + A regular expression (shortened as regex or regexp;[1] also referred to as | |
| 952 | + rational expression[2][3]) is a sequence of characters that specifies a search | |
| 953 | + pattern in text. Usually such patterns are used by string-searching algorithms | |
| 954 | + for "find" or "find and replace" operations on strings, or for input validation. | |
| 955 | + "# | |
| 956 | + .unindent(), | |
| 957 | + ) | |
| 958 | + }); | |
| 959 | + let (_, cx) = cx.add_window_view(|_| EmptyView {}); | |
| 960 | + let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); | |
| 961 | + | |
| 962 | + let search_bar = cx.build_view(|cx| { | |
| 963 | + let mut search_bar = BufferSearchBar::new(cx); | |
| 964 | + search_bar.set_active_pane_item(Some(&editor), cx); | |
| 965 | + search_bar.show(cx); | |
| 966 | + search_bar | |
| 967 | + }); | |
| 968 | + | |
| 969 | + (editor, search_bar, cx) | |
| 970 | + } | |
| 971 | + | |
| 972 | + #[gpui::test] | |
| 973 | + async fn test_search_simple(cx: &mut TestAppContext) { | |
| 974 | + let (editor, search_bar, cx) = init_test(cx); | |
| 975 | + // todo! osiewicz: these tests asserted on background color as well, that should be brought back. | |
| 976 | + let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| { | |
| 977 | + background_highlights | |
| 978 | + .into_iter() | |
| 979 | + .map(|(range, _)| range) | |
| 980 | + .collect::<Vec<_>>() | |
| 981 | + }; | |
| 982 | + // Search for a string that appears with different casing. | |
| 983 | + // By default, search is case-insensitive. | |
| 984 | + search_bar | |
| 985 | + .update(cx, |search_bar, cx| search_bar.search("us", None, cx)) | |
| 986 | + .await | |
| 987 | + .unwrap(); | |
| 988 | + editor.update(cx, |editor, cx| { | |
| 989 | + assert_eq!( | |
| 990 | + display_points_of(editor.all_text_background_highlights(cx)), | |
| 991 | + &[ | |
| 992 | + DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), | |
| 993 | + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), | |
| 994 | + ] | |
| 995 | + ); | |
| 996 | + }); | |
| 997 | + | |
| 998 | + // Switch to a case sensitive search. | |
| 999 | + search_bar.update(cx, |search_bar, cx| { | |
| 1000 | + search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); | |
| 1001 | + }); | |
| 1002 | + let mut editor_notifications = cx.notifications(&editor); | |
| 1003 | + editor_notifications.next().await; | |
| 1004 | + editor.update(cx, |editor, cx| { | |
| 1005 | + assert_eq!( | |
| 1006 | + display_points_of(editor.all_text_background_highlights(cx)), | |
| 1007 | + &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),] | |
| 1008 | + ); | |
| 1009 | + }); | |
| 1010 | + | |
| 1011 | + // Search for a string that appears both as a whole word and | |
| 1012 | + // within other words. By default, all results are found. | |
| 1013 | + search_bar | |
| 1014 | + .update(cx, |search_bar, cx| search_bar.search("or", None, cx)) | |
| 1015 | + .await | |
| 1016 | + .unwrap(); | |
| 1017 | + editor.update(cx, |editor, cx| { | |
| 1018 | + assert_eq!( | |
| 1019 | + display_points_of(editor.all_text_background_highlights(cx)), | |
| 1020 | + &[ | |
| 1021 | + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), | |
| 1022 | + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), | |
| 1023 | + DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), | |
| 1024 | + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), | |
| 1025 | + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), | |
| 1026 | + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), | |
| 1027 | + DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), | |
| 1028 | + ] | |
| 1029 | + ); | |
| 1030 | + }); | |
| 1031 | + | |
| 1032 | + // Switch to a whole word search. | |
| 1033 | + search_bar.update(cx, |search_bar, cx| { | |
| 1034 | + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); | |
| 1035 | + }); | |
| 1036 | + let mut editor_notifications = cx.notifications(&editor); | |
| 1037 | + editor_notifications.next().await; | |
| 1038 | + editor.update(cx, |editor, cx| { | |
| 1039 | + assert_eq!( | |
| 1040 | + display_points_of(editor.all_text_background_highlights(cx)), | |
| 1041 | + &[ | |
| 1042 | + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), | |
| 1043 | + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), | |
| 1044 | + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), | |
| 1045 | + ] | |
| 1046 | + ); | |
| 1047 | + }); | |
| 1048 | + | |
| 1049 | + editor.update(cx, |editor, cx| { | |
| 1050 | + editor.change_selections(None, cx, |s| { | |
| 1051 | + s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]) | |
| 1052 | + }); | |
| 1053 | + }); | |
| 1054 | + search_bar.update(cx, |search_bar, cx| { | |
| 1055 | + assert_eq!(search_bar.active_match_index, Some(0)); | |
| 1056 | + search_bar.select_next_match(&SelectNextMatch, cx); | |
| 1057 | + assert_eq!( | |
| 1058 | + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), | |
| 1059 | + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] | |
| 1060 | + ); | |
| 1061 | + }); | |
| 1062 | + search_bar.update(cx, |search_bar, _| { | |
| 1063 | + assert_eq!(search_bar.active_match_index, Some(0)); | |
| 1064 | + }); | |
| 1065 | + | |
| 1066 | + search_bar.update(cx, |search_bar, cx| { | |
| 1067 | + search_bar.select_next_match(&SelectNextMatch, cx); | |
| 1068 | + assert_eq!( | |
| 1069 | + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), | |
| 1070 | + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] | |
| 1071 | + ); | |
| 1072 | + }); | |
| 1073 | + search_bar.update(cx, |search_bar, _| { | |
| 1074 | + assert_eq!(search_bar.active_match_index, Some(1)); | |
| 1075 | + }); | |
| 1076 | + | |
| 1077 | + search_bar.update(cx, |search_bar, cx| { | |
| 1078 | + search_bar.select_next_match(&SelectNextMatch, cx); | |
| 1079 | + assert_eq!( | |
| 1080 | + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), | |
| 1081 | + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] | |
| 1082 | + ); | |
| 1083 | + }); | |
| 1084 | + search_bar.update(cx, |search_bar, _| { | |
| 1085 | + assert_eq!(search_bar.active_match_index, Some(2)); | |
| 1086 | + }); | |
| 1087 | + | |
| 1088 | + search_bar.update(cx, |search_bar, cx| { | |
| 1089 | + search_bar.select_next_match(&SelectNextMatch, cx); | |
| 1090 | + assert_eq!( | |
| 1091 | + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), | |
| 1092 | + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] | |
| 1093 | + ); | |
| 1094 | + }); | |
| 1095 | + search_bar.update(cx, |search_bar, _| { | |
| 1096 | + assert_eq!(search_bar.active_match_index, Some(0)); | |
| 1097 | + }); | |
| 1098 | + | |
| 1099 | + search_bar.update(cx, |search_bar, cx| { | |
| 1100 | + search_bar.select_prev_match(&SelectPrevMatch, cx); | |
| 1101 | + assert_eq!( | |
| 1102 | + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), | |
| 1103 | + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] | |
| 1104 | + ); | |
| 1105 | + }); | |
| 1106 | + search_bar.update(cx, |search_bar, _| { | |
| 1107 | + assert_eq!(search_bar.active_match_index, Some(2)); | |
| 1108 | + }); | |
| 1109 | + | |
| 1110 | + search_bar.update(cx, |search_bar, cx| { | |
| 1111 | + search_bar.select_prev_match(&SelectPrevMatch, cx); | |
| 1112 | + assert_eq!( | |
| 1113 | + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), | |
| 1114 | + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] | |
| 1115 | + ); | |
| 1116 | + }); | |
| 1117 | + search_bar.update(cx, |search_bar, _| { | |
| 1118 | + assert_eq!(search_bar.active_match_index, Some(1)); | |
| 1119 | + }); | |
| 1120 | + | |
| 1121 | + search_bar.update(cx, |search_bar, cx| { | |
| 1122 | + search_bar.select_prev_match(&SelectPrevMatch, cx); | |
| 1123 | + assert_eq!( | |
| 1124 | + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), | |
| 1125 | + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] | |
| 1126 | + ); | |
| 1127 | + }); | |
| 1128 | + search_bar.update(cx, |search_bar, _| { | |
| 1129 | + assert_eq!(search_bar.active_match_index, Some(0)); | |
| 1130 | + }); | |
| 1131 | + | |
| 1132 | + // Park the cursor in between matches and ensure that going to the previous match selects | |
| 1133 | + // the closest match to the left. | |
| 1134 | + editor.update(cx, |editor, cx| { | |
| 1135 | + editor.change_selections(None, cx, |s| { | |
| 1136 | + s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) | |
| 1137 | + }); | |
| 1138 | + }); | |
| 1139 | + search_bar.update(cx, |search_bar, cx| { | |
| 1140 | + assert_eq!(search_bar.active_match_index, Some(1)); | |
| 1141 | + search_bar.select_prev_match(&SelectPrevMatch, cx); | |
| 1142 | + assert_eq!( | |
| 1143 | + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), | |
| 1144 | + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] | |
| 1145 | + ); | |
| 1146 | + }); | |
| 1147 | + search_bar.update(cx, |search_bar, _| { | |
| 1148 | + assert_eq!(search_bar.active_match_index, Some(0)); | |
| 1149 | + }); | |
598 more lines aren’t shown here.
This diff is large: its 185 lines aren’t shown here.
This diff is large: its 33 lines aren’t shown here.
This diff is large: its 2681 lines aren’t shown here.
This diff is large: its 118 lines aren’t shown here.
This diff is large: its 36 lines aren’t shown here.
This diff is large: its 16 lines aren’t shown here.
This diff is large: its 11 lines aren’t shown here.
This diff is large: its 4 lines aren’t shown here.
This diff is large: its 36 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 13 lines aren’t shown here.
This diff is large: its 45 lines aren’t shown here.
This diff is large: its 97 lines aren’t shown here.
This diff is large: its 35 lines aren’t shown here.
This diff is large: its 66 lines aren’t shown here.
This diff is large: its 18 lines aren’t shown here.
This diff is large: its 40 lines aren’t shown here.
This diff is large: its 142 lines aren’t shown here.
This diff is large: its 2 lines aren’t shown here.
This diff is large: its 102 lines aren’t shown here.
This diff is large: its 42 lines aren’t shown here.
This diff is large: its 18 lines aren’t shown here.
This diff is large: its 88 lines aren’t shown here.
This diff is large: its 764 lines aren’t shown here.
This diff is large: its 18 lines aren’t shown here.
This diff is large: its 161 lines aren’t shown here.
This diff is large: its 20 lines aren’t shown here.
This diff is large: its 11 lines aren’t shown here.
This diff is large: its 9 lines aren’t shown here.
This diff is large: its 39 lines aren’t shown here.
This diff is large: its 15 lines aren’t shown here.
This diff is large: its 150 lines aren’t shown here.
This diff is large: its 42 lines aren’t shown here.
This diff is large: its 6 lines aren’t shown here.
This diff is large: its 138 lines aren’t shown here.
This diff is large: its 35 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 43 lines aren’t shown here.
This diff is large: its 67 lines aren’t shown here.
This diff is large: its 102 lines aren’t shown here.
This diff is large: its 432 lines aren’t shown here.
This diff is large: its 268 lines aren’t shown here.
This diff is large: its 483 lines aren’t shown here.
This diff is large: its 79 lines aren’t shown here.
This diff is large: its 20 lines aren’t shown here.
This diff is large: its 46 lines aren’t shown here.
This diff is large: its 29 lines aren’t shown here.
This diff is large: its 60 lines aren’t shown here.
This diff is large: its 158 lines aren’t shown here.
This diff is large: its 193 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 150 lines aren’t shown here.
This diff is large: its 225 lines aren’t shown here.
This diff is large: its 270 lines aren’t shown here.
This diff is large: its 557 lines aren’t shown here.
This diff is large: its 82 lines aren’t shown here.
This diff is large: its 41 lines aren’t shown here.
This diff is large: its 205 lines aren’t shown here.
This diff is large: its 151 lines aren’t shown here.
This diff is large: its 175 lines aren’t shown here.
This diff is large: its 62 lines aren’t shown here.
This diff is large: its 85 lines aren’t shown here.
This diff is large: its 14 lines aren’t shown here.
This diff is large: its 16 lines aren’t shown here.
This diff is large: its 20 lines aren’t shown here.
This diff is large: its 24 lines aren’t shown here.
This diff is large: its 146 lines aren’t shown here.
This diff is large: its 50 lines aren’t shown here.
This diff is large: its 105 lines aren’t shown here.
This diff is large: its 22 lines aren’t shown here.
This diff is large: its 19 lines aren’t shown here.
This diff is large: its 62 lines aren’t shown here.
This diff is large: its 28 lines aren’t shown here.
This diff is large: its 35 lines aren’t shown here.
This diff is large: its 273 lines aren’t shown here.
This diff is large: its 91 lines aren’t shown here.
This diff is large: its 28 lines aren’t shown here.
This diff is large: its 15 lines aren’t shown here.
This diff is large: its 34 lines aren’t shown here.
This diff is large: its 189 lines aren’t shown here.
This diff is large: its 75 lines aren’t shown here.
This diff is large: its 1112 lines aren’t shown here.
This diff is large: its 38 lines aren’t shown here.
This diff is large: its 48 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 45 lines aren’t shown here.
This diff is large: its 57 lines aren’t shown here.
This diff is large: its 29 lines aren’t shown here.
This diff is large: its 28 lines aren’t shown here.
This diff is large: its 48 lines aren’t shown here.
This diff is large: its 94 lines aren’t shown here.
This diff is large: its 114 lines aren’t shown here.
This diff is large: its 267 lines aren’t shown here.
This diff is large: its 47 lines aren’t shown here.
This diff is large: its 151 lines aren’t shown here.
This diff is large: its 111 lines aren’t shown here.
This diff is large: its 49 lines aren’t shown here.
This diff is large: its 47 lines aren’t shown here.
This diff is large: its 78 lines aren’t shown here.
This diff is large: its 58 lines aren’t shown here.
This diff is large: its 64 lines aren’t shown here.
This diff is large: its 372 lines aren’t shown here.
This diff is large: its 129 lines aren’t shown here.
This diff is large: its 76 lines aren’t shown here.
This diff is large: its 54 lines aren’t shown here.
This diff is large: its 204 lines aren’t shown here.
This diff is large: its 151 lines aren’t shown here.
This diff is large: its 100 lines aren’t shown here.
This diff is large: its 61 lines aren’t shown here.
This diff is large: its 219 lines aren’t shown here.
This diff is large: its 127 lines aren’t shown here.
This diff is large: its 101 lines aren’t shown here.
This diff is large: its 399 lines aren’t shown here.
This diff is large: its 39 lines aren’t shown here.
This diff is large: its 17 lines aren’t shown here.
This diff is large: its 30 lines aren’t shown here.
This diff is large: its 16 lines aren’t shown here.
This diff is large: its 103 lines aren’t shown here.
This diff is large: its 34 lines aren’t shown here.
This diff is large: its 33 lines aren’t shown here.
This diff is large: its 235 lines aren’t shown here.
This diff is large: its 248 lines aren’t shown here.
This diff is large: its 120 lines aren’t shown here.
This diff is large: its 45 lines aren’t shown here.
This diff is large: its 152 lines aren’t shown here.
This diff is large: its 129 lines aren’t shown here.
This diff is large: its 95 lines aren’t shown here.
This diff is large: its 798 lines aren’t shown here.
This diff is large: its 25 lines aren’t shown here.
This diff is large: its 39 lines aren’t shown here.
This diff is large: its 13 lines aren’t shown here.
This diff is large: its 82 lines aren’t shown here.
This diff is large: its 56 lines aren’t shown here.
This diff is large: its 11 lines aren’t shown here.
This diff is large: its 51 lines aren’t shown here.
This diff is large: its 4 lines aren’t shown here.
This diff is large: its 48 lines aren’t shown here.
This diff is large: its 35 lines aren’t shown here.
This diff is large: its 39 lines aren’t shown here.
This diff is large: its 56 lines aren’t shown here.
This diff is large: its 56 lines aren’t shown here.
This diff is large: its 11 lines aren’t shown here.
This diff is large: its 51 lines aren’t shown here.
This diff is large: its 4 lines aren’t shown here.
This diff is large: its 138 lines aren’t shown here.
This diff is large: its 35 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 8 lines aren’t shown here.
This diff is large: its 37 lines aren’t shown here.
This diff is large: its 160 lines aren’t shown here.
This diff is large: its 32 lines aren’t shown here.