Merge branch 'main' into vim-visual-selection

Conrad Irwin created

Change summary

.zed/settings.json                                           |   5 
Cargo.lock                                                   | 284 +-
Cargo.toml                                                   |   1 
Dockerfile                                                   |   2 
assets/fonts/plex/IBMPlexSans-Bold.ttf                       |   0 
assets/fonts/plex/IBMPlexSans-Italic.ttf                     |   0 
assets/fonts/plex/IBMPlexSans-Regular.ttf                    |   0 
assets/fonts/plex/LICENSE.txt                                |  93 
assets/icons/file_icons/file_types.json                      | 332 +-
assets/keymaps/default.json                                  |  14 
crates/ai/src/assistant.rs                                   |  23 
crates/collab/src/tests.rs                                   |  43 
crates/collab/src/tests/integration_tests.rs                 | 140 
crates/collab/src/tests/randomized_integration_tests.rs      |   2 
crates/collab_ui/src/collab_titlebar_item.rs                 |  20 
crates/collab_ui/src/contact_list.rs                         |  16 
crates/collab_ui/src/face_pile.rs                            |   4 
crates/collab_ui/src/incoming_call_notification.rs           |  12 
crates/collab_ui/src/project_shared_notification.rs          |  16 
crates/collab_ui/src/sharing_status_indicator.rs             |   8 
crates/command_palette/src/command_palette.rs                |  24 
crates/context_menu/src/context_menu.rs                      |  22 
crates/copilot/src/sign_in.rs                                |  55 
crates/copilot_button/src/copilot_button.rs                  |   2 
crates/diagnostics/src/diagnostics.rs                        |  10 
crates/diagnostics/src/items.rs                              |   2 
crates/drag_and_drop/src/drag_and_drop.rs                    |  40 
crates/editor/Cargo.toml                                     |   3 
crates/editor/src/display_map/inlay_map.rs                   |   4 
crates/editor/src/editor.rs                                  | 209 +
crates/editor/src/editor_tests.rs                            | 665 +++--
crates/editor/src/element.rs                                 | 205 
crates/editor/src/hover_popover.rs                           |   2 
crates/editor/src/inlay_hint_cache.rs                        | 786 ++---
crates/editor/src/items.rs                                   |   9 
crates/editor/src/scroll.rs                                  |   6 
crates/editor/src/test/editor_lsp_test_context.rs            |   5 
crates/editor/src/test/editor_test_context.rs                |  20 
crates/feedback/src/deploy_feedback_button.rs                |   2 
crates/feedback/src/submit_feedback_button.rs                |   2 
crates/file_finder/src/file_finder.rs                        | 250 -
crates/go_to_line/src/go_to_line.rs                          |   2 
crates/gpui/Cargo.toml                                       |   1 
crates/gpui/examples/corner_radii.rs                         | 155 +
crates/gpui/src/app.rs                                       | 478 +--
crates/gpui/src/app/menu.rs                                  |   6 
crates/gpui/src/app/ref_counts.rs                            |  14 
crates/gpui/src/app/test_app_context.rs                      | 267 +-
crates/gpui/src/app/window.rs                                | 184 
crates/gpui/src/app/window_input_handler.rs                  |  27 
crates/gpui/src/elements.rs                                  |  34 
crates/gpui/src/elements/align.rs                            |   5 
crates/gpui/src/elements/canvas.rs                           |   4 
crates/gpui/src/elements/clipped.rs                          |   5 
crates/gpui/src/elements/constrained_box.rs                  |   5 
crates/gpui/src/elements/container.rs                        |  27 
crates/gpui/src/elements/empty.rs                            |   4 
crates/gpui/src/elements/expanded.rs                         |   5 
crates/gpui/src/elements/flex.rs                             |   8 
crates/gpui/src/elements/hook.rs                             |   5 
crates/gpui/src/elements/image.rs                            |   8 
crates/gpui/src/elements/keystroke_label.rs                  |   2 
crates/gpui/src/elements/label.rs                            |   4 
crates/gpui/src/elements/list.rs                             |  10 
crates/gpui/src/elements/mouse_event_handler.rs              |   6 
crates/gpui/src/elements/overlay.rs                          |   6 
crates/gpui/src/elements/resizable.rs                        |   6 
crates/gpui/src/elements/stack.rs                            |   5 
crates/gpui/src/elements/svg.rs                              |   3 
crates/gpui/src/elements/text.rs                             |   6 
crates/gpui/src/elements/tooltip.rs                          |  15 
crates/gpui/src/elements/uniform_list.rs                     |   4 
crates/gpui/src/fonts.rs                                     |  26 
crates/gpui/src/platform.rs                                  |   8 
crates/gpui/src/platform/mac.rs                              |   2 
crates/gpui/src/platform/mac/platform.rs                     |  14 
crates/gpui/src/platform/mac/renderer.rs                     |  23 
crates/gpui/src/platform/mac/shaders/shaders.h               |  15 
crates/gpui/src/platform/mac/shaders/shaders.metal           |  64 
crates/gpui/src/platform/mac/window.rs                       |  32 
crates/gpui/src/platform/test.rs                             |  40 
crates/gpui/src/scene.rs                                     |  68 
crates/gpui/src/scene/mouse_region.rs                        |   2 
crates/gpui_macros/Cargo.toml                                |   2 
crates/gpui_macros/src/gpui_macros.rs                        |   8 
crates/gpui_macros/tests/test.rs                             |  14 
crates/language/src/language.rs                              |  18 
crates/language_tools/src/lsp_log_tests.rs                   |   4 
crates/live_kit_client/build.rs                              |  12 
crates/lsp/src/lsp.rs                                        |   4 
crates/project/src/lsp_command.rs                            |  56 
crates/project/src/project.rs                                |  29 
crates/project/src/terminals.rs                              |   6 
crates/project/src/worktree.rs                               |   2 
crates/project_panel/src/file_associations.rs                |  11 
crates/project_panel/src/project_panel.rs                    | 146 
crates/project_symbols/src/project_symbols.rs                |   5 
crates/recent_projects/src/highlighted_workspace_location.rs |   3 
crates/recent_projects/src/recent_projects.rs                |   3 
crates/search/src/buffer_search.rs                           | 257 +
crates/search/src/project_search.rs                          | 315 ++
crates/search/src/search.rs                                  | 187 +
crates/semantic_index/Cargo.toml                             |  15 
crates/semantic_index/src/parsing.rs                         |  28 
crates/semantic_index/src/semantic_index.rs                  |   1 
crates/semantic_index/src/semantic_index_tests.rs            | 538 ++++
crates/sum_tree/src/cursor.rs                                |   2 
crates/terminal/src/terminal.rs                              |   6 
crates/terminal_view/src/terminal_element.rs                 |  11 
crates/terminal_view/src/terminal_panel.rs                   |  13 
crates/terminal_view/src/terminal_view.rs                    |  12 
crates/theme/src/ui.rs                                       |   1 
crates/util/src/paths.rs                                     | 151 
crates/vim/src/editor_events.rs                              |   8 
crates/vim/src/mode_indicator.rs                             |   2 
crates/vim/src/normal/search.rs                              |   4 
crates/vim/src/test.rs                                       |   4 
crates/vim/src/test/vim_test_context.rs                      |   4 
crates/workspace/src/dock.rs                                 |  11 
crates/workspace/src/item.rs                                 |  13 
crates/workspace/src/pane.rs                                 |  48 
crates/workspace/src/pane/dragged_item_receiver.rs           |  14 
crates/workspace/src/pane_group.rs                           |   4 
crates/workspace/src/searchable.rs                           |   6 
crates/workspace/src/status_bar.rs                           |   6 
crates/workspace/src/toolbar.rs                              |   4 
crates/workspace/src/workspace.rs                            | 262 -
crates/zed/Cargo.toml                                        |   2 
crates/zed/src/languages/bash/config.toml                    |   3 
crates/zed/src/languages/bash/highlights.scm                 |   2 
crates/zed/src/languages/c/highlights.scm                    |   3 
crates/zed/src/languages/c/injections.scm                    |   4 
crates/zed/src/languages/cpp/highlights.scm                  |   4 
crates/zed/src/languages/cpp/injections.scm                  |   4 
crates/zed/src/languages/css/highlights.scm                  |   2 
crates/zed/src/languages/elixir/embedding.scm                |   6 
crates/zed/src/languages/elixir/highlights.scm               |  18 
crates/zed/src/languages/elixir/injections.scm               |   4 
crates/zed/src/languages/elixir/outline.scm                  |   4 
crates/zed/src/languages/elm/injections.scm                  |   2 
crates/zed/src/languages/erb/injections.scm                  |   8 
crates/zed/src/languages/glsl/highlights.scm                 |   4 
crates/zed/src/languages/heex/injections.scm                 |   6 
crates/zed/src/languages/html/injections.scm                 |   4 
crates/zed/src/languages/javascript/highlights.scm           |   6 
crates/zed/src/languages/lua/config.toml                     |   1 
crates/zed/src/languages/lua/embedding.scm                   |  10 
crates/zed/src/languages/lua/highlights.scm                  |  10 
crates/zed/src/languages/php/config.toml                     |   1 
crates/zed/src/languages/php/embedding.scm                   |  36 
crates/zed/src/languages/php/highlights.scm                  |  10 
crates/zed/src/languages/php/injections.scm                  |   4 
crates/zed/src/languages/php/outline.scm                     |   7 
crates/zed/src/languages/python/highlights.scm               |   8 
crates/zed/src/languages/racket/highlights.scm               |   4 
crates/zed/src/languages/racket/outline.scm                  |   4 
crates/zed/src/languages/ruby/brackets.scm                   |   2 
crates/zed/src/languages/ruby/config.toml                    |   1 
crates/zed/src/languages/ruby/embedding.scm                  |  22 
crates/zed/src/languages/ruby/highlights.scm                 |   8 
crates/zed/src/languages/rust.rs                             |   4 
crates/zed/src/languages/rust/highlights.scm                 |   4 
crates/zed/src/languages/rust/injections.scm                 |   4 
crates/zed/src/languages/scheme/highlights.scm               |   4 
crates/zed/src/languages/scheme/outline.scm                  |   4 
crates/zed/src/languages/svelte/injections.scm               |  14 
crates/zed/src/languages/toml/config.toml                    |   2 
crates/zed/src/languages/typescript/highlights.scm           |  10 
crates/zed/src/main.rs                                       |  80 
crates/zed/src/zed.rs                                        | 197 
rust-toolchain.toml                                          |   2 
styles/package.json                                          |   1 
styles/src/build_themes.ts                                   |   9 
styles/src/build_tokens.ts                                   |   4 
styles/src/common.ts                                         |   1 
styles/src/component/icon_button.ts                          |   5 
styles/src/component/tab_bar_button.ts                       |  67 
styles/src/component/text_button.ts                          |   5 
styles/src/style_tree/app.ts                                 |   2 
styles/src/style_tree/assistant.ts                           |  69 
styles/src/style_tree/editor.ts                              |  36 
styles/src/style_tree/feedback.ts                            |   2 
styles/src/style_tree/picker.ts                              |   2 
styles/src/style_tree/project_panel.ts                       |  16 
styles/src/style_tree/status_bar.ts                          |  12 
styles/src/style_tree/titlebar.ts                            |   4 
styles/src/theme/create_theme.ts                             |  65 
styles/src/theme/syntax.ts                                   | 389 ++
styles/src/theme/theme_config.ts                             |   6 
styles/src/theme/tokens/theme.ts                             |  10 
styles/src/themes/atelier/common.ts                          |   9 
styles/src/themes/ayu/common.ts                              |   6 
styles/src/themes/gruvbox/gruvbox-common.ts                  |   6 
styles/src/themes/one/one-dark.ts                            |   4 
styles/src/themes/one/one-light.ts                           |   2 
styles/src/themes/rose-pine/common.ts                        |   4 
styles/src/types/extract_syntax_types.ts                     | 111 
styles/src/types/syntax.ts                                   | 202 -
styles/tsconfig.json                                         |   4 
199 files changed, 5,446 insertions(+), 3,339 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -141,7 +141,7 @@ source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c087230
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -187,9 +187,9 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
 
 [[package]]
 name = "alsa"
-version = "0.7.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44"
+checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47"
 dependencies = [
  "alsa-sys",
  "bitflags 1.3.2",
@@ -215,9 +215,9 @@ checksum = "ec8ad6edb4840b78c5c3d88de606b22252d552b55f3a4699fbb10fc070ec3049"
 
 [[package]]
 name = "android-activity"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40bc1575e653f158cbdc6ebcd917b9564e66321c5325c232c3591269c257be69"
+checksum = "64529721f27c2314ced0890ce45e469574a73e5e6fdd6e9da1860eb29285f5e0"
 dependencies = [
  "android-properties",
  "bitflags 1.3.2",
@@ -507,7 +507,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -555,7 +555,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -598,7 +598,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -856,7 +856,7 @@ dependencies = [
  "regex",
  "rustc-hash",
  "shlex",
- "syn 2.0.27",
+ "syn 2.0.28",
  "which",
 ]
 
@@ -1040,7 +1040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
 dependencies = [
  "memchr",
- "regex-automata 0.3.3",
+ "regex-automata 0.3.4",
  "serde",
 ]
 
@@ -1358,7 +1358,7 @@ dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -1425,7 +1425,7 @@ dependencies = [
  "sum_tree",
  "tempfile",
  "thiserror",
- "time 0.3.23",
+ "time 0.3.24",
  "tiny_http",
  "url",
  "util",
@@ -1527,7 +1527,7 @@ dependencies = [
  "sha-1 0.9.8",
  "sqlx",
  "theme",
- "time 0.3.23",
+ "time 0.3.24",
  "tokio",
  "tokio-tungstenite",
  "toml 0.5.11",
@@ -1649,6 +1649,21 @@ dependencies = [
  "theme",
 ]
 
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "copilot"
 version = "0.1.0"
@@ -2042,9 +2057,9 @@ dependencies = [
 
 [[package]]
 name = "curl-sys"
-version = "0.4.64+curl-8.2.0"
+version = "0.4.65+curl-8.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f96069f0b1cb1241c838740659a771ef143363f52772a9ce1bd9c04c75eee0dc"
+checksum = "961ba061c9ef2fe34bbd12b807152d96f0badd2bebe7b90ce6c8c8b7572a0986"
 dependencies = [
  "cc",
  "libc",
@@ -2124,6 +2139,28 @@ dependencies = [
  "byteorder",
 ]
 
+[[package]]
+name = "deranged"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
+dependencies = [
+ "convert_case 0.4.0",
+ "proc-macro2",
+ "quote",
+ "rustc_version 0.4.0",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "dhat"
 version = "0.3.2"
@@ -2305,6 +2342,7 @@ dependencies = [
  "clock",
  "collections",
  "context_menu",
+ "convert_case 0.6.0",
  "copilot",
  "ctor",
  "db",
@@ -2341,7 +2379,7 @@ dependencies = [
  "tree-sitter",
  "tree-sitter-html",
  "tree-sitter-rust",
- "tree-sitter-typescript 0.20.2 (git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259)",
+ "tree-sitter-typescript",
  "unindent",
  "util",
  "workspace",
@@ -2425,9 +2463,9 @@ dependencies = [
 
 [[package]]
 name = "errno"
-version = "0.3.1"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f"
 dependencies = [
  "errno-dragonfly",
  "libc",
@@ -2731,7 +2769,7 @@ dependencies = [
  "smol",
  "sum_tree",
  "tempfile",
- "time 0.3.23",
+ "time 0.3.24",
  "util",
 ]
 
@@ -2881,7 +2919,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -3033,9 +3071,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
 
 [[package]]
 name = "globset"
-version = "0.4.11"
+version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
+checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006"
 dependencies = [
  "aho-corasick 1.0.2",
  "bstr",
@@ -3087,6 +3125,7 @@ dependencies = [
  "core-graphics",
  "core-text",
  "ctor",
+ "derive_more",
  "dhat",
  "env_logger 0.9.3",
  "etagere",
@@ -3121,7 +3160,7 @@ dependencies = [
  "smol",
  "sqlez",
  "sum_tree",
- "time 0.3.23",
+ "time 0.3.24",
  "tiny-skia",
  "usvg",
  "util",
@@ -3133,6 +3172,7 @@ dependencies = [
 name = "gpui_macros"
 version = "0.1.0"
 dependencies = [
+ "gpui",
  "proc-macro2",
  "quote",
  "syn 1.0.109",
@@ -3851,7 +3891,7 @@ dependencies = [
  "text",
  "theme",
  "tree-sitter",
- "tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=a2861e88a730287a60c11ea9299c033c7d076e30)",
+ "tree-sitter-elixir",
  "tree-sitter-embedded-template",
  "tree-sitter-heex",
  "tree-sitter-html",
@@ -3860,7 +3900,7 @@ dependencies = [
  "tree-sitter-python",
  "tree-sitter-ruby",
  "tree-sitter-rust",
- "tree-sitter-typescript 0.20.2 (git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259)",
+ "tree-sitter-typescript",
  "unicase",
  "unindent",
  "util",
@@ -4034,9 +4074,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.3"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0"
+checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
 
 [[package]]
 name = "lipsum"
@@ -4744,7 +4784,7 @@ dependencies = [
  "proc-macro-crate 1.3.1",
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -4895,7 +4935,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -5097,7 +5137,7 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff"
 dependencies = [
- "rustc_version",
+ "rustc_version 0.3.3",
 ]
 
 [[package]]
@@ -5134,9 +5174,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
 
 [[package]]
 name = "pest"
-version = "2.7.1"
+version = "2.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d2d1d55045829d65aad9d389139882ad623b33b904e7c9f1b10c5b8927298e5"
+checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a"
 dependencies = [
  "thiserror",
  "ucd-trie",
@@ -5192,7 +5232,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -5230,7 +5270,7 @@ dependencies = [
  "line-wrap",
  "quick-xml",
  "serde",
- "time 0.3.23",
+ "time 0.3.24",
 ]
 
 [[package]]
@@ -5345,7 +5385,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62"
 dependencies = [
  "proc-macro2",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -5905,7 +5945,7 @@ checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
 dependencies = [
  "aho-corasick 1.0.2",
  "memchr",
- "regex-automata 0.3.3",
+ "regex-automata 0.3.4",
  "regex-syntax 0.7.4",
 ]
 
@@ -5920,9 +5960,9 @@ dependencies = [
 
 [[package]]
 name = "regex-automata"
-version = "0.3.3"
+version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
+checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294"
 dependencies = [
  "aho-corasick 1.0.2",
  "memchr",
@@ -6217,7 +6257,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rust-embed-utils",
- "syn 2.0.27",
+ "syn 2.0.28",
  "walkdir",
 ]
 
@@ -6234,13 +6274,12 @@ dependencies = [
 
 [[package]]
 name = "rust_decimal"
-version = "1.30.0"
+version = "1.31.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0446843641c69436765a35a5a77088e28c2e6a12da93e84aa3ab1cd4aa5a042"
+checksum = "4a2ab0025103a60ecaaf3abf24db1db240a4e1c15837090d2c32f625ac98abea"
 dependencies = [
  "arrayvec 0.7.4",
  "borsh",
- "bytecheck",
  "byteorder",
  "bytes 1.4.0",
  "num-traits",
@@ -6268,7 +6307,16 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
 dependencies = [
- "semver",
+ "semver 0.11.0",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver 1.0.18",
 ]
 
 [[package]]
@@ -6294,7 +6342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
 dependencies = [
  "bitflags 1.3.2",
- "errno 0.3.1",
+ "errno 0.3.2",
  "io-lifetimes 1.0.11",
  "libc",
  "linux-raw-sys 0.3.8",
@@ -6308,9 +6356,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
 dependencies = [
  "bitflags 2.3.3",
- "errno 0.3.1",
+ "errno 0.3.2",
  "libc",
- "linux-raw-sys 0.4.3",
+ "linux-raw-sys 0.4.5",
  "windows-sys",
 ]
 
@@ -6509,7 +6557,7 @@ dependencies = [
  "serde_json",
  "sqlx",
  "thiserror",
- "time 0.3.23",
+ "time 0.3.24",
  "tracing",
  "url",
  "uuid 1.4.1",
@@ -6537,7 +6585,7 @@ dependencies = [
  "rust_decimal",
  "sea-query-derive",
  "serde_json",
- "time 0.3.23",
+ "time 0.3.24",
  "uuid 1.4.1",
 ]
 
@@ -6552,7 +6600,7 @@ dependencies = [
  "sea-query",
  "serde_json",
  "sqlx",
- "time 0.3.23",
+ "time 0.3.24",
  "uuid 1.4.1",
 ]
 
@@ -6685,12 +6733,15 @@ dependencies = [
  "theme",
  "tiktoken-rs 0.5.0",
  "tree-sitter",
- "tree-sitter-cpp 0.20.2",
- "tree-sitter-elixir 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "tree-sitter-json 0.19.0",
+ "tree-sitter-cpp",
+ "tree-sitter-elixir",
+ "tree-sitter-json 0.20.0",
+ "tree-sitter-lua",
+ "tree-sitter-php",
+ "tree-sitter-ruby",
  "tree-sitter-rust",
- "tree-sitter-toml 0.20.0",
- "tree-sitter-typescript 0.20.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tree-sitter-toml",
+ "tree-sitter-typescript",
  "unindent",
  "util",
  "workspace",
@@ -6705,6 +6756,12 @@ dependencies = [
  "semver-parser",
 ]
 
+[[package]]
+name = "semver"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
+
 [[package]]
 name = "semver-parser"
 version = "0.10.2"
@@ -6722,22 +6779,22 @@ checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99"
 
 [[package]]
 name = "serde"
-version = "1.0.175"
+version = "1.0.180"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b"
+checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.175"
+version = "1.0.180"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4"
+checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -6762,9 +6819,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.103"
+version = "1.0.104"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
+checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c"
 dependencies = [
  "indexmap 2.0.0",
  "itoa 1.0.9",
@@ -6786,13 +6843,13 @@ dependencies = [
 
 [[package]]
 name = "serde_repr"
-version = "0.1.15"
+version = "0.1.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e168eaaf71e8f9bd6037feb05190485708e019f4fd87d161b3c0a0d37daf85e5"
+checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -7238,7 +7295,7 @@ dependencies = [
  "sqlx-rt",
  "stringprep",
  "thiserror",
- "time 0.3.23",
+ "time 0.3.24",
  "tokio-stream",
  "url",
  "uuid 1.4.1",
@@ -7487,9 +7544,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.27"
+version = "2.0.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0"
+checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -7557,9 +7614,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
 
 [[package]]
 name = "target-lexicon"
-version = "0.12.10"
+version = "0.12.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d2faeef5759ab89935255b1a4cd98e0baf99d1085e37d36599c625dac49ae8e"
+checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a"
 
 [[package]]
 name = "tempdir"
@@ -7742,7 +7799,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -7815,10 +7872,11 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.23"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
+checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b"
 dependencies = [
+ "deranged",
  "itoa 1.0.9",
  "serde",
  "time-core",
@@ -7833,9 +7891,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
 
 [[package]]
 name = "time-macros"
-version = "0.2.10"
+version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
+checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd"
 dependencies = [
  "time-core",
 ]
@@ -7931,7 +7989,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -8153,7 +8211,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -8255,16 +8313,6 @@ dependencies = [
  "tree-sitter",
 ]
 
-[[package]]
-name = "tree-sitter-cpp"
-version = "0.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c88fd925d0333e63ac64e521f5bd79c53019e569ffbbccfeef346a326f459e9"
-dependencies = [
- "cc",
- "tree-sitter",
-]
-
 [[package]]
 name = "tree-sitter-css"
 version = "0.19.0"
@@ -8274,16 +8322,6 @@ dependencies = [
  "tree-sitter",
 ]
 
-[[package]]
-name = "tree-sitter-elixir"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a9916f3e1c80b3c8aab8582604e97e8720cb9b893489b347cf999f80f9d469e"
-dependencies = [
- "cc",
- "tree-sitter",
-]
-
 [[package]]
 name = "tree-sitter-elixir"
 version = "0.1.0"
@@ -8471,26 +8509,6 @@ dependencies = [
  "tree-sitter",
 ]
 
-[[package]]
-name = "tree-sitter-toml"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca517f578a98b23d20780247cc2688407fa81effad5b627a5a364ec3339b53e8"
-dependencies = [
- "cc",
- "tree-sitter",
-]
-
-[[package]]
-name = "tree-sitter-typescript"
-version = "0.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "079c695c32d39ad089101c66393aeaca30e967fba3486a91f573d2f0e12d290a"
-dependencies = [
- "cc",
- "tree-sitter",
-]
-
 [[package]]
 name = "tree-sitter-typescript"
 version = "0.20.2"
@@ -8993,7 +9011,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
  "wasm-bindgen-shared",
 ]
 
@@ -9027,7 +9045,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -9040,9 +9058,9 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
 
 [[package]]
 name = "wasm-encoder"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06a3d1b4a575ffb873679402b2aedb3117555eb65c27b1b86c8a91e574bc2a2a"
+checksum = "41763f20eafed1399fff1afb466496d3a959f58241436cfdc17e3f5ca954de16"
 dependencies = [
  "leb128",
 ]
@@ -9264,9 +9282,9 @@ dependencies = [
 
 [[package]]
 name = "wast"
-version = "62.0.0"
+version = "62.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7f7ee878019d69436895f019b65f62c33da63595d8e857cbdc87c13ecb29a32"
+checksum = "b8ae06f09dbe377b889fbd620ff8fa21e1d49d1d9d364983c0cdbf9870cb9f1f"
 dependencies = [
  "leb128",
  "memchr",
@@ -9276,11 +9294,11 @@ dependencies = [
 
 [[package]]
 name = "wat"
-version = "1.0.68"
+version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "295572bf24aa5b685a971a83ad3e8b6e684aaad8a9be24bc7bf59bed84cc1c08"
+checksum = "842e15861d203fb4a96d314b0751cdeaf0f6f8b35e8d81d2953af2af5e44e637"
 dependencies = [
- "wast 62.0.0",
+ "wast 62.0.1",
 ]
 
 [[package]]
@@ -9657,9 +9675,9 @@ dependencies = [
 
 [[package]]
 name = "winnow"
-version = "0.5.1"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25b5872fa2e10bd067ae946f927e726d7d603eaeb6e02fa6a350e0722d2b8c11"
+checksum = "8bd122eb777186e60c3fdf765a58ac76e41c582f1f535fbf3314434c6b58f3f7"
 dependencies = [
  "memchr",
 ]
@@ -9843,7 +9861,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.98.0"
+version = "0.100.0"
 dependencies = [
  "activity_indicator",
  "ai",
@@ -9930,9 +9948,9 @@ dependencies = [
  "tree-sitter",
  "tree-sitter-bash",
  "tree-sitter-c",
- "tree-sitter-cpp 0.20.0",
+ "tree-sitter-cpp",
  "tree-sitter-css",
- "tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=a2861e88a730287a60c11ea9299c033c7d076e30)",
+ "tree-sitter-elixir",
  "tree-sitter-elm",
  "tree-sitter-embedded-template",
  "tree-sitter-glsl",
@@ -9950,8 +9968,8 @@ dependencies = [
  "tree-sitter-rust",
  "tree-sitter-scheme",
  "tree-sitter-svelte",
- "tree-sitter-toml 0.5.1",
- "tree-sitter-typescript 0.20.2 (git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259)",
+ "tree-sitter-toml",
+ "tree-sitter-typescript",
  "tree-sitter-yaml",
  "unindent",
  "url",
@@ -9988,7 +10006,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.27",
+ "syn 2.0.28",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -79,6 +79,7 @@ resolver = "2"
 anyhow = { version = "1.0.57" }
 async-trait = { version = "0.1" }
 ctor = { version = "0.1" }
+derive_more = { version = "0.99.17" }
 env_logger = { version = "0.9" }
 futures = { version = "0.3" }
 globset = { version = "0.4" }

Dockerfile 🔗

@@ -1,6 +1,6 @@
 # syntax = docker/dockerfile:1.2
 
-FROM rust:1.70-bullseye as builder
+FROM rust:1.71-bullseye as builder
 WORKDIR app
 COPY . .
 

assets/fonts/plex/LICENSE.txt 🔗

@@ -0,0 +1,93 @@
+Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"

+

+This Font Software is licensed under the SIL Open Font License, Version 1.1.

+

+This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL

+

+

+-----------------------------------------------------------

+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007

+-----------------------------------------------------------

+

+PREAMBLE

+The goals of the Open Font License (OFL) are to stimulate worldwide

+development of collaborative font projects, to support the font creation

+efforts of academic and linguistic communities, and to provide a free and

+open framework in which fonts may be shared and improved in partnership

+with others.

+

+The OFL allows the licensed fonts to be used, studied, modified and

+redistributed freely as long as they are not sold by themselves. The

+fonts, including any derivative works, can be bundled, embedded, 

+redistributed and/or sold with any software provided that any reserved

+names are not used by derivative works. The fonts and derivatives,

+however, cannot be released under any other type of license. The

+requirement for fonts to remain under this license does not apply

+to any document created using the fonts or their derivatives.

+

+DEFINITIONS

+"Font Software" refers to the set of files released by the Copyright

+Holder(s) under this license and clearly marked as such. This may

+include source files, build scripts and documentation.

+

+"Reserved Font Name" refers to any names specified as such after the

+copyright statement(s).

+

+"Original Version" refers to the collection of Font Software components as

+distributed by the Copyright Holder(s).

+

+"Modified Version" refers to any derivative made by adding to, deleting,

+or substituting -- in part or in whole -- any of the components of the

+Original Version, by changing formats or by porting the Font Software to a

+new environment.

+

+"Author" refers to any designer, engineer, programmer, technical

+writer or other person who contributed to the Font Software.

+

+PERMISSION & CONDITIONS

+Permission is hereby granted, free of charge, to any person obtaining

+a copy of the Font Software, to use, study, copy, merge, embed, modify,

+redistribute, and sell modified and unmodified copies of the Font

+Software, subject to the following conditions:

+

+1) Neither the Font Software nor any of its individual components,

+in Original or Modified Versions, may be sold by itself.

+

+2) Original or Modified Versions of the Font Software may be bundled,

+redistributed and/or sold with any software, provided that each copy

+contains the above copyright notice and this license. These can be

+included either as stand-alone text files, human-readable headers or

+in the appropriate machine-readable metadata fields within text or

+binary files as long as those fields can be easily viewed by the user.

+

+3) No Modified Version of the Font Software may use the Reserved Font

+Name(s) unless explicit written permission is granted by the corresponding

+Copyright Holder. This restriction only applies to the primary font name as

+presented to the users.

+

+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font

+Software shall not be used to promote, endorse or advertise any

+Modified Version, except to acknowledge the contribution(s) of the

+Copyright Holder(s) and the Author(s) or with their explicit written

+permission.

+

+5) The Font Software, modified or unmodified, in part or in whole,

+must be distributed entirely under this license, and must not be

+distributed under any other license. The requirement for fonts to

+remain under this license does not apply to any document created

+using the Font Software.

+

+TERMINATION

+This license becomes null and void if any of the above conditions are

+not met.

+

+DISCLAIMER

+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,

+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF

+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT

+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE

+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,

+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL

+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING

+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM

+OTHER DEALINGS IN THE FONT SOFTWARE.

assets/icons/file_icons/file_types.json 🔗

@@ -1,159 +1,179 @@
 {
-  "suffixes": {
-    "aac": "audio",
-    "bash": "terminal",
-    "bmp": "image",
-    "c": "code",
-    "conf": "settings",
-    "cpp": "code",
-    "cc": "code",
-    "css": "code",
-    "doc": "document",
-    "docx": "document",
-    "eslintrc": "eslint",
-    "eslintrc.js": "eslint",
-    "eslintrc.json": "eslint",
-    "flac": "audio",
-    "fish": "terminal",
-    "gitattributes": "vcs",
-    "gitignore": "vcs",
-    "gitmodules": "vcs",
-    "gif": "image",
-    "go": "code",
-    "h": "code",
-    "handlebars": "code",
-    "hbs": "template",
-    "htm": "template",
-    "html": "template",
-    "svelte": "template",
-    "hpp": "code",
-    "ico": "image",
-    "ini": "settings",
-    "java": "code",
-    "jpeg": "image",
-    "jpg": "image",
-    "js": "code",
-    "json": "storage",
-    "lock": "lock",
-    "log": "log",
-    "md": "document",
-    "mdx": "document",
-    "mp3": "audio",
-    "mp4": "video",
-    "ods": "document",
-    "odp": "document",
-    "odt": "document",
-    "ogg": "video",
-    "pdf": "document",
-    "php": "code",
-    "png": "image",
-    "ppt": "document",
-    "pptx": "document",
-    "prettierrc": "prettier",
-    "prettierignore": "prettier",
-    "ps1": "terminal",
-    "psd": "image",
-    "py": "code",
-    "rb": "code",
-    "rkt": "code",
-    "rs": "rust",
-    "rtf": "document",
-    "scm": "code",
-    "sh": "terminal",
-    "bashrc": "terminal",
-    "bash_profile": "terminal",
-    "bash_aliases": "terminal",
-    "bash_logout": "terminal",
-    "profile": "terminal",
-    "zshrc": "terminal",
-    "zshenv": "terminal",
-    "zsh_profile": "terminal",
-    "zsh_aliases": "terminal",
-    "zsh_histfile": "terminal",
-    "zlogin": "terminal",
-    "sql": "code",
-    "svg": "image",
-    "swift": "code",
-    "tiff": "image",
-    "toml": "toml",
-    "ts": "typescript",
-    "tsx": "code",
-    "txt": "document",
-    "wav": "audio",
-    "webm": "video",
-    "xls": "document",
-    "xlsx": "document",
-    "xml": "template",
-    "yaml": "settings",
-    "yml": "settings",
-    "zsh": "terminal"
-  },
-  "types": {
-    "audio": {
-      "icon": "icons/file_icons/audio.svg"
-    },
-    "code": {
-      "icon": "icons/file_icons/code.svg"
-    },
-    "collapsed_chevron": {
-      "icon": "icons/file_icons/chevron_right.svg"
-    },
-    "collapsed_folder": {
-      "icon": "icons/file_icons/folder.svg"
-    },
-    "default": {
-      "icon": "icons/file_icons/file.svg"
-    },
-    "document": {
-      "icon": "icons/file_icons/book.svg"
-    },
-    "eslint": {
-      "icon": "icons/file_icons/eslint.svg"
-    },
-    "expanded_chevron": {
-      "icon": "icons/file_icons/chevron_down.svg"
-    },
-    "expanded_folder": {
-      "icon": "icons/file_icons/folder_open.svg"
-    },
-    "image": {
-      "icon": "icons/file_icons/image.svg"
-    },
-    "lock": {
-      "icon": "icons/file_icons/lock.svg"
-    },
-    "log": {
-      "icon": "icons/file_icons/info.svg"
-    },
-    "prettier": {
-      "icon": "icons/file_icons/prettier.svg"
-    },
-    "rust": {
-      "icon": "icons/file_icons/rust.svg"
-    },
-    "settings": {
-      "icon": "icons/file_icons/settings.svg"
-    },
-    "storage": {
-      "icon": "icons/file_icons/database.svg"
-    },
-    "template": {
-      "icon": "icons/file_icons/html.svg"
-    },
-    "terminal": {
-      "icon": "icons/file_icons/terminal.svg"
-    },
-    "toml": {
-      "icon": "icons/file_icons/toml.svg"
-    },
-    "typescript": {
-      "icon": "icons/file_icons/typescript.svg"
-    },
-    "vcs": {
-      "icon": "icons/file_icons/git.svg"
-    },
-    "video": {
-      "icon": "icons/file_icons/video.svg"
+    "suffixes": {
+        "aac": "audio",
+        "accdb": "storage",
+        "bak": "backup",
+        "bash": "terminal",
+        "bash_aliases": "terminal",
+        "bash_logout": "terminal",
+        "bash_profile": "terminal",
+        "bashrc": "terminal",
+        "bmp": "image",
+        "c": "code",
+        "cc": "code",
+        "conf": "settings",
+        "cpp": "code",
+        "css": "code",
+        "csv": "storage",
+        "dat": "storage",
+        "db": "storage",
+        "dbf": "storage",
+        "dll": "storage",
+        "doc": "document",
+        "docx": "document",
+        "eslintrc": "eslint",
+        "eslintrc.js": "eslint",
+        "eslintrc.json": "eslint",
+        "fmp": "storage",
+        "fp7": "storage",
+        "flac": "audio",
+        "fish": "terminal",
+        "frm": "storage",
+        "gdb": "storage",
+        "gitattributes": "vcs",
+        "gitignore": "vcs",
+        "gitmodules": "vcs",
+        "gif": "image",
+        "go": "code",
+        "h": "code",
+        "handlebars": "code",
+        "hbs": "template",
+        "htm": "template",
+        "html": "template",
+        "ib": "storage",
+        "ico": "image",
+        "ini": "settings",
+        "java": "code",
+        "jpeg": "image",
+        "jpg": "image",
+        "js": "code",
+        "json": "storage",
+        "ldf": "storage",
+        "lock": "lock",
+        "log": "log",
+        "mdb": "storage",
+        "md": "document",
+        "mdf": "storage",
+        "mdx": "document",
+        "mp3": "audio",
+        "mp4": "video",
+        "myd": "storage",
+        "myi": "storage",
+        "ods": "document",
+        "odp": "document",
+        "odt": "document",
+        "ogg": "video",
+        "pdb": "storage",
+        "pdf": "document",
+        "php": "code",
+        "png": "image",
+        "ppt": "document",
+        "pptx": "document",
+        "prettierignore": "prettier",
+        "prettierrc": "prettier",
+        "profile": "terminal",
+        "ps1": "terminal",
+        "psd": "image",
+        "py": "code",
+        "rb": "code",
+        "rkt": "code",
+        "rs": "rust",
+        "rtf": "document",
+        "sav": "storage",
+        "scm": "code",
+        "sh": "terminal",
+        "sqlite": "storage",
+        "sdf": "storage",
+        "svelte": "template",
+        "svg": "image",
+        "swift": "code",
+        "ts": "typescript",
+        "tsx": "code",
+        "tiff": "image",
+        "toml": "toml",
+        "tsv": "storage",
+        "txt": "document",
+        "wav": "audio",
+        "webm": "video",
+        "xls": "document",
+        "xlsx": "document",
+        "xml": "template",
+        "yaml": "settings",
+        "yml": "settings",
+        "zlogin": "terminal",
+        "zsh": "terminal",
+        "zsh_aliases": "terminal",
+        "zshenv": "terminal",
+        "zsh_histfile": "terminal",
+        "zsh_profile": "terminal",
+        "zshrc": "terminal"
+    },
+    "types": {
+        "audio": {
+            "icon": "icons/file_icons/audio.svg"
+        },
+        "code": {
+            "icon": "icons/file_icons/code.svg"
+        },
+        "collapsed_chevron": {
+            "icon": "icons/file_icons/chevron_right.svg"
+        },
+        "collapsed_folder": {
+            "icon": "icons/file_icons/folder.svg"
+        },
+        "default": {
+            "icon": "icons/file_icons/file.svg"
+        },
+        "document": {
+            "icon": "icons/file_icons/book.svg"
+        },
+        "eslint": {
+            "icon": "icons/file_icons/eslint.svg"
+        },
+        "expanded_chevron": {
+            "icon": "icons/file_icons/chevron_down.svg"
+        },
+        "expanded_folder": {
+            "icon": "icons/file_icons/folder_open.svg"
+        },
+        "image": {
+            "icon": "icons/file_icons/image.svg"
+        },
+        "lock": {
+            "icon": "icons/file_icons/lock.svg"
+        },
+        "log": {
+            "icon": "icons/file_icons/info.svg"
+        },
+        "prettier": {
+            "icon": "icons/file_icons/prettier.svg"
+        },
+        "rust": {
+            "icon": "icons/file_icons/rust.svg"
+        },
+        "settings": {
+            "icon": "icons/file_icons/settings.svg"
+        },
+        "storage": {
+            "icon": "icons/file_icons/database.svg"
+        },
+        "template": {
+            "icon": "icons/file_icons/html.svg"
+        },
+        "terminal": {
+            "icon": "icons/file_icons/terminal.svg"
+        },
+        "toml": {
+            "icon": "icons/file_icons/toml.svg"
+        },
+        "typescript": {
+            "icon": "icons/file_icons/typescript.svg"
+        },
+        "vcs": {
+            "icon": "icons/file_icons/git.svg"
+        },
+        "video": {
+            "icon": "icons/file_icons/video.svg"
+        }
     }
-  }
 }

assets/keymaps/default.json 🔗

@@ -227,12 +227,26 @@
       "alt-enter": "search::SelectAllMatches"
     }
   },
+  {
+    "context": "BufferSearchBar > Editor",
+    "bindings": {
+      "up": "search::PreviousHistoryQuery",
+      "down": "search::NextHistoryQuery"
+    }
+  },
   {
     "context": "ProjectSearchBar",
     "bindings": {
       "escape": "project_search::ToggleFocus"
     }
   },
+  {
+    "context": "ProjectSearchBar > Editor",
+    "bindings": {
+      "up": "search::PreviousHistoryQuery",
+      "down": "search::NextHistoryQuery"
+    }
+  },
   {
     "context": "ProjectSearchView",
     "bindings": {

crates/ai/src/assistant.rs 🔗

@@ -362,7 +362,7 @@ impl AssistantPanel {
                 this.set_active_editor_index(this.prev_active_editor_index, cx);
             }
         })
-        .with_tooltip::<History>(1, "History".into(), None, tooltip_style, cx)
+        .with_tooltip::<History>(1, "History", None, tooltip_style, cx)
     }
 
     fn render_editor_tools(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement<Self>> {
@@ -394,7 +394,7 @@ impl AssistantPanel {
         })
         .with_tooltip::<Split>(
             1,
-            "Split Message".into(),
+            "Split Message",
             Some(Box::new(Split)),
             tooltip_style,
             cx,
@@ -416,13 +416,7 @@ impl AssistantPanel {
                 active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
             }
         })
-        .with_tooltip::<Assist>(
-            1,
-            "Assist".into(),
-            Some(Box::new(Assist)),
-            tooltip_style,
-            cx,
-        )
+        .with_tooltip::<Assist>(1, "Assist", Some(Box::new(Assist)), tooltip_style, cx)
     }
 
     fn render_quote_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
@@ -446,7 +440,7 @@ impl AssistantPanel {
         })
         .with_tooltip::<QuoteSelection>(
             1,
-            "Quote Selection".into(),
+            "Quote Selection",
             Some(Box::new(QuoteSelection)),
             tooltip_style,
             cx,
@@ -468,7 +462,7 @@ impl AssistantPanel {
         })
         .with_tooltip::<NewConversation>(
             1,
-            "New Conversation".into(),
+            "New Conversation",
             Some(Box::new(NewConversation)),
             tooltip_style,
             cx,
@@ -498,11 +492,7 @@ impl AssistantPanel {
         })
         .with_tooltip::<ToggleZoom>(
             0,
-            if self.zoomed {
-                "Zoom Out".into()
-            } else {
-                "Zoom In".into()
-            },
+            if self.zoomed { "Zoom Out" } else { "Zoom In" },
             Some(Box::new(ToggleZoom)),
             tooltip_style,
             cx,
@@ -1637,6 +1627,7 @@ impl ConversationEditor {
             let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx);
             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
             editor.set_show_gutter(false, cx);
+            editor.set_show_wrap_guides(false, cx);
             editor
         });
 

crates/collab/src/tests.rs 🔗

@@ -12,10 +12,7 @@ use client::{
 use collections::{HashMap, HashSet};
 use fs::FakeFs;
 use futures::{channel::oneshot, StreamExt as _};
-use gpui::{
-    elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, TestAppContext, View,
-    ViewContext, ViewHandle, WeakViewHandle,
-};
+use gpui::{executor::Deterministic, ModelHandle, TestAppContext, WindowHandle};
 use language::LanguageRegistry;
 use parking_lot::Mutex;
 use project::{Project, WorktreeId};
@@ -466,42 +463,8 @@ impl TestClient {
         &self,
         project: &ModelHandle<Project>,
         cx: &mut TestAppContext,
-    ) -> ViewHandle<Workspace> {
-        struct WorkspaceContainer {
-            workspace: Option<WeakViewHandle<Workspace>>,
-        }
-
-        impl Entity for WorkspaceContainer {
-            type Event = ();
-        }
-
-        impl View for WorkspaceContainer {
-            fn ui_name() -> &'static str {
-                "WorkspaceContainer"
-            }
-
-            fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-                if let Some(workspace) = self
-                    .workspace
-                    .as_ref()
-                    .and_then(|workspace| workspace.upgrade(cx))
-                {
-                    ChildView::new(&workspace, cx).into_any()
-                } else {
-                    Empty::new().into_any()
-                }
-            }
-        }
-
-        // We use a workspace container so that we don't need to remove the window in order to
-        // drop the workspace and we can use a ViewHandle instead.
-        let (window_id, container) = cx.add_window(|_| WorkspaceContainer { workspace: None });
-        let workspace = cx.add_view(window_id, |cx| Workspace::test_new(project.clone(), cx));
-        container.update(cx, |container, cx| {
-            container.workspace = Some(workspace.downgrade());
-            cx.notify();
-        });
-        workspace
+    ) -> WindowHandle<Workspace> {
+        cx.add_window(|cx| Workspace::test_new(project.clone(), cx))
     }
 }
 

crates/collab/src/tests/integration_tests.rs 🔗

@@ -7,8 +7,7 @@ use client::{User, RECEIVE_TIMEOUT};
 use collections::HashSet;
 use editor::{
     test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
-    ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions,
-    Undo,
+    ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo,
 };
 use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions};
 use futures::StreamExt as _;
@@ -1208,7 +1207,7 @@ async fn test_share_project(
     cx_c: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
-    let (window_b, _) = cx_b.add_window(|_| EmptyView);
+    let window_b = cx_b.add_window(|_| EmptyView);
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
@@ -1316,7 +1315,7 @@ async fn test_share_project(
         .await
         .unwrap();
 
-    let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
+    let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
 
     // Client A sees client B's selection
     deterministic.run_until_parked();
@@ -1499,8 +1498,8 @@ async fn test_host_disconnect(
     deterministic.run_until_parked();
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
 
-    let (window_id_b, workspace_b) =
-        cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let workspace_b = window_b.root(cx_b);
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "b.txt"), None, true, cx)
@@ -1509,11 +1508,9 @@ async fn test_host_disconnect(
         .unwrap()
         .downcast::<Editor>()
         .unwrap();
-    assert!(cx_b
-        .read_window(window_id_b, |cx| editor_b.is_focused(cx))
-        .unwrap());
+    assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx)));
     editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
-    assert!(cx_b.is_window_edited(workspace_b.window_id()));
+    assert!(window_b.is_edited(cx_b));
 
     // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
     server.forbid_connections();
@@ -1525,10 +1522,10 @@ async fn test_host_disconnect(
     assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
 
     // Ensure client B's edited state is reset and that the whole window is blurred.
-    cx_b.read_window(window_id_b, |cx| {
+    window_b.read_with(cx_b, |cx| {
         assert_eq!(cx.focused_view_id(), None);
     });
-    assert!(!cx_b.is_window_edited(workspace_b.window_id()));
+    assert!(!window_b.is_edited(cx_b));
 
     // Ensure client B is not prompted to save edits when closing window after disconnecting.
     let can_close = workspace_b
@@ -3445,13 +3442,11 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
         .await
         .unwrap();
-    let (window_a, _) = cx_a.add_window(|_| EmptyView);
-    let editor_a = cx_a.add_view(window_a, |cx| {
-        Editor::for_buffer(buffer_a, Some(project_a), cx)
-    });
+    let window_a = cx_a.add_window(|_| EmptyView);
+    let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
     let mut editor_cx_a = EditorTestContext {
         cx: cx_a,
-        window_id: window_a,
+        window: window_a.into(),
         editor: editor_a,
     };
 
@@ -3460,13 +3455,11 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
         .await
         .unwrap();
-    let (window_b, _) = cx_b.add_window(|_| EmptyView);
-    let editor_b = cx_b.add_view(window_b, |cx| {
-        Editor::for_buffer(buffer_b, Some(project_b), cx)
-    });
+    let window_b = cx_b.add_window(|_| EmptyView);
+    let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
     let mut editor_cx_b = EditorTestContext {
         cx: cx_b,
-        window_id: window_b,
+        window: window_b.into(),
         editor: editor_b,
     };
 
@@ -4205,8 +4198,8 @@ async fn test_collaborating_with_completion(
         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
         .await
         .unwrap();
-    let (window_b, _) = cx_b.add_window(|_| EmptyView);
-    let editor_b = cx_b.add_view(window_b, |cx| {
+    let window_b = cx_b.add_window(|_| EmptyView);
+    let editor_b = window_b.add_view(cx_b, |cx| {
         Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
     });
 
@@ -5316,7 +5309,8 @@ async fn test_collaborating_with_code_actions(
 
     // Join the project as client B.
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let workspace_b = window_b.root(cx_b);
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -5540,7 +5534,8 @@ async fn test_collaborating_with_renames(
         .unwrap();
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
 
-    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
+    let workspace_b = window_b.root(cx_b);
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "one.rs"), None, true, cx)
@@ -5571,6 +5566,7 @@ async fn test_collaborating_with_renames(
         .unwrap();
     prepare_rename.await.unwrap();
     editor_b.update(cx_b, |editor, cx| {
+        use editor::ToOffset;
         let rename = editor.pending_rename().unwrap();
         let buffer = editor.buffer().read(cx).snapshot(cx);
         assert_eq!(
@@ -6445,8 +6441,10 @@ async fn test_basic_following(
         .await
         .unwrap();
 
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let window_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = window_a.root(cx_a);
+    let window_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = window_b.root(cx_b);
 
     // Client A opens some editors.
     let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
@@ -6529,7 +6527,8 @@ async fn test_basic_following(
     cx_c.foreground().run_until_parked();
     let active_call_c = cx_c.read(ActiveCall::global);
     let project_c = client_c.build_remote_project(project_id, cx_c).await;
-    let workspace_c = client_c.build_workspace(&project_c, cx_c);
+    let window_c = client_c.build_workspace(&project_c, cx_c);
+    let workspace_c = window_c.root(cx_c);
     active_call_c
         .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
         .await
@@ -6547,7 +6546,7 @@ async fn test_basic_following(
     cx_d.foreground().run_until_parked();
     let active_call_d = cx_d.read(ActiveCall::global);
     let project_d = client_d.build_remote_project(project_id, cx_d).await;
-    let workspace_d = client_d.build_workspace(&project_d, cx_d);
+    let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
     active_call_d
         .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
         .await
@@ -6645,6 +6644,7 @@ async fn test_basic_following(
     }
 
     // Client C closes the project.
+    window_c.remove(cx_c);
     cx_c.drop_last(workspace_c);
 
     // Clients A and B see that client B is following A, and client C is not present in the followers.
@@ -6874,9 +6874,7 @@ async fn test_basic_following(
     });
 
     // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
-    let panel = cx_b.add_view(workspace_b.window_id(), |_| {
-        TestPanel::new(DockPosition::Left)
-    });
+    let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
     workspace_b.update(cx_b, |workspace, cx| {
         workspace.add_panel(panel, cx);
         workspace.toggle_panel_focus::<TestPanel>(cx);
@@ -6904,7 +6902,7 @@ async fn test_basic_following(
 
     // Client B activates an item that doesn't implement following,
     // so the previously-opened screen-sharing item gets activated.
-    let unfollowable_item = cx_b.add_view(workspace_b.window_id(), |_| TestItem::new());
+    let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
     workspace_b.update(cx_b, |workspace, cx| {
         workspace.active_pane().update(cx, |pane, cx| {
             pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
@@ -7066,10 +7064,10 @@ async fn test_following_tab_order(
         .await
         .unwrap();
 
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
     let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
 
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
     let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
 
     let client_b_id = project_a.read_with(cx_a, |project, _| {
@@ -7192,7 +7190,7 @@ async fn test_peers_following_each_other(
         .unwrap();
 
     // Client A opens some editors.
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
     let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
     let _editor_a1 = workspace_a
         .update(cx_a, |workspace, cx| {
@@ -7204,7 +7202,7 @@ async fn test_peers_following_each_other(
         .unwrap();
 
     // Client B opens an editor.
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
     let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
     let _editor_b1 = workspace_b
         .update(cx_b, |workspace, cx| {
@@ -7363,7 +7361,7 @@ async fn test_auto_unfollowing(
         .unwrap();
 
     // Client A opens some editors.
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
     let _editor_a1 = workspace_a
         .update(cx_a, |workspace, cx| {
             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -7374,7 +7372,7 @@ async fn test_auto_unfollowing(
         .unwrap();
 
     // Client B starts following client A.
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
     let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
     let leader_id = project_b.read_with(cx_b, |project, _| {
         project.collaborators().values().next().unwrap().peer_id
@@ -7502,14 +7500,14 @@ async fn test_peers_simultaneously_following_each_other(
 
     client_a.fs.insert_tree("/a", json!({})).await;
     let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
     let project_id = active_call_a
         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
         .await
         .unwrap();
 
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
 
     deterministic.run_until_parked();
     let client_a_id = project_b.read_with(cx_b, |project, _| {
@@ -7601,8 +7599,8 @@ async fn test_on_input_format_from_host_to_guest(
         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
         .await
         .unwrap();
-    let (window_a, _) = cx_a.add_window(|_| EmptyView);
-    let editor_a = cx_a.add_view(window_a, |cx| {
+    let window_a = cx_a.add_window(|_| EmptyView);
+    let editor_a = window_a.add_view(cx_a, |cx| {
         Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)
     });
 
@@ -7730,8 +7728,8 @@ async fn test_on_input_format_from_guest_to_host(
         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
         .await
         .unwrap();
-    let (window_b, _) = cx_b.add_window(|_| EmptyView);
-    let editor_b = cx_b.add_view(window_b, |cx| {
+    let window_b = cx_b.add_window(|_| EmptyView);
+    let editor_b = window_b.add_view(cx_b, |cx| {
         Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
     });
 
@@ -7891,7 +7889,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .await
         .unwrap();
 
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
     cx_a.foreground().start_waiting();
 
     let _buffer_a = project_a
@@ -7955,11 +7953,12 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         );
         let inlay_cache = editor.inlay_hint_cache();
         assert_eq!(
-            inlay_cache.version, edits_made,
+            inlay_cache.version(),
+            edits_made,
             "Host editor update the cache version after every cache/view change",
         );
     });
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -7978,7 +7977,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         );
         let inlay_cache = editor.inlay_hint_cache();
         assert_eq!(
-            inlay_cache.version, edits_made,
+            inlay_cache.version(),
+            edits_made,
             "Guest editor update the cache version after every cache/view change"
         );
     });
@@ -7998,7 +7998,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
             "Host should get hints from the 1st edit and 1st LSP query"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(inlay_cache.version, edits_made);
+        assert_eq!(inlay_cache.version(), edits_made);
     });
     editor_b.update(cx_b, |editor, _| {
         assert_eq!(
@@ -8012,7 +8012,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
             "Guest should get hints the 1st edit and 2nd LSP query"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(inlay_cache.version, edits_made);
+        assert_eq!(inlay_cache.version(), edits_made);
     });
 
     editor_a.update(cx_a, |editor, cx| {
@@ -8037,7 +8037,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
 4th query was made by guest (but not applied) due to cache invalidation logic"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(inlay_cache.version, edits_made);
+        assert_eq!(inlay_cache.version(), edits_made);
     });
     editor_b.update(cx_b, |editor, _| {
         assert_eq!(
@@ -8053,7 +8053,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
             "Guest should get hints from 3rd edit, 6th LSP query"
         );
         let inlay_cache = editor.inlay_hint_cache();
-        assert_eq!(inlay_cache.version, edits_made);
+        assert_eq!(inlay_cache.version(), edits_made);
     });
 
     fake_language_server
@@ -8079,7 +8079,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         );
         let inlay_cache = editor.inlay_hint_cache();
         assert_eq!(
-            inlay_cache.version, edits_made,
+            inlay_cache.version(),
+            edits_made,
             "Host should accepted all edits and bump its cache version every time"
         );
     });
@@ -8100,7 +8101,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         );
         let inlay_cache = editor.inlay_hint_cache();
         assert_eq!(
-            inlay_cache.version,
+            inlay_cache.version(),
             edits_made,
             "Guest should accepted all edits and bump its cache version every time"
         );
@@ -8198,8 +8199,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
         .await
         .unwrap();
 
-    let workspace_a = client_a.build_workspace(&project_a, cx_a);
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
     cx_a.foreground().start_waiting();
     cx_b.foreground().start_waiting();
 
@@ -8266,7 +8267,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
         );
         let inlay_cache = editor.inlay_hint_cache();
         assert_eq!(
-            inlay_cache.version, 0,
+            inlay_cache.version(),
+            0,
             "Host should not increment its cache version due to no changes",
         );
     });
@@ -8281,7 +8283,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
         );
         let inlay_cache = editor.inlay_hint_cache();
         assert_eq!(
-            inlay_cache.version, edits_made,
+            inlay_cache.version(),
+            edits_made,
             "Guest editor update the cache version after every cache/view change"
         );
     });
@@ -8298,7 +8301,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
         );
         let inlay_cache = editor.inlay_hint_cache();
         assert_eq!(
-            inlay_cache.version, 0,
+            inlay_cache.version(),
+            0,
             "Host should not increment its cache version due to no changes",
         );
     });
@@ -8313,7 +8317,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
         );
         let inlay_cache = editor.inlay_hint_cache();
         assert_eq!(
-            inlay_cache.version, edits_made,
+            inlay_cache.version(),
+            edits_made,
             "Guest should accepted all edits and bump its cache version every time"
         );
     });
@@ -8345,13 +8350,10 @@ fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomP
 
 fn extract_hint_labels(editor: &Editor) -> Vec<String> {
     let mut labels = Vec::new();
-    for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
-        let excerpt_hints = excerpt_hints.read();
-        for (_, inlay) in excerpt_hints.hints.iter() {
-            match &inlay.label {
-                project::InlayHintLabel::String(s) => labels.push(s.to_string()),
-                _ => unreachable!(),
-            }
+    for hint in editor.inlay_hint_cache().hints() {
+        match hint.label {
+            project::InlayHintLabel::String(s) => labels.push(s),
+            _ => unreachable!(),
         }
     }
     labels

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -15,8 +15,8 @@ use gpui::{
     geometry::{rect::RectF, vector::vec2f, PathBuilder},
     json::{self, ToJson},
     platform::{CursorStyle, MouseButton},
-    AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    AppContext, Entity, ImageData, LayoutContext, ModelHandle, PaintContext, SceneBuilder,
+    Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use picker::PickerEvent;
 use project::{Project, RepositoryEntry};
@@ -238,7 +238,7 @@ impl CollabTitlebarItem {
                             .left()
                             .with_tooltip::<RecentProjectsTooltip>(
                                 0,
-                                "Recent projects".into(),
+                                "Recent projects",
                                 Some(Box::new(recent_projects::OpenRecent)),
                                 theme.tooltip.clone(),
                                 cx,
@@ -282,7 +282,7 @@ impl CollabTitlebarItem {
                                             .left()
                                             .with_tooltip::<BranchPopoverTooltip>(
                                                 0,
-                                                "Recent branches".into(),
+                                                "Recent branches",
                                                 Some(Box::new(ToggleVcsMenu)),
                                                 theme.tooltip.clone(),
                                                 cx,
@@ -582,7 +582,7 @@ impl CollabTitlebarItem {
                 })
                 .with_tooltip::<ToggleContactsMenu>(
                     0,
-                    "Show contacts menu".into(),
+                    "Show contacts menu",
                     Some(Box::new(ToggleContactsMenu)),
                     theme.tooltip.clone(),
                     cx,
@@ -633,7 +633,7 @@ impl CollabTitlebarItem {
         })
         .with_tooltip::<ToggleScreenSharing>(
             0,
-            tooltip.into(),
+            tooltip,
             Some(Box::new(ToggleScreenSharing)),
             theme.tooltip.clone(),
             cx,
@@ -686,7 +686,7 @@ impl CollabTitlebarItem {
         })
         .with_tooltip::<ToggleMute>(
             0,
-            tooltip.into(),
+            tooltip,
             Some(Box::new(ToggleMute)),
             theme.tooltip.clone(),
             cx,
@@ -734,7 +734,7 @@ impl CollabTitlebarItem {
         })
         .with_tooltip::<ToggleDeafen>(
             0,
-            tooltip.into(),
+            tooltip,
             Some(Box::new(ToggleDeafen)),
             theme.tooltip.clone(),
             cx,
@@ -768,7 +768,7 @@ impl CollabTitlebarItem {
         })
         .with_tooltip::<LeaveCall>(
             0,
-            tooltip.into(),
+            tooltip,
             Some(Box::new(LeaveCall)),
             theme.tooltip.clone(),
             cx,
@@ -1312,7 +1312,7 @@ impl Element<CollabTitlebarItem> for AvatarRibbon {
         _: RectF,
         _: &mut Self::LayoutState,
         _: &mut CollabTitlebarItem,
-        _: &mut ViewContext<CollabTitlebarItem>,
+        _: &mut PaintContext<CollabTitlebarItem>,
     ) -> Self::PaintState {
         let mut path = PathBuilder::new();
         path.reset(bounds.lower_left());

crates/collab_ui/src/contact_list.rs 🔗

@@ -305,18 +305,18 @@ impl ContactList {
             github_login
         );
         let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
-        let window_id = cx.window_id();
+        let window = cx.window();
         cx.spawn(|_, mut cx| async move {
             if answer.next().await == Some(0) {
                 if let Err(e) = user_store
                     .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
                     .await
                 {
-                    cx.prompt(
-                        window_id,
+                    window.prompt(
                         PromptLevel::Info,
                         &format!("Failed to remove contact: {}", e),
                         &["Ok"],
+                        &mut cx,
                     );
                 }
             }
@@ -837,7 +837,7 @@ impl ContactList {
                                 ),
                                 background: Some(tree_branch.color),
                                 border: gpui::Border::default(),
-                                corner_radius: 0.,
+                                corner_radii: Default::default(),
                             });
                             scene.push_quad(gpui::Quad {
                                 bounds: RectF::from_points(
@@ -846,7 +846,7 @@ impl ContactList {
                                 ),
                                 background: Some(tree_branch.color),
                                 border: gpui::Border::default(),
-                                corner_radius: 0.,
+                                corner_radii: Default::default(),
                             });
                         }))
                         .constrained()
@@ -934,7 +934,7 @@ impl ContactList {
                                     ),
                                     background: Some(tree_branch.color),
                                     border: gpui::Border::default(),
-                                    corner_radius: 0.,
+                                    corner_radii: Default::default(),
                                 });
                                 scene.push_quad(gpui::Quad {
                                     bounds: RectF::from_points(
@@ -943,7 +943,7 @@ impl ContactList {
                                     ),
                                     background: Some(tree_branch.color),
                                     border: gpui::Border::default(),
-                                    corner_radius: 0.,
+                                    corner_radii: Default::default(),
                                 });
                             }))
                             .constrained()
@@ -1345,7 +1345,7 @@ impl View for ContactList {
                         })
                         .with_tooltip::<AddContact>(
                             0,
-                            "Search for new contact".into(),
+                            "Search for new contact",
                             None,
                             theme.tooltip.clone(),
                             cx,

crates/collab_ui/src/face_pile.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
     },
     json::ToJson,
     serde_json::{self, json},
-    AnyElement, Axis, Element, LayoutContext, SceneBuilder, ViewContext,
+    AnyElement, Axis, Element, LayoutContext, PaintContext, SceneBuilder, ViewContext,
 };
 
 use crate::CollabTitlebarItem;
@@ -54,7 +54,7 @@ impl Element<CollabTitlebarItem> for FacePile {
         visible_bounds: RectF,
         _layout: &mut Self::LayoutState,
         view: &mut CollabTitlebarItem,
-        cx: &mut ViewContext<CollabTitlebarItem>,
+        cx: &mut PaintContext<CollabTitlebarItem>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 

crates/collab_ui/src/incoming_call_notification.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
-    AnyElement, AppContext, Entity, View, ViewContext,
+    AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
 };
 use util::ResultExt;
 use workspace::AppState;
@@ -16,10 +16,10 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     let app_state = Arc::downgrade(app_state);
     let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
     cx.spawn(|mut cx| async move {
-        let mut notification_windows = Vec::new();
+        let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
         while let Some(incoming_call) = incoming_call.next().await {
-            for window_id in notification_windows.drain(..) {
-                cx.remove_window(window_id);
+            for window in notification_windows.drain(..) {
+                window.remove(&mut cx);
             }
 
             if let Some(incoming_call) = incoming_call {
@@ -31,7 +31,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
 
                 for screen in cx.platform().screens() {
                     let screen_bounds = screen.bounds();
-                    let (window_id, _) = cx.add_window(
+                    let window = cx.add_window(
                         WindowOptions {
                             bounds: WindowBounds::Fixed(RectF::new(
                                 screen_bounds.upper_right()
@@ -49,7 +49,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
                         |_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()),
                     );
 
-                    notification_windows.push(window_id);
+                    notification_windows.push(window);
                 }
             }
         }

crates/collab_ui/src/project_shared_notification.rs 🔗

@@ -26,7 +26,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
 
             for screen in cx.platform().screens() {
                 let screen_bounds = screen.bounds();
-                let (window_id, _) = cx.add_window(
+                let window = cx.add_window(
                     WindowOptions {
                         bounds: WindowBounds::Fixed(RectF::new(
                             screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING),
@@ -52,20 +52,20 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
                 notification_windows
                     .entry(*project_id)
                     .or_insert(Vec::new())
-                    .push(window_id);
+                    .push(window);
             }
         }
         room::Event::RemoteProjectUnshared { project_id } => {
-            if let Some(window_ids) = notification_windows.remove(&project_id) {
-                for window_id in window_ids {
-                    cx.update_window(window_id, |cx| cx.remove_window());
+            if let Some(windows) = notification_windows.remove(&project_id) {
+                for window in windows {
+                    window.remove(cx);
                 }
             }
         }
         room::Event::Left => {
-            for (_, window_ids) in notification_windows.drain() {
-                for window_id in window_ids {
-                    cx.update_window(window_id, |cx| cx.remove_window());
+            for (_, windows) in notification_windows.drain() {
+                for window in windows {
+                    window.remove(cx);
                 }
             }
         }

crates/collab_ui/src/sharing_status_indicator.rs 🔗

@@ -20,11 +20,11 @@ pub fn init(cx: &mut AppContext) {
                 {
                     status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
                 }
-            } else if let Some((window_id, _)) = status_indicator.take() {
-                cx.update_window(window_id, |cx| cx.remove_window());
+            } else if let Some(window) = status_indicator.take() {
+                window.update(cx, |cx| cx.remove_window());
             }
-        } else if let Some((window_id, _)) = status_indicator.take() {
-            cx.update_window(window_id, |cx| cx.remove_window());
+        } else if let Some(window) = status_indicator.take() {
+            window.update(cx, |cx| cx.remove_window());
         }
     })
     .detach();

crates/command_palette/src/command_palette.rs 🔗

@@ -1,8 +1,8 @@
 use collections::CommandPaletteFilter;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, elements::*, keymap_matcher::Keystroke, Action, AppContext, Element, MouseState,
-    ViewContext,
+    actions, anyhow::anyhow, elements::*, keymap_matcher::Keystroke, Action, AnyWindowHandle,
+    AppContext, Element, MouseState, ViewContext,
 };
 use picker::{Picker, PickerDelegate, PickerEvent};
 use std::cmp;
@@ -28,7 +28,7 @@ pub struct CommandPaletteDelegate {
 pub enum Event {
     Dismissed,
     Confirmed {
-        window_id: usize,
+        window: AnyWindowHandle,
         focused_view_id: usize,
         action: Box<dyn Action>,
     },
@@ -80,12 +80,13 @@ impl PickerDelegate for CommandPaletteDelegate {
         query: String,
         cx: &mut ViewContext<Picker<Self>>,
     ) -> gpui::Task<()> {
-        let window_id = cx.window_id();
         let view_id = self.focused_view_id;
+        let window = cx.window();
         cx.spawn(move |picker, mut cx| async move {
-            let actions = cx
-                .available_actions(window_id, view_id)
+            let actions = window
+                .available_actions(view_id, &cx)
                 .into_iter()
+                .flatten()
                 .filter_map(|(name, action, bindings)| {
                     let filtered = cx.read(|cx| {
                         if cx.has_global::<CommandPaletteFilter>() {
@@ -162,13 +163,15 @@ impl PickerDelegate for CommandPaletteDelegate {
 
     fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         if !self.matches.is_empty() {
-            let window_id = cx.window_id();
+            let window = cx.window();
             let focused_view_id = self.focused_view_id;
             let action_ix = self.matches[self.selected_ix].candidate_id;
             let action = self.actions.remove(action_ix).action;
             cx.app_context()
                 .spawn(move |mut cx| async move {
-                    cx.dispatch_action(window_id, focused_view_id, action.as_ref())
+                    window
+                        .dispatch_action(focused_view_id, action.as_ref(), &mut cx)
+                        .ok_or_else(|| anyhow!("window was closed"))
                 })
                 .detach_and_log_err(cx);
         }
@@ -295,8 +298,9 @@ mod tests {
         let app_state = init_test(cx);
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let editor = cx.add_view(window_id, |cx| {
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
+        let editor = window.add_view(cx, |cx| {
             let mut editor = Editor::single_line(None, cx);
             editor.set_text("abc", cx);
             editor

crates/context_menu/src/context_menu.rs 🔗

@@ -1,5 +1,5 @@
 use gpui::{
-    anyhow,
+    anyhow::{self, anyhow},
     elements::*,
     geometry::vector::Vector2F,
     keymap_matcher::KeymapContext,
@@ -218,12 +218,14 @@ impl ContextMenu {
             if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
                 match action {
                     ContextMenuItemAction::Action(action) => {
-                        let window_id = cx.window_id();
+                        let window = cx.window();
                         let view_id = self.parent_view_id;
                         let action = action.boxed_clone();
                         cx.app_context()
                             .spawn(|mut cx| async move {
-                                cx.dispatch_action(window_id, view_id, action.as_ref())
+                                window
+                                    .dispatch_action(view_id, action.as_ref(), &mut cx)
+                                    .ok_or_else(|| anyhow!("window was closed"))
                             })
                             .detach_and_log_err(cx);
                     }
@@ -480,17 +482,19 @@ impl ContextMenu {
                             .on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
                             .on_click(MouseButton::Left, move |_, menu, cx| {
                                 menu.cancel(&Default::default(), cx);
-                                let window_id = cx.window_id();
+                                let window = cx.window();
                                 match &action {
                                     ContextMenuItemAction::Action(action) => {
                                         let action = action.boxed_clone();
                                         cx.app_context()
                                             .spawn(|mut cx| async move {
-                                                cx.dispatch_action(
-                                                    window_id,
-                                                    view_id,
-                                                    action.as_ref(),
-                                                )
+                                                window
+                                                    .dispatch_action(
+                                                        view_id,
+                                                        action.as_ref(),
+                                                        &mut cx,
+                                                    )
+                                                    .ok_or_else(|| anyhow!("window was closed"))
                                             })
                                             .detach_and_log_err(cx);
                                     }

crates/copilot/src/sign_in.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
     geometry::rect::RectF,
     platform::{WindowBounds, WindowKind, WindowOptions},
     AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
-    ViewHandle,
+    WindowHandle,
 };
 use theme::ui::modal;
 
@@ -18,43 +18,43 @@ const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
 
 pub fn init(cx: &mut AppContext) {
     if let Some(copilot) = Copilot::global(cx) {
-        let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
+        let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
         cx.observe(&copilot, move |copilot, cx| {
             let status = copilot.read(cx).status();
 
             match &status {
                 crate::Status::SigningIn { prompt } => {
-                    if let Some(code_verification_handle) = code_verification.as_mut() {
-                        let window_id = code_verification_handle.window_id();
-                        let updated = cx.update_window(window_id, |cx| {
-                            code_verification_handle.update(cx, |code_verification, cx| {
-                                code_verification.set_status(status.clone(), cx)
-                            });
-                            cx.activate_window();
-                        });
-                        if updated.is_none() {
-                            code_verification = Some(create_copilot_auth_window(cx, &status));
+                    if let Some(window) = verification_window.as_mut() {
+                        let updated = window
+                            .root(cx)
+                            .map(|root| {
+                                root.update(cx, |verification, cx| {
+                                    verification.set_status(status.clone(), cx);
+                                    cx.activate_window();
+                                })
+                            })
+                            .is_some();
+                        if !updated {
+                            verification_window = Some(create_copilot_auth_window(cx, &status));
                         }
                     } else if let Some(_prompt) = prompt {
-                        code_verification = Some(create_copilot_auth_window(cx, &status));
+                        verification_window = Some(create_copilot_auth_window(cx, &status));
                     }
                 }
                 Status::Authorized | Status::Unauthorized => {
-                    if let Some(code_verification) = code_verification.as_ref() {
-                        let window_id = code_verification.window_id();
-                        cx.update_window(window_id, |cx| {
-                            code_verification.update(cx, |code_verification, cx| {
-                                code_verification.set_status(status, cx)
+                    if let Some(window) = verification_window.as_ref() {
+                        if let Some(verification) = window.root(cx) {
+                            verification.update(cx, |verification, cx| {
+                                verification.set_status(status, cx);
+                                cx.platform().activate(true);
+                                cx.activate_window();
                             });
-
-                            cx.platform().activate(true);
-                            cx.activate_window();
-                        });
+                        }
                     }
                 }
                 _ => {
-                    if let Some(code_verification) = code_verification.take() {
-                        cx.update_window(code_verification.window_id(), |cx| cx.remove_window());
+                    if let Some(code_verification) = verification_window.take() {
+                        code_verification.update(cx, |cx| cx.remove_window());
                     }
                 }
             }
@@ -66,7 +66,7 @@ pub fn init(cx: &mut AppContext) {
 fn create_copilot_auth_window(
     cx: &mut AppContext,
     status: &Status,
-) -> ViewHandle<CopilotCodeVerification> {
+) -> WindowHandle<CopilotCodeVerification> {
     let window_size = theme::current(cx).copilot.modal.dimensions();
     let window_options = WindowOptions {
         bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
@@ -78,10 +78,9 @@ fn create_copilot_auth_window(
         is_movable: true,
         screen: None,
     };
-    let (_, view) = cx.add_window(window_options, |_cx| {
+    cx.add_window(window_options, |_cx| {
         CopilotCodeVerification::new(status.clone())
-    });
-    view
+    })
 }
 
 pub struct CopilotCodeVerification {

crates/diagnostics/src/diagnostics.rs 🔗

@@ -855,7 +855,8 @@ mod tests {
 
         let language_server_id = LanguageServerId(0);
         let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
 
         // Create some diagnostics
         project.update(cx, |project, cx| {
@@ -942,7 +943,7 @@ mod tests {
         });
 
         // Open the project diagnostics view while there are already diagnostics.
-        let view = cx.add_view(window_id, |cx| {
+        let view = window.add_view(cx, |cx| {
             ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
         });
 
@@ -1248,9 +1249,10 @@ mod tests {
         let server_id_1 = LanguageServerId(100);
         let server_id_2 = LanguageServerId(101);
         let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
 
-        let view = cx.add_view(window_id, |cx| {
+        let view = window.add_view(cx, |cx| {
             ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
         });
 

crates/diagnostics/src/items.rs 🔗

@@ -173,7 +173,7 @@ impl View for DiagnosticIndicator {
             })
             .with_tooltip::<Summary>(
                 0,
-                "Project Diagnostics".to_string(),
+                "Project Diagnostics",
                 Some(Box::new(crate::Deploy)),
                 tooltip_style,
                 cx,

crates/drag_and_drop/src/drag_and_drop.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
     geometry::{rect::RectF, vector::Vector2F},
     platform::{CursorStyle, MouseButton},
     scene::{MouseDown, MouseDrag},
-    AnyElement, Element, View, ViewContext, WeakViewHandle, WindowContext,
+    AnyElement, AnyWindowHandle, Element, View, ViewContext, WeakViewHandle, WindowContext,
 };
 
 const DEAD_ZONE: f32 = 4.;
@@ -21,7 +21,7 @@ enum State<V: View> {
         region: RectF,
     },
     Dragging {
-        window_id: usize,
+        window: AnyWindowHandle,
         position: Vector2F,
         region_offset: Vector2F,
         region: RectF,
@@ -49,14 +49,14 @@ impl<V: View> Clone for State<V> {
                 region,
             },
             State::Dragging {
-                window_id,
+                window,
                 position,
                 region_offset,
                 region,
                 payload,
                 render,
             } => Self::Dragging {
-                window_id: window_id.clone(),
+                window: window.clone(),
                 position: position.clone(),
                 region_offset: region_offset.clone(),
                 region: region.clone(),
@@ -87,16 +87,16 @@ impl<V: View> DragAndDrop<V> {
         self.containers.insert(handle);
     }
 
-    pub fn currently_dragged<T: Any>(&self, window_id: usize) -> Option<(Vector2F, Rc<T>)> {
+    pub fn currently_dragged<T: Any>(&self, window: AnyWindowHandle) -> Option<(Vector2F, Rc<T>)> {
         self.currently_dragged.as_ref().and_then(|state| {
             if let State::Dragging {
                 position,
                 payload,
-                window_id: window_dragged_from,
+                window: window_dragged_from,
                 ..
             } = state
             {
-                if &window_id != window_dragged_from {
+                if &window != window_dragged_from {
                     return None;
                 }
 
@@ -126,9 +126,9 @@ impl<V: View> DragAndDrop<V> {
         cx: &mut WindowContext,
         render: Rc<impl 'static + Fn(&T, &mut ViewContext<V>) -> AnyElement<V>>,
     ) {
-        let window_id = cx.window_id();
+        let window = cx.window();
         cx.update_global(|this: &mut Self, cx| {
-            this.notify_containers_for_window(window_id, cx);
+            this.notify_containers_for_window(window, cx);
 
             match this.currently_dragged.as_ref() {
                 Some(&State::Down {
@@ -141,7 +141,7 @@ impl<V: View> DragAndDrop<V> {
                 }) => {
                     if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE {
                         this.currently_dragged = Some(State::Dragging {
-                            window_id,
+                            window,
                             region_offset,
                             region,
                             position: event.position,
@@ -163,7 +163,7 @@ impl<V: View> DragAndDrop<V> {
                     ..
                 }) => {
                     this.currently_dragged = Some(State::Dragging {
-                        window_id,
+                        window,
                         region_offset,
                         region,
                         position: event.position,
@@ -188,14 +188,14 @@ impl<V: View> DragAndDrop<V> {
                     State::Down { .. } => None,
                     State::DeadZone { .. } => None,
                     State::Dragging {
-                        window_id,
+                        window,
                         region_offset,
                         position,
                         region,
                         payload,
                         render,
                     } => {
-                        if cx.window_id() != window_id {
+                        if cx.window() != window {
                             return None;
                         }
 
@@ -260,27 +260,27 @@ impl<V: View> DragAndDrop<V> {
 
     pub fn cancel_dragging<P: Any>(&mut self, cx: &mut WindowContext) {
         if let Some(State::Dragging {
-            payload, window_id, ..
+            payload, window, ..
         }) = &self.currently_dragged
         {
             if payload.is::<P>() {
-                let window_id = *window_id;
+                let window = *window;
                 self.currently_dragged = Some(State::Canceled);
-                self.notify_containers_for_window(window_id, cx);
+                self.notify_containers_for_window(window, cx);
             }
         }
     }
 
     fn finish_dragging(&mut self, cx: &mut WindowContext) {
-        if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() {
-            self.notify_containers_for_window(window_id, cx);
+        if let Some(State::Dragging { window, .. }) = self.currently_dragged.take() {
+            self.notify_containers_for_window(window, cx);
         }
     }
 
-    fn notify_containers_for_window(&mut self, window_id: usize, cx: &mut WindowContext) {
+    fn notify_containers_for_window(&mut self, window: AnyWindowHandle, cx: &mut WindowContext) {
         self.containers.retain(|container| {
             if let Some(container) = container.upgrade(cx) {
-                if container.window_id() == window_id {
+                if container.window() == window {
                     container.update(cx, |_, cx| cx.notify());
                 }
                 true

crates/editor/Cargo.toml 🔗

@@ -47,6 +47,7 @@ workspace = { path = "../workspace" }
 
 aho-corasick = "0.7"
 anyhow.workspace = true
+convert_case = "0.6.0"
 futures.workspace = true
 indoc = "1.0.4"
 itertools = "0.10"
@@ -56,12 +57,12 @@ ordered-float.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 pulldown-cmark = { version = "0.9.2", default-features = false }
+rand.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 smallvec.workspace = true
 smol.workspace = true
-rand.workspace = true
 
 tree-sitter-rust = { workspace = true, optional = true }
 tree-sitter-html = { workspace = true, optional = true }

crates/editor/src/display_map/inlay_map.rs 🔗

@@ -397,7 +397,7 @@ impl InlayMap {
         buffer_snapshot: MultiBufferSnapshot,
         mut buffer_edits: Vec<text::Edit<usize>>,
     ) -> (InlaySnapshot, Vec<InlayEdit>) {
-        let mut snapshot = &mut self.snapshot;
+        let snapshot = &mut self.snapshot;
 
         if buffer_edits.is_empty() {
             if snapshot.buffer.trailing_excerpt_update_count()
@@ -572,7 +572,6 @@ impl InlayMap {
             })
             .collect();
         let buffer_snapshot = snapshot.buffer.clone();
-        drop(snapshot);
         let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits);
         (snapshot, edits)
     }
@@ -635,7 +634,6 @@ impl InlayMap {
         }
         log::info!("removing inlays: {:?}", to_remove);
 
-        drop(snapshot);
         let (snapshot, edits) = self.splice(to_remove, to_insert);
         (snapshot, edits)
     }

crates/editor/src/editor.rs 🔗

@@ -28,6 +28,7 @@ use blink_manager::BlinkManager;
 use client::{ClickhouseEvent, TelemetrySettings};
 use clock::{Global, ReplicaId};
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
+use convert_case::{Case, Casing};
 use copilot::Copilot;
 pub use display_map::DisplayPoint;
 use display_map::*;
@@ -89,7 +90,7 @@ use std::{
     cmp::{self, Ordering, Reverse},
     mem,
     num::NonZeroU32,
-    ops::{ControlFlow, Deref, DerefMut, Range},
+    ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
     path::Path,
     sync::Arc,
     time::{Duration, Instant},
@@ -231,6 +232,13 @@ actions!(
         SortLinesCaseInsensitive,
         ReverseLines,
         ShuffleLines,
+        ConvertToUpperCase,
+        ConvertToLowerCase,
+        ConvertToTitleCase,
+        ConvertToSnakeCase,
+        ConvertToKebabCase,
+        ConvertToUpperCamelCase,
+        ConvertToLowerCamelCase,
         Transpose,
         Cut,
         Copy,
@@ -353,6 +361,13 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::sort_lines_case_insensitive);
     cx.add_action(Editor::reverse_lines);
     cx.add_action(Editor::shuffle_lines);
+    cx.add_action(Editor::convert_to_upper_case);
+    cx.add_action(Editor::convert_to_lower_case);
+    cx.add_action(Editor::convert_to_title_case);
+    cx.add_action(Editor::convert_to_snake_case);
+    cx.add_action(Editor::convert_to_kebab_case);
+    cx.add_action(Editor::convert_to_upper_camel_case);
+    cx.add_action(Editor::convert_to_lower_camel_case);
     cx.add_action(Editor::delete_to_previous_word_start);
     cx.add_action(Editor::delete_to_previous_subword_start);
     cx.add_action(Editor::delete_to_next_word_end);
@@ -543,6 +558,7 @@ pub struct Editor {
     show_local_selections: bool,
     mode: EditorMode,
     show_gutter: bool,
+    show_wrap_guides: Option<bool>,
     placeholder_text: Option<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
     #[allow(clippy::type_complexity)]
@@ -1375,6 +1391,7 @@ impl Editor {
             show_local_selections: true,
             mode,
             show_gutter: mode == EditorMode::Full,
+            show_wrap_guides: None,
             placeholder_text: None,
             highlighted_rows: None,
             background_highlights: Default::default(),
@@ -2706,7 +2723,7 @@ impl Editor {
             .collect()
     }
 
-    fn excerpt_visible_offsets(
+    pub fn excerpt_visible_offsets(
         &self,
         restrict_to_languages: Option<&HashSet<Arc<Language>>>,
         cx: &mut ViewContext<'_, '_, Editor>,
@@ -4219,7 +4236,7 @@ impl Editor {
         _: &SortLinesCaseSensitive,
         cx: &mut ViewContext<Self>,
     ) {
-        self.manipulate_lines(cx, |text| text.sort())
+        self.manipulate_lines(cx, |lines| lines.sort())
     }
 
     pub fn sort_lines_case_insensitive(
@@ -4227,7 +4244,7 @@ impl Editor {
         _: &SortLinesCaseInsensitive,
         cx: &mut ViewContext<Self>,
     ) {
-        self.manipulate_lines(cx, |text| text.sort_by_key(|line| line.to_lowercase()))
+        self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase()))
     }
 
     pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
@@ -4265,19 +4282,19 @@ impl Editor {
             let text = buffer
                 .text_for_range(start_point..end_point)
                 .collect::<String>();
-            let mut text = text.split("\n").collect_vec();
+            let mut lines = text.split("\n").collect_vec();
 
-            let text_len = text.len();
-            callback(&mut text);
+            let lines_len = lines.len();
+            callback(&mut lines);
 
             // This is a current limitation with selections.
             // If we wanted to support removing or adding lines, we'd need to fix the logic associated with selections.
             debug_assert!(
-                text.len() == text_len,
+                lines.len() == lines_len,
                 "callback should not change the number of lines"
             );
 
-            edits.push((start_point..end_point, text.join("\n")));
+            edits.push((start_point..end_point, lines.join("\n")));
             let start_anchor = buffer.anchor_after(start_point);
             let end_anchor = buffer.anchor_before(end_point);
 
@@ -4304,6 +4321,97 @@ impl Editor {
         });
     }
 
+    pub fn convert_to_upper_case(&mut self, _: &ConvertToUpperCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_uppercase())
+    }
+
+    pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_lowercase())
+    }
+
+    pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_case(Case::Title))
+    }
+
+    pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_case(Case::Snake))
+    }
+
+    pub fn convert_to_kebab_case(&mut self, _: &ConvertToKebabCase, cx: &mut ViewContext<Self>) {
+        self.manipulate_text(cx, |text| text.to_case(Case::Kebab))
+    }
+
+    pub fn convert_to_upper_camel_case(
+        &mut self,
+        _: &ConvertToUpperCamelCase,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.manipulate_text(cx, |text| text.to_case(Case::UpperCamel))
+    }
+
+    pub fn convert_to_lower_camel_case(
+        &mut self,
+        _: &ConvertToLowerCamelCase,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.manipulate_text(cx, |text| text.to_case(Case::Camel))
+    }
+
+    fn manipulate_text<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
+    where
+        Fn: FnMut(&str) -> String,
+    {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = self.buffer.read(cx).snapshot(cx);
+
+        let mut new_selections = Vec::new();
+        let mut edits = Vec::new();
+        let mut selection_adjustment = 0i32;
+
+        for selection in self.selections.all::<usize>(cx) {
+            let selection_is_empty = selection.is_empty();
+
+            let (start, end) = if selection_is_empty {
+                let word_range = movement::surrounding_word(
+                    &display_map,
+                    selection.start.to_display_point(&display_map),
+                );
+                let start = word_range.start.to_offset(&display_map, Bias::Left);
+                let end = word_range.end.to_offset(&display_map, Bias::Left);
+                (start, end)
+            } else {
+                (selection.start, selection.end)
+            };
+
+            let text = buffer.text_for_range(start..end).collect::<String>();
+            let old_length = text.len() as i32;
+            let text = callback(&text);
+
+            new_selections.push(Selection {
+                start: (start as i32 - selection_adjustment) as usize,
+                end: ((start + text.len()) as i32 - selection_adjustment) as usize,
+                goal: SelectionGoal::None,
+                ..selection
+            });
+
+            selection_adjustment += old_length - text.len() as i32;
+
+            edits.push((start..end, text));
+        }
+
+        self.transact(cx, |this, cx| {
+            this.buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, None, cx);
+            });
+
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select(new_selections);
+            });
+
+            this.request_autoscroll(Autoscroll::fit(), cx);
+        });
+    }
+
     pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;
@@ -7187,6 +7295,10 @@ impl Editor {
     pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> {
         let mut wrap_guides = smallvec::smallvec![];
 
+        if self.show_wrap_guides == Some(false) {
+            return wrap_guides;
+        }
+
         let settings = self.buffer.read(cx).settings_at(0, cx);
         if settings.show_wrap_guides {
             if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) {
@@ -7244,6 +7356,11 @@ impl Editor {
         cx.notify();
     }
 
+    pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
+        self.show_wrap_guides = Some(show_gutter);
+        cx.notify();
+    }
+
     pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
         if let Some(buffer) = self.buffer().read(cx).as_singleton() {
             if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
@@ -7432,6 +7549,78 @@ impl Editor {
         results
     }
 
+    pub fn background_highlight_row_ranges<T: 'static>(
+        &self,
+        search_range: Range<Anchor>,
+        display_snapshot: &DisplaySnapshot,
+        count: usize,
+    ) -> Vec<RangeInclusive<DisplayPoint>> {
+        let mut results = Vec::new();
+        let buffer = &display_snapshot.buffer_snapshot;
+        let Some((_, ranges)) = self.background_highlights
+            .get(&TypeId::of::<T>()) else {
+                return vec![];
+            };
+
+        let start_ix = match ranges.binary_search_by(|probe| {
+            let cmp = probe.end.cmp(&search_range.start, buffer);
+            if cmp.is_gt() {
+                Ordering::Greater
+            } else {
+                Ordering::Less
+            }
+        }) {
+            Ok(i) | Err(i) => i,
+        };
+        let mut push_region = |start: Option<Point>, end: Option<Point>| {
+            if let (Some(start_display), Some(end_display)) = (start, end) {
+                results.push(
+                    start_display.to_display_point(display_snapshot)
+                        ..=end_display.to_display_point(display_snapshot),
+                );
+            }
+        };
+        let mut start_row: Option<Point> = None;
+        let mut end_row: Option<Point> = None;
+        if ranges.len() > count {
+            return vec![];
+        }
+        for range in &ranges[start_ix..] {
+            if range.start.cmp(&search_range.end, buffer).is_ge() {
+                break;
+            }
+            let end = range.end.to_point(buffer);
+            if let Some(current_row) = &end_row {
+                if end.row == current_row.row {
+                    continue;
+                }
+            }
+            let start = range.start.to_point(buffer);
+
+            if start_row.is_none() {
+                assert_eq!(end_row, None);
+                start_row = Some(start);
+                end_row = Some(end);
+                continue;
+            }
+            if let Some(current_end) = end_row.as_mut() {
+                if start.row > current_end.row + 1 {
+                    push_region(start_row, end_row);
+                    start_row = Some(start);
+                    end_row = Some(end);
+                } else {
+                    // Merge two hunks.
+                    *current_end = end;
+                }
+            } else {
+                unreachable!();
+            }
+        }
+        // We might still have a hunk that was not rendered (if there was a search hit on the last line)
+        push_region(start_row, end_row);
+        results
+    }
+
     pub fn highlight_text<T: 'static>(
         &mut self,
         ranges: Vec<Range<Anchor>>,
@@ -8496,7 +8685,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
         // We really need to rethink this ID system...
         .with_tooltip::<BlockContextToolip>(
             cx.block_id,
-            "Copy diagnostic message".to_string(),
+            "Copy diagnostic message",
             None,
             tooltip_style,
             cx,

crates/editor/src/editor_tests.rs 🔗

@@ -48,36 +48,40 @@ fn test_edit_events(cx: &mut TestAppContext) {
     });
 
     let events = Rc::new(RefCell::new(Vec::new()));
-    let (_, editor1) = cx.add_window({
-        let events = events.clone();
-        |cx| {
-            cx.subscribe(&cx.handle(), move |_, _, event, _| {
-                if matches!(
-                    event,
-                    Event::Edited | Event::BufferEdited | Event::DirtyChanged
-                ) {
-                    events.borrow_mut().push(("editor1", event.clone()));
-                }
-            })
-            .detach();
-            Editor::for_buffer(buffer.clone(), None, cx)
-        }
-    });
-    let (_, editor2) = cx.add_window({
-        let events = events.clone();
-        |cx| {
-            cx.subscribe(&cx.handle(), move |_, _, event, _| {
-                if matches!(
-                    event,
-                    Event::Edited | Event::BufferEdited | Event::DirtyChanged
-                ) {
-                    events.borrow_mut().push(("editor2", event.clone()));
-                }
-            })
-            .detach();
-            Editor::for_buffer(buffer.clone(), None, cx)
-        }
-    });
+    let editor1 = cx
+        .add_window({
+            let events = events.clone();
+            |cx| {
+                cx.subscribe(&cx.handle(), move |_, _, event, _| {
+                    if matches!(
+                        event,
+                        Event::Edited | Event::BufferEdited | Event::DirtyChanged
+                    ) {
+                        events.borrow_mut().push(("editor1", event.clone()));
+                    }
+                })
+                .detach();
+                Editor::for_buffer(buffer.clone(), None, cx)
+            }
+        })
+        .root(cx);
+    let editor2 = cx
+        .add_window({
+            let events = events.clone();
+            |cx| {
+                cx.subscribe(&cx.handle(), move |_, _, event, _| {
+                    if matches!(
+                        event,
+                        Event::Edited | Event::BufferEdited | Event::DirtyChanged
+                    ) {
+                        events.borrow_mut().push(("editor2", event.clone()));
+                    }
+                })
+                .detach();
+                Editor::for_buffer(buffer.clone(), None, cx)
+            }
+        })
+        .root(cx);
     assert_eq!(mem::take(&mut *events.borrow_mut()), []);
 
     // Mutating editor 1 will emit an `Edited` event only for that editor.
@@ -173,7 +177,9 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
     let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
     let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval());
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
+    let editor = cx
+        .add_window(|cx| build_editor(buffer.clone(), cx))
+        .root(cx);
 
     editor.update(cx, |editor, cx| {
         editor.start_transaction_at(now, cx);
@@ -343,10 +349,12 @@ fn test_ime_composition(cx: &mut TestAppContext) {
 fn test_selection_with_mouse(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, editor) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
-        build_editor(buffer, cx)
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     editor.update(cx, |view, cx| {
         view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
     });
@@ -410,10 +418,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
 fn test_canceling_pending_selection(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
@@ -456,10 +466,12 @@ fn test_clone(cx: &mut TestAppContext) {
         true,
     );
 
-    let (_, editor) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(&text, cx);
-        build_editor(buffer, cx)
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&text, cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
 
     editor.update(cx, |editor, cx| {
         editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
@@ -473,9 +485,11 @@ fn test_clone(cx: &mut TestAppContext) {
         );
     });
 
-    let (_, cloned_editor) = editor.update(cx, |editor, cx| {
-        cx.add_window(Default::default(), |cx| editor.clone(cx))
-    });
+    let cloned_editor = editor
+        .update(cx, |editor, cx| {
+            cx.add_window(Default::default(), |cx| editor.clone(cx))
+        })
+        .root(cx);
 
     let snapshot = editor.update(cx, |e, cx| e.snapshot(cx));
     let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx));
@@ -509,9 +523,10 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
 
     let fs = FakeFs::new(cx.background());
     let project = Project::test(fs, [], cx).await;
-    let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+    let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+    let workspace = window.root(cx);
     let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
-    cx.add_view(window_id, |cx| {
+    window.add_view(cx, |cx| {
         let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
         let mut editor = build_editor(buffer.clone(), cx);
         let handle = cx.handle();
@@ -618,10 +633,12 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
 fn test_cancel(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
@@ -661,9 +678,10 @@ fn test_cancel(cx: &mut TestAppContext) {
 fn test_fold_action(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(
-            &"
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(
+                &"
                 impl Foo {
                     // Hello!
 
@@ -680,11 +698,12 @@ fn test_fold_action(cx: &mut TestAppContext) {
                     }
                 }
             "
-            .unindent(),
-            cx,
-        );
-        build_editor(buffer.clone(), cx)
-    });
+                .unindent(),
+                cx,
+            );
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
@@ -752,7 +771,9 @@ fn test_move_cursor(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
     let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
-    let (_, view) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
+    let view = cx
+        .add_window(|cx| build_editor(buffer.clone(), cx))
+        .root(cx);
 
     buffer.update(cx, |buffer, cx| {
         buffer.edit(
@@ -827,10 +848,12 @@ fn test_move_cursor(cx: &mut TestAppContext) {
 fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx);
-        build_editor(buffer.clone(), cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
 
     assert_eq!('ⓐ'.len_utf8(), 3);
     assert_eq!('α'.len_utf8(), 2);
@@ -932,10 +955,12 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
 fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
-        build_editor(buffer.clone(), cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
@@ -982,10 +1007,12 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
 fn test_beginning_end_of_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\n  def", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\n  def", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
@@ -1145,10 +1172,12 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
 fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n  {baz.qux()}", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n  {baz.qux()}", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
@@ -1197,10 +1226,13 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
 fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("use one::{\n    two::three::four::five\n};", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer =
+                MultiBuffer::build_simple("use one::{\n    two::three::four::five\n};", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.set_wrap_width(Some(140.), cx);
@@ -1257,7 +1289,8 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
     let mut cx = EditorTestContext::new(cx).await;
 
     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-    cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
+    let window = cx.window;
+    window.simulate_resize(vec2f(100., 4. * line_height), &mut cx);
 
     cx.set_state(
         &r#"ˇone
@@ -1368,7 +1401,8 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
     let mut cx = EditorTestContext::new(cx).await;
     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-    cx.simulate_window_resize(cx.window_id, vec2f(1000., 4. * line_height + 0.5));
+    let window = cx.window;
+    window.simulate_resize(vec2f(1000., 4. * line_height + 0.5), &mut cx);
 
     cx.set_state(
         &r#"ˇone
@@ -1406,7 +1440,8 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
     let mut cx = EditorTestContext::new(cx).await;
 
     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-    cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
+    let window = cx.window;
+    window.simulate_resize(vec2f(100., 4. * line_height), &mut cx);
 
     cx.set_state(
         &r#"
@@ -1530,10 +1565,12 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
 fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("one two three four", cx);
-        build_editor(buffer.clone(), cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("one two three four", cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
@@ -1566,10 +1603,12 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
 fn test_newline(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("aaaa\n    bbbb\n", cx);
-        build_editor(buffer.clone(), cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("aaaa\n    bbbb\n", cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
@@ -1589,9 +1628,10 @@ fn test_newline(cx: &mut TestAppContext) {
 fn test_newline_with_old_selections(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, editor) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(
-            "
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(
+                "
                 a
                 b(
                     X
@@ -1600,19 +1640,20 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
                     X
                 )
             "
-            .unindent()
-            .as_str(),
-            cx,
-        );
-        let mut editor = build_editor(buffer.clone(), cx);
-        editor.change_selections(None, cx, |s| {
-            s.select_ranges([
-                Point::new(2, 4)..Point::new(2, 5),
-                Point::new(5, 4)..Point::new(5, 5),
-            ])
-        });
-        editor
-    });
+                .unindent()
+                .as_str(),
+                cx,
+            );
+            let mut editor = build_editor(buffer.clone(), cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([
+                    Point::new(2, 4)..Point::new(2, 5),
+                    Point::new(5, 4)..Point::new(5, 5),
+                ])
+            });
+            editor
+        })
+        .root(cx);
 
     editor.update(cx, |editor, cx| {
         // Edit the buffer directly, deleting ranges surrounding the editor's selections
@@ -1817,12 +1858,14 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
 fn test_insert_with_old_selections(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, editor) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
-        let mut editor = build_editor(buffer.clone(), cx);
-        editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20]));
-        editor
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
+            let mut editor = build_editor(buffer.clone(), cx);
+            editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20]));
+            editor
+        })
+        .root(cx);
 
     editor.update(cx, |editor, cx| {
         // Edit the buffer directly, deleting ranges surrounding the editor's selections
@@ -2329,10 +2372,12 @@ async fn test_delete(cx: &mut gpui::TestAppContext) {
 fn test_delete_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
@@ -2352,10 +2397,12 @@ fn test_delete_line(cx: &mut TestAppContext) {
         );
     });
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)])
@@ -2650,14 +2697,94 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
     "});
 }
 
+#[gpui::test]
+async fn test_manipulate_text(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // Test convert_to_upper_case()
+    cx.set_state(indoc! {"
+        «hello worldˇ»
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «HELLO WORLDˇ»
+    "});
+
+    // Test convert_to_lower_case()
+    cx.set_state(indoc! {"
+        «HELLO WORLDˇ»
+    "});
+    cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «hello worldˇ»
+    "});
+
+    // From here on out, test more complex cases of manipulate_text()
+
+    // Test no selection case - should affect words cursors are in
+    // Cursor at beginning, middle, and end of word
+    cx.set_state(indoc! {"
+        ˇhello big beauˇtiful worldˇ
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
+    "});
+
+    // Test multiple selections on a single line and across multiple lines
+    cx.set_state(indoc! {"
+        «Theˇ» quick «brown
+        foxˇ» jumps «overˇ»
+        the «lazyˇ» dog
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «THEˇ» quick «BROWN
+        FOXˇ» jumps «OVERˇ»
+        the «LAZYˇ» dog
+    "});
+
+    // Test case where text length grows
+    cx.set_state(indoc! {"
+        «tschüߡ»
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «TSCHÜSSˇ»
+    "});
+
+    // Test to make sure we don't crash when text shrinks
+    cx.set_state(indoc! {"
+        aaa_bbbˇ
+    "});
+    cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «aaaBbbˇ»
+    "});
+
+    // Test to make sure we all aware of the fact that each word can grow and shrink
+    // Final selections should be aware of this fact
+    cx.set_state(indoc! {"
+        aaa_bˇbb bbˇb_ccc ˇccc_ddd
+    "});
+    cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
+    "});
+}
+
 #[gpui::test]
 fn test_duplicate_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
@@ -2680,10 +2807,12 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
         );
     });
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
@@ -2707,10 +2836,12 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
 fn test_move_line_up_down(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.fold_ranges(
             vec![
@@ -2806,10 +2937,12 @@ fn test_move_line_up_down(cx: &mut TestAppContext) {
 fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, editor) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
-        build_editor(buffer, cx)
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     editor.update(cx, |editor, cx| {
         let snapshot = editor.buffer.read(cx).snapshot(cx);
         editor.insert_blocks(
@@ -2834,102 +2967,94 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
 fn test_transpose(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    _ = cx
-        .add_window(|cx| {
-            let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
+    _ = cx.add_window(|cx| {
+        let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
 
-            editor.change_selections(None, cx, |s| s.select_ranges([1..1]));
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bac");
-            assert_eq!(editor.selections.ranges(cx), [2..2]);
+        editor.change_selections(None, cx, |s| s.select_ranges([1..1]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bac");
+        assert_eq!(editor.selections.ranges(cx), [2..2]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bca");
-            assert_eq!(editor.selections.ranges(cx), [3..3]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bca");
+        assert_eq!(editor.selections.ranges(cx), [3..3]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bac");
-            assert_eq!(editor.selections.ranges(cx), [3..3]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bac");
+        assert_eq!(editor.selections.ranges(cx), [3..3]);
 
-            editor
-        })
-        .1;
+        editor
+    });
 
-    _ = cx
-        .add_window(|cx| {
-            let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
+    _ = cx.add_window(|cx| {
+        let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
 
-            editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "acb\nde");
-            assert_eq!(editor.selections.ranges(cx), [3..3]);
+        editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "acb\nde");
+        assert_eq!(editor.selections.ranges(cx), [3..3]);
 
-            editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "acbd\ne");
-            assert_eq!(editor.selections.ranges(cx), [5..5]);
+        editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "acbd\ne");
+        assert_eq!(editor.selections.ranges(cx), [5..5]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "acbde\n");
-            assert_eq!(editor.selections.ranges(cx), [6..6]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "acbde\n");
+        assert_eq!(editor.selections.ranges(cx), [6..6]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "acbd\ne");
-            assert_eq!(editor.selections.ranges(cx), [6..6]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "acbd\ne");
+        assert_eq!(editor.selections.ranges(cx), [6..6]);
 
-            editor
-        })
-        .1;
+        editor
+    });
 
-    _ = cx
-        .add_window(|cx| {
-            let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
+    _ = cx.add_window(|cx| {
+        let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
 
-            editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bacd\ne");
-            assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
+        editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bacd\ne");
+        assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bcade\n");
-            assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bcade\n");
+        assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bcda\ne");
-            assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bcda\ne");
+        assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bcade\n");
-            assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bcade\n");
+        assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "bcaed\n");
-            assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "bcaed\n");
+        assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
 
-            editor
-        })
-        .1;
+        editor
+    });
 
-    _ = cx
-        .add_window(|cx| {
-            let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx);
+    _ = cx.add_window(|cx| {
+        let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx);
 
-            editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "🏀🍐✋");
-            assert_eq!(editor.selections.ranges(cx), [8..8]);
+        editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "🏀🍐✋");
+        assert_eq!(editor.selections.ranges(cx), [8..8]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "🏀✋🍐");
-            assert_eq!(editor.selections.ranges(cx), [11..11]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "🏀✋🍐");
+        assert_eq!(editor.selections.ranges(cx), [11..11]);
 
-            editor.transpose(&Default::default(), cx);
-            assert_eq!(editor.text(cx), "🏀🍐✋");
-            assert_eq!(editor.selections.ranges(cx), [11..11]);
+        editor.transpose(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "🏀🍐✋");
+        assert_eq!(editor.selections.ranges(cx), [11..11]);
 
-            editor
-        })
-        .1;
+        editor
+    });
 }
 
 #[gpui::test]
@@ -3132,10 +3257,12 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
 fn test_select_all(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.select_all(&SelectAll, cx);
         assert_eq!(
@@ -3149,10 +3276,12 @@ fn test_select_all(cx: &mut TestAppContext) {
 fn test_select_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
@@ -3196,10 +3325,12 @@ fn test_select_line(cx: &mut TestAppContext) {
 fn test_split_selection_into_lines(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
     view.update(cx, |view, cx| {
         view.fold_ranges(
             vec![
@@ -3267,10 +3398,12 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) {
 fn test_add_selection_above_below(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, view) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
-        build_editor(buffer, cx)
-    });
+    let view = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
+            build_editor(buffer, cx)
+        })
+        .root(cx);
 
     view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
@@ -3555,7 +3688,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
 
     let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, view) = cx.add_window(|cx| build_editor(buffer, cx));
+    let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
 
@@ -3718,7 +3851,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
 
     let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     editor
         .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
         .await;
@@ -4281,7 +4414,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
 
     let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, view) = cx.add_window(|cx| build_editor(buffer, cx));
+    let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
 
@@ -4429,7 +4562,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
 
     let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     editor
         .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
@@ -4519,7 +4652,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) {
     );
 
     let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
 
     editor.update(cx, |editor, cx| {
         let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
@@ -4649,7 +4782,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
     let fake_server = fake_servers.next().await.unwrap();
 
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
     assert!(cx.read(|cx| editor.is_dirty(cx)));
 
@@ -4761,7 +4894,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
     let fake_server = fake_servers.next().await.unwrap();
 
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
     assert!(cx.read(|cx| editor.is_dirty(cx)));
 
@@ -4875,7 +5008,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
     let fake_server = fake_servers.next().await.unwrap();
 
     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
 
     let format = editor.update(cx, |editor, cx| {
@@ -5653,7 +5786,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
         multibuffer
     });
 
-    let (_, view) = cx.add_window(|cx| build_editor(multibuffer, cx));
+    let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
     view.update(cx, |view, cx| {
         assert_eq!(view.text(cx), "aaaa\nbbbb");
         view.change_selections(None, cx, |s| {
@@ -5723,7 +5856,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
         multibuffer
     });
 
-    let (_, view) = cx.add_window(|cx| build_editor(multibuffer, cx));
+    let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
     view.update(cx, |view, cx| {
         let (expected_text, selection_ranges) = marked_text_ranges(
             indoc! {"
@@ -5799,22 +5932,24 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
         multibuffer
     });
 
-    let (_, editor) = cx.add_window(|cx| {
-        let mut editor = build_editor(multibuffer.clone(), cx);
-        let snapshot = editor.snapshot(cx);
-        editor.change_selections(None, cx, |s| {
-            s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
-        });
-        editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx);
-        assert_eq!(
-            editor.selections.ranges(cx),
-            [
-                Point::new(1, 3)..Point::new(1, 3),
-                Point::new(2, 1)..Point::new(2, 1),
-            ]
-        );
-        editor
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let mut editor = build_editor(multibuffer.clone(), cx);
+            let snapshot = editor.snapshot(cx);
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
+            });
+            editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx);
+            assert_eq!(
+                editor.selections.ranges(cx),
+                [
+                    Point::new(1, 3)..Point::new(1, 3),
+                    Point::new(2, 1)..Point::new(2, 1),
+                ]
+            );
+            editor
+        })
+        .root(cx);
 
     // Refreshing selections is a no-op when excerpts haven't changed.
     editor.update(cx, |editor, cx| {
@@ -5884,16 +6019,18 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
         multibuffer
     });
 
-    let (_, editor) = cx.add_window(|cx| {
-        let mut editor = build_editor(multibuffer.clone(), cx);
-        let snapshot = editor.snapshot(cx);
-        editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx);
-        assert_eq!(
-            editor.selections.ranges(cx),
-            [Point::new(1, 3)..Point::new(1, 3)]
-        );
-        editor
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let mut editor = build_editor(multibuffer.clone(), cx);
+            let snapshot = editor.snapshot(cx);
+            editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx);
+            assert_eq!(
+                editor.selections.ranges(cx),
+                [Point::new(1, 3)..Point::new(1, 3)]
+            );
+            editor
+        })
+        .root(cx);
 
     multibuffer.update(cx, |multibuffer, cx| {
         multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);

crates/editor/src/element.rs 🔗

@@ -32,7 +32,7 @@ use gpui::{
     platform::{CursorStyle, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent},
     text_layout::{self, Line, RunStyle, TextLayoutCache},
     AnyElement, Axis, Border, CursorRegion, Element, EventContext, FontCache, LayoutContext,
-    MouseRegion, Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext,
+    MouseRegion, PaintContext, Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext,
 };
 use itertools::Itertools;
 use json::json;
@@ -508,13 +508,13 @@ impl EditorElement {
             bounds: gutter_bounds,
             background: Some(self.style.gutter_background),
             border: Border::new(0., Color::transparent_black()),
-            corner_radius: 0.,
+            corner_radii: Default::default(),
         });
         scene.push_quad(Quad {
             bounds: text_bounds,
             background: Some(self.style.background),
             border: Border::new(0., Color::transparent_black()),
-            corner_radius: 0.,
+            corner_radii: Default::default(),
         });
 
         if let EditorMode::Full = layout.mode {
@@ -542,7 +542,7 @@ impl EditorElement {
                         bounds: RectF::new(origin, size),
                         background: Some(self.style.active_line_background),
                         border: Border::default(),
-                        corner_radius: 0.,
+                        corner_radii: Default::default(),
                     });
                 }
             }
@@ -562,12 +562,24 @@ impl EditorElement {
                     bounds: RectF::new(origin, size),
                     background: Some(self.style.highlighted_line_background),
                     border: Border::default(),
-                    corner_radius: 0.,
+                    corner_radii: Default::default(),
                 });
             }
 
+            let scroll_left =
+                layout.position_map.snapshot.scroll_position().x() * layout.position_map.em_width;
+
             for (wrap_position, active) in layout.wrap_guides.iter() {
-                let x = text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.;
+                let x =
+                    (text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.)
+                        - scroll_left;
+
+                if x < text_bounds.origin_x()
+                    || (layout.show_scrollbars && x > self.scrollbar_left(&bounds))
+                {
+                    continue;
+                }
+
                 let color = if *active {
                     self.style.active_wrap_guide
                 } else {
@@ -580,7 +592,7 @@ impl EditorElement {
                     ),
                     background: Some(color),
                     border: Border::new(0., Color::transparent_black()),
-                    corner_radius: 0.,
+                    corner_radii: Default::default(),
                 });
             }
         }
@@ -681,7 +693,7 @@ impl EditorElement {
                         bounds: highlight_bounds,
                         background: Some(diff_style.modified),
                         border: Border::new(0., Color::transparent_black()),
-                        corner_radius: 1. * line_height,
+                        corner_radii: (1. * line_height).into(),
                     });
 
                     continue;
@@ -714,7 +726,7 @@ impl EditorElement {
                         bounds: highlight_bounds,
                         background: Some(diff_style.deleted),
                         border: Border::new(0., Color::transparent_black()),
-                        corner_radius: 1. * line_height,
+                        corner_radii: (1. * line_height).into(),
                     });
 
                     continue;
@@ -736,7 +748,7 @@ impl EditorElement {
                 bounds: highlight_bounds,
                 background: Some(color),
                 border: Border::new(0., Color::transparent_black()),
-                corner_radius: diff_style.corner_radius * line_height,
+                corner_radii: (diff_style.corner_radius * line_height).into(),
             });
         }
     }
@@ -1056,6 +1068,10 @@ impl EditorElement {
         scene.pop_layer();
     }
 
+    fn scrollbar_left(&self, bounds: &RectF) -> f32 {
+        bounds.max_x() - self.style.theme.scrollbar.width
+    }
+
     fn paint_scrollbar(
         &mut self,
         scene: &mut SceneBuilder,
@@ -1074,7 +1090,7 @@ impl EditorElement {
         let top = bounds.min_y();
         let bottom = bounds.max_y();
         let right = bounds.max_x();
-        let left = right - style.width;
+        let left = self.scrollbar_left(&bounds);
         let row_range = &layout.scrollbar_row_range;
         let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
 
@@ -1111,8 +1127,6 @@ impl EditorElement {
             if layout.is_singleton && scrollbar_settings.selections {
                 let start_anchor = Anchor::min();
                 let end_anchor = Anchor::max();
-                let mut start_row = None;
-                let mut end_row = None;
                 let color = scrollbar_theme.selections;
                 let border = Border {
                     width: 1.,
@@ -1123,54 +1137,32 @@ impl EditorElement {
                     bottom: false,
                     left: true,
                 };
-                let mut push_region = |start, end| {
-                    if let (Some(start_display), Some(end_display)) = (start, end) {
-                        let start_y = y_for_row(start_display as f32);
-                        let mut end_y = y_for_row(end_display as f32);
-                        if end_y - start_y < 1. {
-                            end_y = start_y + 1.;
-                        }
-                        let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
-
-                        scene.push_quad(Quad {
-                            bounds,
-                            background: Some(color),
-                            border,
-                            corner_radius: style.thumb.corner_radius,
-                        })
+                let mut push_region = |start: DisplayPoint, end: DisplayPoint| {
+                    let start_y = y_for_row(start.row() as f32);
+                    let mut end_y = y_for_row(end.row() as f32);
+                    if end_y - start_y < 1. {
+                        end_y = start_y + 1.;
                     }
+                    let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
+
+                    scene.push_quad(Quad {
+                        bounds,
+                        background: Some(color),
+                        border,
+                        corner_radii: style.thumb.corner_radii.into(),
+                    })
                 };
-                for (row, _) in &editor
-                    .background_highlights_in_range_for::<crate::items::BufferSearchHighlights>(
+                let background_ranges = editor
+                    .background_highlight_row_ranges::<crate::items::BufferSearchHighlights>(
                         start_anchor..end_anchor,
                         &layout.position_map.snapshot,
-                        &theme,
-                    )
-                {
-                    let start_display = row.start;
-                    let end_display = row.end;
-
-                    if start_row.is_none() {
-                        assert_eq!(end_row, None);
-                        start_row = Some(start_display.row());
-                        end_row = Some(end_display.row());
-                        continue;
-                    }
-                    if let Some(current_end) = end_row.as_mut() {
-                        if start_display.row() > *current_end + 1 {
-                            push_region(start_row, end_row);
-                            start_row = Some(start_display.row());
-                            end_row = Some(end_display.row());
-                        } else {
-                            // Merge two hunks.
-                            *current_end = end_display.row();
-                        }
-                    } else {
-                        unreachable!();
-                    }
+                        50000,
+                    );
+                for row in background_ranges {
+                    let start = row.start();
+                    let end = row.end();
+                    push_region(*start, *end);
                 }
-                // We might still have a hunk that was not rendered (if there was a search hit on the last line)
-                push_region(start_row, end_row);
             }
 
             if layout.is_singleton && scrollbar_settings.git_diff {
@@ -1217,7 +1209,7 @@ impl EditorElement {
                         bounds,
                         background: Some(color),
                         border,
-                        corner_radius: style.thumb.corner_radius,
+                        corner_radii: style.thumb.corner_radii.into(),
                     })
                 }
             }
@@ -1226,7 +1218,7 @@ impl EditorElement {
                 bounds: thumb_bounds,
                 border: style.thumb.border,
                 background: style.thumb.background_color,
-                corner_radius: style.thumb.corner_radius,
+                corner_radii: style.thumb.corner_radii.into(),
             });
         }
 
@@ -2481,7 +2473,7 @@ impl Element<Editor> for EditorElement {
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         editor: &mut Editor,
-        cx: &mut ViewContext<Editor>,
+        cx: &mut PaintContext<Editor>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
         scene.push_layer(Some(visible_bounds));
@@ -2751,14 +2743,14 @@ impl Cursor {
                 bounds,
                 background: None,
                 border: Border::all(1., self.color),
-                corner_radius: 0.,
+                corner_radii: Default::default(),
             });
         } else {
             scene.push_quad(Quad {
                 bounds,
                 background: Some(self.color),
                 border: Default::default(),
-                corner_radius: 0.,
+                corner_radii: Default::default(),
             });
         }
 
@@ -2998,16 +2990,18 @@ mod tests {
     use language::language_settings;
     use log::info;
     use std::{num::NonZeroU32, sync::Arc};
-    use util::test::{generate_marked_text, sample_text};
+    use util::test::sample_text;
 
     #[gpui::test]
     fn test_layout_line_numbers(cx: &mut TestAppContext) {
         init_test(cx, |_| {});
 
-        let (_, editor) = cx.add_window(|cx| {
-            let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
-            Editor::new(EditorMode::Full, buffer, None, None, cx)
-        });
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
+                Editor::new(EditorMode::Full, buffer, None, None, cx)
+            })
+            .root(cx);
         let element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
 
         let layouts = editor.update(cx, |editor, cx| {
@@ -3023,10 +3017,12 @@ mod tests {
     async fn test_vim_visual_selections(cx: &mut TestAppContext) {
         init_test(cx, |_| {});
 
-        let (_, editor) = cx.add_window(|cx| {
-            let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
-            Editor::new(EditorMode::Full, buffer, None, None, cx)
-        });
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
+                Editor::new(EditorMode::Full, buffer, None, None, cx)
+            })
+            .root(cx);
         let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
         let (_, state) = editor.update(cx, |editor, cx| {
             editor.cursor_shape = CursorShape::Block;
@@ -3099,25 +3095,27 @@ mod tests {
         // 13: bbbbbb
         // 14: cccccc
         // 15: dddddd
-        let (_, editor) = cx.add_window(|cx| {
-            let buffer = MultiBuffer::build_multi(
-                [
-                    (
-                        &(sample_text(8, 6, 'a') + "\n"),
-                        vec![
-                            Point::new(0, 0)..Point::new(3, 0),
-                            Point::new(4, 0)..Point::new(7, 0),
-                        ],
-                    ),
-                    (
-                        &(sample_text(8, 6, 'a') + "\n"),
-                        vec![Point::new(1, 0)..Point::new(3, 0)],
-                    ),
-                ],
-                cx,
-            );
-            Editor::new(EditorMode::Full, buffer, None, None, cx)
-        });
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_multi(
+                    [
+                        (
+                            &(sample_text(8, 6, 'a') + "\n"),
+                            vec![
+                                Point::new(0, 0)..Point::new(3, 0),
+                                Point::new(4, 0)..Point::new(7, 0),
+                            ],
+                        ),
+                        (
+                            &(sample_text(8, 6, 'a') + "\n"),
+                            vec![Point::new(1, 0)..Point::new(3, 0)],
+                        ),
+                    ],
+                    cx,
+                );
+                Editor::new(EditorMode::Full, buffer, None, None, cx)
+            })
+            .root(cx);
         let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
         let (_, state) = editor.update(cx, |editor, cx| {
             editor.cursor_shape = CursorShape::Block;
@@ -3167,10 +3165,12 @@ mod tests {
     fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
         init_test(cx, |_| {});
 
-        let (_, editor) = cx.add_window(|cx| {
-            let buffer = MultiBuffer::build_simple("", cx);
-            Editor::new(EditorMode::Full, buffer, None, None, cx)
-        });
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_simple("", cx);
+                Editor::new(EditorMode::Full, buffer, None, None, cx)
+            })
+            .root(cx);
 
         editor.update(cx, |editor, cx| {
             editor.set_placeholder_text("hello", cx);
@@ -3221,7 +3221,14 @@ mod tests {
         let mut scene = SceneBuilder::new(1.0);
         let bounds = RectF::new(Default::default(), size);
         editor.update(cx, |editor, cx| {
-            element.paint(&mut scene, bounds, bounds, &mut state, editor, cx);
+            element.paint(
+                &mut scene,
+                bounds,
+                bounds,
+                &mut state,
+                editor,
+                &mut PaintContext::new(cx),
+            );
         });
     }
 
@@ -3377,10 +3384,12 @@ mod tests {
         info!(
             "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'"
         );
-        let (_, editor) = cx.add_window(|cx| {
-            let buffer = MultiBuffer::build_simple(&input_text, cx);
-            Editor::new(editor_mode, buffer, None, None, cx)
-        });
+        let editor = cx
+            .add_window(|cx| {
+                let buffer = MultiBuffer::build_simple(&input_text, cx);
+                Editor::new(editor_mode, buffer, None, None, cx)
+            })
+            .root(cx);
 
         let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
         let (_, layout_state) = editor.update(cx, |editor, cx| {

crates/editor/src/hover_popover.rs 🔗

@@ -599,7 +599,7 @@ impl InfoPopover {
                                         bounds,
                                         background: Some(code_span_background_color),
                                         border: Default::default(),
-                                        corner_radius: 2.0,
+                                        corner_radii: (2.0).into(),
                                     });
                                 }
                             },

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -9,7 +9,7 @@ use crate::{
 };
 use anyhow::Context;
 use clock::Global;
-use gpui::{ModelHandle, Task, ViewContext};
+use gpui::{ModelContext, ModelHandle, Task, ViewContext};
 use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
 use log::error;
 use parking_lot::RwLock;
@@ -17,14 +17,21 @@ use project::InlayHint;
 
 use collections::{hash_map, HashMap, HashSet};
 use language::language_settings::InlayHintSettings;
+use sum_tree::Bias;
 use util::post_inc;
 
 pub struct InlayHintCache {
-    pub hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
-    pub allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
-    pub version: usize,
-    pub enabled: bool,
-    update_tasks: HashMap<ExcerptId, UpdateTask>,
+    hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
+    allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
+    version: usize,
+    enabled: bool,
+    update_tasks: HashMap<ExcerptId, TasksForRanges>,
+}
+
+#[derive(Debug)]
+struct TasksForRanges {
+    tasks: Vec<Task<()>>,
+    sorted_ranges: Vec<Range<language::Anchor>>,
 }
 
 #[derive(Debug)]
@@ -32,7 +39,7 @@ pub struct CachedExcerptHints {
     version: usize,
     buffer_version: Global,
     buffer_id: u64,
-    pub hints: Vec<(InlayId, InlayHint)>,
+    hints: Vec<(InlayId, InlayHint)>,
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -48,18 +55,6 @@ pub struct InlaySplice {
     pub to_insert: Vec<Inlay>,
 }
 
-struct UpdateTask {
-    invalidate: InvalidationStrategy,
-    cache_version: usize,
-    task: RunningTask,
-    pending_refresh: Option<ExcerptQuery>,
-}
-
-struct RunningTask {
-    _task: Task<()>,
-    is_running_rx: smol::channel::Receiver<()>,
-}
-
 #[derive(Debug)]
 struct ExcerptHintsUpdate {
     excerpt_id: ExcerptId,
@@ -72,24 +67,10 @@ struct ExcerptHintsUpdate {
 struct ExcerptQuery {
     buffer_id: u64,
     excerpt_id: ExcerptId,
-    dimensions: ExcerptDimensions,
     cache_version: usize,
     invalidate: InvalidationStrategy,
 }
 
-#[derive(Debug, Clone, Copy)]
-struct ExcerptDimensions {
-    excerpt_range_start: language::Anchor,
-    excerpt_range_end: language::Anchor,
-    excerpt_visible_range_start: language::Anchor,
-    excerpt_visible_range_end: language::Anchor,
-}
-
-struct HintFetchRanges {
-    visible_range: Range<language::Anchor>,
-    other_ranges: Vec<Range<language::Anchor>>,
-}
-
 impl InvalidationStrategy {
     fn should_invalidate(&self) -> bool {
         matches!(
@@ -99,35 +80,92 @@ impl InvalidationStrategy {
     }
 }
 
-impl ExcerptQuery {
-    fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges {
-        let visible_range =
-            self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end;
-        let mut other_ranges = Vec::new();
-        if self
-            .dimensions
-            .excerpt_range_start
-            .cmp(&visible_range.start, buffer)
-            .is_lt()
-        {
-            let mut end = visible_range.start;
-            end.offset -= 1;
-            other_ranges.push(self.dimensions.excerpt_range_start..end);
-        }
-        if self
-            .dimensions
-            .excerpt_range_end
-            .cmp(&visible_range.end, buffer)
-            .is_gt()
-        {
-            let mut start = visible_range.end;
-            start.offset += 1;
-            other_ranges.push(start..self.dimensions.excerpt_range_end);
+impl TasksForRanges {
+    fn new(sorted_ranges: Vec<Range<language::Anchor>>, task: Task<()>) -> Self {
+        Self {
+            tasks: vec![task],
+            sorted_ranges,
         }
+    }
 
-        HintFetchRanges {
-            visible_range,
-            other_ranges: other_ranges.into_iter().map(|range| range).collect(),
+    fn update_cached_tasks(
+        &mut self,
+        buffer_snapshot: &BufferSnapshot,
+        query_range: Range<text::Anchor>,
+        invalidate: InvalidationStrategy,
+        spawn_task: impl FnOnce(Vec<Range<language::Anchor>>) -> Task<()>,
+    ) {
+        let ranges_to_query = match invalidate {
+            InvalidationStrategy::None => {
+                let mut ranges_to_query = Vec::new();
+                let mut latest_cached_range = None::<&mut Range<language::Anchor>>;
+                for cached_range in self
+                    .sorted_ranges
+                    .iter_mut()
+                    .skip_while(|cached_range| {
+                        cached_range
+                            .end
+                            .cmp(&query_range.start, buffer_snapshot)
+                            .is_lt()
+                    })
+                    .take_while(|cached_range| {
+                        cached_range
+                            .start
+                            .cmp(&query_range.end, buffer_snapshot)
+                            .is_le()
+                    })
+                {
+                    match latest_cached_range {
+                        Some(latest_cached_range) => {
+                            if latest_cached_range.end.offset.saturating_add(1)
+                                < cached_range.start.offset
+                            {
+                                ranges_to_query.push(latest_cached_range.end..cached_range.start);
+                                cached_range.start = latest_cached_range.end;
+                            }
+                        }
+                        None => {
+                            if query_range
+                                .start
+                                .cmp(&cached_range.start, buffer_snapshot)
+                                .is_lt()
+                            {
+                                ranges_to_query.push(query_range.start..cached_range.start);
+                                cached_range.start = query_range.start;
+                            }
+                        }
+                    }
+                    latest_cached_range = Some(cached_range);
+                }
+
+                match latest_cached_range {
+                    Some(latest_cached_range) => {
+                        if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset
+                        {
+                            ranges_to_query.push(latest_cached_range.end..query_range.end);
+                            latest_cached_range.end = query_range.end;
+                        }
+                    }
+                    None => {
+                        ranges_to_query.push(query_range.clone());
+                        self.sorted_ranges.push(query_range);
+                        self.sorted_ranges.sort_by(|range_a, range_b| {
+                            range_a.start.cmp(&range_b.start, buffer_snapshot)
+                        });
+                    }
+                }
+
+                ranges_to_query
+            }
+            InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => {
+                self.tasks.clear();
+                self.sorted_ranges.clear();
+                vec![query_range]
+            }
+        };
+
+        if !ranges_to_query.is_empty() {
+            self.tasks.push(spawn_task(ranges_to_query));
         }
     }
 }
@@ -168,7 +206,6 @@ impl InlayHintCache {
                     );
                     if new_splice.is_some() {
                         self.version += 1;
-                        self.update_tasks.clear();
                         self.allowed_hint_kinds = new_allowed_hint_kinds;
                     }
                     ControlFlow::Break(new_splice)
@@ -197,7 +234,7 @@ impl InlayHintCache {
 
     pub fn spawn_hint_refresh(
         &mut self,
-        mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
+        excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
         invalidate: InvalidationStrategy,
         cx: &mut ViewContext<Editor>,
     ) -> Option<InlaySplice> {
@@ -205,43 +242,23 @@ impl InlayHintCache {
             return None;
         }
 
-        let update_tasks = &mut self.update_tasks;
         let mut invalidated_hints = Vec::new();
         if invalidate.should_invalidate() {
-            let mut changed = false;
-            update_tasks.retain(|task_excerpt_id, _| {
-                let retain = excerpts_to_query.contains_key(task_excerpt_id);
-                changed |= !retain;
-                retain
-            });
+            self.update_tasks
+                .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
             self.hints.retain(|cached_excerpt, cached_hints| {
                 let retain = excerpts_to_query.contains_key(cached_excerpt);
-                changed |= !retain;
                 if !retain {
                     invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id));
                 }
                 retain
             });
-            if changed {
-                self.version += 1;
-            }
         }
         if excerpts_to_query.is_empty() && invalidated_hints.is_empty() {
             return None;
         }
 
-        let cache_version = self.version;
-        excerpts_to_query.retain(|visible_excerpt_id, _| {
-            match update_tasks.entry(*visible_excerpt_id) {
-                hash_map::Entry::Occupied(o) => match o.get().cache_version.cmp(&cache_version) {
-                    cmp::Ordering::Less => true,
-                    cmp::Ordering::Equal => invalidate.should_invalidate(),
-                    cmp::Ordering::Greater => false,
-                },
-                hash_map::Entry::Vacant(_) => true,
-            }
-        });
-
+        let cache_version = self.version + 1;
         cx.spawn(|editor, mut cx| async move {
             editor
                 .update(&mut cx, |editor, cx| {
@@ -368,6 +385,19 @@ impl InlayHintCache {
         self.update_tasks.clear();
         self.hints.clear();
     }
+
+    pub fn hints(&self) -> Vec<InlayHint> {
+        let mut hints = Vec::new();
+        for excerpt_hints in self.hints.values() {
+            let excerpt_hints = excerpt_hints.read();
+            hints.extend(excerpt_hints.hints.iter().map(|(_, hint)| hint).cloned());
+        }
+        hints
+    }
+
+    pub fn version(&self) -> usize {
+        self.version
+    }
 }
 
 fn spawn_new_update_tasks(
@@ -378,13 +408,14 @@ fn spawn_new_update_tasks(
     cx: &mut ViewContext<'_, '_, Editor>,
 ) {
     let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
-    for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in
+    for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in
         excerpts_to_query
     {
         if excerpt_visible_range.is_empty() {
             continue;
         }
-        let buffer = buffer_handle.read(cx);
+        let buffer = excerpt_buffer.read(cx);
+        let buffer_id = buffer.remote_id();
         let buffer_snapshot = buffer.snapshot();
         if buffer_snapshot
             .version()
@@ -402,203 +433,123 @@ fn spawn_new_update_tasks(
             {
                 continue;
             }
-            if !new_task_buffer_version.changed_since(&cached_buffer_version)
-                && !matches!(invalidate, InvalidationStrategy::RefreshRequested)
-            {
-                continue;
-            }
         };
 
-        let buffer_id = buffer.remote_id();
-        let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
-        let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
-
-        let (multi_buffer_snapshot, full_excerpt_range) =
+        let (multi_buffer_snapshot, Some(query_range)) =
             editor.buffer.update(cx, |multi_buffer, cx| {
-                let multi_buffer_snapshot = multi_buffer.snapshot(cx);
                 (
-                    multi_buffer_snapshot,
-                    multi_buffer
-                        .excerpts_for_buffer(&buffer_handle, cx)
-                        .into_iter()
-                        .find(|(id, _)| id == &excerpt_id)
-                        .map(|(_, range)| range.context),
+                    multi_buffer.snapshot(cx),
+                    determine_query_range(
+                        multi_buffer,
+                        excerpt_id,
+                        &excerpt_buffer,
+                        excerpt_visible_range,
+                        cx,
+                    ),
                 )
-            });
+            }) else { return; };
+        let query = ExcerptQuery {
+            buffer_id,
+            excerpt_id,
+            cache_version: update_cache_version,
+            invalidate,
+        };
 
-        if let Some(full_excerpt_range) = full_excerpt_range {
-            let query = ExcerptQuery {
-                buffer_id,
-                excerpt_id,
-                dimensions: ExcerptDimensions {
-                    excerpt_range_start: full_excerpt_range.start,
-                    excerpt_range_end: full_excerpt_range.end,
-                    excerpt_visible_range_start,
-                    excerpt_visible_range_end,
-                },
-                cache_version: update_cache_version,
-                invalidate,
-            };
+        let new_update_task = |fetch_ranges| {
+            new_update_task(
+                query,
+                fetch_ranges,
+                multi_buffer_snapshot,
+                buffer_snapshot.clone(),
+                Arc::clone(&visible_hints),
+                cached_excerpt_hints,
+                cx,
+            )
+        };
 
-            let new_update_task = |is_refresh_after_regular_task| {
-                new_update_task(
-                    query,
-                    multi_buffer_snapshot,
-                    buffer_snapshot,
-                    Arc::clone(&visible_hints),
-                    cached_excerpt_hints,
-                    is_refresh_after_regular_task,
-                    cx,
-                )
-            };
-            match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
-                hash_map::Entry::Occupied(mut o) => {
-                    let update_task = o.get_mut();
-                    match (update_task.invalidate, invalidate) {
-                        (_, InvalidationStrategy::None) => {}
-                        (
-                            InvalidationStrategy::BufferEdited,
-                            InvalidationStrategy::RefreshRequested,
-                        ) if !update_task.task.is_running_rx.is_closed() => {
-                            update_task.pending_refresh = Some(query);
-                        }
-                        _ => {
-                            o.insert(UpdateTask {
-                                invalidate,
-                                cache_version: query.cache_version,
-                                task: new_update_task(false),
-                                pending_refresh: None,
-                            });
-                        }
-                    }
-                }
-                hash_map::Entry::Vacant(v) => {
-                    v.insert(UpdateTask {
-                        invalidate,
-                        cache_version: query.cache_version,
-                        task: new_update_task(false),
-                        pending_refresh: None,
-                    });
-                }
+        match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
+            hash_map::Entry::Occupied(mut o) => {
+                o.get_mut().update_cached_tasks(
+                    &buffer_snapshot,
+                    query_range,
+                    invalidate,
+                    new_update_task,
+                );
+            }
+            hash_map::Entry::Vacant(v) => {
+                v.insert(TasksForRanges::new(
+                    vec![query_range.clone()],
+                    new_update_task(vec![query_range]),
+                ));
             }
         }
     }
 }
 
+fn determine_query_range(
+    multi_buffer: &mut MultiBuffer,
+    excerpt_id: ExcerptId,
+    excerpt_buffer: &ModelHandle<Buffer>,
+    excerpt_visible_range: Range<usize>,
+    cx: &mut ModelContext<'_, MultiBuffer>,
+) -> Option<Range<language::Anchor>> {
+    let full_excerpt_range = multi_buffer
+        .excerpts_for_buffer(excerpt_buffer, cx)
+        .into_iter()
+        .find(|(id, _)| id == &excerpt_id)
+        .map(|(_, range)| range.context)?;
+
+    let buffer = excerpt_buffer.read(cx);
+    let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start;
+    let start_offset = excerpt_visible_range
+        .start
+        .saturating_sub(excerpt_visible_len)
+        .max(full_excerpt_range.start.offset);
+    let start = buffer.anchor_before(buffer.clip_offset(start_offset, Bias::Left));
+    let end_offset = excerpt_visible_range
+        .end
+        .saturating_add(excerpt_visible_len)
+        .min(full_excerpt_range.end.offset)
+        .min(buffer.len());
+    let end = buffer.anchor_after(buffer.clip_offset(end_offset, Bias::Right));
+    if start.cmp(&end, buffer).is_eq() {
+        None
+    } else {
+        Some(start..end)
+    }
+}
+
 fn new_update_task(
     query: ExcerptQuery,
+    hint_fetch_ranges: Vec<Range<language::Anchor>>,
     multi_buffer_snapshot: MultiBufferSnapshot,
     buffer_snapshot: BufferSnapshot,
     visible_hints: Arc<Vec<Inlay>>,
     cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
-    is_refresh_after_regular_task: bool,
     cx: &mut ViewContext<'_, '_, Editor>,
-) -> RunningTask {
-    let hints_fetch_ranges = query.hints_fetch_ranges(&buffer_snapshot);
-    let (is_running_tx, is_running_rx) = smol::channel::bounded(1);
-    let _task = cx.spawn(|editor, mut cx| async move {
-        let _is_running_tx = is_running_tx;
-        let create_update_task = |range| {
-            fetch_and_update_hints(
-                editor.clone(),
-                multi_buffer_snapshot.clone(),
-                buffer_snapshot.clone(),
-                Arc::clone(&visible_hints),
-                cached_excerpt_hints.as_ref().map(Arc::clone),
-                query,
-                range,
-                cx.clone(),
-            )
-        };
-
-        if is_refresh_after_regular_task {
-            let visible_range_has_updates =
-                match create_update_task(hints_fetch_ranges.visible_range).await {
-                    Ok(updated) => updated,
-                    Err(e) => {
-                        error!("inlay hint visible range update task failed: {e:#}");
-                        return;
-                    }
-                };
-
-            if visible_range_has_updates {
-                let other_update_results = futures::future::join_all(
-                    hints_fetch_ranges
-                        .other_ranges
-                        .into_iter()
-                        .map(create_update_task),
+) -> Task<()> {
+    cx.spawn(|editor, cx| async move {
+        let task_update_results =
+            futures::future::join_all(hint_fetch_ranges.into_iter().map(|range| {
+                fetch_and_update_hints(
+                    editor.clone(),
+                    multi_buffer_snapshot.clone(),
+                    buffer_snapshot.clone(),
+                    Arc::clone(&visible_hints),
+                    cached_excerpt_hints.as_ref().map(Arc::clone),
+                    query,
+                    range,
+                    cx.clone(),
                 )
-                .await;
-
-                for result in other_update_results {
-                    if let Err(e) = result {
-                        error!("inlay hint update task failed: {e:#}");
-                    }
-                }
-            }
-        } else {
-            let task_update_results = futures::future::join_all(
-                std::iter::once(hints_fetch_ranges.visible_range)
-                    .chain(hints_fetch_ranges.other_ranges.into_iter())
-                    .map(create_update_task),
-            )
+            }))
             .await;
 
-            for result in task_update_results {
-                if let Err(e) = result {
-                    error!("inlay hint update task failed: {e:#}");
-                }
+        for result in task_update_results {
+            if let Err(e) = result {
+                error!("inlay hint update task failed: {e:#}");
             }
         }
-
-        editor
-            .update(&mut cx, |editor, cx| {
-                let pending_refresh_query = editor
-                    .inlay_hint_cache
-                    .update_tasks
-                    .get_mut(&query.excerpt_id)
-                    .and_then(|task| task.pending_refresh.take());
-
-                if let Some(pending_refresh_query) = pending_refresh_query {
-                    let refresh_multi_buffer = editor.buffer().read(cx);
-                    let refresh_multi_buffer_snapshot = refresh_multi_buffer.snapshot(cx);
-                    let refresh_visible_hints = Arc::new(editor.visible_inlay_hints(cx));
-                    let refresh_cached_excerpt_hints = editor
-                        .inlay_hint_cache
-                        .hints
-                        .get(&pending_refresh_query.excerpt_id)
-                        .map(Arc::clone);
-                    if let Some(buffer) =
-                        refresh_multi_buffer.buffer(pending_refresh_query.buffer_id)
-                    {
-                        drop(refresh_multi_buffer);
-                        editor.inlay_hint_cache.update_tasks.insert(
-                            pending_refresh_query.excerpt_id,
-                            UpdateTask {
-                                invalidate: InvalidationStrategy::RefreshRequested,
-                                cache_version: editor.inlay_hint_cache.version,
-                                task: new_update_task(
-                                    pending_refresh_query,
-                                    refresh_multi_buffer_snapshot,
-                                    buffer.read(cx).snapshot(),
-                                    refresh_visible_hints,
-                                    refresh_cached_excerpt_hints,
-                                    true,
-                                    cx,
-                                ),
-                                pending_refresh: None,
-                            },
-                        );
-                    }
-                }
-            })
-            .ok();
-    });
-
-    RunningTask {
-        _task,
-        is_running_rx,
-    }
+    })
 }
 
 async fn fetch_and_update_hints(
@@ -610,7 +561,7 @@ async fn fetch_and_update_hints(
     query: ExcerptQuery,
     fetch_range: Range<language::Anchor>,
     mut cx: gpui::AsyncAppContext,
-) -> anyhow::Result<bool> {
+) -> anyhow::Result<()> {
     let inlay_hints_fetch_task = editor
         .update(&mut cx, |editor, cx| {
             editor
@@ -626,11 +577,10 @@ async fn fetch_and_update_hints(
         })
         .ok()
         .flatten();
-    let mut update_happened = false;
-    let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) };
-    let new_hints = inlay_hints_fetch_task
-        .await
-        .context("inlay hint fetch task")?;
+    let new_hints = match inlay_hints_fetch_task {
+        Some(task) => task.await.context("inlay hint fetch task")?,
+        None => return Ok(()),
+    };
     let background_task_buffer_snapshot = buffer_snapshot.clone();
     let backround_fetch_range = fetch_range.clone();
     let new_update = cx
@@ -646,106 +596,21 @@ async fn fetch_and_update_hints(
             )
         })
         .await;
-
-    editor
-        .update(&mut cx, |editor, cx| {
-            if let Some(new_update) = new_update {
-                update_happened = !new_update.add_to_cache.is_empty()
-                    || !new_update.remove_from_cache.is_empty()
-                    || !new_update.remove_from_visible.is_empty();
-
-                let cached_excerpt_hints = editor
-                    .inlay_hint_cache
-                    .hints
-                    .entry(new_update.excerpt_id)
-                    .or_insert_with(|| {
-                        Arc::new(RwLock::new(CachedExcerptHints {
-                            version: query.cache_version,
-                            buffer_version: buffer_snapshot.version().clone(),
-                            buffer_id: query.buffer_id,
-                            hints: Vec::new(),
-                        }))
-                    });
-                let mut cached_excerpt_hints = cached_excerpt_hints.write();
-                match query.cache_version.cmp(&cached_excerpt_hints.version) {
-                    cmp::Ordering::Less => return,
-                    cmp::Ordering::Greater | cmp::Ordering::Equal => {
-                        cached_excerpt_hints.version = query.cache_version;
-                    }
-                }
-                cached_excerpt_hints
-                    .hints
-                    .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
-                cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
-                editor.inlay_hint_cache.version += 1;
-
-                let mut splice = InlaySplice {
-                    to_remove: new_update.remove_from_visible,
-                    to_insert: Vec::new(),
-                };
-
-                for new_hint in new_update.add_to_cache {
-                    let new_hint_position = multi_buffer_snapshot
-                        .anchor_in_excerpt(query.excerpt_id, new_hint.position);
-                    let new_inlay_id = post_inc(&mut editor.next_inlay_id);
-                    if editor
-                        .inlay_hint_cache
-                        .allowed_hint_kinds
-                        .contains(&new_hint.kind)
-                    {
-                        splice.to_insert.push(Inlay::hint(
-                            new_inlay_id,
-                            new_hint_position,
-                            &new_hint,
-                        ));
-                    }
-
-                    cached_excerpt_hints
-                        .hints
-                        .push((InlayId::Hint(new_inlay_id), new_hint));
-                }
-
-                cached_excerpt_hints
-                    .hints
-                    .sort_by(|(_, hint_a), (_, hint_b)| {
-                        hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
-                    });
-                drop(cached_excerpt_hints);
-
-                if query.invalidate.should_invalidate() {
-                    let mut outdated_excerpt_caches = HashSet::default();
-                    for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
-                        let excerpt_hints = excerpt_hints.read();
-                        if excerpt_hints.buffer_id == query.buffer_id
-                            && excerpt_id != &query.excerpt_id
-                            && buffer_snapshot
-                                .version()
-                                .changed_since(&excerpt_hints.buffer_version)
-                        {
-                            outdated_excerpt_caches.insert(*excerpt_id);
-                            splice
-                                .to_remove
-                                .extend(excerpt_hints.hints.iter().map(|(id, _)| id));
-                        }
-                    }
-                    editor
-                        .inlay_hint_cache
-                        .hints
-                        .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
-                }
-
-                let InlaySplice {
-                    to_remove,
-                    to_insert,
-                } = splice;
-                if !to_remove.is_empty() || !to_insert.is_empty() {
-                    editor.splice_inlay_hints(to_remove, to_insert, cx)
-                }
-            }
-        })
-        .ok();
-
-    Ok(update_happened)
+    if let Some(new_update) = new_update {
+        editor
+            .update(&mut cx, |editor, cx| {
+                apply_hint_update(
+                    editor,
+                    new_update,
+                    query,
+                    buffer_snapshot,
+                    multi_buffer_snapshot,
+                    cx,
+                );
+            })
+            .ok();
+    }
+    Ok(())
 }
 
 fn calculate_hint_updates(
@@ -794,19 +659,6 @@ fn calculate_hint_updates(
             visible_hints
                 .iter()
                 .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
-                .filter(|hint| {
-                    contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot)
-                })
-                .filter(|hint| {
-                    fetch_range
-                        .start
-                        .cmp(&hint.position.text_anchor, buffer_snapshot)
-                        .is_le()
-                        && fetch_range
-                            .end
-                            .cmp(&hint.position.text_anchor, buffer_snapshot)
-                            .is_ge()
-                })
                 .map(|inlay_hint| inlay_hint.id)
                 .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
         );
@@ -820,16 +672,6 @@ fn calculate_hint_updates(
                     .filter(|(cached_inlay_id, _)| {
                         !excerpt_hints_to_persist.contains_key(cached_inlay_id)
                     })
-                    .filter(|(_, cached_hint)| {
-                        fetch_range
-                            .start
-                            .cmp(&cached_hint.position, buffer_snapshot)
-                            .is_le()
-                            && fetch_range
-                                .end
-                                .cmp(&cached_hint.position, buffer_snapshot)
-                                .is_ge()
-                    })
                     .map(|(cached_inlay_id, _)| *cached_inlay_id),
             );
         }
@@ -856,6 +698,113 @@ fn contains_position(
         && range.end.cmp(&position, buffer_snapshot).is_ge()
 }
 
+fn apply_hint_update(
+    editor: &mut Editor,
+    new_update: ExcerptHintsUpdate,
+    query: ExcerptQuery,
+    buffer_snapshot: BufferSnapshot,
+    multi_buffer_snapshot: MultiBufferSnapshot,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) {
+    let cached_excerpt_hints = editor
+        .inlay_hint_cache
+        .hints
+        .entry(new_update.excerpt_id)
+        .or_insert_with(|| {
+            Arc::new(RwLock::new(CachedExcerptHints {
+                version: query.cache_version,
+                buffer_version: buffer_snapshot.version().clone(),
+                buffer_id: query.buffer_id,
+                hints: Vec::new(),
+            }))
+        });
+    let mut cached_excerpt_hints = cached_excerpt_hints.write();
+    match query.cache_version.cmp(&cached_excerpt_hints.version) {
+        cmp::Ordering::Less => return,
+        cmp::Ordering::Greater | cmp::Ordering::Equal => {
+            cached_excerpt_hints.version = query.cache_version;
+        }
+    }
+
+    let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
+    cached_excerpt_hints
+        .hints
+        .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
+    let mut splice = InlaySplice {
+        to_remove: new_update.remove_from_visible,
+        to_insert: Vec::new(),
+    };
+    for new_hint in new_update.add_to_cache {
+        let cached_hints = &mut cached_excerpt_hints.hints;
+        let insert_position = match cached_hints
+            .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot))
+        {
+            Ok(i) => {
+                if cached_hints[i].1.text() == new_hint.text() {
+                    None
+                } else {
+                    Some(i)
+                }
+            }
+            Err(i) => Some(i),
+        };
+
+        if let Some(insert_position) = insert_position {
+            let new_inlay_id = post_inc(&mut editor.next_inlay_id);
+            if editor
+                .inlay_hint_cache
+                .allowed_hint_kinds
+                .contains(&new_hint.kind)
+            {
+                let new_hint_position =
+                    multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position);
+                splice
+                    .to_insert
+                    .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
+            }
+            cached_hints.insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint));
+            cached_inlays_changed = true;
+        }
+    }
+    cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
+    drop(cached_excerpt_hints);
+
+    if query.invalidate.should_invalidate() {
+        let mut outdated_excerpt_caches = HashSet::default();
+        for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
+            let excerpt_hints = excerpt_hints.read();
+            if excerpt_hints.buffer_id == query.buffer_id
+                && excerpt_id != &query.excerpt_id
+                && buffer_snapshot
+                    .version()
+                    .changed_since(&excerpt_hints.buffer_version)
+            {
+                outdated_excerpt_caches.insert(*excerpt_id);
+                splice
+                    .to_remove
+                    .extend(excerpt_hints.hints.iter().map(|(id, _)| id));
+            }
+        }
+        cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
+        editor
+            .inlay_hint_cache
+            .hints
+            .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
+    }
+
+    let InlaySplice {
+        to_remove,
+        to_insert,
+    } = splice;
+    let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty();
+    if cached_inlays_changed || displayed_inlays_changed {
+        editor.inlay_hint_cache.version += 1;
+    }
+    if displayed_inlays_changed {
+        editor.splice_inlay_hints(to_remove, to_insert, cx)
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
@@ -867,6 +816,7 @@ mod tests {
     };
     use futures::StreamExt;
     use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
+    use itertools::Itertools;
     use language::{
         language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
     };
@@ -874,7 +824,7 @@ mod tests {
     use parking_lot::Mutex;
     use project::{FakeFs, Project};
     use settings::SettingsStore;
-    use text::Point;
+    use text::{Point, ToPoint};
     use workspace::Workspace;
 
     use crate::editor_tests::update_test_language_settings;
@@ -1136,7 +1086,9 @@ mod tests {
                 )
                 .await;
         let project = Project::test(fs, ["/a".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let worktree_id = workspace.update(cx, |workspace, cx| {
             workspace.project().read_with(cx, |project, cx| {
                 project.worktrees(cx).next().unwrap().read(cx).id()
@@ -1836,7 +1788,9 @@ mod tests {
         .await;
         let project = Project::test(fs, ["/a".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let worktree_id = workspace.update(cx, |workspace, cx| {
             workspace.project().read_with(cx, |project, cx| {
                 project.worktrees(cx).next().unwrap().read(cx).id()
@@ -1876,7 +1830,7 @@ mod tests {
 
                     task_lsp_request_ranges.lock().push(params.range);
                     let query_start = params.range.start;
-                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1;
                     Ok(Some(vec![lsp::InlayHint {
                         position: query_start,
                         label: lsp::InlayHintLabel::String(i.to_string()),
@@ -1891,18 +1845,44 @@ mod tests {
             })
             .next()
             .await;
+        fn editor_visible_range(
+            editor: &ViewHandle<Editor>,
+            cx: &mut gpui::TestAppContext,
+        ) -> Range<Point> {
+            let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx));
+            assert_eq!(
+                ranges.len(),
+                1,
+                "Single buffer should produce a single excerpt with visible range"
+            );
+            let (_, (excerpt_buffer, _, excerpt_visible_range)) =
+                ranges.into_iter().next().unwrap();
+            excerpt_buffer.update(cx, |buffer, _| {
+                let snapshot = buffer.snapshot();
+                let start = buffer
+                    .anchor_before(excerpt_visible_range.start)
+                    .to_point(&snapshot);
+                let end = buffer
+                    .anchor_after(excerpt_visible_range.end)
+                    .to_point(&snapshot);
+                start..end
+            })
+        }
+
+        let initial_visible_range = editor_visible_range(&editor, cx);
+        let expected_initial_query_range_end =
+            lsp::Position::new(initial_visible_range.end.row * 2, 1);
         cx.foreground().run_until_parked();
         editor.update(cx, |editor, cx| {
-            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
-            ranges.sort_by_key(|range| range.start);
-            assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
-            assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
-            assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line");
-            assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent");
-
-            assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2,
-                "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
-            let expected_layers = vec!["1".to_string(), "2".to_string()];
+            let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            assert_eq!(ranges.len(), 1,
+                "When scroll is at the edge of a big document, double of its visible part range should be queried for hints in one single big request, but got: {ranges:?}");
+            let query_range = &ranges[0];
+            assert_eq!(query_range.start, lsp::Position::new(0, 0), "Should query initially from the beginning of the document");
+            assert_eq!(query_range.end, expected_initial_query_range_end, "Should query initially for double lines of the visible part of the document");
+
+            assert_eq!(lsp_request_count.load(Ordering::Acquire), 1);
+            let expected_layers = vec!["1".to_string()];
             assert_eq!(
                 expected_layers,
                 cached_hint_labels(editor),

crates/editor/src/items.rs 🔗

@@ -28,7 +28,10 @@ use std::{
     path::{Path, PathBuf},
 };
 use text::Selection;
-use util::{paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
+use util::{
+    paths::{PathExt, FILE_ROW_COLUMN_DELIMITER},
+    ResultExt, TryFutureExt,
+};
 use workspace::item::{BreadcrumbText, FollowableItemHandle};
 use workspace::{
     item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
@@ -546,9 +549,7 @@ impl Item for Editor {
             .and_then(|f| f.as_local())?
             .abs_path(cx);
 
-        let file_path = util::paths::compact(&file_path)
-            .to_string_lossy()
-            .to_string();
+        let file_path = file_path.compact().to_string_lossy().to_string();
 
         Some(file_path.into())
     }

crates/editor/src/scroll.rs 🔗

@@ -13,7 +13,7 @@ use gpui::{
 };
 use language::{Bias, Point};
 use util::ResultExt;
-use workspace::{item::Item, WorkspaceId};
+use workspace::WorkspaceId;
 
 use crate::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
@@ -333,9 +333,7 @@ impl Editor {
             cx,
         );
 
-        if !self.is_singleton(cx) {
-            self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
-        }
+        self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
     }
 
     pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {

crates/editor/src/test/editor_lsp_test_context.rs 🔗

@@ -69,7 +69,8 @@ impl<'a> EditorLspTestContext<'a> {
             .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
             .await;
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         project
             .update(cx, |project, cx| {
                 project.find_or_create_local_worktree("/root", true, cx)
@@ -98,7 +99,7 @@ impl<'a> EditorLspTestContext<'a> {
         Self {
             cx: EditorTestContext {
                 cx,
-                window_id,
+                window: window.into(),
                 editor,
             },
             lsp,

crates/editor/src/test/editor_test_context.rs 🔗

@@ -3,7 +3,8 @@ use crate::{
 };
 use futures::Future;
 use gpui::{
-    keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle,
+    keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, ModelContext,
+    ViewContext, ViewHandle,
 };
 use indoc::indoc;
 use language::{Buffer, BufferSnapshot};
@@ -21,7 +22,7 @@ use super::build_editor;
 
 pub struct EditorTestContext<'a> {
     pub cx: &'a mut gpui::TestAppContext,
-    pub window_id: usize,
+    pub window: AnyWindowHandle,
     pub editor: ViewHandle<Editor>,
 }
 
@@ -32,16 +33,14 @@ impl<'a> EditorTestContext<'a> {
         let buffer = project
             .update(cx, |project, cx| project.create_buffer("", None, cx))
             .unwrap();
-        let (window_id, editor) = cx.update(|cx| {
-            cx.add_window(Default::default(), |cx| {
-                cx.focus_self();
-                build_editor(MultiBuffer::build_from_buffer(buffer, cx), cx)
-            })
+        let window = cx.add_window(|cx| {
+            cx.focus_self();
+            build_editor(MultiBuffer::build_from_buffer(buffer, cx), cx)
         });
-
+        let editor = window.root(cx);
         Self {
             cx,
-            window_id,
+            window: window.into(),
             editor,
         }
     }
@@ -113,7 +112,8 @@ impl<'a> EditorTestContext<'a> {
         let keystroke_under_test_handle =
             self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
         let keystroke = Keystroke::parse(keystroke_text).unwrap();
-        self.cx.dispatch_keystroke(self.window_id, keystroke, false);
+
+        self.cx.dispatch_keystroke(self.window, keystroke, false);
         keystroke_under_test_handle
     }
 

crates/feedback/src/submit_feedback_button.rs 🔗

@@ -80,7 +80,7 @@ impl View for SubmitFeedbackButton {
         .with_margin_left(theme.feedback.button_margin)
         .with_tooltip::<Self>(
             0,
-            "cmd-s".into(),
+            "cmd-s",
             Some(Box::new(SubmitFeedback)),
             theme.tooltip.clone(),
             cx,

crates/file_finder/src/file_finder.rs 🔗

@@ -617,8 +617,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        cx.dispatch_action(window_id, Toggle);
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        cx.dispatch_action(window.into(), Toggle);
 
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
         finder
@@ -631,8 +632,8 @@ mod tests {
         });
 
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
-        cx.dispatch_action(window_id, SelectNext);
-        cx.dispatch_action(window_id, Confirm);
+        cx.dispatch_action(window.into(), SelectNext);
+        cx.dispatch_action(window.into(), Confirm);
         active_pane
             .condition(cx, |pane, _| pane.active_item().is_some())
             .await;
@@ -671,8 +672,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        cx.dispatch_action(window_id, Toggle);
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        cx.dispatch_action(window.into(), Toggle);
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
 
         let file_query = &first_file_name[..3];
@@ -704,8 +706,8 @@ mod tests {
         });
 
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
-        cx.dispatch_action(window_id, SelectNext);
-        cx.dispatch_action(window_id, Confirm);
+        cx.dispatch_action(window.into(), SelectNext);
+        cx.dispatch_action(window.into(), Confirm);
         active_pane
             .condition(cx, |pane, _| pane.active_item().is_some())
             .await;
@@ -754,8 +756,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        cx.dispatch_action(window_id, Toggle);
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        cx.dispatch_action(window.into(), Toggle);
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
 
         let file_query = &first_file_name[..3];
@@ -787,8 +790,8 @@ mod tests {
         });
 
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
-        cx.dispatch_action(window_id, SelectNext);
-        cx.dispatch_action(window_id, Confirm);
+        cx.dispatch_action(window.into(), SelectNext);
+        cx.dispatch_action(window.into(), Confirm);
         active_pane
             .condition(cx, |pane, _| pane.active_item().is_some())
             .await;
@@ -837,19 +840,23 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        let (_, finder) = cx.add_window(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace.downgrade(),
-                    workspace.read(cx).project().clone(),
-                    None,
-                    Vec::new(),
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
+        let finder = cx
+            .add_window(|cx| {
+                Picker::new(
+                    FileFinderDelegate::new(
+                        workspace.downgrade(),
+                        workspace.read(cx).project().clone(),
+                        None,
+                        Vec::new(),
+                        cx,
+                    ),
                     cx,
-                ),
-                cx,
-            )
-        });
+                )
+            })
+            .root(cx);
 
         let query = test_path_like("hi");
         finder
@@ -931,19 +938,23 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        let (_, finder) = cx.add_window(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace.downgrade(),
-                    workspace.read(cx).project().clone(),
-                    None,
-                    Vec::new(),
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
+        let finder = cx
+            .add_window(|cx| {
+                Picker::new(
+                    FileFinderDelegate::new(
+                        workspace.downgrade(),
+                        workspace.read(cx).project().clone(),
+                        None,
+                        Vec::new(),
+                        cx,
+                    ),
                     cx,
-                ),
-                cx,
-            )
-        });
+                )
+            })
+            .root(cx);
         finder
             .update(cx, |f, cx| {
                 f.delegate_mut().spawn_search(test_path_like("hi"), cx)
@@ -967,19 +978,23 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        let (_, finder) = cx.add_window(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace.downgrade(),
-                    workspace.read(cx).project().clone(),
-                    None,
-                    Vec::new(),
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
+        let finder = cx
+            .add_window(|cx| {
+                Picker::new(
+                    FileFinderDelegate::new(
+                        workspace.downgrade(),
+                        workspace.read(cx).project().clone(),
+                        None,
+                        Vec::new(),
+                        cx,
+                    ),
                     cx,
-                ),
-                cx,
-            )
-        });
+                )
+            })
+            .root(cx);
 
         // Even though there is only one worktree, that worktree's filename
         // is included in the matching, because the worktree is a single file.
@@ -1015,61 +1030,6 @@ mod tests {
         finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
     }
 
-    #[gpui::test]
-    async fn test_multiple_matches_with_same_relative_path(cx: &mut TestAppContext) {
-        let app_state = init_test(cx);
-        app_state
-            .fs
-            .as_fake()
-            .insert_tree(
-                "/root",
-                json!({
-                    "dir1": { "a.txt": "" },
-                    "dir2": { "a.txt": "" }
-                }),
-            )
-            .await;
-
-        let project = Project::test(
-            app_state.fs.clone(),
-            ["/root/dir1".as_ref(), "/root/dir2".as_ref()],
-            cx,
-        )
-        .await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-
-        let (_, finder) = cx.add_window(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace.downgrade(),
-                    workspace.read(cx).project().clone(),
-                    None,
-                    Vec::new(),
-                    cx,
-                ),
-                cx,
-            )
-        });
-
-        // Run a search that matches two files with the same relative path.
-        finder
-            .update(cx, |f, cx| {
-                f.delegate_mut().spawn_search(test_path_like("a.t"), cx)
-            })
-            .await;
-
-        // Can switch between different matches with the same relative path.
-        finder.update(cx, |finder, cx| {
-            let delegate = finder.delegate_mut();
-            assert_eq!(delegate.matches.len(), 2);
-            assert_eq!(delegate.selected_index(), 0);
-            delegate.set_selected_index(1, cx);
-            assert_eq!(delegate.selected_index(), 1);
-            delegate.set_selected_index(0, cx);
-            assert_eq!(delegate.selected_index(), 0);
-        });
-    }
-
     #[gpui::test]
     async fn test_path_distance_ordering(cx: &mut TestAppContext) {
         let app_state = init_test(cx);
@@ -1089,7 +1049,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
         let worktree_id = cx.read(|cx| {
             let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
             assert_eq!(worktrees.len(), 1);
@@ -1103,18 +1065,20 @@ mod tests {
             worktree_id,
             path: Arc::from(Path::new("/root/dir2/b.txt")),
         }));
-        let (_, finder) = cx.add_window(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace.downgrade(),
-                    workspace.read(cx).project().clone(),
-                    b_path,
-                    Vec::new(),
+        let finder = cx
+            .add_window(|cx| {
+                Picker::new(
+                    FileFinderDelegate::new(
+                        workspace.downgrade(),
+                        workspace.read(cx).project().clone(),
+                        b_path,
+                        Vec::new(),
+                        cx,
+                    ),
                     cx,
-                ),
-                cx,
-            )
-        });
+                )
+            })
+            .root(cx);
 
         finder
             .update(cx, |f, cx| {
@@ -1151,19 +1115,23 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        let (_, finder) = cx.add_window(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace.downgrade(),
-                    workspace.read(cx).project().clone(),
-                    None,
-                    Vec::new(),
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
+        let finder = cx
+            .add_window(|cx| {
+                Picker::new(
+                    FileFinderDelegate::new(
+                        workspace.downgrade(),
+                        workspace.read(cx).project().clone(),
+                        None,
+                        Vec::new(),
+                        cx,
+                    ),
                     cx,
-                ),
-                cx,
-            )
-        });
+                )
+            })
+            .root(cx);
         finder
             .update(cx, |f, cx| {
                 f.delegate_mut().spawn_search(test_path_like("dir"), cx)
@@ -1198,7 +1166,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
         let worktree_id = cx.read(|cx| {
             let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
             assert_eq!(worktrees.len(), 1);
@@ -1216,7 +1185,7 @@ mod tests {
             "fir",
             1,
             "first.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1231,7 +1200,7 @@ mod tests {
             "sec",
             1,
             "second.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1253,7 +1222,7 @@ mod tests {
             "thi",
             1,
             "third.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1285,7 +1254,7 @@ mod tests {
             "sec",
             1,
             "second.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1324,7 +1293,7 @@ mod tests {
             "thi",
             1,
             "third.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1404,7 +1373,8 @@ mod tests {
         .detach();
         deterministic.run_until_parked();
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
         let worktree_id = cx.read(|cx| {
             let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
             assert_eq!(worktrees.len(), 1,);
@@ -1439,7 +1409,7 @@ mod tests {
             "sec",
             1,
             "second.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1461,7 +1431,7 @@ mod tests {
             "fir",
             1,
             "first.rs",
-            window_id,
+            window.into(),
             &workspace,
             &deterministic,
             cx,
@@ -1493,12 +1463,12 @@ mod tests {
         input: &str,
         expected_matches: usize,
         expected_editor_title: &str,
-        window_id: usize,
+        window: gpui::AnyWindowHandle,
         workspace: &ViewHandle<Workspace>,
         deterministic: &gpui::executor::Deterministic,
         cx: &mut gpui::TestAppContext,
     ) -> Vec<FoundPath> {
-        cx.dispatch_action(window_id, Toggle);
+        cx.dispatch_action(window, Toggle);
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
         finder
             .update(cx, |finder, cx| {
@@ -1515,8 +1485,8 @@ mod tests {
         });
 
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
-        cx.dispatch_action(window_id, SelectNext);
-        cx.dispatch_action(window_id, Confirm);
+        cx.dispatch_action(window, SelectNext);
+        cx.dispatch_action(window, Confirm);
         deterministic.run_until_parked();
         active_pane
             .condition(cx, |pane, _| pane.active_item().is_some())

crates/go_to_line/src/go_to_line.rs 🔗

@@ -135,7 +135,7 @@ impl Entity for GoToLine {
 
     fn release(&mut self, cx: &mut AppContext) {
         let scroll_position = self.prev_scroll_position.take();
-        cx.update_window(self.active_editor.window_id(), |cx| {
+        self.active_editor.window().update(cx, |cx| {
             self.active_editor.update(cx, |editor, cx| {
                 editor.highlight_rows(None);
                 if let Some(scroll_position) = scroll_position {

crates/gpui/Cargo.toml 🔗

@@ -22,6 +22,7 @@ sqlez = { path = "../sqlez" }
 async-task = "4.0.3"
 backtrace = { version = "0.3", optional = true }
 ctor.workspace = true
+derive_more.workspace = true
 dhat = { version = "0.3", optional = true }
 env_logger = { version = "0.9", optional = true }
 etagere = "0.2"

crates/gpui/examples/corner_radii.rs 🔗

@@ -0,0 +1,155 @@
+use gpui::{
+    color::Color, geometry::rect::RectF, scene::Shadow, AnyElement, App, Element, Entity, Quad,
+    View,
+};
+use log::LevelFilter;
+use pathfinder_geometry::vector::vec2f;
+use simplelog::SimpleLogger;
+
+fn main() {
+    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
+
+    App::new(()).unwrap().run(|cx| {
+        cx.platform().activate(true);
+        cx.add_window(Default::default(), |_| CornersView);
+    });
+}
+
+struct CornersView;
+
+impl Entity for CornersView {
+    type Event = ();
+}
+
+impl View for CornersView {
+    fn ui_name() -> &'static str {
+        "CornersView"
+    }
+
+    fn render(&mut self, _: &mut gpui::ViewContext<Self>) -> AnyElement<CornersView> {
+        CornersElement.into_any()
+    }
+}
+
+struct CornersElement;
+
+impl<V: View> gpui::Element<V> for CornersElement {
+    type LayoutState = ();
+
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: gpui::SizeConstraint,
+        _: &mut V,
+        _: &mut gpui::LayoutContext<V>,
+    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+        (constraint.max, ())
+    }
+
+    fn paint(
+        &mut self,
+        scene: &mut gpui::SceneBuilder,
+        bounds: pathfinder_geometry::rect::RectF,
+        _: pathfinder_geometry::rect::RectF,
+        _: &mut Self::LayoutState,
+        _: &mut V,
+        _: &mut gpui::PaintContext<V>,
+    ) -> Self::PaintState {
+        scene.push_quad(Quad {
+            bounds,
+            background: Some(Color::white()),
+            ..Default::default()
+        });
+
+        scene.push_layer(None);
+
+        scene.push_quad(Quad {
+            bounds: RectF::new(vec2f(100., 100.), vec2f(100., 100.)),
+            background: Some(Color::red()),
+            border: Default::default(),
+            corner_radii: gpui::scene::CornerRadii {
+                top_left: 20.,
+                ..Default::default()
+            },
+        });
+
+        scene.push_quad(Quad {
+            bounds: RectF::new(vec2f(200., 100.), vec2f(100., 100.)),
+            background: Some(Color::green()),
+            border: Default::default(),
+            corner_radii: gpui::scene::CornerRadii {
+                top_right: 20.,
+                ..Default::default()
+            },
+        });
+
+        scene.push_quad(Quad {
+            bounds: RectF::new(vec2f(100., 200.), vec2f(100., 100.)),
+            background: Some(Color::blue()),
+            border: Default::default(),
+            corner_radii: gpui::scene::CornerRadii {
+                bottom_left: 20.,
+                ..Default::default()
+            },
+        });
+
+        scene.push_quad(Quad {
+            bounds: RectF::new(vec2f(200., 200.), vec2f(100., 100.)),
+            background: Some(Color::yellow()),
+            border: Default::default(),
+            corner_radii: gpui::scene::CornerRadii {
+                bottom_right: 20.,
+                ..Default::default()
+            },
+        });
+
+        scene.push_shadow(Shadow {
+            bounds: RectF::new(vec2f(400., 100.), vec2f(100., 100.)),
+            corner_radii: gpui::scene::CornerRadii {
+                bottom_right: 20.,
+                ..Default::default()
+            },
+            sigma: 20.0,
+            color: Color::black(),
+        });
+
+        scene.push_layer(None);
+        scene.push_quad(Quad {
+            bounds: RectF::new(vec2f(400., 100.), vec2f(100., 100.)),
+            background: Some(Color::red()),
+            border: Default::default(),
+            corner_radii: gpui::scene::CornerRadii {
+                bottom_right: 20.,
+                ..Default::default()
+            },
+        });
+
+        scene.pop_layer();
+        scene.pop_layer();
+    }
+
+    fn rect_for_text_range(
+        &self,
+        _: std::ops::Range<usize>,
+        _: pathfinder_geometry::rect::RectF,
+        _: pathfinder_geometry::rect::RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &V,
+        _: &gpui::ViewContext<V>,
+    ) -> Option<pathfinder_geometry::rect::RectF> {
+        unimplemented!()
+    }
+
+    fn debug(
+        &self,
+        _: pathfinder_geometry::rect::RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &V,
+        _: &gpui::ViewContext<V>,
+    ) -> serde_json::Value {
+        unimplemented!()
+    }
+}

crates/gpui/src/app.rs 🔗

@@ -23,6 +23,8 @@ use std::{
 };
 
 use anyhow::{anyhow, Context, Result};
+
+use derive_more::Deref;
 use parking_lot::Mutex;
 use postage::oneshot;
 use smallvec::SmallVec;
@@ -44,6 +46,7 @@ use window_input_handler::WindowInputHandler;
 use crate::{
     elements::{AnyElement, AnyRootElement, RootElement},
     executor::{self, Task},
+    fonts::TextStyle,
     json,
     keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
     platform::{
@@ -130,8 +133,20 @@ pub trait BorrowAppContext {
 }
 
 pub trait BorrowWindowContext {
-    fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T;
-    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T;
+    type Result<T>;
+
+    fn read_window<T, F>(&self, window: AnyWindowHandle, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&WindowContext) -> T;
+    fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&WindowContext) -> Option<T>;
+    fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&mut WindowContext) -> T;
+    fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&mut WindowContext) -> Option<T>;
 }
 
 #[derive(Clone)]
@@ -294,13 +309,12 @@ impl App {
         result
     }
 
-    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
-        &mut self,
-        window_id: usize,
-        callback: F,
-    ) -> Option<T> {
+    fn update_window<T, F>(&mut self, window: AnyWindowHandle, callback: F) -> Option<T>
+    where
+        F: FnOnce(&mut WindowContext) -> T,
+    {
         let mut state = self.0.borrow_mut();
-        let result = state.update_window(window_id, callback);
+        let result = state.update_window(window, callback);
         state.pending_notifications.clear();
         result
     }
@@ -327,67 +341,8 @@ impl AsyncAppContext {
         self.0.borrow_mut().update(callback)
     }
 
-    pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
-        &self,
-        window_id: usize,
-        callback: F,
-    ) -> Option<T> {
-        self.0.borrow_mut().read_window(window_id, callback)
-    }
-
-    pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
-        &mut self,
-        window_id: usize,
-        callback: F,
-    ) -> Option<T> {
-        self.0.borrow_mut().update_window(window_id, callback)
-    }
-
-    pub fn debug_elements(&self, window_id: usize) -> Option<json::Value> {
-        self.0.borrow().read_window(window_id, |cx| {
-            let root_view = cx.window.root_view();
-            let root_element = cx.window.rendered_views.get(&root_view.id())?;
-            root_element.debug(cx).log_err()
-        })?
-    }
-
-    pub fn dispatch_action(
-        &mut self,
-        window_id: usize,
-        view_id: usize,
-        action: &dyn Action,
-    ) -> Result<()> {
-        self.0
-            .borrow_mut()
-            .update_window(window_id, |window| {
-                window.dispatch_action(Some(view_id), action);
-            })
-            .ok_or_else(|| anyhow!("window not found"))
-    }
-
-    pub fn available_actions(
-        &self,
-        window_id: usize,
-        view_id: usize,
-    ) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
-        self.read_window(window_id, |cx| cx.available_actions(view_id))
-            .unwrap_or_default()
-    }
-
-    pub fn has_window(&self, window_id: usize) -> bool {
-        self.read(|cx| cx.windows.contains_key(&window_id))
-    }
-
-    pub fn window_is_active(&self, window_id: usize) -> bool {
-        self.read(|cx| cx.windows.get(&window_id).map_or(false, |w| w.is_active))
-    }
-
-    pub fn root_view(&self, window_id: usize) -> Option<AnyViewHandle> {
-        self.read(|cx| cx.windows.get(&window_id).map(|w| w.root_view().clone()))
-    }
-
-    pub fn window_ids(&self) -> Vec<usize> {
-        self.read(|cx| cx.windows.keys().copied().collect())
+    pub fn windows(&self) -> Vec<AnyWindowHandle> {
+        self.0.borrow().windows().collect()
     }
 
     pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
@@ -402,7 +357,7 @@ impl AsyncAppContext {
         &mut self,
         window_options: WindowOptions,
         build_root_view: F,
-    ) -> (usize, ViewHandle<T>)
+    ) -> WindowHandle<T>
     where
         T: View,
         F: FnOnce(&mut ViewContext<T>) -> T,
@@ -410,25 +365,6 @@ impl AsyncAppContext {
         self.update(|cx| cx.add_window(window_options, build_root_view))
     }
 
-    pub fn remove_window(&mut self, window_id: usize) {
-        self.update_window(window_id, |cx| cx.remove_window());
-    }
-
-    pub fn activate_window(&mut self, window_id: usize) {
-        self.update_window(window_id, |cx| cx.activate_window());
-    }
-
-    // TODO: Can we eliminate this method and move it to WindowContext then call it with update_window?s
-    pub fn prompt(
-        &mut self,
-        window_id: usize,
-        level: PromptLevel,
-        msg: &str,
-        answers: &[&str],
-    ) -> Option<oneshot::Receiver<usize>> {
-        self.update_window(window_id, |cx| cx.prompt(level, msg, answers))
-    }
-
     pub fn platform(&self) -> Arc<dyn Platform> {
         self.0.borrow().platform().clone()
     }
@@ -452,6 +388,42 @@ impl BorrowAppContext for AsyncAppContext {
     }
 }
 
+impl BorrowWindowContext for AsyncAppContext {
+    type Result<T> = Option<T>;
+
+    fn read_window<T, F>(&self, window: AnyWindowHandle, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&WindowContext) -> T,
+    {
+        self.0.borrow().read_with(|cx| cx.read_window(window, f))
+    }
+
+    fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&WindowContext) -> Option<T>,
+    {
+        self.0
+            .borrow_mut()
+            .update(|cx| cx.read_window_optional(window, f))
+    }
+
+    fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&mut WindowContext) -> T,
+    {
+        self.0.borrow_mut().update(|cx| cx.update_window(window, f))
+    }
+
+    fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&mut WindowContext) -> Option<T>,
+    {
+        self.0
+            .borrow_mut()
+            .update(|cx| cx.update_window_optional(window, f))
+    }
+}
+
 type ActionCallback = dyn FnMut(&mut dyn AnyView, &dyn Action, &mut WindowContext, usize);
 type GlobalActionCallback = dyn FnMut(&dyn Action, &mut AppContext);
 
@@ -473,9 +445,9 @@ type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut AppContext) -> b
 
 pub struct AppContext {
     models: HashMap<usize, Box<dyn AnyModel>>,
-    views: HashMap<(usize, usize), Box<dyn AnyView>>,
-    views_metadata: HashMap<(usize, usize), ViewMetadata>,
-    windows: HashMap<usize, Window>,
+    views: HashMap<(AnyWindowHandle, usize), Box<dyn AnyView>>,
+    views_metadata: HashMap<(AnyWindowHandle, usize), ViewMetadata>,
+    windows: HashMap<AnyWindowHandle, Window>,
     globals: HashMap<TypeId, Box<dyn Any>>,
     element_states: HashMap<ElementStateId, Box<dyn Any>>,
     background: Arc<executor::Background>,
@@ -494,8 +466,8 @@ pub struct AppContext {
     // Action Types -> Action Handlers
     global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
     keystroke_matcher: KeymapMatcher,
-    next_entity_id: usize,
-    next_window_id: usize,
+    next_id: usize,
+    // next_window: AnyWindowHandle,
     next_subscription_id: usize,
     frame_count: usize,
 
@@ -506,10 +478,10 @@ pub struct AppContext {
     focus_observations: CallbackCollection<usize, FocusObservationCallback>,
     release_observations: CallbackCollection<usize, ReleaseObservationCallback>,
     action_dispatch_observations: CallbackCollection<(), ActionObservationCallback>,
-    window_activation_observations: CallbackCollection<usize, WindowActivationCallback>,
-    window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
-    window_bounds_observations: CallbackCollection<usize, WindowBoundsCallback>,
-    keystroke_observations: CallbackCollection<usize, KeystrokeCallback>,
+    window_activation_observations: CallbackCollection<AnyWindowHandle, WindowActivationCallback>,
+    window_fullscreen_observations: CallbackCollection<AnyWindowHandle, WindowFullscreenCallback>,
+    window_bounds_observations: CallbackCollection<AnyWindowHandle, WindowBoundsCallback>,
+    keystroke_observations: CallbackCollection<AnyWindowHandle, KeystrokeCallback>,
     active_labeled_task_observations: CallbackCollection<(), ActiveLabeledTasksCallback>,
 
     foreground: Rc<executor::Foreground>,
@@ -554,8 +526,7 @@ impl AppContext {
             actions: Default::default(),
             global_actions: Default::default(),
             keystroke_matcher: KeymapMatcher::default(),
-            next_entity_id: 0,
-            next_window_id: 0,
+            next_id: 0,
             next_subscription_id: 0,
             frame_count: 0,
             subscriptions: Default::default(),
@@ -756,13 +727,13 @@ impl AppContext {
         }
     }
 
-    pub fn view_ui_name(&self, window_id: usize, view_id: usize) -> Option<&'static str> {
-        Some(self.views.get(&(window_id, view_id))?.ui_name())
+    pub fn view_ui_name(&self, window: AnyWindowHandle, view_id: usize) -> Option<&'static str> {
+        Some(self.views.get(&(window, view_id))?.ui_name())
     }
 
-    pub fn view_type_id(&self, window_id: usize, view_id: usize) -> Option<TypeId> {
+    pub fn view_type_id(&self, window: AnyWindowHandle, view_id: usize) -> Option<TypeId> {
         self.views_metadata
-            .get(&(window_id, view_id))
+            .get(&(window, view_id))
             .map(|metadata| metadata.type_id)
     }
 
@@ -783,39 +754,22 @@ impl AppContext {
         result
     }
 
-    pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
+    fn read_window<T, F: FnOnce(&WindowContext) -> T>(
         &self,
-        window_id: usize,
+        handle: AnyWindowHandle,
         callback: F,
     ) -> Option<T> {
-        let window = self.windows.get(&window_id)?;
-        let window_context = WindowContext::immutable(self, &window, window_id);
+        let window = self.windows.get(&handle)?;
+        let window_context = WindowContext::immutable(self, &window, handle);
         Some(callback(&window_context))
     }
 
-    pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
-        &mut self,
-        window_id: usize,
-        callback: F,
-    ) -> Option<T> {
-        self.update(|app_context| {
-            let mut window = app_context.windows.remove(&window_id)?;
-            let mut window_context = WindowContext::mutable(app_context, &mut window, window_id);
-            let result = callback(&mut window_context);
-            if !window_context.removed {
-                app_context.windows.insert(window_id, window);
-            }
-            Some(result)
-        })
-    }
-
     pub fn update_active_window<T, F: FnOnce(&mut WindowContext) -> T>(
         &mut self,
         callback: F,
     ) -> Option<T> {
-        self.platform
-            .main_window_id()
-            .and_then(|id| self.update_window(id, callback))
+        self.active_window()
+            .and_then(|window| window.update(self, callback))
     }
 
     pub fn prompt_for_paths(
@@ -1053,10 +1007,10 @@ impl AppContext {
         }
     }
 
-    fn notify_view(&mut self, window_id: usize, view_id: usize) {
+    fn notify_view(&mut self, window: AnyWindowHandle, view_id: usize) {
         if self.pending_notifications.insert(view_id) {
             self.pending_effects
-                .push_back(Effect::ViewNotification { window_id, view_id });
+                .push_back(Effect::ViewNotification { window, view_id });
         }
     }
 
@@ -1074,13 +1028,13 @@ impl AppContext {
     pub fn is_action_available(&self, action: &dyn Action) -> bool {
         let mut available_in_window = false;
         let action_id = action.id();
-        if let Some(window_id) = self.platform.main_window_id() {
+        if let Some(window) = self.active_window() {
             available_in_window = self
-                .read_window(window_id, |cx| {
+                .read_window(window, |cx| {
                     if let Some(focused_view_id) = cx.focused_view_id() {
                         for view_id in cx.ancestors(focused_view_id) {
                             if let Some(view_metadata) =
-                                cx.views_metadata.get(&(window_id, view_id))
+                                cx.views_metadata.get(&(cx.window_handle, view_id))
                             {
                                 if let Some(actions) = cx.actions.get(&view_metadata.type_id) {
                                     if actions.contains_key(&action_id) {
@@ -1128,6 +1082,12 @@ impl AppContext {
         self.keystroke_matcher.clear_bindings();
     }
 
+    pub fn binding_for_action(&self, action: &dyn Action) -> Option<&Binding> {
+        self.keystroke_matcher
+            .bindings_for_action(action.id())
+            .find(|binding| binding.action().eq(action))
+    }
+
     pub fn default_global<T: 'static + Default>(&mut self) -> &T {
         let type_id = TypeId::of::<T>();
         self.update(|this| {
@@ -1220,7 +1180,7 @@ impl AppContext {
         F: FnOnce(&mut ModelContext<T>) -> T,
     {
         self.update(|this| {
-            let model_id = post_inc(&mut this.next_entity_id);
+            let model_id = post_inc(&mut this.next_id);
             let handle = ModelHandle::new(model_id, &this.ref_counts);
             let mut cx = ModelContext::new(this, model_id);
             let model = build_model(&mut cx);
@@ -1294,46 +1254,40 @@ impl AppContext {
         &mut self,
         window_options: WindowOptions,
         build_root_view: F,
-    ) -> (usize, ViewHandle<V>)
+    ) -> WindowHandle<V>
     where
         V: View,
         F: FnOnce(&mut ViewContext<V>) -> V,
     {
         self.update(|this| {
-            let window_id = post_inc(&mut this.next_window_id);
+            let handle = WindowHandle::<V>::new(post_inc(&mut this.next_id));
             let platform_window =
                 this.platform
-                    .open_window(window_id, window_options, this.foreground.clone());
-            let window = this.build_window(window_id, platform_window, build_root_view);
-            let root_view = window.root_view().clone().downcast::<V>().unwrap();
-            this.windows.insert(window_id, window);
-            (window_id, root_view)
+                    .open_window(handle.into(), window_options, this.foreground.clone());
+            let window = this.build_window(handle.into(), platform_window, build_root_view);
+            this.windows.insert(handle.into(), window);
+            handle
         })
     }
 
-    pub fn add_status_bar_item<V, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<V>)
+    pub fn add_status_bar_item<V, F>(&mut self, build_root_view: F) -> WindowHandle<V>
     where
         V: View,
         F: FnOnce(&mut ViewContext<V>) -> V,
     {
         self.update(|this| {
-            let window_id = post_inc(&mut this.next_window_id);
-            let platform_window = this.platform.add_status_item(window_id);
-            let window = this.build_window(window_id, platform_window, build_root_view);
-            let root_view = window.root_view().clone().downcast::<V>().unwrap();
-
-            this.windows.insert(window_id, window);
-            this.update_window(window_id, |cx| {
-                root_view.update(cx, |view, cx| view.focus_in(cx.handle().into_any(), cx))
-            });
-
-            (window_id, root_view)
+            let handle = WindowHandle::<V>::new(post_inc(&mut this.next_id));
+            let platform_window = this.platform.add_status_item(handle.into());
+            let window = this.build_window(handle.into(), platform_window, build_root_view);
+            this.windows.insert(handle.into(), window);
+            handle.update_root(this, |view, cx| view.focus_in(cx.handle().into_any(), cx));
+            handle
         })
     }
 
     pub fn build_window<V, F>(
         &mut self,
-        window_id: usize,
+        handle: AnyWindowHandle,
         mut platform_window: Box<dyn platform::Window>,
         build_root_view: F,
     ) -> Window
@@ -1345,7 +1299,7 @@ impl AppContext {
             let mut app = self.upgrade();
 
             platform_window.on_event(Box::new(move |event| {
-                app.update_window(window_id, |cx| {
+                app.update_window(handle, |cx| {
                     if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = &event {
                         if cx.dispatch_keystroke(keystroke) {
                             return true;
@@ -1361,35 +1315,35 @@ impl AppContext {
         {
             let mut app = self.upgrade();
             platform_window.on_active_status_change(Box::new(move |is_active| {
-                app.update(|cx| cx.window_changed_active_status(window_id, is_active))
+                app.update(|cx| cx.window_changed_active_status(handle, is_active))
             }));
         }
 
         {
             let mut app = self.upgrade();
             platform_window.on_resize(Box::new(move || {
-                app.update(|cx| cx.window_was_resized(window_id))
+                app.update(|cx| cx.window_was_resized(handle))
             }));
         }
 
         {
             let mut app = self.upgrade();
             platform_window.on_moved(Box::new(move || {
-                app.update(|cx| cx.window_was_moved(window_id))
+                app.update(|cx| cx.window_was_moved(handle))
             }));
         }
 
         {
             let mut app = self.upgrade();
             platform_window.on_fullscreen(Box::new(move |is_fullscreen| {
-                app.update(|cx| cx.window_was_fullscreen_changed(window_id, is_fullscreen))
+                app.update(|cx| cx.window_was_fullscreen_changed(handle, is_fullscreen))
             }));
         }
 
         {
             let mut app = self.upgrade();
             platform_window.on_close(Box::new(move || {
-                app.update(|cx| cx.update_window(window_id, |cx| cx.remove_window()));
+                app.update(|cx| cx.update_window(handle, |cx| cx.remove_window()));
             }));
         }
 
@@ -1401,31 +1355,27 @@ impl AppContext {
 
         platform_window.set_input_handler(Box::new(WindowInputHandler {
             app: self.upgrade().0,
-            window_id,
+            window: handle,
         }));
 
-        let mut window = Window::new(window_id, platform_window, self, build_root_view);
-        let mut cx = WindowContext::mutable(self, &mut window, window_id);
+        let mut window = Window::new(handle, platform_window, self, build_root_view);
+        let mut cx = WindowContext::mutable(self, &mut window, handle);
         cx.layout(false).expect("initial layout should not error");
         let scene = cx.paint().expect("initial paint should not error");
         window.platform_window.present_scene(scene);
         window
     }
 
-    pub fn replace_root_view<V, F>(
-        &mut self,
-        window_id: usize,
-        build_root_view: F,
-    ) -> Option<ViewHandle<V>>
-    where
-        V: View,
-        F: FnOnce(&mut ViewContext<V>) -> V,
-    {
-        self.update_window(window_id, |cx| cx.replace_root_view(build_root_view))
+    pub fn active_window(&self) -> Option<AnyWindowHandle> {
+        self.platform.main_window()
+    }
+
+    pub fn windows(&self) -> impl '_ + Iterator<Item = AnyWindowHandle> {
+        self.windows.keys().copied()
     }
 
     pub fn read_view<T: View>(&self, handle: &ViewHandle<T>) -> &T {
-        if let Some(view) = self.views.get(&(handle.window_id, handle.view_id)) {
+        if let Some(view) = self.views.get(&(handle.window, handle.view_id)) {
             view.as_any().downcast_ref().expect("downcast is type safe")
         } else {
             panic!("circular view reference for type {}", type_name::<T>());
@@ -1435,7 +1385,7 @@ impl AppContext {
     fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
         if self.ref_counts.lock().is_entity_alive(handle.view_id) {
             Some(ViewHandle::new(
-                handle.window_id,
+                handle.window,
                 handle.view_id,
                 &self.ref_counts,
             ))
@@ -1447,7 +1397,7 @@ impl AppContext {
     fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
         if self.ref_counts.lock().is_entity_alive(handle.view_id) {
             Some(AnyViewHandle::new(
-                handle.window_id,
+                handle.window,
                 handle.view_id,
                 handle.view_type,
                 self.ref_counts.clone(),
@@ -1477,13 +1427,13 @@ impl AppContext {
                     .push_back(Effect::ModelRelease { model_id, model });
             }
 
-            for (window_id, view_id) in dropped_views {
+            for (window, view_id) in dropped_views {
                 self.subscriptions.remove(view_id);
                 self.observations.remove(view_id);
-                self.views_metadata.remove(&(window_id, view_id));
-                let mut view = self.views.remove(&(window_id, view_id)).unwrap();
+                self.views_metadata.remove(&(window, view_id));
+                let mut view = self.views.remove(&(window, view_id)).unwrap();
                 view.release(self);
-                if let Some(window) = self.windows.get_mut(&window_id) {
+                if let Some(window) = self.windows.get_mut(&window) {
                     window.parents.remove(&view_id);
                     window
                         .invalidation
@@ -1511,7 +1461,7 @@ impl AppContext {
 
             let mut refreshing = false;
             let mut updated_windows = HashSet::default();
-            let mut focus_effects = HashMap::<usize, FocusEffect>::default();
+            let mut focus_effects = HashMap::<AnyWindowHandle, FocusEffect>::default();
             loop {
                 self.remove_dropped_entities();
                 if let Some(effect) = self.pending_effects.pop_front() {
@@ -1555,9 +1505,10 @@ impl AppContext {
                             observations.emit(model_id, |callback| callback(self));
                         }
 
-                        Effect::ViewNotification { window_id, view_id } => {
-                            self.handle_view_notification_effect(window_id, view_id)
-                        }
+                        Effect::ViewNotification {
+                            window: window_id,
+                            view_id,
+                        } => self.handle_view_notification_effect(window_id, view_id),
 
                         Effect::GlobalNotification { type_id } => {
                             let mut subscriptions = self.global_observations.clone();
@@ -1588,13 +1539,13 @@ impl AppContext {
 
                         Effect::Focus(mut effect) => {
                             if focus_effects
-                                .get(&effect.window_id())
+                                .get(&effect.window())
                                 .map_or(false, |prev_effect| prev_effect.is_forced())
                             {
                                 effect.force();
                             }
 
-                            focus_effects.insert(effect.window_id(), effect);
+                            focus_effects.insert(effect.window(), effect);
                         }
 
                         Effect::FocusObservation {
@@ -1609,42 +1560,38 @@ impl AppContext {
                             );
                         }
 
-                        Effect::ResizeWindow { window_id } => {
-                            if let Some(window) = self.windows.get_mut(&window_id) {
+                        Effect::ResizeWindow { window } => {
+                            if let Some(window) = self.windows.get_mut(&window) {
                                 window
                                     .invalidation
                                     .get_or_insert(WindowInvalidation::default());
                             }
-                            self.handle_window_moved(window_id);
+                            self.handle_window_moved(window);
                         }
 
-                        Effect::MoveWindow { window_id } => {
-                            self.handle_window_moved(window_id);
+                        Effect::MoveWindow { window } => {
+                            self.handle_window_moved(window);
                         }
 
                         Effect::WindowActivationObservation {
-                            window_id,
+                            window,
                             subscription_id,
                             callback,
                         } => self.window_activation_observations.add_callback(
-                            window_id,
+                            window,
                             subscription_id,
                             callback,
                         ),
 
-                        Effect::ActivateWindow {
-                            window_id,
-                            is_active,
-                        } => {
-                            if self.handle_window_activation_effect(window_id, is_active)
-                                && is_active
+                        Effect::ActivateWindow { window, is_active } => {
+                            if self.handle_window_activation_effect(window, is_active) && is_active
                             {
                                 focus_effects
-                                    .entry(window_id)
+                                    .entry(window)
                                     .or_insert_with(|| FocusEffect::View {
-                                        window_id,
+                                        window,
                                         view_id: self
-                                            .read_window(window_id, |cx| cx.focused_view_id())
+                                            .read_window(window, |cx| cx.focused_view_id())
                                             .flatten(),
                                         is_forced: true,
                                     })
@@ -1653,26 +1600,26 @@ impl AppContext {
                         }
 
                         Effect::WindowFullscreenObservation {
-                            window_id,
+                            window,
                             subscription_id,
                             callback,
                         } => self.window_fullscreen_observations.add_callback(
-                            window_id,
+                            window,
                             subscription_id,
                             callback,
                         ),
 
                         Effect::FullscreenWindow {
-                            window_id,
+                            window,
                             is_fullscreen,
-                        } => self.handle_fullscreen_effect(window_id, is_fullscreen),
+                        } => self.handle_fullscreen_effect(window, is_fullscreen),
 
                         Effect::WindowBoundsObservation {
-                            window_id,
+                            window,
                             subscription_id,
                             callback,
                         } => self.window_bounds_observations.add_callback(
-                            window_id,
+                            window,
                             subscription_id,
                             callback,
                         ),
@@ -1684,18 +1631,15 @@ impl AppContext {
                         Effect::ActionDispatchNotification { action_id } => {
                             self.handle_action_dispatch_notification_effect(action_id)
                         }
-                        Effect::WindowShouldCloseSubscription {
-                            window_id,
-                            callback,
-                        } => {
-                            self.handle_window_should_close_subscription_effect(window_id, callback)
+                        Effect::WindowShouldCloseSubscription { window, callback } => {
+                            self.handle_window_should_close_subscription_effect(window, callback)
                         }
                         Effect::Keystroke {
-                            window_id,
+                            window,
                             keystroke,
                             handled_by,
                             result,
-                        } => self.handle_keystroke_effect(window_id, keystroke, handled_by, result),
+                        } => self.handle_keystroke_effect(window, keystroke, handled_by, result),
                         Effect::ActiveLabeledTasksChanged => {
                             self.handle_active_labeled_tasks_changed_effect()
                         }
@@ -1710,8 +1654,8 @@ impl AppContext {
                     }
                     self.pending_notifications.clear();
                 } else {
-                    for window_id in self.windows.keys().cloned().collect::<Vec<_>>() {
-                        self.update_window(window_id, |cx| {
+                    for window in self.windows().collect::<Vec<_>>() {
+                        self.update_window(window, |cx| {
                             let invalidation = if refreshing {
                                 let mut invalidation =
                                     cx.window.invalidation.take().unwrap_or_default();
@@ -1727,7 +1671,7 @@ impl AppContext {
                                 let appearance = cx.window.platform_window.appearance();
                                 cx.invalidate(invalidation, appearance);
                                 if let Some(old_parents) = cx.layout(refreshing).log_err() {
-                                    updated_windows.insert(window_id);
+                                    updated_windows.insert(window);
 
                                     if let Some(focused_view_id) = cx.focused_view_id() {
                                         let old_ancestors = std::iter::successors(
@@ -1742,15 +1686,14 @@ impl AppContext {
                                         for old_ancestor in old_ancestors.iter().copied() {
                                             if !new_ancestors.contains(&old_ancestor) {
                                                 if let Some(mut view) =
-                                                    cx.views.remove(&(window_id, old_ancestor))
+                                                    cx.views.remove(&(window, old_ancestor))
                                                 {
                                                     view.focus_out(
                                                         focused_view_id,
                                                         cx,
                                                         old_ancestor,
                                                     );
-                                                    cx.views
-                                                        .insert((window_id, old_ancestor), view);
+                                                    cx.views.insert((window, old_ancestor), view);
                                                 }
                                             }
                                         }
@@ -1759,15 +1702,14 @@ impl AppContext {
                                         for new_ancestor in new_ancestors.iter().copied() {
                                             if !old_ancestors.contains(&new_ancestor) {
                                                 if let Some(mut view) =
-                                                    cx.views.remove(&(window_id, new_ancestor))
+                                                    cx.views.remove(&(window, new_ancestor))
                                                 {
                                                     view.focus_in(
                                                         focused_view_id,
                                                         cx,
                                                         new_ancestor,
                                                     );
-                                                    cx.views
-                                                        .insert((window_id, new_ancestor), view);
+                                                    cx.views.insert((window, new_ancestor), view);
                                                 }
                                             }
                                         }
@@ -1776,13 +1718,13 @@ impl AppContext {
                                         // there isn't any pending focus, focus the root view.
                                         let root_view_id = cx.window.root_view().id();
                                         if focused_view_id != root_view_id
-                                            && !cx.views.contains_key(&(window_id, focused_view_id))
-                                            && !focus_effects.contains_key(&window_id)
+                                            && !cx.views.contains_key(&(window, focused_view_id))
+                                            && !focus_effects.contains_key(&window)
                                         {
                                             focus_effects.insert(
-                                                window_id,
+                                                window,
                                                 FocusEffect::View {
-                                                    window_id,
+                                                    window,
                                                     view_id: Some(root_view_id),
                                                     is_forced: false,
                                                 },
@@ -1803,8 +1745,8 @@ impl AppContext {
                             callback(self);
                         }
 
-                        for window_id in updated_windows.drain() {
-                            self.update_window(window_id, |cx| {
+                        for window in updated_windows.drain() {
+                            self.update_window(window, |cx| {
                                 if let Some(scene) = cx.paint().log_err() {
                                     cx.window.platform_window.present_scene(scene);
                                 }
@@ -1825,39 +1767,37 @@ impl AppContext {
         }
     }
 
-    fn window_was_resized(&mut self, window_id: usize) {
+    fn window_was_resized(&mut self, window: AnyWindowHandle) {
         self.pending_effects
-            .push_back(Effect::ResizeWindow { window_id });
+            .push_back(Effect::ResizeWindow { window });
     }
 
-    fn window_was_moved(&mut self, window_id: usize) {
+    fn window_was_moved(&mut self, window: AnyWindowHandle) {
         self.pending_effects
-            .push_back(Effect::MoveWindow { window_id });
+            .push_back(Effect::MoveWindow { window });
     }
 
-    fn window_was_fullscreen_changed(&mut self, window_id: usize, is_fullscreen: bool) {
+    fn window_was_fullscreen_changed(&mut self, window: AnyWindowHandle, is_fullscreen: bool) {
         self.pending_effects.push_back(Effect::FullscreenWindow {
-            window_id,
+            window,
             is_fullscreen,
         });
     }
 
-    fn window_changed_active_status(&mut self, window_id: usize, is_active: bool) {
-        self.pending_effects.push_back(Effect::ActivateWindow {
-            window_id,
-            is_active,
-        });
+    fn window_changed_active_status(&mut self, window: AnyWindowHandle, is_active: bool) {
+        self.pending_effects
+            .push_back(Effect::ActivateWindow { window, is_active });
     }
 
     fn keystroke(
         &mut self,
-        window_id: usize,
+        window: AnyWindowHandle,
         keystroke: Keystroke,
         handled_by: Option<Box<dyn Action>>,
         result: MatchResult,
     ) {
         self.pending_effects.push_back(Effect::Keystroke {
-            window_id,
+            window,
             keystroke,
             handled_by,
             result,
@@ -1880,16 +1820,16 @@ impl AppContext {
 
     fn handle_view_notification_effect(
         &mut self,
-        observed_window_id: usize,
+        observed_window: AnyWindowHandle,
         observed_view_id: usize,
     ) {
-        let view_key = (observed_window_id, observed_view_id);
+        let view_key = (observed_window, observed_view_id);
         if let Some((view, mut view_metadata)) = self
             .views
             .remove(&view_key)
             .zip(self.views_metadata.remove(&view_key))
         {
-            if let Some(window) = self.windows.get_mut(&observed_window_id) {
+            if let Some(window) = self.windows.get_mut(&observed_window) {
                 window
                     .invalidation
                     .get_or_insert_with(Default::default)
@@ -1916,17 +1856,17 @@ impl AppContext {
             })
     }
 
-    fn handle_fullscreen_effect(&mut self, window_id: usize, is_fullscreen: bool) {
-        self.update_window(window_id, |cx| {
+    fn handle_fullscreen_effect(&mut self, window: AnyWindowHandle, is_fullscreen: bool) {
+        self.update_window(window, |cx| {
             cx.window.is_fullscreen = is_fullscreen;
 
             let mut fullscreen_observations = cx.window_fullscreen_observations.clone();
-            fullscreen_observations.emit(window_id, |callback| callback(is_fullscreen, cx));
+            fullscreen_observations.emit(window, |callback| callback(is_fullscreen, cx));
 
             if let Some(uuid) = cx.window_display_uuid() {
                 let bounds = cx.window_bounds();
                 let mut bounds_observations = cx.window_bounds_observations.clone();
-                bounds_observations.emit(window_id, |callback| callback(bounds, uuid, cx));
+                bounds_observations.emit(window, |callback| callback(bounds, uuid, cx));
             }
 
             Some(())
@@ -1935,42 +1875,42 @@ impl AppContext {
 
     fn handle_keystroke_effect(
         &mut self,
-        window_id: usize,
+        window: AnyWindowHandle,
         keystroke: Keystroke,
         handled_by: Option<Box<dyn Action>>,
         result: MatchResult,
     ) {
-        self.update_window(window_id, |cx| {
+        self.update_window(window, |cx| {
             let mut observations = cx.keystroke_observations.clone();
-            observations.emit(window_id, move |callback| {
+            observations.emit(window, move |callback| {
                 callback(&keystroke, &result, handled_by.as_ref(), cx)
             });
         });
     }
 
-    fn handle_window_activation_effect(&mut self, window_id: usize, active: bool) -> bool {
-        self.update_window(window_id, |cx| {
+    fn handle_window_activation_effect(&mut self, window: AnyWindowHandle, active: bool) -> bool {
+        self.update_window(window, |cx| {
             if cx.window.is_active == active {
                 return false;
             }
             cx.window.is_active = active;
 
             let mut observations = cx.window_activation_observations.clone();
-            observations.emit(window_id, |callback| callback(active, cx));
+            observations.emit(window, |callback| callback(active, cx));
             true
         })
         .unwrap_or(false)
     }
 
     fn handle_focus_effect(&mut self, effect: FocusEffect) {
-        let window_id = effect.window_id();
-        self.update_window(window_id, |cx| {
+        let window = effect.window();
+        self.update_window(window, |cx| {
             // Ensure the newly-focused view still exists, otherwise focus
             // the root view instead.
             let focused_id = match effect {
                 FocusEffect::View { view_id, .. } => {
                     if let Some(view_id) = view_id {
-                        if cx.views.contains_key(&(window_id, view_id)) {
+                        if cx.views.contains_key(&(window, view_id)) {
                             Some(view_id)
                         } else {
                             Some(cx.root_view().id())
@@ -1995,9 +1935,9 @@ impl AppContext {
             if focus_changed {
                 if let Some(blurred_id) = blurred_id {
                     for view_id in cx.ancestors(blurred_id).collect::<Vec<_>>() {
-                        if let Some(mut view) = cx.views.remove(&(window_id, view_id)) {
+                        if let Some(mut view) = cx.views.remove(&(window, view_id)) {
                             view.focus_out(blurred_id, cx, view_id);
-                            cx.views.insert((window_id, view_id), view);
+                            cx.views.insert((window, view_id), view);
                         }
                     }
 
@@ -2009,9 +1949,9 @@ impl AppContext {
             if focus_changed || effect.is_forced() {
                 if let Some(focused_id) = focused_id {
                     for view_id in cx.ancestors(focused_id).collect::<Vec<_>>() {
-                        if let Some(mut view) = cx.views.remove(&(window_id, view_id)) {
+                        if let Some(mut view) = cx.views.remove(&(window, view_id)) {
                             view.focus_in(focused_id, cx, view_id);
-                            cx.views.insert((window_id, view_id), view);
+                            cx.views.insert((window, view_id), view);
                         }
                     }
 
@@ -2033,24 +1973,24 @@ impl AppContext {
 
     fn handle_window_should_close_subscription_effect(
         &mut self,
-        window_id: usize,
+        window: AnyWindowHandle,
         mut callback: WindowShouldCloseSubscriptionCallback,
     ) {
         let mut app = self.upgrade();
-        if let Some(window) = self.windows.get_mut(&window_id) {
+        if let Some(window) = self.windows.get_mut(&window) {
             window
                 .platform_window
                 .on_should_close(Box::new(move || app.update(|cx| callback(cx))))
         }
     }
 
-    fn handle_window_moved(&mut self, window_id: usize) {
-        self.update_window(window_id, |cx| {
+    fn handle_window_moved(&mut self, window: AnyWindowHandle) {
+        self.update_window(window, |cx| {
             if let Some(display) = cx.window_display_uuid() {
                 let bounds = cx.window_bounds();
                 cx.window_bounds_observations
                     .clone()
-                    .emit(window_id, move |callback| {
+                    .emit(window, move |callback| {
                         callback(bounds, display, cx);
                         true
                     });
@@ -2067,10 +2007,10 @@ impl AppContext {
             });
     }
 
-    pub fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
+    pub fn focus(&mut self, window: AnyWindowHandle, view_id: Option<usize>) {
         self.pending_effects
             .push_back(Effect::Focus(FocusEffect::View {
-                window_id,
+                window,
                 view_id,
                 is_forced: false,
             }));

crates/gpui/src/app/menu.rs 🔗

@@ -77,9 +77,9 @@ pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform,
         let cx = app.0.clone();
         move |action| {
             let mut cx = cx.borrow_mut();
-            if let Some(main_window_id) = cx.platform.main_window_id() {
-                let dispatched = cx
-                    .update_window(main_window_id, |cx| {
+            if let Some(main_window) = cx.active_window() {
+                let dispatched = main_window
+                    .update(&mut *cx, |cx| {
                         if let Some(view_id) = cx.focused_view_id() {
                             cx.dispatch_action(Some(view_id), action);
                             true

crates/gpui/src/app/ref_counts.rs 🔗

@@ -9,7 +9,7 @@ use collections::{hash_map::Entry, HashMap, HashSet};
 
 #[cfg(any(test, feature = "test-support"))]
 use crate::util::post_inc;
-use crate::ElementStateId;
+use crate::{AnyWindowHandle, ElementStateId};
 
 lazy_static! {
     static ref LEAK_BACKTRACE: bool =
@@ -26,7 +26,7 @@ pub struct RefCounts {
     entity_counts: HashMap<usize, usize>,
     element_state_counts: HashMap<ElementStateId, ElementStateRefCount>,
     dropped_models: HashSet<usize>,
-    dropped_views: HashSet<(usize, usize)>,
+    dropped_views: HashSet<(AnyWindowHandle, usize)>,
     dropped_element_states: HashSet<ElementStateId>,
 
     #[cfg(any(test, feature = "test-support"))]
@@ -55,12 +55,12 @@ impl RefCounts {
         }
     }
 
-    pub fn inc_view(&mut self, window_id: usize, view_id: usize) {
+    pub fn inc_view(&mut self, window: AnyWindowHandle, view_id: usize) {
         match self.entity_counts.entry(view_id) {
             Entry::Occupied(mut entry) => *entry.get_mut() += 1,
             Entry::Vacant(entry) => {
                 entry.insert(1);
-                self.dropped_views.remove(&(window_id, view_id));
+                self.dropped_views.remove(&(window, view_id));
             }
         }
     }
@@ -94,12 +94,12 @@ impl RefCounts {
         }
     }
 
-    pub fn dec_view(&mut self, window_id: usize, view_id: usize) {
+    pub fn dec_view(&mut self, window: AnyWindowHandle, view_id: usize) {
         let count = self.entity_counts.get_mut(&view_id).unwrap();
         *count -= 1;
         if *count == 0 {
             self.entity_counts.remove(&view_id);
-            self.dropped_views.insert((window_id, view_id));
+            self.dropped_views.insert((window, view_id));
         }
     }
 
@@ -120,7 +120,7 @@ impl RefCounts {
         &mut self,
     ) -> (
         HashSet<usize>,
-        HashSet<(usize, usize)>,
+        HashSet<(AnyWindowHandle, usize)>,
         HashSet<ElementStateId>,
     ) {
         (

crates/gpui/src/app/test_app_context.rs 🔗

@@ -4,9 +4,9 @@ use crate::{
     keymap_matcher::{Binding, Keystroke},
     platform,
     platform::{Event, InputHandler, KeyDownEvent, Platform},
-    Action, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache, Handle,
-    ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakHandle,
-    WindowContext,
+    Action, AnyWindowHandle, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache,
+    Handle, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
+    WeakHandle, WindowContext, WindowHandle,
 };
 use collections::BTreeMap;
 use futures::Future;
@@ -60,7 +60,7 @@ impl TestAppContext {
             RefCounts::new(leak_detector),
             (),
         );
-        cx.next_entity_id = first_entity_id;
+        cx.next_id = first_entity_id;
         let cx = TestAppContext {
             cx: Rc::new(RefCell::new(cx)),
             foreground_platform,
@@ -72,8 +72,8 @@ impl TestAppContext {
         cx
     }
 
-    pub fn dispatch_action<A: Action>(&mut self, window_id: usize, action: A) {
-        self.update_window(window_id, |window| {
+    pub fn dispatch_action<A: Action>(&mut self, window: AnyWindowHandle, action: A) {
+        self.update_window(window, |window| {
             window.dispatch_action(window.focused_view_id(), &action);
         })
         .expect("window not found");
@@ -81,10 +81,10 @@ impl TestAppContext {
 
     pub fn available_actions(
         &self,
-        window_id: usize,
+        window: AnyWindowHandle,
         view_id: usize,
     ) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
-        self.read_window(window_id, |cx| cx.available_actions(view_id))
+        self.read_window(window, |cx| cx.available_actions(view_id))
             .unwrap_or_default()
     }
 
@@ -92,33 +92,34 @@ impl TestAppContext {
         self.update(|cx| cx.dispatch_global_action_any(&action));
     }
 
-    pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
-        let handled = self
-            .cx
-            .borrow_mut()
-            .update_window(window_id, |cx| {
-                if cx.dispatch_keystroke(&keystroke) {
-                    return true;
-                }
+    pub fn dispatch_keystroke(
+        &mut self,
+        window: AnyWindowHandle,
+        keystroke: Keystroke,
+        is_held: bool,
+    ) {
+        let handled = window.update(self, |cx| {
+            if cx.dispatch_keystroke(&keystroke) {
+                return true;
+            }
 
-                if cx.dispatch_event(
-                    Event::KeyDown(KeyDownEvent {
-                        keystroke: keystroke.clone(),
-                        is_held,
-                    }),
-                    false,
-                ) {
-                    return true;
-                }
+            if cx.dispatch_event(
+                Event::KeyDown(KeyDownEvent {
+                    keystroke: keystroke.clone(),
+                    is_held,
+                }),
+                false,
+            ) {
+                return true;
+            }
 
-                false
-            })
-            .unwrap_or(false);
+            false
+        });
 
         if !handled && !keystroke.cmd && !keystroke.ctrl {
             WindowInputHandler {
                 app: self.cx.clone(),
-                window_id,
+                window,
             }
             .replace_text_in_range(None, &keystroke.key)
         }
@@ -126,18 +127,18 @@ impl TestAppContext {
 
     pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
         &self,
-        window_id: usize,
+        window: AnyWindowHandle,
         callback: F,
     ) -> Option<T> {
-        self.cx.borrow().read_window(window_id, callback)
+        self.cx.borrow().read_window(window, callback)
     }
 
     pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
         &mut self,
-        window_id: usize,
+        window: AnyWindowHandle,
         callback: F,
     ) -> Option<T> {
-        self.cx.borrow_mut().update_window(window_id, callback)
+        self.cx.borrow_mut().update_window(window, callback)
     }
 
     pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
@@ -148,26 +149,17 @@ impl TestAppContext {
         self.cx.borrow_mut().add_model(build_model)
     }
 
-    pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
+    pub fn add_window<V, F>(&mut self, build_root_view: F) -> WindowHandle<V>
     where
-        T: View,
-        F: FnOnce(&mut ViewContext<T>) -> T,
+        V: View,
+        F: FnOnce(&mut ViewContext<V>) -> V,
     {
-        let (window_id, view) = self
+        let window = self
             .cx
             .borrow_mut()
             .add_window(Default::default(), build_root_view);
-        self.simulate_window_activation(Some(window_id));
-        (window_id, view)
-    }
-
-    pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
-    where
-        T: View,
-        F: FnOnce(&mut ViewContext<T>) -> T,
-    {
-        self.update_window(window_id, |cx| cx.add_view(build_view))
-            .expect("window not found")
+        window.simulate_activation(self);
+        window
     }
 
     pub fn observe_global<E, F>(&mut self, callback: F) -> Subscription
@@ -190,8 +182,8 @@ impl TestAppContext {
         self.cx.borrow_mut().subscribe_global(callback)
     }
 
-    pub fn window_ids(&self) -> Vec<usize> {
-        self.cx.borrow().windows.keys().copied().collect()
+    pub fn windows(&self) -> Vec<AnyWindowHandle> {
+        self.cx.borrow().windows().collect()
     }
 
     pub fn remove_all_windows(&mut self) {
@@ -261,76 +253,6 @@ impl TestAppContext {
         self.foreground_platform.as_ref().did_prompt_for_new_path()
     }
 
-    pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) {
-        use postage::prelude::Sink as _;
-
-        let mut done_tx = self
-            .platform_window_mut(window_id)
-            .pending_prompts
-            .borrow_mut()
-            .pop_front()
-            .expect("prompt was not called");
-        done_tx.try_send(answer).ok();
-    }
-
-    pub fn has_pending_prompt(&self, window_id: usize) -> bool {
-        let window = self.platform_window_mut(window_id);
-        let prompts = window.pending_prompts.borrow_mut();
-        !prompts.is_empty()
-    }
-
-    pub fn current_window_title(&self, window_id: usize) -> Option<String> {
-        self.platform_window_mut(window_id).title.clone()
-    }
-
-    pub fn simulate_window_close(&self, window_id: usize) -> bool {
-        let handler = self
-            .platform_window_mut(window_id)
-            .should_close_handler
-            .take();
-        if let Some(mut handler) = handler {
-            let should_close = handler();
-            self.platform_window_mut(window_id).should_close_handler = Some(handler);
-            should_close
-        } else {
-            false
-        }
-    }
-
-    pub fn simulate_window_resize(&self, window_id: usize, size: Vector2F) {
-        let mut window = self.platform_window_mut(window_id);
-        window.size = size;
-        let mut handlers = mem::take(&mut window.resize_handlers);
-        drop(window);
-        for handler in &mut handlers {
-            handler();
-        }
-        self.platform_window_mut(window_id).resize_handlers = handlers;
-    }
-
-    pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
-        self.cx.borrow_mut().update(|cx| {
-            let other_window_ids = cx
-                .windows
-                .keys()
-                .filter(|window_id| Some(**window_id) != to_activate)
-                .copied()
-                .collect::<Vec<_>>();
-
-            for window_id in other_window_ids {
-                cx.window_changed_active_status(window_id, false)
-            }
-
-            if let Some(to_activate) = to_activate {
-                cx.window_changed_active_status(to_activate, true)
-            }
-        });
-    }
-
-    pub fn is_window_edited(&self, window_id: usize) -> bool {
-        self.platform_window_mut(window_id).edited
-    }
-
     pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
         self.cx.borrow().leak_detector()
     }
@@ -351,18 +273,6 @@ impl TestAppContext {
         self.assert_dropped(weak);
     }
 
-    fn platform_window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
-        std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
-            let window = state.windows.get_mut(&window_id).unwrap();
-            let test_window = window
-                .platform_window
-                .as_any_mut()
-                .downcast_mut::<platform::test::Window>()
-                .unwrap();
-            test_window
-        })
-    }
-
     pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
         self.condition_duration = duration;
     }
@@ -405,19 +315,39 @@ impl BorrowAppContext for TestAppContext {
 }
 
 impl BorrowWindowContext for TestAppContext {
-    fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
+    type Result<T> = T;
+
+    fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
         self.cx
             .borrow()
-            .read_window(window_id, f)
+            .read_window(window, f)
             .expect("window was closed")
     }
 
-    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
+    fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&WindowContext) -> Option<T>,
+    {
+        BorrowWindowContext::read_window(self, window, f)
+    }
+
+    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
+        &mut self,
+        window: AnyWindowHandle,
+        f: F,
+    ) -> T {
         self.cx
             .borrow_mut()
-            .update_window(window_id, f)
+            .update_window(window, f)
             .expect("window was closed")
     }
+
+    fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&mut WindowContext) -> Option<T>,
+    {
+        BorrowWindowContext::update_window(self, window, f)
+    }
 }
 
 impl<T: Entity> ModelHandle<T> {
@@ -532,6 +462,71 @@ impl<T: Entity> ModelHandle<T> {
     }
 }
 
+impl AnyWindowHandle {
+    pub fn has_pending_prompt(&self, cx: &mut TestAppContext) -> bool {
+        let window = self.platform_window_mut(cx);
+        let prompts = window.pending_prompts.borrow_mut();
+        !prompts.is_empty()
+    }
+
+    pub fn current_title(&self, cx: &mut TestAppContext) -> Option<String> {
+        self.platform_window_mut(cx).title.clone()
+    }
+
+    pub fn simulate_close(&self, cx: &mut TestAppContext) -> bool {
+        let handler = self.platform_window_mut(cx).should_close_handler.take();
+        if let Some(mut handler) = handler {
+            let should_close = handler();
+            self.platform_window_mut(cx).should_close_handler = Some(handler);
+            should_close
+        } else {
+            false
+        }
+    }
+
+    pub fn simulate_resize(&self, size: Vector2F, cx: &mut TestAppContext) {
+        let mut window = self.platform_window_mut(cx);
+        window.size = size;
+        let mut handlers = mem::take(&mut window.resize_handlers);
+        drop(window);
+        for handler in &mut handlers {
+            handler();
+        }
+        self.platform_window_mut(cx).resize_handlers = handlers;
+    }
+
+    pub fn is_edited(&self, cx: &mut TestAppContext) -> bool {
+        self.platform_window_mut(cx).edited
+    }
+
+    pub fn simulate_prompt_answer(&self, answer: usize, cx: &mut TestAppContext) {
+        use postage::prelude::Sink as _;
+
+        let mut done_tx = self
+            .platform_window_mut(cx)
+            .pending_prompts
+            .borrow_mut()
+            .pop_front()
+            .expect("prompt was not called");
+        done_tx.try_send(answer).ok();
+    }
+
+    fn platform_window_mut<'a>(
+        &self,
+        cx: &'a mut TestAppContext,
+    ) -> std::cell::RefMut<'a, platform::test::Window> {
+        std::cell::RefMut::map(cx.cx.borrow_mut(), |state| {
+            let window = state.windows.get_mut(&self).unwrap();
+            let test_window = window
+                .platform_window
+                .as_any_mut()
+                .downcast_mut::<platform::test::Window>()
+                .unwrap();
+            test_window
+        })
+    }
+}
+
 impl<T: View> ViewHandle<T> {
     pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
         use postage::prelude::{Sink as _, Stream as _};

crates/gpui/src/app/window.rs 🔗

@@ -13,9 +13,10 @@ use crate::{
     },
     text_layout::TextLayoutCache,
     util::post_inc,
-    Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
-    Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription,
-    View, ViewContext, ViewHandle, WindowInvalidation,
+    Action, AnyView, AnyViewHandle, AnyWindowHandle, AppContext, BorrowAppContext,
+    BorrowWindowContext, Effect, Element, Entity, Handle, LayoutContext, MouseRegion,
+    MouseRegionId, PaintContext, SceneBuilder, Subscription, View, ViewContext, ViewHandle,
+    WindowInvalidation,
 };
 use anyhow::{anyhow, bail, Result};
 use collections::{HashMap, HashSet};
@@ -51,8 +52,8 @@ pub struct Window {
     cursor_regions: Vec<CursorRegion>,
     mouse_regions: Vec<(MouseRegion, usize)>,
     last_mouse_moved_event: Option<Event>,
-    pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
-    pub(crate) clicked_region_ids: HashSet<MouseRegionId>,
+    pub(crate) hovered_region_ids: Vec<MouseRegionId>,
+    pub(crate) clicked_region_ids: Vec<MouseRegionId>,
     pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
     mouse_position: Vector2F,
     text_layout_cache: TextLayoutCache,
@@ -60,7 +61,7 @@ pub struct Window {
 
 impl Window {
     pub fn new<V, F>(
-        window_id: usize,
+        handle: AnyWindowHandle,
         platform_window: Box<dyn platform::Window>,
         cx: &mut AppContext,
         build_view: F,
@@ -92,7 +93,7 @@ impl Window {
             appearance,
         };
 
-        let mut window_context = WindowContext::mutable(cx, &mut window, window_id);
+        let mut window_context = WindowContext::mutable(cx, &mut window, handle);
         let root_view = window_context.add_view(|cx| build_view(cx));
         if let Some(invalidation) = window_context.window.invalidation.take() {
             window_context.invalidate(invalidation, appearance);
@@ -113,7 +114,7 @@ impl Window {
 pub struct WindowContext<'a> {
     pub(crate) app_context: Reference<'a, AppContext>,
     pub(crate) window: Reference<'a, Window>,
-    pub(crate) window_id: usize,
+    pub(crate) window_handle: AnyWindowHandle,
     pub(crate) removed: bool,
 }
 
@@ -142,42 +143,66 @@ impl BorrowAppContext for WindowContext<'_> {
 }
 
 impl BorrowWindowContext for WindowContext<'_> {
-    fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
-        if self.window_id == window_id {
+    type Result<T> = T;
+
+    fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, handle: AnyWindowHandle, f: F) -> T {
+        if self.window_handle == handle {
             f(self)
         } else {
             panic!("read_with called with id of window that does not belong to this context")
         }
     }
 
-    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
-        if self.window_id == window_id {
+    fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&WindowContext) -> Option<T>,
+    {
+        BorrowWindowContext::read_window(self, window, f)
+    }
+
+    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
+        &mut self,
+        handle: AnyWindowHandle,
+        f: F,
+    ) -> T {
+        if self.window_handle == handle {
             f(self)
         } else {
             panic!("update called with id of window that does not belong to this context")
         }
     }
+
+    fn update_window_optional<T, F>(&mut self, handle: AnyWindowHandle, f: F) -> Option<T>
+    where
+        F: FnOnce(&mut WindowContext) -> Option<T>,
+    {
+        BorrowWindowContext::update_window(self, handle, f)
+    }
 }
 
 impl<'a> WindowContext<'a> {
     pub fn mutable(
         app_context: &'a mut AppContext,
         window: &'a mut Window,
-        window_id: usize,
+        handle: AnyWindowHandle,
     ) -> Self {
         Self {
             app_context: Reference::Mutable(app_context),
             window: Reference::Mutable(window),
-            window_id,
+            window_handle: handle,
             removed: false,
         }
     }
 
-    pub fn immutable(app_context: &'a AppContext, window: &'a Window, window_id: usize) -> Self {
+    pub fn immutable(
+        app_context: &'a AppContext,
+        window: &'a Window,
+        handle: AnyWindowHandle,
+    ) -> Self {
         Self {
             app_context: Reference::Immutable(app_context),
             window: Reference::Immutable(window),
-            window_id,
+            window_handle: handle,
             removed: false,
         }
     }
@@ -186,8 +211,8 @@ impl<'a> WindowContext<'a> {
         self.removed = true;
     }
 
-    pub fn window_id(&self) -> usize {
-        self.window_id
+    pub fn window(&self) -> AnyWindowHandle {
+        self.window_handle
     }
 
     pub fn app_context(&mut self) -> &mut AppContext {
@@ -210,10 +235,10 @@ impl<'a> WindowContext<'a> {
     where
         F: FnOnce(&mut dyn AnyView, &mut Self) -> T,
     {
-        let window_id = self.window_id;
-        let mut view = self.views.remove(&(window_id, view_id))?;
+        let handle = self.window_handle;
+        let mut view = self.views.remove(&(handle, view_id))?;
         let result = f(view.as_mut(), self);
-        self.views.insert((window_id, view_id), view);
+        self.views.insert((handle, view_id), view);
         Some(result)
     }
 
@@ -238,9 +263,9 @@ impl<'a> WindowContext<'a> {
     }
 
     pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut WindowContext)) {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         self.app_context.defer(move |cx| {
-            cx.update_window(window_id, |cx| callback(cx));
+            cx.update_window(handle, |cx| callback(cx));
         })
     }
 
@@ -280,10 +305,10 @@ impl<'a> WindowContext<'a> {
         H: Handle<E>,
         F: 'static + FnMut(H, &E::Event, &mut WindowContext) -> bool,
     {
-        let window_id = self.window_id;
+        let window_handle = self.window_handle;
         self.app_context
             .subscribe_internal(handle, move |emitter, event, cx| {
-                cx.update_window(window_id, |cx| callback(emitter, event, cx))
+                cx.update_window(window_handle, |cx| callback(emitter, event, cx))
                     .unwrap_or(false)
             })
     }
@@ -292,17 +317,17 @@ impl<'a> WindowContext<'a> {
     where
         F: 'static + FnMut(bool, &mut WindowContext) -> bool,
     {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         let subscription_id = post_inc(&mut self.next_subscription_id);
         self.pending_effects
             .push_back(Effect::WindowActivationObservation {
-                window_id,
+                window: handle,
                 subscription_id,
                 callback: Box::new(callback),
             });
         Subscription::WindowActivationObservation(
             self.window_activation_observations
-                .subscribe(window_id, subscription_id),
+                .subscribe(handle, subscription_id),
         )
     }
 
@@ -310,17 +335,17 @@ impl<'a> WindowContext<'a> {
     where
         F: 'static + FnMut(bool, &mut WindowContext) -> bool,
     {
-        let window_id = self.window_id;
+        let window = self.window_handle;
         let subscription_id = post_inc(&mut self.next_subscription_id);
         self.pending_effects
             .push_back(Effect::WindowFullscreenObservation {
-                window_id,
+                window,
                 subscription_id,
                 callback: Box::new(callback),
             });
         Subscription::WindowActivationObservation(
             self.window_activation_observations
-                .subscribe(window_id, subscription_id),
+                .subscribe(window, subscription_id),
         )
     }
 
@@ -328,17 +353,17 @@ impl<'a> WindowContext<'a> {
     where
         F: 'static + FnMut(WindowBounds, Uuid, &mut WindowContext) -> bool,
     {
-        let window_id = self.window_id;
+        let window = self.window_handle;
         let subscription_id = post_inc(&mut self.next_subscription_id);
         self.pending_effects
             .push_back(Effect::WindowBoundsObservation {
-                window_id,
+                window,
                 subscription_id,
                 callback: Box::new(callback),
             });
         Subscription::WindowBoundsObservation(
             self.window_bounds_observations
-                .subscribe(window_id, subscription_id),
+                .subscribe(window, subscription_id),
         )
     }
 
@@ -347,13 +372,13 @@ impl<'a> WindowContext<'a> {
         F: 'static
             + FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut WindowContext) -> bool,
     {
-        let window_id = self.window_id;
+        let window = self.window_handle;
         let subscription_id = post_inc(&mut self.next_subscription_id);
         self.keystroke_observations
-            .add_callback(window_id, subscription_id, Box::new(callback));
+            .add_callback(window, subscription_id, Box::new(callback));
         Subscription::KeystrokeObservation(
             self.keystroke_observations
-                .subscribe(window_id, subscription_id),
+                .subscribe(window, subscription_id),
         )
     }
 
@@ -361,11 +386,11 @@ impl<'a> WindowContext<'a> {
         &self,
         view_id: usize,
     ) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         let mut contexts = Vec::new();
         let mut handler_depths_by_action_id = HashMap::<TypeId, usize>::default();
         for (depth, view_id) in self.ancestors(view_id).enumerate() {
-            if let Some(view_metadata) = self.views_metadata.get(&(window_id, view_id)) {
+            if let Some(view_metadata) = self.views_metadata.get(&(handle, view_id)) {
                 contexts.push(view_metadata.keymap_context.clone());
                 if let Some(actions) = self.actions.get(&view_metadata.type_id) {
                     handler_depths_by_action_id
@@ -410,13 +435,13 @@ impl<'a> WindowContext<'a> {
     }
 
     pub(crate) fn dispatch_keystroke(&mut self, keystroke: &Keystroke) -> bool {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         if let Some(focused_view_id) = self.focused_view_id() {
             let dispatch_path = self
                 .ancestors(focused_view_id)
                 .filter_map(|view_id| {
                     self.views_metadata
-                        .get(&(window_id, view_id))
+                        .get(&(handle, view_id))
                         .map(|view| (view_id, view.keymap_context.clone()))
                 })
                 .collect();
@@ -441,15 +466,10 @@ impl<'a> WindowContext<'a> {
                 }
             };
 
-            self.keystroke(
-                window_id,
-                keystroke.clone(),
-                handled_by,
-                match_result.clone(),
-            );
+            self.keystroke(handle, keystroke.clone(), handled_by, match_result.clone());
             keystroke_handled
         } else {
-            self.keystroke(window_id, keystroke.clone(), None, MatchResult::None);
+            self.keystroke(handle, keystroke.clone(), None, MatchResult::None);
             false
         }
     }
@@ -457,7 +477,7 @@ impl<'a> WindowContext<'a> {
     pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
         let mut mouse_events = SmallVec::<[_; 2]>::new();
         let mut notified_views: HashSet<usize> = Default::default();
-        let window_id = self.window_id;
+        let handle = self.window_handle;
 
         // 1. Handle platform event. Keyboard events get dispatched immediately, while mouse events
         //    get mapped into the mouse-specific MouseEvent type.
@@ -658,6 +678,7 @@ impl<'a> WindowContext<'a> {
                     let mut highest_z_index = None;
                     let mouse_position = self.window.mouse_position.clone();
                     let window = &mut *self.window;
+                    let prev_hovered_regions = mem::take(&mut window.hovered_region_ids);
                     for (region, z_index) in window.mouse_regions.iter().rev() {
                         // Allow mouse regions to appear transparent to hovers
                         if !region.hoverable {
@@ -676,7 +697,11 @@ impl<'a> WindowContext<'a> {
                         // highest_z_index is set.
                         if contains_mouse && z_index == highest_z_index.unwrap() {
                             //Ensure that hover entrance events aren't sent twice
-                            if window.hovered_region_ids.insert(region.id()) {
+                            if let Err(ix) = window.hovered_region_ids.binary_search(&region.id()) {
+                                window.hovered_region_ids.insert(ix, region.id());
+                            }
+                            // window.hovered_region_ids.insert(region.id());
+                            if !prev_hovered_regions.contains(&region.id()) {
                                 valid_regions.push(region.clone());
                                 if region.notify_on_hover {
                                     notified_views.insert(region.id().view_id());
@@ -684,7 +709,7 @@ impl<'a> WindowContext<'a> {
                             }
                         } else {
                             // Ensure that hover exit events aren't sent twice
-                            if window.hovered_region_ids.remove(&region.id()) {
+                            if prev_hovered_regions.contains(&region.id()) {
                                 valid_regions.push(region.clone());
                                 if region.notify_on_hover {
                                     notified_views.insert(region.id().view_id());
@@ -821,19 +846,19 @@ impl<'a> WindowContext<'a> {
         }
 
         for view_id in notified_views {
-            self.notify_view(window_id, view_id);
+            self.notify_view(handle, view_id);
         }
 
         any_event_handled
     }
 
     pub(crate) fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         if let Some(focused_view_id) = self.window.focused_view_id {
             for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
-                if let Some(mut view) = self.views.remove(&(window_id, view_id)) {
+                if let Some(mut view) = self.views.remove(&(handle, view_id)) {
                     let handled = view.key_down(event, self, view_id);
-                    self.views.insert((window_id, view_id), view);
+                    self.views.insert((handle, view_id), view);
                     if handled {
                         return true;
                     }
@@ -847,12 +872,12 @@ impl<'a> WindowContext<'a> {
     }
 
     pub(crate) fn dispatch_key_up(&mut self, event: &KeyUpEvent) -> bool {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         if let Some(focused_view_id) = self.window.focused_view_id {
             for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
-                if let Some(mut view) = self.views.remove(&(window_id, view_id)) {
+                if let Some(mut view) = self.views.remove(&(handle, view_id)) {
                     let handled = view.key_up(event, self, view_id);
-                    self.views.insert((window_id, view_id), view);
+                    self.views.insert((handle, view_id), view);
                     if handled {
                         return true;
                     }
@@ -866,12 +891,12 @@ impl<'a> WindowContext<'a> {
     }
 
     pub(crate) fn dispatch_modifiers_changed(&mut self, event: &ModifiersChangedEvent) -> bool {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         if let Some(focused_view_id) = self.window.focused_view_id {
             for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
-                if let Some(mut view) = self.views.remove(&(window_id, view_id)) {
+                if let Some(mut view) = self.views.remove(&(handle, view_id)) {
                     let handled = view.modifiers_changed(event, self, view_id);
-                    self.views.insert((window_id, view_id), view);
+                    self.views.insert((handle, view_id), view);
                     if handled {
                         return true;
                     }
@@ -906,14 +931,14 @@ impl<'a> WindowContext<'a> {
     }
 
     pub fn render_view(&mut self, params: RenderParams) -> Result<Box<dyn AnyRootElement>> {
-        let window_id = self.window_id;
+        let handle = self.window_handle;
         let view_id = params.view_id;
         let mut view = self
             .views
-            .remove(&(window_id, view_id))
+            .remove(&(handle, view_id))
             .ok_or_else(|| anyhow!("view not found"))?;
         let element = view.render(self, view_id);
-        self.views.insert((window_id, view_id), view);
+        self.views.insert((handle, view_id), view);
         Ok(element)
     }
 
@@ -941,9 +966,9 @@ impl<'a> WindowContext<'a> {
                 } else if old_parent_id == new_parent_id {
                     current_view_id = *old_parent_id.unwrap();
                 } else {
-                    let window_id = self.window_id;
+                    let handle = self.window_handle;
                     for view_id_to_notify in view_ids_to_notify {
-                        self.notify_view(window_id, view_id_to_notify);
+                        self.notify_view(handle, view_id_to_notify);
                     }
                     break;
                 }
@@ -1111,7 +1136,7 @@ impl<'a> WindowContext<'a> {
     }
 
     pub fn focus(&mut self, view_id: Option<usize>) {
-        self.app_context.focus(self.window_id, view_id);
+        self.app_context.focus(self.window_handle, view_id);
     }
 
     pub fn window_bounds(&self) -> WindowBounds {
@@ -1151,17 +1176,6 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.prompt(level, msg, answers)
     }
 
-    pub fn replace_root_view<V, F>(&mut self, build_root_view: F) -> ViewHandle<V>
-    where
-        V: View,
-        F: FnOnce(&mut ViewContext<V>) -> V,
-    {
-        let root_view = self.add_view(|cx| build_root_view(cx));
-        self.window.root_view = Some(root_view.clone().into_any());
-        self.window.focused_view_id = Some(root_view.id());
-        root_view
-    }
-
     pub fn add_view<T, F>(&mut self, build_view: F) -> ViewHandle<T>
     where
         T: View,
@@ -1175,26 +1189,26 @@ impl<'a> WindowContext<'a> {
         T: View,
         F: FnOnce(&mut ViewContext<T>) -> Option<T>,
     {
-        let window_id = self.window_id;
-        let view_id = post_inc(&mut self.next_entity_id);
+        let handle = self.window_handle;
+        let view_id = post_inc(&mut self.next_id);
         let mut cx = ViewContext::mutable(self, view_id);
         let handle = if let Some(view) = build_view(&mut cx) {
             let mut keymap_context = KeymapContext::default();
             view.update_keymap_context(&mut keymap_context, cx.app_context());
             self.views_metadata.insert(
-                (window_id, view_id),
+                (handle, view_id),
                 ViewMetadata {
                     type_id: TypeId::of::<T>(),
                     keymap_context,
                 },
             );
-            self.views.insert((window_id, view_id), Box::new(view));
+            self.views.insert((handle, view_id), Box::new(view));
             self.window
                 .invalidation
                 .get_or_insert_with(Default::default)
                 .updated
                 .insert(view_id);
-            Some(ViewHandle::new(window_id, view_id, &self.ref_counts))
+            Some(ViewHandle::new(handle, view_id, &self.ref_counts))
         } else {
             None
         };
@@ -1371,7 +1385,7 @@ pub struct ChildView {
 
 impl ChildView {
     pub fn new(view: &AnyViewHandle, cx: &AppContext) -> Self {
-        let view_name = cx.view_ui_name(view.window_id(), view.id()).unwrap();
+        let view_name = cx.view_ui_name(view.window, view.id()).unwrap();
         Self {
             view_id: view.id(),
             view_name,
@@ -1420,7 +1434,7 @@ impl<V: View> Element<V> for ChildView {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         _: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         if let Some(mut rendered_view) = cx.window.rendered_views.remove(&self.view_id) {
             rendered_view

crates/gpui/src/app/window_input_handler.rs 🔗

@@ -2,11 +2,11 @@ use std::{cell::RefCell, ops::Range, rc::Rc};
 
 use pathfinder_geometry::rect::RectF;
 
-use crate::{platform::InputHandler, window::WindowContext, AnyView, AppContext};
+use crate::{platform::InputHandler, window::WindowContext, AnyView, AnyWindowHandle, AppContext};
 
 pub struct WindowInputHandler {
     pub app: Rc<RefCell<AppContext>>,
-    pub window_id: usize,
+    pub window: AnyWindowHandle,
 }
 
 impl WindowInputHandler {
@@ -21,13 +21,12 @@ impl WindowInputHandler {
         //
         // See https://github.com/zed-industries/community/issues/444
         let mut app = self.app.try_borrow_mut().ok()?;
-        app.update_window(self.window_id, |cx| {
+        self.window.update_optional(&mut *app, |cx| {
             let view_id = cx.window.focused_view_id?;
-            let view = cx.views.get(&(self.window_id, view_id))?;
+            let view = cx.views.get(&(self.window, view_id))?;
             let result = f(view.as_ref(), &cx);
             Some(result)
         })
-        .flatten()
     }
 
     fn update_focused_view<T, F>(&mut self, f: F) -> Option<T>
@@ -35,11 +34,12 @@ impl WindowInputHandler {
         F: FnOnce(&mut dyn AnyView, &mut WindowContext, usize) -> T,
     {
         let mut app = self.app.try_borrow_mut().ok()?;
-        app.update_window(self.window_id, |cx| {
-            let view_id = cx.window.focused_view_id?;
-            cx.update_any_view(view_id, |view, cx| f(view, cx, view_id))
-        })
-        .flatten()
+        self.window
+            .update(&mut *app, |cx| {
+                let view_id = cx.window.focused_view_id?;
+                cx.update_any_view(view_id, |view, cx| f(view, cx, view_id))
+            })
+            .flatten()
     }
 }
 
@@ -83,9 +83,8 @@ impl InputHandler for WindowInputHandler {
     }
 
     fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
-        self.app
-            .borrow()
-            .read_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16))
-            .flatten()
+        self.window.read_optional_with(&*self.app.borrow(), |cx| {
+            cx.rect_for_text_range(range_utf16)
+        })
     }
 }

crates/gpui/src/elements.rs 🔗

@@ -33,8 +33,8 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    json, Action, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, WeakViewHandle,
-    WindowContext,
+    json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    WeakViewHandle, WindowContext,
 };
 use anyhow::{anyhow, Result};
 use collections::HashMap;
@@ -61,7 +61,7 @@ pub trait Element<V: View>: 'static {
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState;
 
     fn rect_for_text_range(
@@ -170,7 +170,7 @@ pub trait Element<V: View>: 'static {
     fn with_tooltip<Tag: 'static>(
         self,
         id: usize,
-        text: String,
+        text: impl Into<Cow<'static, str>>,
         action: Option<Box<dyn Action>>,
         style: TooltipStyle,
         cx: &mut ViewContext<V>,
@@ -178,7 +178,7 @@ pub trait Element<V: View>: 'static {
     where
         Self: 'static + Sized,
     {
-        Tooltip::new::<Tag, V>(id, text, action, style, self.into_any(), cx)
+        Tooltip::new::<Tag>(id, text, action, style, self.into_any(), cx)
     }
 
     fn resizable(
@@ -201,6 +201,10 @@ pub trait Element<V: View>: 'static {
     }
 }
 
+pub trait RenderElement {
+    fn render<V: View>(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
+}
+
 trait AnyElementState<V: View> {
     fn layout(
         &mut self,
@@ -298,7 +302,14 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
                 mut layout,
             } => {
                 let bounds = RectF::new(origin, size);
-                let paint = element.paint(scene, bounds, visible_bounds, &mut layout, view, cx);
+                let paint = element.paint(
+                    scene,
+                    bounds,
+                    visible_bounds,
+                    &mut layout,
+                    view,
+                    &mut PaintContext::new(cx),
+                );
                 ElementState::PostPaint {
                     element,
                     constraint,
@@ -316,7 +327,14 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
                 ..
             } => {
                 let bounds = RectF::new(origin, bounds.size());
-                let paint = element.paint(scene, bounds, visible_bounds, &mut layout, view, cx);
+                let paint = element.paint(
+                    scene,
+                    bounds,
+                    visible_bounds,
+                    &mut layout,
+                    view,
+                    &mut PaintContext::new(cx),
+                );
                 ElementState::PostPaint {
                     element,
                     constraint,
@@ -513,7 +531,7 @@ impl<V: View> Element<V> for AnyElement<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         self.paint(scene, bounds.origin(), visible_bounds, view, cx);
     }

crates/gpui/src/elements/align.rs 🔗

@@ -1,6 +1,7 @@
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 use json::ToJson;
 
@@ -69,7 +70,7 @@ impl<V: View> Element<V> for Align<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let my_center = bounds.size() / 2.;
         let my_target = my_center + my_center * self.alignment;

crates/gpui/src/elements/canvas.rs 🔗

@@ -3,7 +3,7 @@ use std::marker::PhantomData;
 use super::Element;
 use crate::{
     json::{self, json},
-    SceneBuilder, View, ViewContext,
+    PaintContext, SceneBuilder, View, ViewContext,
 };
 use json::ToJson;
 use pathfinder_geometry::{
@@ -56,7 +56,7 @@ where
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         self.0(scene, bounds, visible_bounds, view, cx)
     }

crates/gpui/src/elements/clipped.rs 🔗

@@ -4,7 +4,8 @@ use pathfinder_geometry::{rect::RectF, vector::Vector2F};
 use serde_json::json;
 
 use crate::{
-    json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 
 pub struct Clipped<V: View> {
@@ -37,7 +38,7 @@ impl<V: View> Element<V> for Clipped<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         scene.paint_layer(Some(bounds), |scene| {
             self.child

crates/gpui/src/elements/constrained_box.rs 🔗

@@ -5,7 +5,8 @@ use serde_json::json;
 
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 
 pub struct ConstrainedBox<V: View> {
@@ -156,7 +157,7 @@ impl<V: View> Element<V> for ConstrainedBox<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         scene.paint_layer(Some(visible_bounds), |scene| {
             self.child

crates/gpui/src/elements/container.rs 🔗

@@ -9,8 +9,9 @@ use crate::{
     },
     json::ToJson,
     platform::CursorStyle,
-    scene::{self, Border, CursorRegion, Quad},
-    AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    scene::{self, Border, CornerRadii, CursorRegion, Quad},
+    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -29,7 +30,8 @@ pub struct ContainerStyle {
     #[serde(default)]
     pub border: Border,
     #[serde(default)]
-    pub corner_radius: f32,
+    #[serde(alias = "corner_radius")]
+    pub corner_radii: CornerRadii,
     #[serde(default)]
     pub shadow: Option<Shadow>,
     #[serde(default)]
@@ -132,7 +134,10 @@ impl<V: View> Container<V> {
     }
 
     pub fn with_corner_radius(mut self, radius: f32) -> Self {
-        self.style.corner_radius = radius;
+        self.style.corner_radii.top_left = radius;
+        self.style.corner_radii.top_right = radius;
+        self.style.corner_radii.bottom_right = radius;
+        self.style.corner_radii.bottom_left = radius;
         self
     }
 
@@ -214,7 +219,7 @@ impl<V: View> Element<V> for Container<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let quad_bounds = RectF::from_points(
             bounds.origin() + vec2f(self.style.margin.left, self.style.margin.top),
@@ -224,7 +229,7 @@ impl<V: View> Element<V> for Container<V> {
         if let Some(shadow) = self.style.shadow.as_ref() {
             scene.push_shadow(scene::Shadow {
                 bounds: quad_bounds + shadow.offset,
-                corner_radius: self.style.corner_radius,
+                corner_radii: self.style.corner_radii,
                 sigma: shadow.blur,
                 color: shadow.color,
             });
@@ -247,7 +252,7 @@ impl<V: View> Element<V> for Container<V> {
                 bounds: quad_bounds,
                 background: self.style.background_color,
                 border: Default::default(),
-                corner_radius: self.style.corner_radius,
+                corner_radii: self.style.corner_radii.into(),
             });
 
             self.child
@@ -258,7 +263,7 @@ impl<V: View> Element<V> for Container<V> {
                 bounds: quad_bounds,
                 background: self.style.overlay_color,
                 border: self.style.border,
-                corner_radius: self.style.corner_radius,
+                corner_radii: self.style.corner_radii.into(),
             });
             scene.pop_layer();
         } else {
@@ -266,7 +271,7 @@ impl<V: View> Element<V> for Container<V> {
                 bounds: quad_bounds,
                 background: self.style.background_color,
                 border: self.style.border,
-                corner_radius: self.style.corner_radius,
+                corner_radii: self.style.corner_radii.into(),
             });
 
             let child_origin = child_origin
@@ -283,7 +288,7 @@ impl<V: View> Element<V> for Container<V> {
                     bounds: quad_bounds,
                     background: self.style.overlay_color,
                     border: Default::default(),
-                    corner_radius: 0.,
+                    corner_radii: self.style.corner_radii.into(),
                 });
                 scene.pop_layer();
             }
@@ -327,7 +332,7 @@ impl ToJson for ContainerStyle {
             "padding": self.padding.to_json(),
             "background_color": self.background_color.to_json(),
             "border": self.border.to_json(),
-            "corner_radius": self.corner_radius,
+            "corner_radius": self.corner_radii,
             "shadow": self.shadow.to_json(),
         })
     }

crates/gpui/src/elements/empty.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{json, ToJson},
-    LayoutContext, SceneBuilder, View, ViewContext,
+    LayoutContext, PaintContext, SceneBuilder, View, ViewContext,
 };
 use crate::{Element, SizeConstraint};
 
@@ -57,7 +57,7 @@ impl<V: View> Element<V> for Empty {
         _: RectF,
         _: &mut Self::LayoutState,
         _: &mut V,
-        _: &mut ViewContext<V>,
+        _: &mut PaintContext<V>,
     ) -> Self::PaintState {
     }
 

crates/gpui/src/elements/expanded.rs 🔗

@@ -2,7 +2,8 @@ use std::ops::Range;
 
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 use serde_json::json;
 
@@ -61,7 +62,7 @@ impl<V: View> Element<V> for Expanded<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         self.child
             .paint(scene, bounds.origin(), visible_bounds, view, cx);

crates/gpui/src/elements/flex.rs 🔗

@@ -2,8 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
 
 use crate::{
     json::{self, ToJson, Value},
-    AnyElement, Axis, Element, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint,
-    Vector2FExt, View, ViewContext,
+    AnyElement, Axis, Element, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder,
+    SizeConstraint, Vector2FExt, View, ViewContext,
 };
 use pathfinder_geometry::{
     rect::RectF,
@@ -258,7 +258,7 @@ impl<V: View> Element<V> for Flex<V> {
         visible_bounds: RectF,
         remaining_space: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 
@@ -449,7 +449,7 @@ impl<V: View> Element<V> for FlexItem<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         self.child
             .paint(scene, bounds.origin(), visible_bounds, view, cx)

crates/gpui/src/elements/hook.rs 🔗

@@ -3,7 +3,8 @@ use std::ops::Range;
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
-    AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 
 pub struct Hook<V: View> {
@@ -52,7 +53,7 @@ impl<V: View> Element<V> for Hook<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         self.child
             .paint(scene, bounds.origin(), visible_bounds, view, cx);

crates/gpui/src/elements/image.rs 🔗

@@ -5,8 +5,8 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{json, ToJson},
-    scene, Border, Element, ImageData, LayoutContext, SceneBuilder, SizeConstraint, View,
-    ViewContext,
+    scene, Border, Element, ImageData, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
+    View, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -97,13 +97,13 @@ impl<V: View> Element<V> for Image {
         _: RectF,
         layout: &mut Self::LayoutState,
         _: &mut V,
-        _: &mut ViewContext<V>,
+        _: &mut PaintContext<V>,
     ) -> Self::PaintState {
         if let Some(data) = layout {
             scene.push_image(scene::Image {
                 bounds,
                 border: self.style.border,
-                corner_radius: self.style.corner_radius,
+                corner_radii: self.style.corner_radius.into(),
                 grayscale: self.style.grayscale,
                 data: data.clone(),
             });

crates/gpui/src/elements/keystroke_label.rs 🔗

@@ -66,7 +66,7 @@ impl<V: View> Element<V> for KeystrokeLabel {
         visible_bounds: RectF,
         element: &mut AnyElement<V>,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         element.paint(scene, bounds.origin(), visible_bounds, view, cx);
     }

crates/gpui/src/elements/label.rs 🔗

@@ -8,7 +8,7 @@ use crate::{
     },
     json::{ToJson, Value},
     text_layout::{Line, RunStyle},
-    Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -163,7 +163,7 @@ impl<V: View> Element<V> for Label {
         visible_bounds: RectF,
         line: &mut Self::LayoutState,
         _: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
         line.paint(

crates/gpui/src/elements/list.rs 🔗

@@ -4,8 +4,8 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::json,
-    AnyElement, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
-    ViewContext,
+    AnyElement, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder, SizeConstraint,
+    View, ViewContext,
 };
 use std::{cell::RefCell, collections::VecDeque, fmt::Debug, ops::Range, rc::Rc};
 use sum_tree::{Bias, SumTree};
@@ -255,7 +255,7 @@ impl<V: View> Element<V> for List<V> {
         visible_bounds: RectF,
         scroll_top: &mut ListOffset,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
         scene.push_layer(Some(visible_bounds));
@@ -647,7 +647,7 @@ impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{elements::Empty, geometry::vector::vec2f, Entity};
+    use crate::{elements::Empty, geometry::vector::vec2f, Entity, PaintContext};
     use rand::prelude::*;
     use std::env;
 
@@ -988,7 +988,7 @@ mod tests {
             _: RectF,
             _: &mut (),
             _: &mut V,
-            _: &mut ViewContext<V>,
+            _: &mut PaintContext<V>,
         ) {
             unimplemented!()
         }

crates/gpui/src/elements/mouse_event_handler.rs 🔗

@@ -10,8 +10,8 @@ use crate::{
         CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
         MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
     },
-    AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder,
-    SizeConstraint, View, ViewContext,
+    AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, PaintContext,
+    SceneBuilder, SizeConstraint, View, ViewContext,
 };
 use serde_json::json;
 use std::{marker::PhantomData, ops::Range};
@@ -256,7 +256,7 @@ impl<Tag, V: View> Element<V> for MouseEventHandler<Tag, V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         if self.above {
             self.child

crates/gpui/src/elements/overlay.rs 🔗

@@ -3,8 +3,8 @@ use std::ops::Range;
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::ToJson,
-    AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
-    ViewContext,
+    AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
+    SizeConstraint, View, ViewContext,
 };
 use serde_json::json;
 
@@ -143,7 +143,7 @@ impl<V: View> Element<V> for Overlay<V> {
         _: RectF,
         size: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         let (anchor_position, mut bounds) = match self.position_mode {
             OverlayPositionMode::Window => {

crates/gpui/src/elements/resizable.rs 🔗

@@ -7,8 +7,8 @@ use crate::{
     geometry::rect::RectF,
     platform::{CursorStyle, MouseButton},
     scene::MouseDrag,
-    AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
-    ViewContext,
+    AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
+    SizeConstraint, View, ViewContext,
 };
 
 #[derive(Copy, Clone, Debug)]
@@ -125,7 +125,7 @@ impl<V: View> Element<V> for Resizable<V> {
         visible_bounds: pathfinder_geometry::rect::RectF,
         constraint: &mut SizeConstraint,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         scene.push_stacking_context(None, None);
 

crates/gpui/src/elements/stack.rs 🔗

@@ -3,7 +3,8 @@ use std::ops::Range;
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::{self, json, ToJson},
-    AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
+    AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View,
+    ViewContext,
 };
 
 /// Element which renders it's children in a stack on top of each other.
@@ -57,7 +58,7 @@ impl<V: View> Element<V> for Stack<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         for child in &mut self.children {
             scene.paint_layer(None, |scene| {

crates/gpui/src/elements/svg.rs 🔗

@@ -1,5 +1,6 @@
 use super::constrain_size_preserving_aspect_ratio;
 use crate::json::ToJson;
+use crate::PaintContext;
 use crate::{
     color::Color,
     geometry::{
@@ -73,7 +74,7 @@ impl<V: View> Element<V> for Svg {
         _visible_bounds: RectF,
         svg: &mut Self::LayoutState,
         _: &mut V,
-        _: &mut ViewContext<V>,
+        _: &mut PaintContext<V>,
     ) {
         if let Some(svg) = svg.clone() {
             scene.push_icon(scene::Icon {

crates/gpui/src/elements/text.rs 🔗

@@ -7,8 +7,8 @@ use crate::{
     },
     json::{ToJson, Value},
     text_layout::{Line, RunStyle, ShapedBoundary},
-    AppContext, Element, FontCache, LayoutContext, SceneBuilder, SizeConstraint, TextLayoutCache,
-    View, ViewContext,
+    AppContext, Element, FontCache, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
+    TextLayoutCache, View, ViewContext,
 };
 use log::warn;
 use serde_json::json;
@@ -171,7 +171,7 @@ impl<V: View> Element<V> for Text {
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         _: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let mut origin = bounds.origin();
         let empty = Vec::new();

crates/gpui/src/elements/tooltip.rs 🔗

@@ -6,12 +6,13 @@ use crate::{
     fonts::TextStyle,
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
-    Action, Axis, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint, Task, View,
-    ViewContext,
+    Action, Axis, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
+    Task, View, ViewContext,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
 use std::{
+    borrow::Cow,
     cell::{Cell, RefCell},
     ops::Range,
     rc::Rc,
@@ -52,9 +53,9 @@ pub struct KeystrokeStyle {
 }
 
 impl<V: View> Tooltip<V> {
-    pub fn new<Tag: 'static, T: View>(
+    pub fn new<Tag: 'static>(
         id: usize,
-        text: String,
+        text: impl Into<Cow<'static, str>>,
         action: Option<Box<dyn Action>>,
         style: TooltipStyle,
         child: AnyElement<V>,
@@ -66,6 +67,8 @@ impl<V: View> Tooltip<V> {
 
         let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
         let state = state_handle.read(cx).clone();
+        let text = text.into();
+
         let tooltip = if state.visible.get() {
             let mut collapsed_tooltip = Self::render_tooltip(
                 focused_view_id,
@@ -127,7 +130,7 @@ impl<V: View> Tooltip<V> {
 
     pub fn render_tooltip(
         focused_view_id: Option<usize>,
-        text: String,
+        text: impl Into<Cow<'static, str>>,
         style: TooltipStyle,
         action: Option<Box<dyn Action>>,
         measure: bool,
@@ -194,7 +197,7 @@ impl<V: View> Element<V> for Tooltip<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) {
         self.child
             .paint(scene, bounds.origin(), visible_bounds, view, cx);

crates/gpui/src/elements/uniform_list.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
     },
     json::{self, json},
     platform::ScrollWheelEvent,
-    AnyElement, LayoutContext, MouseRegion, SceneBuilder, View, ViewContext,
+    AnyElement, LayoutContext, MouseRegion, PaintContext, SceneBuilder, View, ViewContext,
 };
 use json::ToJson;
 use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -278,7 +278,7 @@ impl<V: View> Element<V> for UniformList<V> {
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
 

crates/gpui/src/fonts.rs 🔗

@@ -71,6 +71,32 @@ pub struct TextStyle {
     pub underline: Underline,
 }
 
+impl TextStyle {
+    pub fn refine(self, refinement: TextStyleRefinement) -> TextStyle {
+        TextStyle {
+            color: refinement.color.unwrap_or(self.color),
+            font_family_name: refinement
+                .font_family_name
+                .unwrap_or_else(|| self.font_family_name.clone()),
+            font_family_id: refinement.font_family_id.unwrap_or(self.font_family_id),
+            font_id: refinement.font_id.unwrap_or(self.font_id),
+            font_size: refinement.font_size.unwrap_or(self.font_size),
+            font_properties: refinement.font_properties.unwrap_or(self.font_properties),
+            underline: refinement.underline.unwrap_or(self.underline),
+        }
+    }
+}
+
+pub struct TextStyleRefinement {
+    pub color: Option<Color>,
+    pub font_family_name: Option<Arc<str>>,
+    pub font_family_id: Option<FamilyId>,
+    pub font_id: Option<FontId>,
+    pub font_size: Option<f32>,
+    pub font_properties: Option<Properties>,
+    pub underline: Option<Underline>,
+}
+
 #[derive(JsonSchema)]
 #[serde(remote = "Properties")]
 pub struct PropertiesDef {

crates/gpui/src/platform.rs 🔗

@@ -19,7 +19,7 @@ use crate::{
     },
     keymap_matcher::KeymapMatcher,
     text_layout::{LineLayout, RunStyle},
-    Action, ClipboardItem, Menu, Scene,
+    Action, AnyWindowHandle, ClipboardItem, Menu, Scene,
 };
 use anyhow::{anyhow, bail, Result};
 use async_task::Runnable;
@@ -58,13 +58,13 @@ pub trait Platform: Send + Sync {
 
     fn open_window(
         &self,
-        id: usize,
+        handle: AnyWindowHandle,
         options: WindowOptions,
         executor: Rc<executor::Foreground>,
     ) -> Box<dyn Window>;
-    fn main_window_id(&self) -> Option<usize>;
+    fn main_window(&self) -> Option<AnyWindowHandle>;
 
-    fn add_status_item(&self, id: usize) -> Box<dyn Window>;
+    fn add_status_item(&self, handle: AnyWindowHandle) -> Box<dyn Window>;
 
     fn write_to_clipboard(&self, item: ClipboardItem);
     fn read_from_clipboard(&self) -> Option<ClipboardItem>;

crates/gpui/src/platform/mac.rs 🔗

@@ -21,7 +21,7 @@ pub use fonts::FontSystem;
 use platform::{MacForegroundPlatform, MacPlatform};
 pub use renderer::Surface;
 use std::{ops::Range, rc::Rc, sync::Arc};
-use window::Window;
+use window::MacWindow;
 
 use crate::executor;
 

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -1,12 +1,12 @@
 use super::{
     event::key_to_native, screen::Screen, status_item::StatusItem, BoolExt as _, Dispatcher,
-    FontSystem, Window,
+    FontSystem, MacWindow,
 };
 use crate::{
     executor,
     keymap_matcher::KeymapMatcher,
     platform::{self, AppVersion, CursorStyle, Event},
-    Action, ClipboardItem, Menu, MenuItem,
+    Action, AnyWindowHandle, ClipboardItem, Menu, MenuItem,
 };
 use anyhow::{anyhow, Result};
 use block::ConcreteBlock;
@@ -590,18 +590,18 @@ impl platform::Platform for MacPlatform {
 
     fn open_window(
         &self,
-        id: usize,
+        handle: AnyWindowHandle,
         options: platform::WindowOptions,
         executor: Rc<executor::Foreground>,
     ) -> Box<dyn platform::Window> {
-        Box::new(Window::open(id, options, executor, self.fonts()))
+        Box::new(MacWindow::open(handle, options, executor, self.fonts()))
     }
 
-    fn main_window_id(&self) -> Option<usize> {
-        Window::main_window_id()
+    fn main_window(&self) -> Option<AnyWindowHandle> {
+        MacWindow::main_window()
     }
 
-    fn add_status_item(&self, _id: usize) -> Box<dyn platform::Window> {
+    fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn platform::Window> {
         Box::new(StatusItem::add(self.fonts()))
     }
 

crates/gpui/src/platform/mac/renderer.rs 🔗

@@ -509,10 +509,14 @@ impl Renderer {
         };
         for (ix, shadow) in shadows.iter().enumerate() {
             let shape_bounds = shadow.bounds * scale_factor;
+            let corner_radii = shadow.corner_radii * scale_factor;
             let shader_shadow = shaders::GPUIShadow {
                 origin: shape_bounds.origin().to_float2(),
                 size: shape_bounds.size().to_float2(),
-                corner_radius: shadow.corner_radius * scale_factor,
+                corner_radius_top_left: corner_radii.top_left,
+                corner_radius_top_right: corner_radii.top_right,
+                corner_radius_bottom_right: corner_radii.bottom_right,
+                corner_radius_bottom_left: corner_radii.bottom_left,
                 sigma: shadow.sigma,
                 color: shadow.color.to_uchar4(),
             };
@@ -586,7 +590,10 @@ impl Renderer {
                 border_bottom: border_width * (quad.border.bottom as usize as f32),
                 border_left: border_width * (quad.border.left as usize as f32),
                 border_color: quad.border.color.to_uchar4(),
-                corner_radius: quad.corner_radius * scale_factor,
+                corner_radius_top_left: quad.corner_radii.top_left * scale_factor,
+                corner_radius_top_right: quad.corner_radii.top_right * scale_factor,
+                corner_radius_bottom_right: quad.corner_radii.bottom_right * scale_factor,
+                corner_radius_bottom_left: quad.corner_radii.bottom_left * scale_factor,
             };
             unsafe {
                 *(buffer_contents.add(ix)) = shader_quad;
@@ -738,7 +745,7 @@ impl Renderer {
         for image in images {
             let origin = image.bounds.origin() * scale_factor;
             let target_size = image.bounds.size() * scale_factor;
-            let corner_radius = image.corner_radius * scale_factor;
+            let corner_radii = image.corner_radii * scale_factor;
             let border_width = image.border.width * scale_factor;
             let (alloc_id, atlas_bounds) = self.image_cache.render(&image.data);
             images_by_atlas
@@ -754,7 +761,10 @@ impl Renderer {
                     border_bottom: border_width * (image.border.bottom as usize as f32),
                     border_left: border_width * (image.border.left as usize as f32),
                     border_color: image.border.color.to_uchar4(),
-                    corner_radius,
+                    corner_radius_top_left: corner_radii.top_left,
+                    corner_radius_top_right: corner_radii.top_right,
+                    corner_radius_bottom_right: corner_radii.bottom_right,
+                    corner_radius_bottom_left: corner_radii.bottom_left,
                     grayscale: image.grayscale as u8,
                 });
         }
@@ -777,7 +787,10 @@ impl Renderer {
                         border_bottom: 0.,
                         border_left: 0.,
                         border_color: Default::default(),
-                        corner_radius: 0.,
+                        corner_radius_top_left: 0.,
+                        corner_radius_top_right: 0.,
+                        corner_radius_bottom_right: 0.,
+                        corner_radius_bottom_left: 0.,
                         grayscale: false as u8,
                     });
             } else {

crates/gpui/src/platform/mac/shaders/shaders.h 🔗

@@ -19,7 +19,10 @@ typedef struct {
   float border_bottom;
   float border_left;
   vector_uchar4 border_color;
-  float corner_radius;
+  float corner_radius_top_left;
+  float corner_radius_top_right;
+  float corner_radius_bottom_right;
+  float corner_radius_bottom_left;
 } GPUIQuad;
 
 typedef enum {
@@ -31,7 +34,10 @@ typedef enum {
 typedef struct {
   vector_float2 origin;
   vector_float2 size;
-  float corner_radius;
+  float corner_radius_top_left;
+  float corner_radius_top_right;
+  float corner_radius_bottom_right;
+  float corner_radius_bottom_left;
   float sigma;
   vector_uchar4 color;
 } GPUIShadow;
@@ -89,7 +95,10 @@ typedef struct {
   float border_bottom;
   float border_left;
   vector_uchar4 border_color;
-  float corner_radius;
+  float corner_radius_top_left;
+  float corner_radius_top_right;
+  float corner_radius_bottom_right;
+  float corner_radius_bottom_left;
   uint8_t grayscale;
 } GPUIImage;
 

crates/gpui/src/platform/mac/shaders/shaders.metal 🔗

@@ -43,7 +43,10 @@ struct QuadFragmentInput {
     float border_bottom;
     float border_left;
     float4 border_color;
-    float corner_radius;
+    float corner_radius_top_left;
+    float corner_radius_top_right;
+    float corner_radius_bottom_right;
+    float corner_radius_bottom_left;
     uchar grayscale; // only used in image shader
 };
 
@@ -51,12 +54,27 @@ float4 quad_sdf(QuadFragmentInput input) {
     float2 half_size = input.size / 2.;
     float2 center = input.origin + half_size;
     float2 center_to_point = input.position.xy - center;
-    float2 rounded_edge_to_point = abs(center_to_point) - half_size + input.corner_radius;
-    float distance = length(max(0., rounded_edge_to_point)) + min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - input.corner_radius;
+    float corner_radius;
+    if (center_to_point.x < 0.) {
+        if (center_to_point.y < 0.) {
+            corner_radius = input.corner_radius_top_left;
+        } else {
+            corner_radius = input.corner_radius_bottom_left;
+        }
+    } else {
+        if (center_to_point.y < 0.) {
+            corner_radius = input.corner_radius_top_right;
+        } else {
+            corner_radius = input.corner_radius_bottom_right;
+        }
+    }
+
+    float2 rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
+    float distance = length(max(0., rounded_edge_to_point)) + min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - corner_radius;
 
     float vertical_border = center_to_point.x <= 0. ? input.border_left : input.border_right;
     float horizontal_border = center_to_point.y <= 0. ? input.border_top : input.border_bottom;
-    float2 inset_size = half_size - input.corner_radius - float2(vertical_border, horizontal_border);
+    float2 inset_size = half_size - corner_radius - float2(vertical_border, horizontal_border);
     float2 point_to_inset_corner = abs(center_to_point) - inset_size;
     float border_width;
     if (point_to_inset_corner.x < 0. && point_to_inset_corner.y < 0.) {
@@ -110,7 +128,10 @@ vertex QuadFragmentInput quad_vertex(
         quad.border_bottom,
         quad.border_left,
         coloru_to_colorf(quad.border_color),
-        quad.corner_radius,
+        quad.corner_radius_top_left,
+        quad.corner_radius_top_right,
+        quad.corner_radius_bottom_right,
+        quad.corner_radius_bottom_left,
         0,
     };
 }
@@ -125,7 +146,10 @@ struct ShadowFragmentInput {
     float4 position [[position]];
     vector_float2 origin;
     vector_float2 size;
-    float corner_radius;
+    float corner_radius_top_left;
+    float corner_radius_top_right;
+    float corner_radius_bottom_right;
+    float corner_radius_bottom_left;
     float sigma;
     vector_uchar4 color;
 };
@@ -148,7 +172,10 @@ vertex ShadowFragmentInput shadow_vertex(
         device_position,
         shadow.origin,
         shadow.size,
-        shadow.corner_radius,
+        shadow.corner_radius_top_left,
+        shadow.corner_radius_top_right,
+        shadow.corner_radius_bottom_right,
+        shadow.corner_radius_bottom_left,
         shadow.sigma,
         shadow.color,
     };
@@ -158,10 +185,24 @@ fragment float4 shadow_fragment(
     ShadowFragmentInput input [[stage_in]]
 ) {
     float sigma = input.sigma;
-    float corner_radius = input.corner_radius;
     float2 half_size = input.size / 2.;
     float2 center = input.origin + half_size;
     float2 point = input.position.xy - center;
+    float2 center_to_point = input.position.xy - center;
+    float corner_radius;
+    if (center_to_point.x < 0.) {
+        if (center_to_point.y < 0.) {
+            corner_radius = input.corner_radius_top_left;
+        } else {
+            corner_radius = input.corner_radius_bottom_left;
+        }
+    } else {
+        if (center_to_point.y < 0.) {
+            corner_radius = input.corner_radius_top_right;
+        } else {
+            corner_radius = input.corner_radius_bottom_right;
+        }
+    }
 
     // The signal is only non-zero in a limited range, so don't waste samples
     float low = point.y - half_size.y;
@@ -252,7 +293,10 @@ vertex QuadFragmentInput image_vertex(
         image.border_bottom,
         image.border_left,
         coloru_to_colorf(image.border_color),
-        image.corner_radius,
+        image.corner_radius_top_left,
+        image.corner_radius_top_right,
+        image.corner_radius_bottom_right,
+        image.corner_radius_bottom_left,
         image.grayscale,
     };
 }
@@ -266,7 +310,7 @@ fragment float4 image_fragment(
     if (input.grayscale) {
         float grayscale =
             0.2126 * input.background_color.r +
-            0.7152 * input.background_color.g + 
+            0.7152 * input.background_color.g +
             0.0722 * input.background_color.b;
         input.background_color = float4(grayscale, grayscale, grayscale, input.background_color.a);
     }

crates/gpui/src/platform/mac/window.rs 🔗

@@ -13,6 +13,7 @@ use crate::{
         Event, InputHandler, KeyDownEvent, Modifiers, ModifiersChangedEvent, MouseButton,
         MouseButtonEvent, MouseMovedEvent, Scene, WindowBounds, WindowKind,
     },
+    AnyWindowHandle,
 };
 use block::ConcreteBlock;
 use cocoa::{
@@ -282,7 +283,7 @@ struct InsertText {
 }
 
 struct WindowState {
-    id: usize,
+    handle: AnyWindowHandle,
     native_window: id,
     kind: WindowKind,
     event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
@@ -422,11 +423,11 @@ impl WindowState {
     }
 }
 
-pub struct Window(Rc<RefCell<WindowState>>);
+pub struct MacWindow(Rc<RefCell<WindowState>>);
 
-impl Window {
+impl MacWindow {
     pub fn open(
-        id: usize,
+        handle: AnyWindowHandle,
         options: platform::WindowOptions,
         executor: Rc<executor::Foreground>,
         fonts: Arc<dyn platform::FontSystem>,
@@ -504,7 +505,7 @@ impl Window {
             assert!(!native_view.is_null());
 
             let window = Self(Rc::new(RefCell::new(WindowState {
-                id,
+                handle,
                 native_window,
                 kind: options.kind,
                 event_callback: None,
@@ -621,13 +622,13 @@ impl Window {
         }
     }
 
-    pub fn main_window_id() -> Option<usize> {
+    pub fn main_window() -> Option<AnyWindowHandle> {
         unsafe {
             let app = NSApplication::sharedApplication(nil);
             let main_window: id = msg_send![app, mainWindow];
             if msg_send![main_window, isKindOfClass: WINDOW_CLASS] {
-                let id = get_window_state(&*main_window).borrow().id;
-                Some(id)
+                let handle = get_window_state(&*main_window).borrow().handle;
+                Some(handle)
             } else {
                 None
             }
@@ -635,7 +636,7 @@ impl Window {
     }
 }
 
-impl Drop for Window {
+impl Drop for MacWindow {
     fn drop(&mut self) {
         let this = self.0.borrow();
         let window = this.native_window;
@@ -649,7 +650,7 @@ impl Drop for Window {
     }
 }
 
-impl platform::Window for Window {
+impl platform::Window for MacWindow {
     fn bounds(&self) -> WindowBounds {
         self.0.as_ref().borrow().bounds()
     }
@@ -881,7 +882,7 @@ impl platform::Window for Window {
 
     fn is_topmost_for_position(&self, position: Vector2F) -> bool {
         let self_borrow = self.0.borrow();
-        let self_id = self_borrow.id;
+        let self_handle = self_borrow.handle;
 
         unsafe {
             let app = NSApplication::sharedApplication(nil);
@@ -898,8 +899,8 @@ impl platform::Window for Window {
             let is_panel: BOOL = msg_send![top_most_window, isKindOfClass: PANEL_CLASS];
             let is_window: BOOL = msg_send![top_most_window, isKindOfClass: WINDOW_CLASS];
             if is_panel == YES || is_window == YES {
-                let topmost_window_id = get_window_state(&*top_most_window).borrow().id;
-                topmost_window_id == self_id
+                let topmost_window = get_window_state(&*top_most_window).borrow().handle;
+                topmost_window == self_handle
             } else {
                 // Someone else's window is on top
                 false
@@ -1086,7 +1087,10 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                 button: MouseButton::Left,
                 modifiers: Modifiers { ctrl: true, .. },
                 ..
-            }) => return,
+            }) => {
+                window_state_borrow.synthetic_drag_counter += 1;
+                return;
+            }
 
             _ => None,
         };

crates/gpui/src/platform/test.rs 🔗

@@ -5,7 +5,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     keymap_matcher::KeymapMatcher,
-    Action, ClipboardItem, Menu,
+    Action, AnyWindowHandle, ClipboardItem, Menu,
 };
 use anyhow::{anyhow, Result};
 use collections::VecDeque;
@@ -102,7 +102,7 @@ pub struct Platform {
     fonts: Arc<dyn super::FontSystem>,
     current_clipboard_item: Mutex<Option<ClipboardItem>>,
     cursor: Mutex<CursorStyle>,
-    active_window_id: Arc<Mutex<Option<usize>>>,
+    active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
 }
 
 impl Platform {
@@ -112,7 +112,7 @@ impl Platform {
             fonts: Arc::new(super::current::FontSystem::new()),
             current_clipboard_item: Default::default(),
             cursor: Mutex::new(CursorStyle::Arrow),
-            active_window_id: Default::default(),
+            active_window: Default::default(),
         }
     }
 }
@@ -146,30 +146,30 @@ impl super::Platform for Platform {
 
     fn open_window(
         &self,
-        id: usize,
+        handle: AnyWindowHandle,
         options: super::WindowOptions,
         _executor: Rc<super::executor::Foreground>,
     ) -> Box<dyn super::Window> {
-        *self.active_window_id.lock() = Some(id);
+        *self.active_window.lock() = Some(handle);
         Box::new(Window::new(
-            id,
+            handle,
             match options.bounds {
                 WindowBounds::Maximized | WindowBounds::Fullscreen => vec2f(1024., 768.),
                 WindowBounds::Fixed(rect) => rect.size(),
             },
-            self.active_window_id.clone(),
+            self.active_window.clone(),
         ))
     }
 
-    fn main_window_id(&self) -> Option<usize> {
-        self.active_window_id.lock().clone()
+    fn main_window(&self) -> Option<AnyWindowHandle> {
+        self.active_window.lock().clone()
     }
 
-    fn add_status_item(&self, id: usize) -> Box<dyn crate::platform::Window> {
+    fn add_status_item(&self, handle: AnyWindowHandle) -> Box<dyn crate::platform::Window> {
         Box::new(Window::new(
-            id,
+            handle,
             vec2f(24., 24.),
-            self.active_window_id.clone(),
+            self.active_window.clone(),
         ))
     }
 
@@ -256,7 +256,7 @@ impl super::Screen for Screen {
 }
 
 pub struct Window {
-    id: usize,
+    handle: AnyWindowHandle,
     pub(crate) size: Vector2F,
     scale_factor: f32,
     current_scene: Option<crate::Scene>,
@@ -270,13 +270,17 @@ pub struct Window {
     pub(crate) title: Option<String>,
     pub(crate) edited: bool,
     pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
-    active_window_id: Arc<Mutex<Option<usize>>>,
+    active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
 }
 
 impl Window {
-    pub fn new(id: usize, size: Vector2F, active_window_id: Arc<Mutex<Option<usize>>>) -> Self {
+    pub fn new(
+        handle: AnyWindowHandle,
+        size: Vector2F,
+        active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
+    ) -> Self {
         Self {
-            id,
+            handle,
             size,
             event_handlers: Default::default(),
             resize_handlers: Default::default(),
@@ -290,7 +294,7 @@ impl Window {
             title: None,
             edited: false,
             pending_prompts: Default::default(),
-            active_window_id,
+            active_window,
         }
     }
 
@@ -342,7 +346,7 @@ impl super::Window for Window {
     }
 
     fn activate(&self) {
-        *self.active_window_id.lock() = Some(self.id);
+        *self.active_window.lock() = Some(self.handle);
     }
 
     fn set_title(&mut self, title: &str) {

crates/gpui/src/scene.rs 🔗

@@ -3,8 +3,10 @@ mod mouse_region;
 
 #[cfg(debug_assertions)]
 use collections::HashSet;
+use derive_more::Mul;
 use schemars::JsonSchema;
 use serde::Deserialize;
+use serde_derive::Serialize;
 use serde_json::json;
 use std::{borrow::Cow, sync::Arc};
 
@@ -65,13 +67,73 @@ pub struct Quad {
     pub bounds: RectF,
     pub background: Option<Color>,
     pub border: Border,
-    pub corner_radius: f32,
+    pub corner_radii: CornerRadii,
+}
+
+#[derive(Default, Debug, Mul, Clone, Copy, Serialize, JsonSchema)]
+pub struct CornerRadii {
+    pub top_left: f32,
+    pub top_right: f32,
+    pub bottom_right: f32,
+    pub bottom_left: f32,
+}
+
+impl<'de> Deserialize<'de> for CornerRadii {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        #[derive(Deserialize)]
+        pub struct CornerRadiiHelper {
+            pub top_left: Option<f32>,
+            pub top_right: Option<f32>,
+            pub bottom_right: Option<f32>,
+            pub bottom_left: Option<f32>,
+        }
+
+        #[derive(Deserialize)]
+        #[serde(untagged)]
+        enum RadiusOrRadii {
+            Radius(f32),
+            Radii(CornerRadiiHelper),
+        }
+
+        let json = RadiusOrRadii::deserialize(deserializer)?;
+
+        let result = match json {
+            RadiusOrRadii::Radius(radius) => CornerRadii::from(radius),
+            RadiusOrRadii::Radii(CornerRadiiHelper {
+                top_left,
+                top_right,
+                bottom_right,
+                bottom_left,
+            }) => CornerRadii {
+                top_left: top_left.unwrap_or(0.0),
+                top_right: top_right.unwrap_or(0.0),
+                bottom_right: bottom_right.unwrap_or(0.0),
+                bottom_left: bottom_left.unwrap_or(0.0),
+            },
+        };
+
+        Ok(result)
+    }
+}
+
+impl From<f32> for CornerRadii {
+    fn from(radius: f32) -> Self {
+        Self {
+            top_left: radius,
+            top_right: radius,
+            bottom_right: radius,
+            bottom_left: radius,
+        }
+    }
 }
 
 #[derive(Debug)]
 pub struct Shadow {
     pub bounds: RectF,
-    pub corner_radius: f32,
+    pub corner_radii: CornerRadii,
     pub sigma: f32,
     pub color: Color,
 }
@@ -177,7 +239,7 @@ pub struct PathVertex {
 pub struct Image {
     pub bounds: RectF,
     pub border: Border,
-    pub corner_radius: f32,
+    pub corner_radii: CornerRadii,
     pub grayscale: bool,
     pub data: Arc<ImageData>,
 }

crates/gpui/src/scene/mouse_region.rs 🔗

@@ -177,7 +177,7 @@ impl MouseRegion {
     }
 }
 
-#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
+#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)]
 pub struct MouseRegionId {
     view_id: usize,
     tag: TypeId,

crates/gpui_macros/src/gpui_macros.rs 🔗

@@ -283,8 +283,12 @@ pub fn element_derive(input: TokenStream) -> TokenStream {
 
     // The name of the struct/enum
     let name = input.ident;
+    let must_implement = format_ident!("{}MustImplementRenderElement", name);
 
     let expanded = quote! {
+        trait #must_implement : gpui::elements::RenderElement {}
+        impl #must_implement for #name {}
+
         impl<V: gpui::View> gpui::elements::Element<V> for #name {
             type LayoutState = gpui::elements::AnyElement<V>;
             type PaintState = ();
@@ -307,7 +311,7 @@ pub fn element_derive(input: TokenStream) -> TokenStream {
                 visible_bounds: gpui::geometry::rect::RectF,
                 element: &mut gpui::elements::AnyElement<V>,
                 view: &mut V,
-                cx: &mut gpui::ViewContext<V>,
+                cx: &mut gpui::PaintContext<V>,
             ) {
                 element.paint(scene, bounds.origin(), visible_bounds, view, cx);
             }
@@ -332,7 +336,7 @@ pub fn element_derive(input: TokenStream) -> TokenStream {
                 _: &(),
                 view: &V,
                 cx: &gpui::ViewContext<V>,
-            ) -> serde_json::Value {
+            ) -> gpui::serde_json::Value {
                 element.debug(view, cx)
             }
         }

crates/gpui_macros/tests/test.rs 🔗

@@ -0,0 +1,14 @@
+use gpui::{elements::RenderElement, View, ViewContext};
+use gpui_macros::Element;
+
+#[test]
+fn test_derive_render_element() {
+    #[derive(Element)]
+    struct TestElement {}
+
+    impl RenderElement for TestElement {
+        fn render<V: View>(&mut self, _: &mut V, _: &mut ViewContext<V>) -> gpui::AnyElement<V> {
+            unimplemented!()
+        }
+    }
+}

crates/language/src/language.rs 🔗

@@ -45,7 +45,7 @@ use syntax_map::SyntaxSnapshot;
 use theme::{SyntaxTheme, Theme};
 use tree_sitter::{self, Query};
 use unicase::UniCase;
-use util::http::HttpClient;
+use util::{http::HttpClient, paths::PathExt};
 use util::{merge_json_value_into, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture};
 
 #[cfg(any(test, feature = "test-support"))]
@@ -182,8 +182,8 @@ impl CachedLspAdapter {
         self.adapter.workspace_configuration(cx)
     }
 
-    pub async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
-        self.adapter.process_diagnostics(params).await
+    pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
+        self.adapter.process_diagnostics(params)
     }
 
     pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) {
@@ -262,7 +262,7 @@ pub trait LspAdapter: 'static + Send + Sync {
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary>;
 
-    async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
 
     async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
 
@@ -777,7 +777,7 @@ impl LanguageRegistry {
     ) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
         let path = path.as_ref();
         let filename = path.file_name().and_then(|name| name.to_str());
-        let extension = path.extension().and_then(|name| name.to_str());
+        let extension = path.extension_or_hidden_file_name();
         let path_suffixes = [extension, filename];
         self.get_or_load_language(|config| {
             let path_matches = config
@@ -1487,12 +1487,6 @@ impl Language {
         None
     }
 
-    pub async fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) {
-        for adapter in &self.adapters {
-            adapter.process_diagnostics(diagnostics).await;
-        }
-    }
-
     pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) {
         for adapter in &self.adapters {
             adapter.process_completion(completion).await;
@@ -1756,7 +1750,7 @@ impl LspAdapter for Arc<FakeLspAdapter> {
         unreachable!();
     }
 
-    async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
 
     async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
         self.disk_based_diagnostics_sources.clone()

crates/language_tools/src/lsp_log_tests.rs 🔗

@@ -61,7 +61,9 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
         .receive_notification::<lsp::notification::DidOpenTextDocument>()
         .await;
 
-    let (_, log_view) = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx));
+    let log_view = cx
+        .add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx))
+        .root(cx);
 
     language_server.notify::<lsp::notification::LogMessage>(lsp::LogMessageParams {
         message: "hello from the server".into(),

crates/live_kit_client/build.rs 🔗

@@ -58,11 +58,14 @@ fn build_bridge(swift_target: &SwiftTarget) {
         "cargo:rerun-if-changed={}/Package.resolved",
         SWIFT_PACKAGE_NAME
     );
+
     let swift_package_root = swift_package_root();
+    let swift_target_folder = swift_target_folder();
     if !Command::new("swift")
         .arg("build")
         .args(["--configuration", &env::var("PROFILE").unwrap()])
         .args(["--triple", &swift_target.target.triple])
+        .args(["--build-path".into(), swift_target_folder])
         .current_dir(&swift_package_root)
         .status()
         .unwrap()
@@ -128,6 +131,12 @@ fn swift_package_root() -> PathBuf {
     env::current_dir().unwrap().join(SWIFT_PACKAGE_NAME)
 }
 
+fn swift_target_folder() -> PathBuf {
+    env::current_dir()
+        .unwrap()
+        .join(format!("../../target/{SWIFT_PACKAGE_NAME}"))
+}
+
 fn copy_dir(source: &Path, destination: &Path) {
     assert!(
         Command::new("rm")
@@ -155,8 +164,7 @@ fn copy_dir(source: &Path, destination: &Path) {
 
 impl SwiftTarget {
     fn out_dir_path(&self) -> PathBuf {
-        swift_package_root()
-            .join(".build")
+        swift_target_folder()
             .join(&self.target.unversioned_triple)
             .join(env::var("PROFILE").unwrap())
     }

crates/lsp/src/lsp.rs 🔗

@@ -434,7 +434,9 @@ impl LanguageServer {
                         ..Default::default()
                     }),
                     inlay_hint: Some(InlayHintClientCapabilities {
-                        resolve_support: None,
+                        resolve_support: Some(InlayHintResolveClientCapabilities {
+                            properties: vec!["textEdits".to_string(), "tooltip".to_string()],
+                        }),
                         dynamic_registration: Some(false),
                     }),
                     ..Default::default()

crates/project/src/lsp_command.rs 🔗

@@ -1954,7 +1954,7 @@ impl LspCommand for InlayHints {
         _: &mut Project,
         _: PeerId,
         buffer_version: &clock::Global,
-        cx: &mut AppContext,
+        _: &mut AppContext,
     ) -> proto::InlayHintsResponse {
         proto::InlayHintsResponse {
             hints: response
@@ -1963,51 +1963,17 @@ impl LspCommand for InlayHints {
                     position: Some(language::proto::serialize_anchor(&response_hint.position)),
                     padding_left: response_hint.padding_left,
                     padding_right: response_hint.padding_right,
-                    label: Some(proto::InlayHintLabel {
-                        label: Some(match response_hint.label {
-                            InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s),
-                            InlayHintLabel::LabelParts(label_parts) => {
-                                proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts {
-                                    parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart {
-                                        value: label_part.value,
-                                        tooltip: label_part.tooltip.map(|tooltip| {
-                                            let proto_tooltip = match tooltip {
-                                                InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s),
-                                                InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent {
-                                                    kind: markup_content.kind,
-                                                    value: markup_content.value,
-                                                }),
-                                            };
-                                            proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)}
-                                        }),
-                                        location: label_part.location.map(|location| proto::Location {
-                                            start: Some(serialize_anchor(&location.range.start)),
-                                            end: Some(serialize_anchor(&location.range.end)),
-                                            buffer_id: location.buffer.read(cx).remote_id(),
-                                        }),
-                                    }).collect()
-                                })
-                            }
-                        }),
-                    }),
                     kind: response_hint.kind.map(|kind| kind.name().to_string()),
-                    tooltip: response_hint.tooltip.map(|response_tooltip| {
-                        let proto_tooltip = match response_tooltip {
-                            InlayHintTooltip::String(s) => {
-                                proto::inlay_hint_tooltip::Content::Value(s)
-                            }
-                            InlayHintTooltip::MarkupContent(markup_content) => {
-                                proto::inlay_hint_tooltip::Content::MarkupContent(
-                                    proto::MarkupContent {
-                                        kind: markup_content.kind,
-                                        value: markup_content.value,
-                                    },
-                                )
-                            }
-                        };
-                        proto::InlayHintTooltip {
-                            content: Some(proto_tooltip),
-                        }
+                    // Do not pass extra data such as tooltips to clients: host can put tooltip data from the cache during resolution.
+                    tooltip: None,
+                    // Similarly, do not pass label parts to clients: host can return a detailed list during resolution.
+                    label: Some(proto::InlayHintLabel {
+                        label: Some(proto::inlay_hint_label::Label::Value(
+                            match response_hint.label {
+                                InlayHintLabel::String(s) => s,
+                                InlayHintLabel::LabelParts(_) => response_hint.text(),
+                            },
+                        )),
                     }),
                 })
                 .collect(),

crates/project/src/project.rs 🔗

@@ -2769,24 +2769,21 @@ impl Project {
         language_server
             .on_notification::<lsp::notification::PublishDiagnostics, _>({
                 let adapter = adapter.clone();
-                move |mut params, cx| {
+                move |mut params, mut cx| {
                     let this = this;
                     let adapter = adapter.clone();
-                    cx.spawn(|mut cx| async move {
-                        adapter.process_diagnostics(&mut params).await;
-                        if let Some(this) = this.upgrade(&cx) {
-                            this.update(&mut cx, |this, cx| {
-                                this.update_diagnostics(
-                                    server_id,
-                                    params,
-                                    &adapter.disk_based_diagnostic_sources,
-                                    cx,
-                                )
-                                .log_err();
-                            });
-                        }
-                    })
-                    .detach();
+                    adapter.process_diagnostics(&mut params);
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(&mut cx, |this, cx| {
+                            this.update_diagnostics(
+                                server_id,
+                                params,
+                                &adapter.disk_based_diagnostic_sources,
+                                cx,
+                            )
+                            .log_err();
+                        });
+                    }
                 }
             })
             .detach();

crates/project/src/terminals.rs 🔗

@@ -1,5 +1,5 @@
 use crate::Project;
-use gpui::{ModelContext, ModelHandle, WeakModelHandle};
+use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle};
 use std::path::PathBuf;
 use terminal::{Terminal, TerminalBuilder, TerminalSettings};
 
@@ -11,7 +11,7 @@ impl Project {
     pub fn create_terminal(
         &mut self,
         working_directory: Option<PathBuf>,
-        window_id: usize,
+        window: AnyWindowHandle,
         cx: &mut ModelContext<Self>,
     ) -> anyhow::Result<ModelHandle<Terminal>> {
         if self.is_remote() {
@@ -27,7 +27,7 @@ impl Project {
                 settings.env.clone(),
                 Some(settings.blinking.clone()),
                 settings.alternate_scroll,
-                window_id,
+                window,
             )
             .map(|builder| {
                 let terminal_handle = cx.add_model(|cx| builder.subscribe(cx));

crates/project/src/worktree.rs 🔗

@@ -2369,7 +2369,7 @@ impl BackgroundScannerState {
         }
 
         // Remove any git repositories whose .git entry no longer exists.
-        let mut snapshot = &mut self.snapshot;
+        let snapshot = &mut self.snapshot;
         let mut repositories = mem::take(&mut snapshot.git_repositories);
         let mut repository_entries = mem::take(&mut snapshot.repository_entries);
         repositories.retain(|work_directory_id, _| {

crates/project_panel/src/file_associations.rs 🔗

@@ -4,7 +4,7 @@ use collections::HashMap;
 
 use gpui::{AppContext, AssetSource};
 use serde_derive::Deserialize;
-use util::iife;
+use util::{iife, paths::PathExt};
 
 #[derive(Deserialize, Debug)]
 struct TypeConfig {
@@ -48,14 +48,7 @@ impl FileAssociations {
             // FIXME: Associate a type with the languages and have the file's langauge
             //        override these associations
             iife!({
-                let suffix = path
-                    .file_name()
-                    .and_then(|os_str| os_str.to_str())
-                    .and_then(|file_name| {
-                        file_name
-                            .find('.')
-                            .and_then(|dot_index| file_name.get(dot_index + 1..))
-                    })?;
+                let suffix = path.icon_suffix()?;
 
                 this.suffixes
                     .get(suffix)

crates/project_panel/src/project_panel.rs 🔗

@@ -115,6 +115,7 @@ actions!(
     [
         ExpandSelectedEntry,
         CollapseSelectedEntry,
+        CollapseAllEntries,
         NewDirectory,
         NewFile,
         Copy,
@@ -140,6 +141,7 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
     file_associations::init(assets, cx);
     cx.add_action(ProjectPanel::expand_selected_entry);
     cx.add_action(ProjectPanel::collapse_selected_entry);
+    cx.add_action(ProjectPanel::collapse_all_entries);
     cx.add_action(ProjectPanel::select_prev);
     cx.add_action(ProjectPanel::select_next);
     cx.add_action(ProjectPanel::new_file);
@@ -514,6 +516,12 @@ impl ProjectPanel {
         }
     }
 
+    pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
+        self.expanded_dir_ids.clear();
+        self.update_visible_entries(None, cx);
+        cx.notify();
+    }
+
     fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
         if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
             if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
@@ -1407,7 +1415,7 @@ impl ProjectPanel {
 
             if cx
                 .global::<DragAndDrop<Workspace>>()
-                .currently_dragged::<ProjectEntryId>(cx.window_id())
+                .currently_dragged::<ProjectEntryId>(cx.window())
                 .is_some()
                 && dragged_entry_destination
                     .as_ref()
@@ -1451,7 +1459,7 @@ impl ProjectPanel {
         .on_up(MouseButton::Left, move |_, this, cx| {
             if let Some((_, dragged_entry)) = cx
                 .global::<DragAndDrop<Workspace>>()
-                .currently_dragged::<ProjectEntryId>(cx.window_id())
+                .currently_dragged::<ProjectEntryId>(cx.window())
             {
                 this.move_entry(
                     *dragged_entry,
@@ -1464,7 +1472,7 @@ impl ProjectPanel {
         .on_move(move |_, this, cx| {
             if cx
                 .global::<DragAndDrop<Workspace>>()
-                .currently_dragged::<ProjectEntryId>(cx.window_id())
+                .currently_dragged::<ProjectEntryId>(cx.window())
                 .is_some()
             {
                 this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) {
@@ -1718,7 +1726,7 @@ impl ClipboardEntry {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use gpui::{TestAppContext, ViewHandle};
+    use gpui::{AnyWindowHandle, TestAppContext, ViewHandle, WindowHandle};
     use pretty_assertions::assert_eq;
     use project::FakeFs;
     use serde_json::json;
@@ -1772,7 +1780,9 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..50, cx),
@@ -1860,7 +1870,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         select_path(&panel, "root1", cx);
@@ -1882,7 +1893,7 @@ mod tests {
         // Add a file with the root folder selected. The filename editor is placed
         // before the first file in the root folder.
         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             let panel = panel.read(cx);
             assert!(panel.filename_editor.is_focused(cx));
         });
@@ -2211,7 +2222,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         select_path(&panel, "root1", cx);
@@ -2233,7 +2245,7 @@ mod tests {
         // Add a file with the root folder selected. The filename editor is placed
         // before the first file in the root folder.
         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             let panel = panel.read(cx);
             assert!(panel.filename_editor.is_focused(cx));
         });
@@ -2311,7 +2323,9 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         panel.update(cx, |panel, cx| {
@@ -2384,7 +2398,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         toggle_expand_dir(&panel, "src/test", cx);
@@ -2401,9 +2416,9 @@ mod tests {
                 "          third.rs"
             ]
         );
-        ensure_single_file_is_opened(window_id, &workspace, "test/first.rs", cx);
+        ensure_single_file_is_opened(window, "test/first.rs", cx);
 
-        submit_deletion(window_id, &panel, cx);
+        submit_deletion(window.into(), &panel, cx);
         assert_eq!(
             visible_entries_as_strings(&panel, 0..10, cx),
             &[
@@ -2414,7 +2429,7 @@ mod tests {
             ],
             "Project panel should have no deleted file, no other file is selected in it"
         );
-        ensure_no_open_items_and_panes(window_id, &workspace, cx);
+        ensure_no_open_items_and_panes(window.into(), &workspace, cx);
 
         select_path(&panel, "src/test/second.rs", cx);
         panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
@@ -2428,9 +2443,9 @@ mod tests {
                 "          third.rs"
             ]
         );
-        ensure_single_file_is_opened(window_id, &workspace, "test/second.rs", cx);
+        ensure_single_file_is_opened(window, "test/second.rs", cx);
 
-        cx.update_window(window_id, |cx| {
+        window.update(cx, |cx| {
             let active_items = workspace
                 .read(cx)
                 .panes()
@@ -2446,13 +2461,13 @@ mod tests {
                 .expect("Open item should be an editor");
             open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
         });
-        submit_deletion(window_id, &panel, cx);
+        submit_deletion(window.into(), &panel, cx);
         assert_eq!(
             visible_entries_as_strings(&panel, 0..10, cx),
             &["v src", "    v test", "          third.rs"],
             "Project panel should have no deleted file, with one last file remaining"
         );
-        ensure_no_open_items_and_panes(window_id, &workspace, cx);
+        ensure_no_open_items_and_panes(window.into(), &workspace, cx);
     }
 
     #[gpui::test]
@@ -2473,7 +2488,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         select_path(&panel, "src/", cx);
@@ -2484,7 +2500,7 @@ mod tests {
             &["v src  <== selected", "    > test"]
         );
         panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             let panel = panel.read(cx);
             assert!(panel.filename_editor.is_focused(cx));
         });
@@ -2515,7 +2531,7 @@ mod tests {
             &["v src", "    > test  <== selected"]
         );
         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             let panel = panel.read(cx);
             assert!(panel.filename_editor.is_focused(cx));
         });
@@ -2565,7 +2581,7 @@ mod tests {
             ],
         );
         panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             let panel = panel.read(cx);
             assert!(panel.filename_editor.is_focused(cx));
         });
@@ -2619,7 +2635,9 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         let new_search_events_count = Arc::new(AtomicUsize::new(0));
@@ -2678,6 +2696,65 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/project_root",
+            json!({
+                "dir_1": {
+                    "nested_dir": {
+                        "file_a.py": "# File contents",
+                        "file_b.py": "# File contents",
+                        "file_c.py": "# File contents",
+                    },
+                    "file_1.py": "# File contents",
+                    "file_2.py": "# File contents",
+                    "file_3.py": "# File contents",
+                },
+                "dir_2": {
+                    "file_1.py": "# File contents",
+                    "file_2.py": "# File contents",
+                    "file_3.py": "# File contents",
+                }
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
+        let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
+
+        panel.update(cx, |panel, cx| {
+            panel.collapse_all_entries(&CollapseAllEntries, cx)
+        });
+        cx.foreground().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v project_root", "    > dir_1", "    > dir_2",]
+        );
+
+        // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
+        toggle_expand_dir(&panel, "project_root/dir_1", cx);
+        cx.foreground().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v project_root",
+                "    v dir_1  <== selected",
+                "        > nested_dir",
+                "          file_1.py",
+                "          file_2.py",
+                "          file_3.py",
+                "    > dir_2",
+            ]
+        );
+    }
+
     fn toggle_expand_dir(
         panel: &ViewHandle<ProjectPanel>,
         path: impl AsRef<Path>,
@@ -2801,13 +2878,11 @@ mod tests {
     }
 
     fn ensure_single_file_is_opened(
-        window_id: usize,
-        workspace: &ViewHandle<Workspace>,
+        window: WindowHandle<Workspace>,
         expected_path: &str,
         cx: &mut TestAppContext,
     ) {
-        cx.read_window(window_id, |cx| {
-            let workspace = workspace.read(cx);
+        window.update_root(cx, |workspace, cx| {
             let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
             assert_eq!(worktrees.len(), 1);
             let worktree_id = WorktreeId::from_usize(worktrees[0].id());
@@ -2829,12 +2904,12 @@ mod tests {
     }
 
     fn submit_deletion(
-        window_id: usize,
+        window: AnyWindowHandle,
         panel: &ViewHandle<ProjectPanel>,
         cx: &mut TestAppContext,
     ) {
         assert!(
-            !cx.has_pending_prompt(window_id),
+            !window.has_pending_prompt(cx),
             "Should have no prompts before the deletion"
         );
         panel.update(cx, |panel, cx| {
@@ -2844,27 +2919,27 @@ mod tests {
                 .detach_and_log_err(cx);
         });
         assert!(
-            cx.has_pending_prompt(window_id),
+            window.has_pending_prompt(cx),
             "Should have a prompt after the deletion"
         );
-        cx.simulate_prompt_answer(window_id, 0);
+        window.simulate_prompt_answer(0, cx);
         assert!(
-            !cx.has_pending_prompt(window_id),
+            !window.has_pending_prompt(cx),
             "Should have no prompts after prompt was replied to"
         );
         cx.foreground().run_until_parked();
     }
 
     fn ensure_no_open_items_and_panes(
-        window_id: usize,
+        window: AnyWindowHandle,
         workspace: &ViewHandle<Workspace>,
         cx: &mut TestAppContext,
     ) {
         assert!(
-            !cx.has_pending_prompt(window_id),
+            !window.has_pending_prompt(cx),
             "Should have no prompts after deletion operation closes the file"
         );
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             let open_project_paths = workspace
                 .read(cx)
                 .panes()
@@ -2878,3 +2953,4 @@ mod tests {
         });
     }
 }
+// TODO - a workspace command?

crates/project_symbols/src/project_symbols.rs 🔗

@@ -326,10 +326,11 @@ mod tests {
             },
         );
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
 
         // Create the project symbols view.
-        let symbols = cx.add_view(window_id, |cx| {
+        let symbols = window.add_view(cx, |cx| {
             ProjectSymbols::new(
                 ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()),
                 cx,

crates/recent_projects/src/highlighted_workspace_location.rs 🔗

@@ -5,6 +5,7 @@ use gpui::{
     elements::{Label, LabelStyle},
     AnyElement, Element, View,
 };
+use util::paths::PathExt;
 use workspace::WorkspaceLocation;
 
 pub struct HighlightedText {
@@ -61,7 +62,7 @@ impl HighlightedWorkspaceLocation {
             .paths()
             .iter()
             .map(|path| {
-                let path = util::paths::compact(&path);
+                let path = path.compact();
                 let highlighted_text = Self::highlights_for_path(
                     path.as_ref(),
                     &string_match.positions,

crates/recent_projects/src/recent_projects.rs 🔗

@@ -11,6 +11,7 @@ use highlighted_workspace_location::HighlightedWorkspaceLocation;
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate, PickerEvent};
 use std::sync::Arc;
+use util::paths::PathExt;
 use workspace::{
     notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
     WORKSPACE_DB,
@@ -134,7 +135,7 @@ impl PickerDelegate for RecentProjectsDelegate {
                 let combined_string = location
                     .paths()
                     .iter()
-                    .map(|path| util::paths::compact(&path).to_string_lossy().into_owned())
+                    .map(|path| path.compact().to_string_lossy().into_owned())
                     .collect::<Vec<_>>()
                     .join("");
                 StringMatchCandidate::new(id, combined_string)

crates/search/src/buffer_search.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
-    ToggleRegex, ToggleWholeWord,
+    NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectAllMatches,
+    SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord,
 };
 use collections::HashMap;
 use editor::Editor;
@@ -46,6 +46,8 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(BufferSearchBar::select_prev_match_on_pane);
     cx.add_action(BufferSearchBar::select_all_matches_on_pane);
     cx.add_action(BufferSearchBar::handle_editor_cancel);
+    cx.add_action(BufferSearchBar::next_history_query);
+    cx.add_action(BufferSearchBar::previous_history_query);
     add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
     add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
     add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
@@ -65,7 +67,7 @@ fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContex
 }
 
 pub struct BufferSearchBar {
-    pub query_editor: ViewHandle<Editor>,
+    query_editor: ViewHandle<Editor>,
     active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
     active_match_index: Option<usize>,
     active_searchable_item_subscription: Option<Subscription>,
@@ -76,6 +78,7 @@ pub struct BufferSearchBar {
     default_options: SearchOptions,
     query_contains_error: bool,
     dismissed: bool,
+    search_history: SearchHistory,
 }
 
 impl Entity for BufferSearchBar {
@@ -106,6 +109,48 @@ impl View for BufferSearchBar {
             .map(|active_searchable_item| active_searchable_item.supported_options())
             .unwrap_or_default();
 
+        let previous_query_keystrokes =
+            cx.binding_for_action(&PreviousHistoryQuery {})
+                .map(|binding| {
+                    binding
+                        .keystrokes()
+                        .iter()
+                        .map(|k| k.to_string())
+                        .collect::<Vec<_>>()
+                });
+        let next_query_keystrokes = cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
+            binding
+                .keystrokes()
+                .iter()
+                .map(|k| k.to_string())
+                .collect::<Vec<_>>()
+        });
+        let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
+            (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
+                format!(
+                    "Search ({}/{} for previous/next query)",
+                    previous_query_keystrokes.join(" "),
+                    next_query_keystrokes.join(" ")
+                )
+            }
+            (None, Some(next_query_keystrokes)) => {
+                format!(
+                    "Search ({} for next query)",
+                    next_query_keystrokes.join(" ")
+                )
+            }
+            (Some(previous_query_keystrokes), None) => {
+                format!(
+                    "Search ({} for previous query)",
+                    previous_query_keystrokes.join(" ")
+                )
+            }
+            (None, None) => String::new(),
+        };
+        self.query_editor.update(cx, |editor, cx| {
+            editor.set_placeholder_text(new_placeholder_text, cx);
+        });
+
         Flex::row()
             .with_child(
                 Flex::row()
@@ -258,6 +303,7 @@ impl BufferSearchBar {
             pending_search: None,
             query_contains_error: false,
             dismissed: true,
+            search_history: SearchHistory::default(),
         }
     }
 
@@ -341,7 +387,7 @@ impl BufferSearchBar {
         cx: &mut ViewContext<Self>,
     ) -> oneshot::Receiver<()> {
         let options = options.unwrap_or(self.default_options);
-        if query != self.query_editor.read(cx).text(cx) || self.search_options != options {
+        if query != self.query(cx) || self.search_options != options {
             self.query_editor.update(cx, |query_editor, cx| {
                 query_editor.buffer().update(cx, |query_buffer, cx| {
                     let len = query_buffer.len(cx);
@@ -674,7 +720,7 @@ impl BufferSearchBar {
 
     fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
         let (done_tx, done_rx) = oneshot::channel();
-        let query = self.query_editor.read(cx).text(cx);
+        let query = self.query(cx);
         self.pending_search.take();
         if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
             if query.is_empty() {
@@ -707,6 +753,7 @@ impl BufferSearchBar {
                     )
                 };
 
+                let query_text = query.as_str().to_string();
                 let matches = active_searchable_item.find_matches(query, cx);
 
                 let active_searchable_item = active_searchable_item.downgrade();
@@ -720,6 +767,7 @@ impl BufferSearchBar {
                                 .insert(active_searchable_item.downgrade(), matches);
 
                             this.update_match_index(cx);
+                            this.search_history.add(query_text);
                             if !this.dismissed {
                                 let matches = this
                                     .searchable_items_with_matches
@@ -753,6 +801,28 @@ impl BufferSearchBar {
             cx.notify();
         }
     }
+
+    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
+        if let Some(new_query) = self.search_history.next().map(str::to_string) {
+            let _ = self.search(&new_query, Some(self.search_options), cx);
+        } else {
+            self.search_history.reset_selection();
+            let _ = self.search("", Some(self.search_options), cx);
+        }
+    }
+
+    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
+        if self.query(cx).is_empty() {
+            if let Some(new_query) = self.search_history.current().map(str::to_string) {
+                let _ = self.search(&new_query, Some(self.search_options), cx);
+                return;
+            }
+        }
+
+        if let Some(new_query) = self.search_history.previous().map(str::to_string) {
+            let _ = self.search(&new_query, Some(self.search_options), cx);
+        }
+    }
 }
 
 #[cfg(test)]
@@ -779,11 +849,10 @@ mod tests {
                 cx,
             )
         });
-        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
-
-        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
+        let window = cx.add_window(|_| EmptyView);
+        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
 
-        let search_bar = cx.add_view(window_id, |cx| {
+        let search_bar = window.add_view(cx, |cx| {
             let mut search_bar = BufferSearchBar::new(cx);
             search_bar.set_active_pane_item(Some(&editor), cx);
             search_bar.show(cx);
@@ -1159,11 +1228,10 @@ mod tests {
             "Should pick a query with multiple results"
         );
         let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
-        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
-
-        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
+        let window = cx.add_window(|_| EmptyView);
+        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
 
-        let search_bar = cx.add_view(window_id, |cx| {
+        let search_bar = window.add_view(cx, |cx| {
             let mut search_bar = BufferSearchBar::new(cx);
             search_bar.set_active_pane_item(Some(&editor), cx);
             search_bar.show(cx);
@@ -1179,12 +1247,13 @@ mod tests {
             search_bar.activate_current_match(cx);
         });
 
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             assert!(
                 !editor.is_focused(cx),
                 "Initially, the editor should not be focused"
             );
         });
+
         let initial_selections = editor.update(cx, |editor, cx| {
             let initial_selections = editor.selections.display_ranges(cx);
             assert_eq!(
@@ -1201,7 +1270,7 @@ mod tests {
             cx.focus(search_bar.query_editor.as_any());
             search_bar.select_all_matches(&SelectAllMatches, cx);
         });
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             assert!(
                 editor.is_focused(cx),
                 "Should focus editor after successful SelectAllMatches"
@@ -1225,7 +1294,7 @@ mod tests {
         search_bar.update(cx, |search_bar, cx| {
             search_bar.select_next_match(&SelectNextMatch, cx);
         });
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             assert!(
                 editor.is_focused(cx),
                 "Should still have editor focused after SelectNextMatch"
@@ -1254,7 +1323,7 @@ mod tests {
             cx.focus(search_bar.query_editor.as_any());
             search_bar.select_all_matches(&SelectAllMatches, cx);
         });
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             assert!(
                 editor.is_focused(cx),
                 "Should focus editor after successful SelectAllMatches"
@@ -1278,7 +1347,7 @@ mod tests {
         search_bar.update(cx, |search_bar, cx| {
             search_bar.select_prev_match(&SelectPrevMatch, cx);
         });
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             assert!(
                 editor.is_focused(cx),
                 "Should still have editor focused after SelectPrevMatch"
@@ -1314,7 +1383,7 @@ mod tests {
         search_bar.update(cx, |search_bar, cx| {
             search_bar.select_all_matches(&SelectAllMatches, cx);
         });
-        cx.read_window(window_id, |cx| {
+        window.read_with(cx, |cx| {
             assert!(
                 !editor.is_focused(cx),
                 "Should not switch focus to editor if SelectAllMatches does not find any matches"
@@ -1333,4 +1402,154 @@ mod tests {
             );
         });
     }
+
+    #[gpui::test]
+    async fn test_search_query_history(cx: &mut TestAppContext) {
+        crate::project_search::tests::init_test(cx);
+
+        let buffer_text = r#"
+        A regular expression (shortened as regex or regexp;[1] also referred to as
+        rational expression[2][3]) is a sequence of characters that specifies a search
+        pattern in text. Usually such patterns are used by string-searching algorithms
+        for "find" or "find and replace" operations on strings, or for input validation.
+        "#
+        .unindent();
+        let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
+        let window = cx.add_window(|_| EmptyView);
+
+        let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
+
+        let search_bar = window.add_view(cx, |cx| {
+            let mut search_bar = BufferSearchBar::new(cx);
+            search_bar.set_active_pane_item(Some(&editor), cx);
+            search_bar.show(cx);
+            search_bar
+        });
+
+        // Add 3 search items into the history.
+        search_bar
+            .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
+            .await
+            .unwrap();
+        search_bar
+            .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
+            .await
+            .unwrap();
+        search_bar
+            .update(cx, |search_bar, cx| {
+                search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
+            })
+            .await
+            .unwrap();
+        // Ensure that the latest search is active.
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "c");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Next history query after the latest should set the query to the empty string.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // First previous query for empty current query should set the query to the latest.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "c");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Further previous items should go over the history in reverse order.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "b");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Previous items should never go behind the first history item.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "a");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "a");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Next items should go over the history in the original order.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "b");
+            assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        search_bar
+            .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
+            .await
+            .unwrap();
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "ba");
+            assert_eq!(search_bar.search_options, SearchOptions::NONE);
+        });
+
+        // New search input should add another entry to history and move the selection to the end of the history.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "c");
+            assert_eq!(search_bar.search_options, SearchOptions::NONE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "b");
+            assert_eq!(search_bar.search_options, SearchOptions::NONE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "c");
+            assert_eq!(search_bar.search_options, SearchOptions::NONE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "ba");
+            assert_eq!(search_bar.search_options, SearchOptions::NONE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_bar.read_with(cx, |search_bar, cx| {
+            assert_eq!(search_bar.query(cx), "");
+            assert_eq!(search_bar.search_options, SearchOptions::NONE);
+        });
+    }
 }

crates/search/src/project_search.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
-    ToggleWholeWord,
+    NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectNextMatch,
+    SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord,
 };
 use anyhow::Context;
 use collections::HashMap;
@@ -56,6 +56,8 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(ProjectSearchBar::search_in_new);
     cx.add_action(ProjectSearchBar::select_next_match);
     cx.add_action(ProjectSearchBar::select_prev_match);
+    cx.add_action(ProjectSearchBar::next_history_query);
+    cx.add_action(ProjectSearchBar::previous_history_query);
     cx.capture_action(ProjectSearchBar::tab);
     cx.capture_action(ProjectSearchBar::tab_previous);
     add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
@@ -83,6 +85,7 @@ struct ProjectSearch {
     match_ranges: Vec<Range<Anchor>>,
     active_query: Option<SearchQuery>,
     search_id: usize,
+    search_history: SearchHistory,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -131,6 +134,7 @@ impl ProjectSearch {
             match_ranges: Default::default(),
             active_query: None,
             search_id: 0,
+            search_history: SearchHistory::default(),
         }
     }
 
@@ -144,6 +148,7 @@ impl ProjectSearch {
             match_ranges: self.match_ranges.clone(),
             active_query: self.active_query.clone(),
             search_id: self.search_id,
+            search_history: self.search_history.clone(),
         })
     }
 
@@ -152,6 +157,7 @@ impl ProjectSearch {
             .project
             .update(cx, |project, cx| project.search(query.clone(), cx));
         self.search_id += 1;
+        self.search_history.add(query.as_str().to_string());
         self.active_query = Some(query);
         self.match_ranges.clear();
         self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
@@ -202,6 +208,7 @@ impl ProjectSearch {
         });
         self.search_id += 1;
         self.match_ranges.clear();
+        self.search_history.add(query.as_str().to_string());
         self.pending_search = Some(cx.spawn(|this, mut cx| async move {
             let results = search?.await.log_err()?;
 
@@ -278,6 +285,49 @@ impl View for ProjectSearchView {
                 Cow::Borrowed("No results")
             };
 
+            let previous_query_keystrokes =
+                cx.binding_for_action(&PreviousHistoryQuery {})
+                    .map(|binding| {
+                        binding
+                            .keystrokes()
+                            .iter()
+                            .map(|k| k.to_string())
+                            .collect::<Vec<_>>()
+                    });
+            let next_query_keystrokes =
+                cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
+                    binding
+                        .keystrokes()
+                        .iter()
+                        .map(|k| k.to_string())
+                        .collect::<Vec<_>>()
+                });
+            let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
+                (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
+                    format!(
+                        "Search ({}/{} for previous/next query)",
+                        previous_query_keystrokes.join(" "),
+                        next_query_keystrokes.join(" ")
+                    )
+                }
+                (None, Some(next_query_keystrokes)) => {
+                    format!(
+                        "Search ({} for next query)",
+                        next_query_keystrokes.join(" ")
+                    )
+                }
+                (Some(previous_query_keystrokes), None) => {
+                    format!(
+                        "Search ({} for previous query)",
+                        previous_query_keystrokes.join(" ")
+                    )
+                }
+                (None, None) => String::new(),
+            };
+            self.query_editor.update(cx, |editor, cx| {
+                editor.set_placeholder_text(new_placeholder_text, cx);
+            });
+
             MouseEventHandler::<Status, _>::new(0, cx, |_, _| {
                 Label::new(text, theme.search.results_status.clone())
                     .aligned()
@@ -1152,6 +1202,47 @@ impl ProjectSearchBar {
             false
         }
     }
+
+    fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
+        if let Some(search_view) = self.active_project_search.as_ref() {
+            search_view.update(cx, |search_view, cx| {
+                let new_query = search_view.model.update(cx, |model, _| {
+                    if let Some(new_query) = model.search_history.next().map(str::to_string) {
+                        new_query
+                    } else {
+                        model.search_history.reset_selection();
+                        String::new()
+                    }
+                });
+                search_view.set_query(&new_query, cx);
+            });
+        }
+    }
+
+    fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
+        if let Some(search_view) = self.active_project_search.as_ref() {
+            search_view.update(cx, |search_view, cx| {
+                if search_view.query_editor.read(cx).text(cx).is_empty() {
+                    if let Some(new_query) = search_view
+                        .model
+                        .read(cx)
+                        .search_history
+                        .current()
+                        .map(str::to_string)
+                    {
+                        search_view.set_query(&new_query, cx);
+                        return;
+                    }
+                }
+
+                if let Some(new_query) = search_view.model.update(cx, |model, _| {
+                    model.search_history.previous().map(str::to_string)
+                }) {
+                    search_view.set_query(&new_query, cx);
+                }
+            });
+        }
+    }
 }
 
 impl Entity for ProjectSearchBar {
@@ -1333,6 +1424,7 @@ pub mod tests {
     use editor::DisplayPoint;
     use gpui::{color::Color, executor::Deterministic, TestAppContext};
     use project::FakeFs;
+    use semantic_index::semantic_index_settings::SemanticIndexSettings;
     use serde_json::json;
     use settings::SettingsStore;
     use std::sync::Arc;
@@ -1355,7 +1447,9 @@ pub mod tests {
         .await;
         let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
-        let (_, search_view) = cx.add_window(|cx| ProjectSearchView::new(search.clone(), cx));
+        let search_view = cx
+            .add_window(|cx| ProjectSearchView::new(search.clone(), cx))
+            .root(cx);
 
         search_view.update(cx, |search_view, cx| {
             search_view
@@ -1472,7 +1566,8 @@ pub mod tests {
         )
         .await;
         let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         let active_item = cx.read(|cx| {
             workspace
@@ -1503,9 +1598,9 @@ pub mod tests {
         };
         let search_view_id = search_view.id();
 
-        cx.spawn(
-            |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
-        )
+        cx.spawn(|mut cx| async move {
+            window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
+        })
         .detach();
         deterministic.run_until_parked();
         search_view.update(cx, |search_view, cx| {
@@ -1556,7 +1651,7 @@ pub mod tests {
             );
         });
         cx.spawn(
-            |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
+            |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) },
         )
         .detach();
         deterministic.run_until_parked();
@@ -1587,9 +1682,9 @@ pub mod tests {
                 "Search view with mismatching query should be focused after search results are available",
             );
         });
-        cx.spawn(
-            |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
-        )
+        cx.spawn(|mut cx| async move {
+            window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
+        })
         .detach();
         deterministic.run_until_parked();
         search_view.update(cx, |search_view, cx| {
@@ -1617,9 +1712,9 @@ pub mod tests {
             );
         });
 
-        cx.spawn(
-            |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
-        )
+        cx.spawn(|mut cx| async move {
+            window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
+        })
         .detach();
         deterministic.run_until_parked();
         search_view.update(cx, |search_view, cx| {
@@ -1656,7 +1751,9 @@ pub mod tests {
         let worktree_id = project.read_with(cx, |project, cx| {
             project.worktrees(cx).next().unwrap().read(cx).id()
         });
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
 
         let active_item = cx.read(|cx| {
             workspace
@@ -1758,6 +1855,193 @@ pub mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_search_query_history(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/dir",
+            json!({
+                "one.rs": "const ONE: usize = 1;",
+                "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+                "three.rs": "const THREE: usize = one::ONE + two::TWO;",
+                "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
+            }),
+        )
+        .await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        workspace.update(cx, |workspace, cx| {
+            ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
+        });
+
+        let search_view = cx.read(|cx| {
+            workspace
+                .read(cx)
+                .active_pane()
+                .read(cx)
+                .active_item()
+                .and_then(|item| item.downcast::<ProjectSearchView>())
+                .expect("Search view expected to appear after new search event trigger")
+        });
+
+        let search_bar = window.add_view(cx, |cx| {
+            let mut search_bar = ProjectSearchBar::new();
+            search_bar.set_active_pane_item(Some(&search_view), cx);
+            // search_bar.show(cx);
+            search_bar
+        });
+
+        // Add 3 search items into the history + another unsubmitted one.
+        search_view.update(cx, |search_view, cx| {
+            search_view.search_options = SearchOptions::CASE_SENSITIVE;
+            search_view
+                .query_editor
+                .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx));
+            search_view.search(cx);
+        });
+        cx.foreground().run_until_parked();
+        search_view.update(cx, |search_view, cx| {
+            search_view
+                .query_editor
+                .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
+            search_view.search(cx);
+        });
+        cx.foreground().run_until_parked();
+        search_view.update(cx, |search_view, cx| {
+            search_view
+                .query_editor
+                .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx));
+            search_view.search(cx);
+        });
+        cx.foreground().run_until_parked();
+        search_view.update(cx, |search_view, cx| {
+            search_view.query_editor.update(cx, |query_editor, cx| {
+                query_editor.set_text("JUST_TEXT_INPUT", cx)
+            });
+        });
+        cx.foreground().run_until_parked();
+
+        // Ensure that the latest input with search settings is active.
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(
+                search_view.query_editor.read(cx).text(cx),
+                "JUST_TEXT_INPUT"
+            );
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Next history query after the latest should set the query to the empty string.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // First previous query for empty current query should set the query to the latest submitted one.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Further previous items should go over the history in reverse order.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Previous items should never go behind the first history item.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // Next items should go over the history in the original order.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        search_view.update(cx, |search_view, cx| {
+            search_view
+                .query_editor
+                .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx));
+            search_view.search(cx);
+        });
+        cx.foreground().run_until_parked();
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+
+        // New search input should add another entry to history and move the selection to the end of the history.
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.next_history_query(&NextHistoryQuery, cx);
+        });
+        search_view.update(cx, |search_view, cx| {
+            assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+            assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+        });
+    }
+
     pub fn init_test(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
         let fonts = cx.font_cache();
@@ -1767,6 +2051,7 @@ pub mod tests {
         cx.update(|cx| {
             cx.set_global(SettingsStore::test(cx));
             cx.set_global(ActiveSearches::default());
+            settings::register::<SemanticIndexSettings>(cx);
 
             theme::init((), cx);
             cx.update_global::<SettingsStore, _, _>(|store, _| {

crates/search/src/search.rs 🔗

@@ -3,6 +3,7 @@ pub use buffer_search::BufferSearchBar;
 use gpui::{actions, Action, AppContext};
 use project::search::SearchQuery;
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
+use smallvec::SmallVec;
 
 pub mod buffer_search;
 pub mod project_search;
@@ -21,6 +22,8 @@ actions!(
         SelectNextMatch,
         SelectPrevMatch,
         SelectAllMatches,
+        NextHistoryQuery,
+        PreviousHistoryQuery,
     ]
 );
 
@@ -65,3 +68,187 @@ impl SearchOptions {
         options
     }
 }
+
+const SEARCH_HISTORY_LIMIT: usize = 20;
+
+#[derive(Default, Debug, Clone)]
+pub struct SearchHistory {
+    history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
+    selected: Option<usize>,
+}
+
+impl SearchHistory {
+    pub fn add(&mut self, search_string: String) {
+        if let Some(i) = self.selected {
+            if search_string == self.history[i] {
+                return;
+            }
+        }
+
+        if let Some(previously_searched) = self.history.last_mut() {
+            if search_string.find(previously_searched.as_str()).is_some() {
+                *previously_searched = search_string;
+                self.selected = Some(self.history.len() - 1);
+                return;
+            }
+        }
+
+        self.history.push(search_string);
+        if self.history.len() > SEARCH_HISTORY_LIMIT {
+            self.history.remove(0);
+        }
+        self.selected = Some(self.history.len() - 1);
+    }
+
+    pub fn next(&mut self) -> Option<&str> {
+        let history_size = self.history.len();
+        if history_size == 0 {
+            return None;
+        }
+
+        let selected = self.selected?;
+        if selected == history_size - 1 {
+            return None;
+        }
+        let next_index = selected + 1;
+        self.selected = Some(next_index);
+        Some(&self.history[next_index])
+    }
+
+    pub fn current(&self) -> Option<&str> {
+        Some(&self.history[self.selected?])
+    }
+
+    pub fn previous(&mut self) -> Option<&str> {
+        let history_size = self.history.len();
+        if history_size == 0 {
+            return None;
+        }
+
+        let prev_index = match self.selected {
+            Some(selected_index) => {
+                if selected_index == 0 {
+                    return None;
+                } else {
+                    selected_index - 1
+                }
+            }
+            None => history_size - 1,
+        };
+
+        self.selected = Some(prev_index);
+        Some(&self.history[prev_index])
+    }
+
+    pub fn reset_selection(&mut self) {
+        self.selected = None;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_add() {
+        let mut search_history = SearchHistory::default();
+        assert_eq!(
+            search_history.current(),
+            None,
+            "No current selection should be set fo the default search history"
+        );
+
+        search_history.add("rust".to_string());
+        assert_eq!(
+            search_history.current(),
+            Some("rust"),
+            "Newly added item should be selected"
+        );
+
+        // check if duplicates are not added
+        search_history.add("rust".to_string());
+        assert_eq!(
+            search_history.history.len(),
+            1,
+            "Should not add a duplicate"
+        );
+        assert_eq!(search_history.current(), Some("rust"));
+
+        // check if new string containing the previous string replaces it
+        search_history.add("rustlang".to_string());
+        assert_eq!(
+            search_history.history.len(),
+            1,
+            "Should replace previous item if it's a substring"
+        );
+        assert_eq!(search_history.current(), Some("rustlang"));
+
+        // push enough items to test SEARCH_HISTORY_LIMIT
+        for i in 0..SEARCH_HISTORY_LIMIT * 2 {
+            search_history.add(format!("item{i}"));
+        }
+        assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
+    }
+
+    #[test]
+    fn test_next_and_previous() {
+        let mut search_history = SearchHistory::default();
+        assert_eq!(
+            search_history.next(),
+            None,
+            "Default search history should not have a next item"
+        );
+
+        search_history.add("Rust".to_string());
+        assert_eq!(search_history.next(), None);
+        search_history.add("JavaScript".to_string());
+        assert_eq!(search_history.next(), None);
+        search_history.add("TypeScript".to_string());
+        assert_eq!(search_history.next(), None);
+
+        assert_eq!(search_history.current(), Some("TypeScript"));
+
+        assert_eq!(search_history.previous(), Some("JavaScript"));
+        assert_eq!(search_history.current(), Some("JavaScript"));
+
+        assert_eq!(search_history.previous(), Some("Rust"));
+        assert_eq!(search_history.current(), Some("Rust"));
+
+        assert_eq!(search_history.previous(), None);
+        assert_eq!(search_history.current(), Some("Rust"));
+
+        assert_eq!(search_history.next(), Some("JavaScript"));
+        assert_eq!(search_history.current(), Some("JavaScript"));
+
+        assert_eq!(search_history.next(), Some("TypeScript"));
+        assert_eq!(search_history.current(), Some("TypeScript"));
+
+        assert_eq!(search_history.next(), None);
+        assert_eq!(search_history.current(), Some("TypeScript"));
+    }
+
+    #[test]
+    fn test_reset_selection() {
+        let mut search_history = SearchHistory::default();
+        search_history.add("Rust".to_string());
+        search_history.add("JavaScript".to_string());
+        search_history.add("TypeScript".to_string());
+
+        assert_eq!(search_history.current(), Some("TypeScript"));
+        search_history.reset_selection();
+        assert_eq!(search_history.current(), None);
+        assert_eq!(
+            search_history.previous(),
+            Some("TypeScript"),
+            "Should start from the end after reset on previous item query"
+        );
+
+        search_history.previous();
+        assert_eq!(search_history.current(), Some("JavaScript"));
+        search_history.previous();
+        assert_eq!(search_history.current(), Some("Rust"));
+
+        search_history.reset_selection();
+        assert_eq!(search_history.current(), None);
+    }
+}

crates/semantic_index/Cargo.toml 🔗

@@ -54,9 +54,12 @@ tempdir.workspace = true
 ctor.workspace = true
 env_logger.workspace = true
 
-tree-sitter-typescript = "*"
-tree-sitter-json = "*"
-tree-sitter-rust = "*"
-tree-sitter-toml = "*"
-tree-sitter-cpp = "*"
-tree-sitter-elixir = "*"
+tree-sitter-typescript.workspace = true
+tree-sitter-json.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-toml.workspace = true
+tree-sitter-cpp.workspace = true
+tree-sitter-elixir.workspace = true
+tree-sitter-lua.workspace = true
+tree-sitter-ruby.workspace = true
+tree-sitter-php.workspace = true

crates/semantic_index/src/parsing.rs 🔗

@@ -21,7 +21,9 @@ const CODE_CONTEXT_TEMPLATE: &str =
     "The below code snippet is from file '<path>'\n\n```<language>\n<item>\n```";
 const ENTIRE_FILE_TEMPLATE: &str =
     "The below snippet is from file '<path>'\n\n```<language>\n<item>\n```";
-pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = &["TOML", "YAML", "CSS"];
+const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file '<path>'\n\n<item>";
+pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] =
+    &["TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML"];
 
 pub struct CodeContextRetriever {
     pub parser: Parser,
@@ -59,7 +61,7 @@ impl CodeContextRetriever {
         let document_span = ENTIRE_FILE_TEMPLATE
             .replace("<path>", relative_path.to_string_lossy().as_ref())
             .replace("<language>", language_name.as_ref())
-            .replace("item", &content);
+            .replace("<item>", &content);
 
         Ok(vec![Document {
             range: 0..content.len(),
@@ -69,6 +71,19 @@ impl CodeContextRetriever {
         }])
     }
 
+    fn parse_markdown_file(&self, relative_path: &Path, content: &str) -> Result<Vec<Document>> {
+        let document_span = MARKDOWN_CONTEXT_TEMPLATE
+            .replace("<path>", relative_path.to_string_lossy().as_ref())
+            .replace("<item>", &content);
+
+        Ok(vec![Document {
+            range: 0..content.len(),
+            content: document_span,
+            embedding: Vec::new(),
+            name: "Markdown".to_string(),
+        }])
+    }
+
     fn get_matches_in_file(
         &mut self,
         content: &str,
@@ -135,6 +150,8 @@ impl CodeContextRetriever {
 
         if PARSEABLE_ENTIRE_FILE_TYPES.contains(&language_name.as_ref()) {
             return self.parse_entire_file(relative_path, language_name, &content);
+        } else if &language_name.to_string() == &"Markdown".to_string() {
+            return self.parse_markdown_file(relative_path, &content);
         }
 
         let mut documents = self.parse_file(content, language)?;
@@ -200,7 +217,12 @@ impl CodeContextRetriever {
 
             let mut document_content = String::new();
             for context_range in &context_match.context_ranges {
-                document_content.push_str(&content[context_range.clone()]);
+                add_content_from_range(
+                    &mut document_content,
+                    content,
+                    context_range.clone(),
+                    context_match.start_col,
+                );
                 document_content.push_str("\n");
             }
 

crates/semantic_index/src/semantic_index.rs 🔗

@@ -612,6 +612,7 @@ impl SemanticIndex {
                                 .await
                             {
                                 if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref())
+                                    && &language.name().as_ref() != &"Markdown"
                                     && language
                                         .grammar()
                                         .and_then(|grammar| grammar.embedding_config.as_ref())

crates/semantic_index/src/semantic_index_tests.rs 🔗

@@ -485,6 +485,79 @@ async fn test_code_context_retrieval_javascript() {
     )
 }
 
+#[gpui::test]
+async fn test_code_context_retrieval_lua() {
+    let language = lua_lang();
+    let mut retriever = CodeContextRetriever::new();
+
+    let text = r#"
+        -- Creates a new class
+        -- @param baseclass The Baseclass of this class, or nil.
+        -- @return A new class reference.
+        function classes.class(baseclass)
+            -- Create the class definition and metatable.
+            local classdef = {}
+            -- Find the super class, either Object or user-defined.
+            baseclass = baseclass or classes.Object
+            -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable.
+            setmetatable(classdef, { __index = baseclass })
+            -- All class instances have a reference to the class object.
+            classdef.class = classdef
+            --- Recursivly allocates the inheritance tree of the instance.
+            -- @param mastertable The 'root' of the inheritance tree.
+            -- @return Returns the instance with the allocated inheritance tree.
+            function classdef.alloc(mastertable)
+                -- All class instances have a reference to a superclass object.
+                local instance = { super = baseclass.alloc(mastertable) }
+                -- Any functions this instance does not know of will 'look up' to the superclass definition.
+                setmetatable(instance, { __index = classdef, __newindex = mastertable })
+                return instance
+            end
+        end
+        "#.unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (r#"
+                -- Creates a new class
+                -- @param baseclass The Baseclass of this class, or nil.
+                -- @return A new class reference.
+                function classes.class(baseclass)
+                    -- Create the class definition and metatable.
+                    local classdef = {}
+                    -- Find the super class, either Object or user-defined.
+                    baseclass = baseclass or classes.Object
+                    -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable.
+                    setmetatable(classdef, { __index = baseclass })
+                    -- All class instances have a reference to the class object.
+                    classdef.class = classdef
+                    --- Recursivly allocates the inheritance tree of the instance.
+                    -- @param mastertable The 'root' of the inheritance tree.
+                    -- @return Returns the instance with the allocated inheritance tree.
+                    function classdef.alloc(mastertable)
+                        --[ ... ]--
+                        --[ ... ]--
+                    end
+                end"#.unindent(),
+            114),
+            (r#"
+            --- Recursivly allocates the inheritance tree of the instance.
+            -- @param mastertable The 'root' of the inheritance tree.
+            -- @return Returns the instance with the allocated inheritance tree.
+            function classdef.alloc(mastertable)
+                -- All class instances have a reference to a superclass object.
+                local instance = { super = baseclass.alloc(mastertable) }
+                -- Any functions this instance does not know of will 'look up' to the superclass definition.
+                setmetatable(instance, { __index = classdef, __newindex = mastertable })
+                return instance
+            end"#.unindent(), 809),
+        ]
+    );
+}
+
 #[gpui::test]
 async fn test_code_context_retrieval_elixir() {
     let language = elixir_lang();
@@ -753,6 +826,346 @@ async fn test_code_context_retrieval_cpp() {
     );
 }
 
+#[gpui::test]
+async fn test_code_context_retrieval_ruby() {
+    let language = ruby_lang();
+    let mut retriever = CodeContextRetriever::new();
+
+    let text = r#"
+        # This concern is inspired by "sudo mode" on GitHub. It
+        # is a way to re-authenticate a user before allowing them
+        # to see or perform an action.
+        #
+        # Add `before_action :require_challenge!` to actions you
+        # want to protect.
+        #
+        # The user will be shown a page to enter the challenge (which
+        # is either the password, or just the username when no
+        # password exists). Upon passing, there is a grace period
+        # during which no challenge will be asked from the user.
+        #
+        # Accessing challenge-protected resources during the grace
+        # period will refresh the grace period.
+        module ChallengableConcern
+            extend ActiveSupport::Concern
+
+            CHALLENGE_TIMEOUT = 1.hour.freeze
+
+            def require_challenge!
+                return if skip_challenge?
+
+                if challenge_passed_recently?
+                    session[:challenge_passed_at] = Time.now.utc
+                    return
+                end
+
+                @challenge = Form::Challenge.new(return_to: request.url)
+
+                if params.key?(:form_challenge)
+                    if challenge_passed?
+                        session[:challenge_passed_at] = Time.now.utc
+                    else
+                        flash.now[:alert] = I18n.t('challenge.invalid_password')
+                        render_challenge
+                    end
+                else
+                    render_challenge
+                end
+            end
+
+            def challenge_passed?
+                current_user.valid_password?(challenge_params[:current_password])
+            end
+        end
+
+        class Animal
+            include Comparable
+
+            attr_reader :legs
+
+            def initialize(name, legs)
+                @name, @legs = name, legs
+            end
+
+            def <=>(other)
+                legs <=> other.legs
+            end
+        end
+
+        # Singleton method for car object
+        def car.wheels
+            puts "There are four wheels"
+        end"#
+        .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (
+                r#"
+        # This concern is inspired by "sudo mode" on GitHub. It
+        # is a way to re-authenticate a user before allowing them
+        # to see or perform an action.
+        #
+        # Add `before_action :require_challenge!` to actions you
+        # want to protect.
+        #
+        # The user will be shown a page to enter the challenge (which
+        # is either the password, or just the username when no
+        # password exists). Upon passing, there is a grace period
+        # during which no challenge will be asked from the user.
+        #
+        # Accessing challenge-protected resources during the grace
+        # period will refresh the grace period.
+        module ChallengableConcern
+            extend ActiveSupport::Concern
+
+            CHALLENGE_TIMEOUT = 1.hour.freeze
+
+            def require_challenge!
+                # ...
+            end
+
+            def challenge_passed?
+                # ...
+            end
+        end"#
+                    .unindent(),
+                558,
+            ),
+            (
+                r#"
+            def require_challenge!
+                return if skip_challenge?
+
+                if challenge_passed_recently?
+                    session[:challenge_passed_at] = Time.now.utc
+                    return
+                end
+
+                @challenge = Form::Challenge.new(return_to: request.url)
+
+                if params.key?(:form_challenge)
+                    if challenge_passed?
+                        session[:challenge_passed_at] = Time.now.utc
+                    else
+                        flash.now[:alert] = I18n.t('challenge.invalid_password')
+                        render_challenge
+                    end
+                else
+                    render_challenge
+                end
+            end"#
+                    .unindent(),
+                663,
+            ),
+            (
+                r#"
+                def challenge_passed?
+                    current_user.valid_password?(challenge_params[:current_password])
+                end"#
+                    .unindent(),
+                1254,
+            ),
+            (
+                r#"
+                class Animal
+                    include Comparable
+
+                    attr_reader :legs
+
+                    def initialize(name, legs)
+                        # ...
+                    end
+
+                    def <=>(other)
+                        # ...
+                    end
+                end"#
+                    .unindent(),
+                1363,
+            ),
+            (
+                r#"
+                def initialize(name, legs)
+                    @name, @legs = name, legs
+                end"#
+                    .unindent(),
+                1427,
+            ),
+            (
+                r#"
+                def <=>(other)
+                    legs <=> other.legs
+                end"#
+                    .unindent(),
+                1501,
+            ),
+            (
+                r#"
+                # Singleton method for car object
+                def car.wheels
+                    puts "There are four wheels"
+                end"#
+                    .unindent(),
+                1591,
+            ),
+        ],
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_php() {
+    let language = php_lang();
+    let mut retriever = CodeContextRetriever::new();
+
+    let text = r#"
+        <?php
+
+        namespace LevelUp\Experience\Concerns;
+
+        /*
+        This is a multiple-lines comment block
+        that spans over multiple
+        lines
+        */
+        function functionName() {
+            echo "Hello world!";
+        }
+
+        trait HasAchievements
+        {
+            /**
+            * @throws \Exception
+            */
+            public function grantAchievement(Achievement $achievement, $progress = null): void
+            {
+                if ($progress > 100) {
+                    throw new Exception(message: 'Progress cannot be greater than 100');
+                }
+
+                if ($this->achievements()->find($achievement->id)) {
+                    throw new Exception(message: 'User already has this Achievement');
+                }
+
+                $this->achievements()->attach($achievement, [
+                    'progress' => $progress ?? null,
+                ]);
+
+                $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this)));
+            }
+
+            public function achievements(): BelongsToMany
+            {
+                return $this->belongsToMany(related: Achievement::class)
+                ->withPivot(columns: 'progress')
+                ->where('is_secret', false)
+                ->using(AchievementUser::class);
+            }
+        }
+
+        interface Multiplier
+        {
+            public function qualifies(array $data): bool;
+
+            public function setMultiplier(): int;
+        }
+
+        enum AuditType: string
+        {
+            case Add = 'add';
+            case Remove = 'remove';
+            case Reset = 'reset';
+            case LevelUp = 'level_up';
+        }
+
+        ?>"#
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (
+                r#"
+        /*
+        This is a multiple-lines comment block
+        that spans over multiple
+        lines
+        */
+        function functionName() {
+            echo "Hello world!";
+        }"#
+                .unindent(),
+                123,
+            ),
+            (
+                r#"
+        trait HasAchievements
+        {
+            /**
+            * @throws \Exception
+            */
+            public function grantAchievement(Achievement $achievement, $progress = null): void
+            {/* ... */}
+
+            public function achievements(): BelongsToMany
+            {/* ... */}
+        }"#
+                .unindent(),
+                177,
+            ),
+            (r#"
+            /**
+            * @throws \Exception
+            */
+            public function grantAchievement(Achievement $achievement, $progress = null): void
+            {
+                if ($progress > 100) {
+                    throw new Exception(message: 'Progress cannot be greater than 100');
+                }
+
+                if ($this->achievements()->find($achievement->id)) {
+                    throw new Exception(message: 'User already has this Achievement');
+                }
+
+                $this->achievements()->attach($achievement, [
+                    'progress' => $progress ?? null,
+                ]);
+
+                $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this)));
+            }"#.unindent(), 245),
+            (r#"
+                public function achievements(): BelongsToMany
+                {
+                    return $this->belongsToMany(related: Achievement::class)
+                    ->withPivot(columns: 'progress')
+                    ->where('is_secret', false)
+                    ->using(AchievementUser::class);
+                }"#.unindent(), 902),
+            (r#"
+                interface Multiplier
+                {
+                    public function qualifies(array $data): bool;
+
+                    public function setMultiplier(): int;
+                }"#.unindent(),
+                1146),
+            (r#"
+                enum AuditType: string
+                {
+                    case Add = 'add';
+                    case Remove = 'remove';
+                    case Reset = 'reset';
+                    case LevelUp = 'level_up';
+                }"#.unindent(), 1265)
+        ],
+    );
+}
+
 #[gpui::test]
 fn test_dot_product(mut rng: StdRng) {
     assert_eq!(dot(&[1., 0., 0., 0., 0.], &[0., 1., 0., 0., 0.]), 0.);
@@ -1083,6 +1496,131 @@ fn cpp_lang() -> Arc<Language> {
     )
 }
 
+fn lua_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Lua".into(),
+                path_suffixes: vec!["lua".into()],
+                collapsed_placeholder: "--[ ... ]--".to_string(),
+                ..Default::default()
+            },
+            Some(tree_sitter_lua::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (comment)* @context
+                .
+                (function_declaration
+                    "function" @name
+                    name: (_) @name
+                    (comment)* @collapse
+                    body: (block) @collapse
+                ) @item
+            )
+        "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn php_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "PHP".into(),
+                path_suffixes: vec!["php".into()],
+                collapsed_placeholder: "/* ... */".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_php::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (comment)* @context
+                .
+                [
+                    (function_definition
+                        "function" @name
+                        name: (_) @name
+                        body: (_
+                            "{" @keep
+                            "}" @keep) @collapse
+                        )
+
+                    (trait_declaration
+                        "trait" @name
+                        name: (_) @name)
+
+                    (method_declaration
+                        "function" @name
+                        name: (_) @name
+                        body: (_
+                            "{" @keep
+                            "}" @keep) @collapse
+                        )
+
+                    (interface_declaration
+                        "interface" @name
+                        name: (_) @name
+                        )
+
+                    (enum_declaration
+                        "enum" @name
+                        name: (_) @name
+                        )
+
+                ] @item
+            )
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn ruby_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Ruby".into(),
+                path_suffixes: vec!["rb".into()],
+                collapsed_placeholder: "# ...".to_string(),
+                ..Default::default()
+            },
+            Some(tree_sitter_ruby::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (comment)* @context
+                .
+                [
+                (module
+                    "module" @name
+                    name: (_) @name)
+                (method
+                    "def" @name
+                    name: (_) @name
+                    body: (body_statement) @collapse)
+                (class
+                    "class" @name
+                    name: (_) @name)
+                (singleton_method
+                    "def" @name
+                    object: (_) @name
+                    "." @name
+                    name: (_) @name
+                    body: (body_statement) @collapse)
+                ] @item
+            )
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
 fn elixir_lang() -> Arc<Language> {
     Arc::new(
         Language::new(

crates/sum_tree/src/cursor.rs 🔗

@@ -202,7 +202,7 @@ where
                 self.position = D::default();
             }
 
-            let mut entry = self.stack.last_mut().unwrap();
+            let entry = self.stack.last_mut().unwrap();
             if !descending {
                 if entry.index == 0 {
                     self.stack.pop();

crates/terminal/src/terminal.rs 🔗

@@ -53,7 +53,7 @@ use gpui::{
     keymap_matcher::Keystroke,
     platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase},
     scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
-    AppContext, ClipboardItem, Entity, ModelContext, Task,
+    AnyWindowHandle, AppContext, ClipboardItem, Entity, ModelContext, Task,
 };
 
 use crate::mappings::{
@@ -404,7 +404,7 @@ impl TerminalBuilder {
         mut env: HashMap<String, String>,
         blink_settings: Option<TerminalBlink>,
         alternate_scroll: AlternateScroll,
-        window_id: usize,
+        window: AnyWindowHandle,
     ) -> Result<TerminalBuilder> {
         let pty_config = {
             let alac_shell = match shell.clone() {
@@ -462,7 +462,7 @@ impl TerminalBuilder {
         let pty = match tty::new(
             &pty_config,
             TerminalSize::default().into(),
-            window_id as u64,
+            window.id() as u64,
         ) {
             Ok(pty) => pty,
             Err(error) => {

crates/terminal_view/src/terminal_element.rs 🔗

@@ -10,8 +10,9 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     serde_json::json,
     text_layout::{Line, RunStyle},
-    AnyElement, Element, EventContext, FontCache, LayoutContext, ModelContext, MouseRegion, Quad,
-    SceneBuilder, SizeConstraint, TextLayoutCache, ViewContext, WeakModelHandle,
+    AnyElement, Element, EventContext, FontCache, LayoutContext, ModelContext, MouseRegion,
+    PaintContext, Quad, SceneBuilder, SizeConstraint, TextLayoutCache, ViewContext,
+    WeakModelHandle,
 };
 use itertools::Itertools;
 use language::CursorShape;
@@ -152,7 +153,7 @@ impl LayoutRect {
             bounds: RectF::new(position, size),
             background: Some(self.color),
             border: Default::default(),
-            corner_radius: 0.,
+            corner_radii: Default::default(),
         })
     }
 }
@@ -734,7 +735,7 @@ impl Element<TerminalView> for TerminalElement {
         visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         view: &mut TerminalView,
-        cx: &mut ViewContext<TerminalView>,
+        cx: &mut PaintContext<TerminalView>,
     ) -> Self::PaintState {
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 
@@ -762,7 +763,7 @@ impl Element<TerminalView> for TerminalElement {
                     bounds: RectF::new(bounds.origin(), bounds.size()),
                     background: Some(layout.background_color),
                     border: Default::default(),
-                    corner_radius: 0.,
+                    corner_radii: Default::default(),
                 });
 
                 for rect in &layout.rects {

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -48,7 +48,7 @@ impl TerminalPanel {
     fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
         let weak_self = cx.weak_handle();
         let pane = cx.add_view(|cx| {
-            let window_id = cx.window_id();
+            let window = cx.window();
             let mut pane = Pane::new(
                 workspace.weak_handle(),
                 workspace.project().clone(),
@@ -60,7 +60,7 @@ impl TerminalPanel {
             pane.set_can_navigate(false, cx);
             pane.on_can_drop(move |drag_and_drop, cx| {
                 drag_and_drop
-                    .currently_dragged::<DraggedItem>(window_id)
+                    .currently_dragged::<DraggedItem>(window)
                     .map_or(false, |(_, item)| {
                         item.handle.act_as::<TerminalView>(cx).is_some()
                     })
@@ -72,10 +72,7 @@ impl TerminalPanel {
                         0,
                         "icons/plus_12.svg",
                         false,
-                        Some((
-                            "New Terminal".into(),
-                            Some(Box::new(workspace::NewTerminal)),
-                        )),
+                        Some(("New Terminal", Some(Box::new(workspace::NewTerminal)))),
                         cx,
                         move |_, cx| {
                             let this = this.clone();
@@ -255,10 +252,10 @@ impl TerminalPanel {
                     .clone();
                 let working_directory =
                     crate::get_working_directory(workspace, cx, working_directory_strategy);
-                let window_id = cx.window_id();
+                let window = cx.window();
                 if let Some(terminal) = workspace.project().update(cx, |project, cx| {
                     project
-                        .create_terminal(working_directory, window_id, cx)
+                        .create_terminal(working_directory, window, cx)
                         .log_err()
                 }) {
                     let terminal = Box::new(cx.add_view(|cx| {

crates/terminal_view/src/terminal_view.rs 🔗

@@ -112,11 +112,11 @@ impl TerminalView {
         let working_directory =
             get_working_directory(workspace, cx, strategy.working_directory.clone());
 
-        let window_id = cx.window_id();
+        let window = cx.window();
         let terminal = workspace
             .project()
             .update(cx, |project, cx| {
-                project.create_terminal(working_directory, window_id, cx)
+                project.create_terminal(working_directory, window, cx)
             })
             .notify_err(workspace, cx);
 
@@ -741,7 +741,7 @@ impl Item for TerminalView {
         item_id: workspace::ItemId,
         cx: &mut ViewContext<Pane>,
     ) -> Task<anyhow::Result<ViewHandle<Self>>> {
-        let window_id = cx.window_id();
+        let window = cx.window();
         cx.spawn(|pane, mut cx| async move {
             let cwd = TERMINAL_DB
                 .get_working_directory(item_id, workspace_id)
@@ -762,7 +762,7 @@ impl Item for TerminalView {
                 });
 
             let terminal = project.update(&mut cx, |project, cx| {
-                project.create_terminal(cwd, window_id, cx)
+                project.create_terminal(cwd, window, cx)
             })?;
             Ok(pane.update(&mut cx, |_, cx| {
                 cx.add_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx))
@@ -1070,7 +1070,9 @@ mod tests {
         });
 
         let project = Project::test(params.fs.clone(), [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
 
         (project, workspace)
     }

crates/theme/src/ui.rs 🔗

@@ -192,7 +192,6 @@ where
     F: FnOnce(&mut gpui::ViewContext<V>) -> D,
 {
     const TITLEBAR_HEIGHT: f32 = 28.;
-    // let active = cx.window_is_active(cx.window_id());
 
     Flex::column()
         .with_child(

crates/util/src/paths.rs 🔗

@@ -30,49 +30,57 @@ pub mod legacy {
     }
 }
 
-/// Compacts a given file path by replacing the user's home directory
-/// prefix with a tilde (`~`).
-///
-/// # Arguments
-///
-/// * `path` - A reference to a `Path` representing the file path to compact.
-///
-/// # Examples
-///
-/// ```
-/// use std::path::{Path, PathBuf};
-/// use util::paths::compact;
-/// let path: PathBuf = [
-///     util::paths::HOME.to_string_lossy().to_string(),
-///     "some_file.txt".to_string(),
-///  ]
-///  .iter()
-///  .collect();
-/// if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
-///     assert_eq!(compact(&path).to_str(), Some("~/some_file.txt"));
-/// } else {
-///     assert_eq!(compact(&path).to_str(), path.to_str());
-/// }
-/// ```
-///
-/// # Returns
-///
-/// * A `PathBuf` containing the compacted file path. If the input path
-///   does not have the user's home directory prefix, or if we are not on
-///   Linux or macOS, the original path is returned unchanged.
-pub fn compact(path: &Path) -> PathBuf {
-    if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
-        match path.strip_prefix(HOME.as_path()) {
-            Ok(relative_path) => {
-                let mut shortened_path = PathBuf::new();
-                shortened_path.push("~");
-                shortened_path.push(relative_path);
-                shortened_path
+pub trait PathExt {
+    fn compact(&self) -> PathBuf;
+    fn icon_suffix(&self) -> Option<&str>;
+    fn extension_or_hidden_file_name(&self) -> Option<&str>;
+}
+
+impl<T: AsRef<Path>> PathExt for T {
+    /// Compacts a given file path by replacing the user's home directory
+    /// prefix with a tilde (`~`).
+    ///
+    /// # Returns
+    ///
+    /// * A `PathBuf` containing the compacted file path. If the input path
+    ///   does not have the user's home directory prefix, or if we are not on
+    ///   Linux or macOS, the original path is returned unchanged.
+    fn compact(&self) -> PathBuf {
+        if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
+            match self.as_ref().strip_prefix(HOME.as_path()) {
+                Ok(relative_path) => {
+                    let mut shortened_path = PathBuf::new();
+                    shortened_path.push("~");
+                    shortened_path.push(relative_path);
+                    shortened_path
+                }
+                Err(_) => self.as_ref().to_path_buf(),
             }
-            Err(_) => path.to_path_buf(),
+        } else {
+            self.as_ref().to_path_buf()
+        }
+    }
+
+    /// Returns a suffix of the path that is used to determine which file icon to use
+    fn icon_suffix(&self) -> Option<&str> {
+        let file_name = self.as_ref().file_name()?.to_str()?;
+
+        if file_name.starts_with(".") {
+            return file_name.strip_prefix(".");
         }
-    } else {
-        path.to_path_buf()
+
+        self.as_ref()
+            .extension()
+            .and_then(|extension| extension.to_str())
+    }
+
+    /// Returns a file's extension or, if the file is hidden, its name without the leading dot
+    fn extension_or_hidden_file_name(&self) -> Option<&str> {
+        if let Some(extension) = self.as_ref().extension() {
+            return extension.to_str();
+        }
+
+        self.as_ref().file_name()?.to_str()?.split('.').last()
     }
 }
 
@@ -279,4 +287,65 @@ mod tests {
             );
         }
     }
+
+    #[test]
+    fn test_path_compact() {
+        let path: PathBuf = [
+            HOME.to_string_lossy().to_string(),
+            "some_file.txt".to_string(),
+        ]
+        .iter()
+        .collect();
+        if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
+            assert_eq!(path.compact().to_str(), Some("~/some_file.txt"));
+        } else {
+            assert_eq!(path.compact().to_str(), path.to_str());
+        }
+    }
+
+    #[test]
+    fn test_icon_suffix() {
+        // No dots in name
+        let path = Path::new("/a/b/c/file_name.rs");
+        assert_eq!(path.icon_suffix(), Some("rs"));
+
+        // Single dot in name
+        let path = Path::new("/a/b/c/file.name.rs");
+        assert_eq!(path.icon_suffix(), Some("rs"));
+
+        // Multiple dots in name
+        let path = Path::new("/a/b/c/long.file.name.rs");
+        assert_eq!(path.icon_suffix(), Some("rs"));
+
+        // Hidden file, no extension
+        let path = Path::new("/a/b/c/.gitignore");
+        assert_eq!(path.icon_suffix(), Some("gitignore"));
+
+        // Hidden file, with extension
+        let path = Path::new("/a/b/c/.eslintrc.js");
+        assert_eq!(path.icon_suffix(), Some("eslintrc.js"));
+    }
+
+    #[test]
+    fn test_extension_or_hidden_file_name() {
+        // No dots in name
+        let path = Path::new("/a/b/c/file_name.rs");
+        assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
+
+        // Single dot in name
+        let path = Path::new("/a/b/c/file.name.rs");
+        assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
+
+        // Multiple dots in name
+        let path = Path::new("/a/b/c/long.file.name.rs");
+        assert_eq!(path.extension_or_hidden_file_name(), Some("rs"));
+
+        // Hidden file, no extension
+        let path = Path::new("/a/b/c/.gitignore");
+        assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore"));
+
+        // Hidden file, with extension
+        let path = Path::new("/a/b/c/.eslintrc.js");
+        assert_eq!(path.extension_or_hidden_file_name(), Some("js"));
+    }
 }

crates/vim/src/editor_events.rs 🔗

@@ -10,7 +10,7 @@ pub fn init(cx: &mut AppContext) {
 
 fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
     if let Some(previously_active_editor) = Vim::read(cx).active_editor.clone() {
-        cx.update_window(previously_active_editor.window_id(), |cx| {
+        previously_active_editor.window().update(cx, |cx| {
             Vim::update(cx, |vim, cx| {
                 vim.update_active_editor(cx, |previously_active_editor, cx| {
                     vim.unhook_vim_settings(previously_active_editor, cx)
@@ -19,7 +19,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
         });
     }
 
-    cx.update_window(editor.window_id(), |cx| {
+    editor.window().update(cx, |cx| {
         Vim::update(cx, |vim, cx| {
             vim.set_active_editor(editor.clone(), cx);
         });
@@ -27,7 +27,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
 }
 
 fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
-    cx.update_window(editor.window_id(), |cx| {
+    editor.window().update(cx, |cx| {
         Vim::update(cx, |vim, cx| {
             if let Some(previous_editor) = vim.active_editor.clone() {
                 if previous_editor == editor.clone() {
@@ -41,7 +41,7 @@ fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
 }
 
 fn released(EditorReleased(editor): &EditorReleased, cx: &mut AppContext) {
-    cx.update_window(editor.window_id(), |cx| {
+    editor.window().update(cx, |cx| {
         cx.update_default_global(|vim: &mut Vim, _| {
             if let Some(previous_editor) = vim.active_editor.clone() {
                 if previous_editor == editor.clone() {

crates/vim/src/mode_indicator.rs 🔗

@@ -20,7 +20,7 @@ impl ModeIndicator {
             if let Some(mode_indicator) = handle.upgrade(cx) {
                 match event {
                     VimEvent::ModeChanged { mode } => {
-                        cx.update_window(mode_indicator.window_id(), |cx| {
+                        mode_indicator.window().update(cx, |cx| {
                             mode_indicator.update(cx, move |mode_indicator, cx| {
                                 mode_indicator.set_mode(mode, cx);
                             })

crates/vim/src/normal/search.rs 🔗

@@ -93,7 +93,7 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte
         pane.update(cx, |pane, cx| {
             if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
                 search_bar.update(cx, |search_bar, cx| {
-                    let mut state = &mut vim.state.search;
+                    let state = &mut vim.state.search;
                     let mut count = state.count;
 
                     // in the case that the query has changed, the search bar
@@ -222,7 +222,7 @@ mod test {
         });
 
         search_bar.read_with(cx.cx, |bar, cx| {
-            assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
+            assert_eq!(bar.query(cx), "cc");
         });
 
         deterministic.run_until_parked();

crates/vim/src/test.rs 🔗

@@ -99,7 +99,7 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
     });
 
     search_bar.read_with(cx.cx, |bar, cx| {
-        assert_eq!(bar.query_editor.read(cx).text(cx), "");
+        assert_eq!(bar.query(cx), "");
     })
 }
 
@@ -175,7 +175,7 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
     });
 
     search_bar.read_with(cx.cx, |bar, cx| {
-        assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
+        assert_eq!(bar.query(cx), "cc");
     });
 
     // wait for the query editor change event to fire.

crates/vim/src/test/vim_test_context.rs 🔗

@@ -85,9 +85,9 @@ impl<'a> VimTestContext<'a> {
     }
 
     pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle {
-        let window_id = self.window_id;
+        let window = self.window;
         let context_handle = self.cx.set_state(text);
-        self.update_window(window_id, |cx| {
+        window.update(self.cx.cx.cx, |cx| {
             Vim::update(cx, |vim, cx| {
                 vim.switch_mode(mode, true, cx);
             })

crates/workspace/src/dock.rs 🔗

@@ -203,7 +203,7 @@ impl Dock {
     pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option<usize> {
         self.panel_entries.iter().position(|entry| {
             let panel = entry.panel.as_any();
-            cx.view_ui_name(panel.window_id(), panel.id()) == Some(ui_name)
+            cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name)
         })
     }
 
@@ -530,16 +530,15 @@ impl View for PanelButtons {
                                     tooltip_action.as_ref().map(|action| action.boxed_clone());
                                 move |_, this, cx| {
                                     if let Some(tooltip_action) = &tooltip_action {
-                                        let window_id = cx.window_id();
+                                        let window = cx.window();
                                         let view_id = this.workspace.id();
                                         let tooltip_action = tooltip_action.boxed_clone();
                                         cx.spawn(|_, mut cx| async move {
-                                            cx.dispatch_action(
-                                                window_id,
+                                            window.dispatch_action(
                                                 view_id,
                                                 &*tooltip_action,
-                                            )
-                                            .ok();
+                                                &mut cx,
+                                            );
                                         })
                                         .detach();
                                     }

crates/workspace/src/item.rs 🔗

@@ -6,6 +6,7 @@ use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
 use anyhow::Result;
 use client::{proto, Client};
 use gpui::geometry::vector::Vector2F;
+use gpui::AnyWindowHandle;
 use gpui::{
     fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, ModelHandle, Task, View,
     ViewContext, ViewHandle, WeakViewHandle, WindowContext,
@@ -250,7 +251,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
     fn workspace_deactivated(&self, cx: &mut WindowContext);
     fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool;
     fn id(&self) -> usize;
-    fn window_id(&self) -> usize;
+    fn window(&self) -> AnyWindowHandle;
     fn as_any(&self) -> &AnyViewHandle;
     fn is_dirty(&self, cx: &AppContext) -> bool;
     fn has_conflict(&self, cx: &AppContext) -> bool;
@@ -280,7 +281,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
 
 pub trait WeakItemHandle {
     fn id(&self) -> usize;
-    fn window_id(&self) -> usize;
+    fn window(&self) -> AnyWindowHandle;
     fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
 }
 
@@ -542,8 +543,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         self.id()
     }
 
-    fn window_id(&self) -> usize {
-        self.window_id()
+    fn window(&self) -> AnyWindowHandle {
+        AnyViewHandle::window(self)
     }
 
     fn as_any(&self) -> &AnyViewHandle {
@@ -649,8 +650,8 @@ impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
         self.id()
     }
 
-    fn window_id(&self) -> usize {
-        self.window_id()
+    fn window(&self) -> AnyWindowHandle {
+        self.window()
     }
 
     fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {

crates/workspace/src/pane.rs 🔗

@@ -25,8 +25,8 @@ use gpui::{
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel},
     Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
-    LayoutContext, ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle,
-    WeakViewHandle, WindowContext,
+    LayoutContext, ModelHandle, MouseRegion, PaintContext, Quad, Task, View, ViewContext,
+    ViewHandle, WeakViewHandle, WindowContext,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
 use serde::Deserialize;
@@ -303,10 +303,10 @@ impl Pane {
                         let tooltip_label;
                         if pane.is_zoomed() {
                             icon_path = "icons/minimize_8.svg";
-                            tooltip_label = "Zoom In".into();
+                            tooltip_label = "Zoom In";
                         } else {
                             icon_path = "icons/maximize_8.svg";
-                            tooltip_label = "Zoom In".into();
+                            tooltip_label = "Zoom In";
                         }
 
                         Pane::render_tab_bar_button(
@@ -1397,7 +1397,7 @@ impl Pane {
                         bounds: square,
                         background: Some(color),
                         border: Default::default(),
-                        corner_radius: diameter / 2.,
+                        corner_radii: (diameter / 2.).into(),
                     });
                 }
             })
@@ -1477,7 +1477,7 @@ impl Pane {
         index: usize,
         icon: &'static str,
         is_active: bool,
-        tooltip: Option<(String, Option<Box<dyn Action>>)>,
+        tooltip: Option<(&'static str, Option<Box<dyn Action>>)>,
         cx: &mut ViewContext<Pane>,
         on_click: F1,
         on_down: F2,
@@ -1900,7 +1900,7 @@ impl<V: View> Element<V> for PaneBackdrop<V> {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut V,
-        cx: &mut ViewContext<V>,
+        cx: &mut PaintContext<V>,
     ) -> Self::PaintState {
         let background = theme::current(cx).editor.background;
 
@@ -1917,8 +1917,8 @@ impl<V: View> Element<V> for PaneBackdrop<V> {
             MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
                 gpui::platform::MouseButton::Left,
                 move |_, _: &mut V, cx| {
-                    let window_id = cx.window_id();
-                    cx.app_context().focus(window_id, Some(child_view_id))
+                    let window = cx.window();
+                    cx.app_context().focus(window, Some(child_view_id))
                 },
             ),
         );
@@ -1972,7 +1972,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         pane.update(cx, |pane, cx| {
@@ -1987,7 +1988,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // 1. Add with a destination index
@@ -2065,7 +2067,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // 1. Add with a destination index
@@ -2141,7 +2144,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // singleton view
@@ -2209,7 +2213,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         add_labeled_item(&pane, "A", false, cx);
@@ -2256,7 +2261,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
@@ -2276,7 +2282,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         add_labeled_item(&pane, "A", true, cx);
@@ -2299,7 +2306,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
@@ -2319,7 +2327,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx);
@@ -2339,7 +2348,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         add_labeled_item(&pane, "A", false, cx);

crates/workspace/src/pane/dragged_item_receiver.rs 🔗

@@ -28,11 +28,11 @@ where
     let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
     let drag_position = if (pane.can_drop)(drag_and_drop, cx) {
         drag_and_drop
-            .currently_dragged::<DraggedItem>(cx.window_id())
+            .currently_dragged::<DraggedItem>(cx.window())
             .map(|(drag_position, _)| drag_position)
             .or_else(|| {
                 drag_and_drop
-                    .currently_dragged::<ProjectEntryId>(cx.window_id())
+                    .currently_dragged::<ProjectEntryId>(cx.window())
                     .map(|(drag_position, _)| drag_position)
             })
     } else {
@@ -61,7 +61,7 @@ where
                                 bounds: overlay_region,
                                 background: Some(overlay_color(cx)),
                                 border: Default::default(),
-                                corner_radius: 0.,
+                                corner_radii: Default::default(),
                             });
                         });
                     }
@@ -91,10 +91,10 @@ where
                 let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
 
                 if drag_and_drop
-                    .currently_dragged::<DraggedItem>(cx.window_id())
+                    .currently_dragged::<DraggedItem>(cx.window())
                     .is_some()
                     || drag_and_drop
-                        .currently_dragged::<ProjectEntryId>(cx.window_id())
+                        .currently_dragged::<ProjectEntryId>(cx.window())
                         .is_some()
                 {
                     cx.notify();
@@ -122,11 +122,11 @@ pub fn handle_dropped_item<V: View>(
     }
     let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
     let action = if let Some((_, dragged_item)) =
-        drag_and_drop.currently_dragged::<DraggedItem>(cx.window_id())
+        drag_and_drop.currently_dragged::<DraggedItem>(cx.window())
     {
         Action::Move(dragged_item.pane.clone(), dragged_item.handle.id())
     } else if let Some((_, project_entry)) =
-        drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window_id())
+        drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window())
     {
         Action::Open(*project_entry)
     } else {

crates/workspace/src/pane_group.rs 🔗

@@ -595,7 +595,7 @@ mod element {
         platform::{CursorStyle, MouseButton},
         scene::MouseDrag,
         AnyElement, Axis, CursorRegion, Element, EventContext, LayoutContext, MouseRegion,
-        RectFExt, SceneBuilder, SizeConstraint, Vector2FExt, ViewContext,
+        PaintContext, RectFExt, SceneBuilder, SizeConstraint, Vector2FExt, ViewContext,
     };
 
     use crate::{
@@ -856,7 +856,7 @@ mod element {
             visible_bounds: RectF,
             remaining_space: &mut Self::LayoutState,
             view: &mut Workspace,
-            cx: &mut ViewContext<Workspace>,
+            cx: &mut PaintContext<Workspace>,
         ) -> Self::PaintState {
             let can_resize = settings::get::<WorkspaceSettings>(cx).active_pane_magnification == 1.;
             let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();

crates/workspace/src/searchable.rs 🔗

@@ -235,7 +235,7 @@ impl From<&Box<dyn SearchableItemHandle>> for AnyViewHandle {
 
 impl PartialEq for Box<dyn SearchableItemHandle> {
     fn eq(&self, other: &Self) -> bool {
-        self.id() == other.id() && self.window_id() == other.window_id()
+        self.id() == other.id() && self.window() == other.window()
     }
 }
 
@@ -259,7 +259,7 @@ impl<T: SearchableItem> WeakSearchableItemHandle for WeakViewHandle<T> {
 
 impl PartialEq for Box<dyn WeakSearchableItemHandle> {
     fn eq(&self, other: &Self) -> bool {
-        self.id() == other.id() && self.window_id() == other.window_id()
+        self.id() == other.id() && self.window() == other.window()
     }
 }
 
@@ -267,6 +267,6 @@ impl Eq for Box<dyn WeakSearchableItemHandle> {}
 
 impl std::hash::Hash for Box<dyn WeakSearchableItemHandle> {
     fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        (self.id(), self.window_id()).hash(state)
+        (self.id(), self.window().id()).hash(state)
     }
 }

crates/workspace/src/status_bar.rs 🔗

@@ -8,8 +8,8 @@ use gpui::{
         vector::{vec2f, Vector2F},
     },
     json::{json, ToJson},
-    AnyElement, AnyViewHandle, Entity, LayoutContext, SceneBuilder, SizeConstraint, Subscription,
-    View, ViewContext, ViewHandle, WindowContext,
+    AnyElement, AnyViewHandle, Entity, LayoutContext, PaintContext, SceneBuilder, SizeConstraint,
+    Subscription, View, ViewContext, ViewHandle, WindowContext,
 };
 
 pub trait StatusItemView: View {
@@ -231,7 +231,7 @@ impl Element<StatusBar> for StatusBarElement {
         visible_bounds: RectF,
         _: &mut Self::LayoutState,
         view: &mut StatusBar,
-        cx: &mut ViewContext<StatusBar>,
+        cx: &mut PaintContext<StatusBar>,
     ) -> Self::PaintState {
         let origin_y = bounds.upper_right().y();
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();

crates/workspace/src/toolbar.rs 🔗

@@ -220,7 +220,7 @@ fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>
     spacing: f32,
     on_click: F,
     tooltip_action: A,
-    action_name: &str,
+    action_name: &'static str,
     cx: &mut ViewContext<Toolbar>,
 ) -> AnyElement<Toolbar> {
     MouseEventHandler::<A, _>::new(0, cx, |state, _| {
@@ -252,7 +252,7 @@ fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>
     })
     .with_tooltip::<A>(
         0,
-        action_name.to_string(),
+        action_name,
         Some(Box::new(tooltip_action)),
         tooltip_style,
         cx,

crates/workspace/src/workspace.rs 🔗

@@ -37,7 +37,7 @@ use gpui::{
     },
     AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
     ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
-    WeakViewHandle, WindowContext,
+    WeakViewHandle, WindowContext, WindowHandle,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
 use itertools::Itertools;
@@ -749,7 +749,7 @@ impl Workspace {
     fn new_local(
         abs_paths: Vec<PathBuf>,
         app_state: Arc<AppState>,
-        requesting_window_id: Option<usize>,
+        requesting_window: Option<WindowHandle<Workspace>>,
         cx: &mut AppContext,
     ) -> Task<(
         WeakViewHandle<Workspace>,
@@ -793,20 +793,13 @@ impl Workspace {
                 DB.next_id().await.unwrap_or(0)
             };
 
-            let workspace = requesting_window_id
-                .and_then(|window_id| {
-                    cx.update(|cx| {
-                        cx.replace_root_view(window_id, |cx| {
-                            Workspace::new(
-                                workspace_id,
-                                project_handle.clone(),
-                                app_state.clone(),
-                                cx,
-                            )
-                        })
-                    })
-                })
-                .unwrap_or_else(|| {
+            let window = if let Some(window) = requesting_window {
+                window.replace_root(&mut cx, |cx| {
+                    Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
+                });
+                window
+            } else {
+                {
                     let window_bounds_override = window_bounds_env_override(&cx);
                     let (bounds, display) = if let Some(bounds) = window_bounds_override {
                         (Some(bounds), None)
@@ -852,8 +845,12 @@ impl Workspace {
                             )
                         },
                     )
-                    .1
-                });
+                }
+            };
+
+            // We haven't yielded the main thread since obtaining the window handle,
+            // so the window exists.
+            let workspace = window.root(&cx).unwrap();
 
             (app_state.initialize_workspace)(
                 workspace.downgrade(),
@@ -864,7 +861,7 @@ impl Workspace {
             .await
             .log_err();
 
-            cx.update_window(workspace.window_id(), |cx| cx.activate_window());
+            window.update(&mut cx, |cx| cx.activate_window());
 
             let workspace = workspace.downgrade();
             notify_if_database_failed(&workspace, &mut cx);
@@ -1235,14 +1232,14 @@ impl Workspace {
 
     pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
         cx.spawn(|mut cx| async move {
-            let id = cx
-                .window_ids()
+            let window = cx
+                .windows()
                 .into_iter()
-                .find(|&id| cx.window_is_active(id));
-            if let Some(id) = id {
+                .find(|window| window.is_active(&cx).unwrap_or(false));
+            if let Some(window) = window {
                 //This can only get called when the window's project connection has been lost
                 //so we don't need to prompt the user for anything and instead just close the window
-                cx.remove_window(id);
+                window.remove(&mut cx);
             }
         })
         .detach();
@@ -1253,11 +1250,11 @@ impl Workspace {
         _: &CloseWindow,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
-        let window_id = cx.window_id();
+        let window = cx.window();
         let prepare = self.prepare_to_close(false, cx);
         Some(cx.spawn(|_, mut cx| async move {
             if prepare.await? {
-                cx.remove_window(window_id);
+                window.remove(&mut cx);
             }
             Ok(())
         }))
@@ -1269,13 +1266,13 @@ impl Workspace {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<bool>> {
         let active_call = self.active_call().cloned();
-        let window_id = cx.window_id();
+        let window = cx.window();
 
         cx.spawn(|this, mut cx| async move {
             let workspace_count = cx
-                .window_ids()
+                .windows()
                 .into_iter()
-                .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
+                .filter(|window| window.root_is::<Workspace>())
                 .count();
 
             if let Some(active_call) = active_call {
@@ -1283,11 +1280,11 @@ impl Workspace {
                     && workspace_count == 1
                     && active_call.read_with(&cx, |call, _| call.room().is_some())
                 {
-                    let answer = cx.prompt(
-                        window_id,
+                    let answer = window.prompt(
                         PromptLevel::Warning,
                         "Do you want to leave the current call?",
                         &["Close window and hang up", "Cancel"],
+                        &mut cx,
                     );
 
                     if let Some(mut answer) = answer {
@@ -1393,7 +1390,7 @@ impl Workspace {
         paths: Vec<PathBuf>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
-        let window_id = cx.window_id();
+        let window = cx.window().downcast::<Self>();
         let is_remote = self.project.read(cx).is_remote();
         let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
         let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
@@ -1405,15 +1402,15 @@ impl Workspace {
         let app_state = self.app_state.clone();
 
         cx.spawn(|_, mut cx| async move {
-            let window_id_to_replace = if let Some(close_task) = close_task {
+            let window_to_replace = if let Some(close_task) = close_task {
                 if !close_task.await? {
                     return Ok(());
                 }
-                Some(window_id)
+                window
             } else {
                 None
             };
-            cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx))
+            cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx))
                 .await?;
             Ok(())
         })
@@ -3184,7 +3181,7 @@ impl Workspace {
             let left_visible = left_dock.is_open();
             let left_active_panel = left_dock.visible_panel().and_then(|panel| {
                 Some(
-                    cx.view_ui_name(panel.as_any().window_id(), panel.id())?
+                    cx.view_ui_name(panel.as_any().window(), panel.id())?
                         .to_string(),
                 )
             });
@@ -3197,7 +3194,7 @@ impl Workspace {
             let right_visible = right_dock.is_open();
             let right_active_panel = right_dock.visible_panel().and_then(|panel| {
                 Some(
-                    cx.view_ui_name(panel.as_any().window_id(), panel.id())?
+                    cx.view_ui_name(panel.as_any().window(), panel.id())?
                         .to_string(),
                 )
             });
@@ -3210,7 +3207,7 @@ impl Workspace {
             let bottom_visible = bottom_dock.is_open();
             let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
                 Some(
-                    cx.view_ui_name(panel.as_any().window_id(), panel.id())?
+                    cx.view_ui_name(panel.as_any().window(), panel.id())?
                         .to_string(),
                 )
             });
@@ -3617,7 +3614,7 @@ fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppCo
                                 bounds,
                                 background: Some(code_span_background_color),
                                 border: Default::default(),
-                                corner_radius: 2.0,
+                                corner_radii: (2.0).into(),
                             })
                         })
                         .into_any()
@@ -3830,9 +3827,9 @@ pub fn activate_workspace_for_project(
     cx: &mut AsyncAppContext,
     predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
 ) -> Option<WeakViewHandle<Workspace>> {
-    for window_id in cx.window_ids() {
-        let handle = cx
-            .update_window(window_id, |cx| {
+    for window in cx.windows() {
+        let handle = window
+            .update(cx, |cx| {
                 if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
                     let project = workspace_handle.read(cx).project.clone();
                     if project.update(cx, &predicate) {
@@ -3859,7 +3856,7 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
 pub fn open_paths(
     abs_paths: &[PathBuf],
     app_state: &Arc<AppState>,
-    requesting_window_id: Option<usize>,
+    requesting_window: Option<WindowHandle<Workspace>>,
     cx: &mut AppContext,
 ) -> Task<
     Result<(
@@ -3887,7 +3884,7 @@ pub fn open_paths(
         } else {
             Ok(cx
                 .update(|cx| {
-                    Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx)
+                    Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx)
                 })
                 .await)
         }
@@ -3948,18 +3945,23 @@ pub fn join_remote_project(
 ) -> Task<Result<()>> {
     cx.spawn(|mut cx| async move {
         let existing_workspace = cx
-            .window_ids()
+            .windows()
             .into_iter()
-            .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
-            .find(|workspace| {
-                cx.read_window(workspace.window_id(), |cx| {
-                    workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
+            .find_map(|window| {
+                window.downcast::<Workspace>().and_then(|window| {
+                    window.read_root_with(&cx, |workspace, cx| {
+                        if workspace.project().read(cx).remote_id() == Some(project_id) {
+                            Some(cx.handle().downgrade())
+                        } else {
+                            None
+                        }
+                    })
                 })
-                .unwrap_or(false)
-            });
+            })
+            .flatten();
 
         let workspace = if let Some(existing_workspace) = existing_workspace {
-            existing_workspace.downgrade()
+            existing_workspace
         } else {
             let active_call = cx.read(ActiveCall::global);
             let room = active_call
@@ -3977,7 +3979,7 @@ pub fn join_remote_project(
                 .await?;
 
             let window_bounds_override = window_bounds_env_override(&cx);
-            let (_, workspace) = cx.add_window(
+            let window = cx.add_window(
                 (app_state.build_window_options)(
                     window_bounds_override,
                     None,
@@ -3985,6 +3987,7 @@ pub fn join_remote_project(
                 ),
                 |cx| Workspace::new(0, project, app_state.clone(), cx),
             );
+            let workspace = window.root(&cx).unwrap();
             (app_state.initialize_workspace)(
                 workspace.downgrade(),
                 false,
@@ -3997,7 +4000,7 @@ pub fn join_remote_project(
             workspace.downgrade()
         };
 
-        cx.activate_window(workspace.window_id());
+        workspace.window().activate(&mut cx);
         cx.platform().activate(true);
 
         workspace.update(&mut cx, |workspace, cx| {
@@ -4036,29 +4039,22 @@ pub fn join_remote_project(
 pub fn restart(_: &Restart, cx: &mut AppContext) {
     let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
     cx.spawn(|mut cx| async move {
-        let mut workspaces = cx
-            .window_ids()
+        let mut workspace_windows = cx
+            .windows()
             .into_iter()
-            .filter_map(|window_id| {
-                Some(
-                    cx.root_view(window_id)?
-                        .clone()
-                        .downcast::<Workspace>()?
-                        .downgrade(),
-                )
-            })
+            .filter_map(|window| window.downcast::<Workspace>())
             .collect::<Vec<_>>();
 
         // If multiple windows have unsaved changes, and need a save prompt,
         // prompt in the active window before switching to a different window.
-        workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
+        workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
 
-        if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
-            let answer = cx.prompt(
-                workspace.window_id(),
+        if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
+            let answer = window.prompt(
                 PromptLevel::Info,
                 "Are you sure you want to restart?",
                 &["Restart", "Cancel"],
+                &mut cx,
             );
 
             if let Some(mut answer) = answer {
@@ -4070,14 +4066,13 @@ pub fn restart(_: &Restart, cx: &mut AppContext) {
         }
 
         // If the user cancels any save prompt, then keep the app open.
-        for workspace in workspaces {
-            if !workspace
-                .update(&mut cx, |workspace, cx| {
-                    workspace.prepare_to_close(true, cx)
-                })?
-                .await?
-            {
-                return Ok(());
+        for window in workspace_windows {
+            if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
+                workspace.prepare_to_close(true, cx)
+            }) {
+                if !should_close.await? {
+                    return Ok(());
+                }
             }
         }
         cx.platform().restart();
@@ -4113,10 +4108,11 @@ mod tests {
 
         let fs = FakeFs::new(cx.background());
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
 
         // Adding an item with no ambiguity renders the tab without detail.
-        let item1 = cx.add_view(window_id, |_| {
+        let item1 = window.add_view(cx, |_| {
             let mut item = TestItem::new();
             item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
             item
@@ -4128,7 +4124,7 @@ mod tests {
 
         // Adding an item that creates ambiguity increases the level of detail on
         // both tabs.
-        let item2 = cx.add_view(window_id, |_| {
+        let item2 = window.add_view(cx, |_| {
             let mut item = TestItem::new();
             item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
             item
@@ -4142,7 +4138,7 @@ mod tests {
         // Adding an item that creates ambiguity increases the level of detail only
         // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
         // we stop at the highest detail available.
-        let item3 = cx.add_view(window_id, |_| {
+        let item3 = window.add_view(cx, |_| {
             let mut item = TestItem::new();
             item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
             item
@@ -4177,16 +4173,17 @@ mod tests {
         .await;
 
         let project = Project::test(fs, ["root1".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
         let worktree_id = project.read_with(cx, |project, cx| {
             project.worktrees(cx).next().unwrap().read(cx).id()
         });
 
-        let item1 = cx.add_view(window_id, |cx| {
+        let item1 = window.add_view(cx, |cx| {
             TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
         });
-        let item2 = cx.add_view(window_id, |cx| {
+        let item2 = window.add_view(cx, |cx| {
             TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
         });
 
@@ -4200,17 +4197,11 @@ mod tests {
                     .map(|e| e.id)
             );
         });
-        assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
-            Some("one.txt — root1")
-        );
+        assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1"));
 
         // Add a second item to a non-empty pane
         workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
-        assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
-            Some("two.txt — root1")
-        );
+        assert_eq!(window.current_title(cx).as_deref(), Some("two.txt — root1"));
         project.read_with(cx, |project, cx| {
             assert_eq!(
                 project.active_entry(),
@@ -4226,10 +4217,7 @@ mod tests {
         })
         .await
         .unwrap();
-        assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
-            Some("one.txt — root1")
-        );
+        assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1"));
         project.read_with(cx, |project, cx| {
             assert_eq!(
                 project.active_entry(),
@@ -4247,16 +4235,13 @@ mod tests {
             .await
             .unwrap();
         assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
+            window.current_title(cx).as_deref(),
             Some("one.txt — root1, root2")
         );
 
         // Remove a project folder
         project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
-        assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
-            Some("one.txt — root2")
-        );
+        assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root2"));
     }
 
     #[gpui::test]
@@ -4267,18 +4252,19 @@ mod tests {
         fs.insert_tree("/root", json!({ "one": "" })).await;
 
         let project = Project::test(fs, ["root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
 
         // When there are no dirty items, there's nothing to do.
-        let item1 = cx.add_view(window_id, |_| TestItem::new());
+        let item1 = window.add_view(cx, |_| TestItem::new());
         workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
         let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
         assert!(task.await.unwrap());
 
         // When there are dirty untitled items, prompt to save each one. If the user
         // cancels any prompt, then abort.
-        let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true));
-        let item3 = cx.add_view(window_id, |cx| {
+        let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true));
+        let item3 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
@@ -4289,9 +4275,9 @@ mod tests {
         });
         let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
         cx.foreground().run_until_parked();
-        cx.simulate_prompt_answer(window_id, 2 /* cancel */);
+        window.simulate_prompt_answer(2, cx); // cancel
         cx.foreground().run_until_parked();
-        assert!(!cx.has_pending_prompt(window_id));
+        assert!(!window.has_pending_prompt(cx));
         assert!(!task.await.unwrap());
     }
 
@@ -4302,26 +4288,27 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
-        let item1 = cx.add_view(window_id, |cx| {
+        let item1 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
         });
-        let item2 = cx.add_view(window_id, |cx| {
+        let item2 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_conflict(true)
                 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
         });
-        let item3 = cx.add_view(window_id, |cx| {
+        let item3 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_conflict(true)
                 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
         });
-        let item4 = cx.add_view(window_id, |cx| {
+        let item4 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_project_items(&[TestProjectItem::new_untitled(cx)])
@@ -4349,10 +4336,10 @@ mod tests {
             assert_eq!(pane.items_len(), 4);
             assert_eq!(pane.active_item().unwrap().id(), item1.id());
         });
-        assert!(cx.has_pending_prompt(window_id));
+        assert!(window.has_pending_prompt(cx));
 
         // Confirm saving item 1.
-        cx.simulate_prompt_answer(window_id, 0);
+        window.simulate_prompt_answer(0, cx);
         cx.foreground().run_until_parked();
 
         // Item 1 is saved. There's a prompt to save item 3.
@@ -4363,10 +4350,10 @@ mod tests {
             assert_eq!(pane.items_len(), 3);
             assert_eq!(pane.active_item().unwrap().id(), item3.id());
         });
-        assert!(cx.has_pending_prompt(window_id));
+        assert!(window.has_pending_prompt(cx));
 
         // Cancel saving item 3.
-        cx.simulate_prompt_answer(window_id, 1);
+        window.simulate_prompt_answer(1, cx);
         cx.foreground().run_until_parked();
 
         // Item 3 is reloaded. There's a prompt to save item 4.
@@ -4377,10 +4364,10 @@ mod tests {
             assert_eq!(pane.items_len(), 2);
             assert_eq!(pane.active_item().unwrap().id(), item4.id());
         });
-        assert!(cx.has_pending_prompt(window_id));
+        assert!(window.has_pending_prompt(cx));
 
         // Confirm saving item 4.
-        cx.simulate_prompt_answer(window_id, 0);
+        window.simulate_prompt_answer(0, cx);
         cx.foreground().run_until_parked();
 
         // There's a prompt for a path for item 4.
@@ -4404,13 +4391,14 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         // Create several workspace items with single project entries, and two
         // workspace items with multiple project entries.
         let single_entry_items = (0..=4)
             .map(|project_entry_id| {
-                cx.add_view(window_id, |cx| {
+                window.add_view(cx, |cx| {
                     TestItem::new()
                         .with_dirty(true)
                         .with_project_items(&[TestProjectItem::new(
@@ -4421,7 +4409,7 @@ mod tests {
                 })
             })
             .collect::<Vec<_>>();
-        let item_2_3 = cx.add_view(window_id, |cx| {
+        let item_2_3 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_singleton(false)
@@ -4430,7 +4418,7 @@ mod tests {
                     single_entry_items[3].read(cx).project_items[0].clone(),
                 ])
         });
-        let item_3_4 = cx.add_view(window_id, |cx| {
+        let item_3_4 = window.add_view(cx, |cx| {
             TestItem::new()
                 .with_dirty(true)
                 .with_singleton(false)
@@ -4482,7 +4470,7 @@ mod tests {
                 &[ProjectEntryId::from_proto(0)]
             );
         });
-        cx.simulate_prompt_answer(window_id, 0);
+        window.simulate_prompt_answer(0, cx);
 
         cx.foreground().run_until_parked();
         left_pane.read_with(cx, |pane, cx| {
@@ -4491,7 +4479,7 @@ mod tests {
                 &[ProjectEntryId::from_proto(2)]
             );
         });
-        cx.simulate_prompt_answer(window_id, 0);
+        window.simulate_prompt_answer(0, cx);
 
         cx.foreground().run_until_parked();
         close.await.unwrap();
@@ -4507,10 +4495,11 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
-        let item = cx.add_view(window_id, |cx| {
+        let item = window.add_view(cx, |cx| {
             TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
         });
         let item_id = item.id();
@@ -4529,7 +4518,7 @@ mod tests {
         });
 
         // Deactivating the window saves the file.
-        cx.simulate_window_activation(None);
+        window.simulate_deactivation(cx);
         deterministic.run_until_parked();
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
 
@@ -4550,12 +4539,12 @@ mod tests {
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
 
         // Deactivating the window still saves the file.
-        cx.simulate_window_activation(Some(window_id));
+        window.simulate_activation(cx);
         item.update(cx, |item, cx| {
             cx.focus_self();
             item.is_dirty = true;
         });
-        cx.simulate_window_activation(None);
+        window.simulate_deactivation(cx);
 
         deterministic.run_until_parked();
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
@@ -4592,7 +4581,7 @@ mod tests {
         pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
             .await
             .unwrap();
-        assert!(!cx.has_pending_prompt(window_id));
+        assert!(!window.has_pending_prompt(cx));
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
 
         // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
@@ -4613,7 +4602,7 @@ mod tests {
         let _close_items =
             pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
         deterministic.run_until_parked();
-        assert!(cx.has_pending_prompt(window_id));
+        assert!(window.has_pending_prompt(cx));
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
     }
 
@@ -4624,9 +4613,10 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
-        let item = cx.add_view(window_id, |cx| {
+        let item = window.add_view(cx, |cx| {
             TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
         });
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
@@ -4677,7 +4667,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         let panel = workspace.update(cx, |workspace, cx| {
             let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
@@ -4824,7 +4815,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
             // Add panel_1 on the left, panel_2 on the right.
@@ -4979,7 +4971,7 @@ mod tests {
 
         // If focus is transferred to another view that's not a panel or another pane, we still show
         // the panel as zoomed.
-        let focus_receiver = cx.add_view(window_id, |_| EmptyView);
+        let focus_receiver = window.add_view(cx, |_| EmptyView);
         focus_receiver.update(cx, |_, cx| cx.focus_self());
         workspace.read_with(cx, |workspace, _| {
             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.98.0"
+version = "0.100.0"
 publish = false
 
 [lib]

crates/zed/src/languages/bash/config.toml 🔗

@@ -1,5 +1,6 @@
 name = "Shell Script"
-path_suffixes = [".sh", ".bash", ".bashrc", ".bash_profile", ".bash_aliases", ".bash_logout", ".profile", ".zsh", ".zshrc", ".zshenv", ".zsh_profile", ".zsh_aliases", ".zsh_histfile", ".zlogin"]
+path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile"]
+line_comment = "# "
 first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b"
 brackets = [
     { start = "[", end = "]", close = true, newline = false },

crates/zed/src/languages/c/highlights.scm 🔗

@@ -86,7 +86,7 @@
 (identifier) @variable
 
 ((identifier) @constant
-    (.match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+ (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
 
 (call_expression
   function: (identifier) @function)
@@ -106,3 +106,4 @@
   (primitive_type)
   (sized_type_specifier)
 ] @type
+

crates/zed/src/languages/c/injections.scm 🔗

@@ -1,7 +1,7 @@
 (preproc_def
     value: (preproc_arg) @content
-    (.set! "language" "c"))
+    (#set! "language" "c"))
 
 (preproc_function_def
     value: (preproc_arg) @content
-    (.set! "language" "c"))
+    (#set! "language" "c"))

crates/zed/src/languages/cpp/highlights.scm 🔗

@@ -31,13 +31,13 @@
   declarator: (field_identifier) @function)
 
 ((namespace_identifier) @type
-    (.match? @type "^[A-Z]"))
+ (#match? @type "^[A-Z]"))
 
 (auto) @type
 (type_identifier) @type
 
 ((identifier) @constant
-    (.match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+ (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
 
 (field_identifier) @property
 (statement_identifier) @label

crates/zed/src/languages/cpp/injections.scm 🔗

@@ -1,7 +1,7 @@
 (preproc_def
     value: (preproc_arg) @content
-    (.set! "language" "c++"))
+    (#set! "language" "c++"))
 
 (preproc_function_def
     value: (preproc_arg) @content
-    (.set! "language" "c++"))
+    (#set! "language" "c++"))

crates/zed/src/languages/elixir/embedding.scm 🔗

@@ -3,7 +3,7 @@
         operator: "@"
         operand: (call
             target: (identifier) @unary
-            (.match? @unary "^(doc)$"))
+            (#match? @unary "^(doc)$"))
         ) @context
     .
     (call
@@ -18,10 +18,10 @@
                     target: (identifier) @name)
                     operator: "when")
             ])
-        (.match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
+        (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
         )
 
     (call
         target: (identifier) @name
         (arguments (alias) @name)
-        (.match? @name "^(defmodule|defprotocol)$")) @item
+        (#match? @name "^(defmodule|defprotocol)$")) @item

crates/zed/src/languages/elixir/highlights.scm 🔗

@@ -54,13 +54,13 @@
   (sigil_name) @__name__
   quoted_start: _ @string
   quoted_end: _ @string
-  (.match? @__name__ "^[sS]$")) @string
+  (#match? @__name__ "^[sS]$")) @string
 
 (sigil
   (sigil_name) @__name__
   quoted_start: _ @string.regex
   quoted_end: _ @string.regex
-  (.match? @__name__ "^[rR]$")) @string.regex
+  (#match? @__name__ "^[rR]$")) @string.regex
 
 (sigil
   (sigil_name) @__name__
@@ -69,7 +69,7 @@
 
 (
   (identifier) @comment.unused
-  (.match? @comment.unused "^_")
+  (#match? @comment.unused "^_")
 )
 
 (call
@@ -91,7 +91,7 @@
         operator: "|>"
         right: (identifier))
     ])
-  (.match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
+  (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
 
 (binary_operator
   operator: "|>"
@@ -99,15 +99,15 @@
 
 (call
   target: (identifier) @keyword
-  (.match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$"))
+  (#match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$"))
 
 (call
   target: (identifier) @keyword
-  (.match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$"))
+  (#match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$"))
 
 (
   (identifier) @constant.builtin
-  (.match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$")
+  (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$")
 )
 
 (unary_operator
@@ -121,7 +121,7 @@
         (sigil)
         (boolean)
       ] @comment.doc))
-  (.match? @__attribute__ "^(moduledoc|typedoc|doc)$"))
+  (#match? @__attribute__ "^(moduledoc|typedoc|doc)$"))
 
 (comment) @comment
 
@@ -150,4 +150,4 @@
 ((sigil
   (sigil_name) @_sigil_name
   (quoted_content) @embedded)
- (.eq? @_sigil_name "H"))
+ (#eq? @_sigil_name "H"))

crates/zed/src/languages/elixir/outline.scm 🔗

@@ -1,7 +1,7 @@
 (call
   target: (identifier) @context
   (arguments (alias) @name)
-  (.match? @context "^(defmodule|defprotocol)$")) @item
+  (#match? @context "^(defmodule|defprotocol)$")) @item
 
 (call
   target: (identifier) @context
@@ -23,4 +23,4 @@
                 ")" @context.extra))
         operator: "when")
     ])
-  (.match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
+  (#match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item

crates/zed/src/languages/erb/injections.scm 🔗

@@ -1,7 +1,7 @@
 ((code) @content
- (.set! "language" "ruby")
- (.set! "combined"))
+ (#set! "language" "ruby")
+ (#set! "combined"))
 
 ((content) @content
- (.set! "language" "html")
- (.set! "combined"))
+ (#set! "language" "html")
+ (#set! "combined"))

crates/zed/src/languages/glsl/highlights.scm 🔗

@@ -74,7 +74,7 @@
 (sized_type_specifier) @type
 
 ((identifier) @constant
-    (.match? @constant "^[A-Z][A-Z\\d_]*$"))
+ (#match? @constant "^[A-Z][A-Z\\d_]*$"))
 
 (identifier) @variable
 
@@ -114,5 +114,5 @@
 
 (
   (identifier) @variable.builtin
-  (.match? @variable.builtin "^gl_")
+  (#match? @variable.builtin "^gl_")
 )

crates/zed/src/languages/heex/injections.scm 🔗

@@ -5,9 +5,9 @@
       (expression_value)
       (ending_expression_value)
     ] @content)
-  (.set! language "elixir")
-  (.set! combined)
+  (#set! language "elixir")
+  (#set! combined)
 )
 
 ((expression (expression_value) @content)
- (.set! language "elixir"))
+ (#set! language "elixir"))

crates/zed/src/languages/html/injections.scm 🔗

@@ -1,7 +1,7 @@
 (script_element
   (raw_text) @content
-  (.set! "language" "javascript"))
+  (#set! "language" "javascript"))
 
 (style_element
   (raw_text) @content
-  (.set! "language" "css"))
+  (#set! "language" "css"))

crates/zed/src/languages/javascript/highlights.scm 🔗

@@ -44,7 +44,7 @@
 ; Special identifiers
 
 ((identifier) @type
-    (.match? @type "^[A-Z]"))
+ (#match? @type "^[A-Z]"))
 (type_identifier) @type
 (predefined_type) @type.builtin
 
@@ -53,7 +53,7 @@
   (shorthand_property_identifier)
   (shorthand_property_identifier_pattern)
  ] @constant
-(.match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
+ (#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
 
 ; Literals
 
@@ -214,4 +214,4 @@
   "type"
   "readonly"
   "override"
-] @keyword
+] @keyword

crates/zed/src/languages/lua/config.toml 🔗

@@ -7,3 +7,4 @@ brackets = [
     { start = "[", end = "]", close = true, newline = true },
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
 ]
+collapsed_placeholder = "--[ ... ]--"

crates/zed/src/languages/lua/highlights.scm 🔗

@@ -127,7 +127,7 @@
 (identifier) @variable
 
 ((identifier) @variable.special
- (.eq? @variable.special "self"))
+ (#eq? @variable.special "self"))
 
 (variable_list
    attribute: (attribute
@@ -137,7 +137,7 @@
 ;; Constants
 
 ((identifier) @constant
- (.match? @constant "^[A-Z][A-Z_0-9]*$"))
+ (#match? @constant "^[A-Z][A-Z_0-9]*$"))
 
 (vararg_expression) @constant
 
@@ -158,7 +158,7 @@
 [
   "{"
   "}"
-] @method.constructor)
+] @constructor)
 
 ;; Functions
 
@@ -180,7 +180,7 @@
 
 (function_call
   (identifier) @function.builtin
-  (.any-of? @function.builtin
+  (#any-of? @function.builtin
     ;; built-in functions in Lua 5.1
     "assert" "collectgarbage" "dofile" "error" "getfenv" "getmetatable" "ipairs"
     "load" "loadfile" "loadstring" "module" "next" "pairs" "pcall" "print"
@@ -195,4 +195,4 @@
 
 (number) @number
 
-(string) @string
+(string) @string

crates/zed/src/languages/php/config.toml 🔗

@@ -9,3 +9,4 @@ brackets = [
     { start = "(", end = ")", close = true, newline = true },
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
 ]
+collapsed_placeholder = "/* ... */"

crates/zed/src/languages/php/embedding.scm 🔗

@@ -0,0 +1,36 @@
+(
+    (comment)* @context
+    .
+    [
+        (function_definition
+            "function" @name
+            name: (_) @name
+            body: (_
+                "{" @keep
+                "}" @keep) @collapse
+            )
+
+        (trait_declaration
+            "trait" @name
+            name: (_) @name)
+
+        (method_declaration
+            "function" @name
+            name: (_) @name
+            body: (_
+                "{" @keep
+                "}" @keep) @collapse
+            )
+
+        (interface_declaration
+            "interface" @name
+            name: (_) @name
+            )
+
+        (enum_declaration
+            "enum" @name
+            name: (_) @name
+            )
+
+        ] @item
+    )

crates/zed/src/languages/php/highlights.scm 🔗

@@ -43,15 +43,15 @@
 (relative_scope) @variable.builtin
 
 ((name) @constant
-    (.match? @constant "^_?[A-Z][A-Z\\d_]+$"))
+ (#match? @constant "^_?[A-Z][A-Z\\d_]+$"))
 ((name) @constant.builtin
- (.match? @constant.builtin "^__[A-Z][A-Z\d_]+__$"))
+ (#match? @constant.builtin "^__[A-Z][A-Z\d_]+__$"))
 
-((name) @method.constructor
-(.match? @method.constructor "^[A-Z]"))
+((name) @constructor
+ (#match? @constructor "^[A-Z]"))
 
 ((name) @variable.builtin
- (.eq? @variable.builtin "this"))
+ (#eq? @variable.builtin "this"))
 
 (variable_name) @variable
 

crates/zed/src/languages/php/outline.scm 🔗

@@ -8,8 +8,6 @@
     name: (_) @name
     ) @item
 
-
-
 (method_declaration
     "function" @context
     name: (_) @name
@@ -24,3 +22,8 @@
     "enum" @context
     name: (_) @name
     ) @item
+
+(trait_declaration
+    "trait" @context
+    name: (_) @name
+    ) @item

crates/zed/src/languages/python/highlights.scm 🔗

@@ -18,16 +18,16 @@
 ; Identifier naming conventions
 
 ((identifier) @type
-    (.match? @type "^[A-Z]"))
+ (#match? @type "^[A-Z]"))
 
 ((identifier) @constant
-    (.match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+ (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
 
 ; Builtin functions
 
 ((call
   function: (identifier) @function.builtin)
- (.match?
+ (#match?
    @function.builtin
    "^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$"))
 
@@ -122,4 +122,4 @@
   "yield"
   "match"
   "case"
-] @keyword
+] @keyword

crates/zed/src/languages/racket/highlights.scm 🔗

@@ -22,7 +22,7 @@
 (lang_name) @variable.special
 
 ((symbol) @operator
-    (.match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))
+ (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))
 
 (list
   .
@@ -31,9 +31,10 @@
 (list
   .
   (symbol) @keyword
-  (.match? @keyword
+  (#match? @keyword

crates/zed/src/languages/ruby/brackets.scm 🔗

@@ -11,4 +11,4 @@
 (begin "begin" @open "end" @close)
 (module "module" @open "end" @close)
 (_ . "def" @open "end" @close)
-(_ . "class" @open "end" @close)
+(_ . "class" @open "end" @close)

crates/zed/src/languages/ruby/config.toml 🔗

@@ -10,3 +10,4 @@ brackets = [
   { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
   { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
 ]
+collapsed_placeholder = "# ..."

crates/zed/src/languages/ruby/embedding.scm 🔗

@@ -0,0 +1,22 @@
+(
+    (comment)* @context
+    .
+    [
+        (module
+            "module" @name
+            name: (_) @name)
+        (method
+            "def" @name
+            name: (_) @name
+            body: (body_statement) @collapse)
+        (class
+            "class" @name
+            name: (_) @name)
+        (singleton_method
+            "def" @name
+            object: (_) @name
+            "." @name
+            name: (_) @name
+            body: (body_statement) @collapse)
+        ] @item
+    )

crates/zed/src/languages/ruby/highlights.scm 🔗

@@ -33,12 +33,12 @@
 (identifier) @variable
 
 ((identifier) @keyword
-    (.match? @keyword "^(private|protected|public)$"))
+ (#match? @keyword "^(private|protected|public)$"))
 
 ; Function calls
 
 ((identifier) @function.method.builtin
-    (.eq? @function.method.builtin "require"))
+ (#eq? @function.method.builtin "require"))
 
 "defined?" @function.method.builtin
 
@@ -60,7 +60,7 @@
 ] @property
 
 ((identifier) @constant.builtin
- (.match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
+ (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
 
 (file) @constant.builtin
 (line) @constant.builtin
@@ -71,7 +71,7 @@
 ) @constant.builtin
 
 ((constant) @constant
- (.match? @constant "^[A-Z\\d_]+$"))
+ (#match? @constant "^[A-Z\\d_]+$"))
 
 (constant) @type
 

crates/zed/src/languages/rust.rs 🔗

@@ -102,7 +102,7 @@ impl LspAdapter for RustLspAdapter {
         Some("rust-analyzer/flycheck".into())
     }
 
-    async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
+    fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
         lazy_static! {
             static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
         }
@@ -310,7 +310,7 @@ mod tests {
                 },
             ],
         };
-        RustLspAdapter.process_diagnostics(&mut params).await;
+        RustLspAdapter.process_diagnostics(&mut params);
 
         assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
 

crates/zed/src/languages/rust/highlights.scm 🔗

@@ -38,11 +38,11 @@
 
 ; Assume uppercase names are types/enum-constructors
 ((identifier) @type
-    (.match? @type "^[A-Z]"))
+ (#match? @type "^[A-Z]"))
 
 ; Assume all-caps names are constants
 ((identifier) @constant
-    (.match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+ (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
 
 [
   "("

crates/zed/src/languages/rust/injections.scm 🔗

@@ -1,7 +1,7 @@
 (macro_invocation
   (token_tree) @content
-  (.set! "language" "rust"))
+  (#set! "language" "rust"))
 
 (macro_rule
   (token_tree) @content
-  (.set! "language" "rust"))
+  (#set! "language" "rust"))

crates/zed/src/languages/scheme/highlights.scm 🔗

@@ -14,7 +14,7 @@
  (directive)] @comment
 
 ((symbol) @operator
-    (.match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))
+ (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))
 
 (list
   .
@@ -23,6 +23,6 @@
 (list
   .
   (symbol) @keyword
-  (.match? @keyword
+  (#match? @keyword
    "^(define-syntax|let\\*|lambda|λ|case|=>|quote-splicing|unquote-splicing|set!|let|letrec|letrec-syntax|let-values|let\\*-values|do|else|define|cond|syntax-rules|unquote|begin|quote|let-syntax|and|if|quasiquote|letrec|delay|or|when|unless|identifier-syntax|assert|library|export|import|rename|only|except|prefix)$"
    ))

crates/zed/src/languages/svelte/injections.scm 🔗

@@ -2,27 +2,27 @@
 ; --------------
 (script_element
   (raw_text) @content
-  (.set! "language" "javascript"))
+  (#set! "language" "javascript"))
 
  ((script_element
      (start_tag
        (attribute
          (quoted_attribute_value (attribute_value) @_language)))
       (raw_text) @content)
-    (.eq? @_language "ts")
-    (.set! "language" "typescript"))
+    (#eq? @_language "ts")
+    (#set! "language" "typescript"))
 
 ((script_element
     (start_tag
         (attribute
         (quoted_attribute_value (attribute_value) @_language)))
     (raw_text) @content)
-  (.eq? @_language "typescript")
-  (.set! "language" "typescript"))
+  (#eq? @_language "typescript")
+  (#set! "language" "typescript"))
 
 (style_element
   (raw_text) @content
-  (.set! "language" "css"))
+  (#set! "language" "css"))
 
 ((raw_text_expr) @content
-    (.set! "language" "javascript"))
+  (#set! "language" "javascript"))

crates/zed/src/languages/typescript/highlights.scm 🔗

@@ -43,11 +43,11 @@
 
 ; Special identifiers
 
-((identifier) @method.constructor
-    (.match? @method.constructor "^[A-Z]"))
+((identifier) @constructor
+ (#match? @constructor "^[A-Z]"))
 
 ((identifier) @type
-    (.match? @type "^[A-Z]"))
+ (#match? @type "^[A-Z]"))
 (type_identifier) @type
 (predefined_type) @type.builtin
 
@@ -56,7 +56,7 @@
   (shorthand_property_identifier)
   (shorthand_property_identifier_pattern)
  ] @constant
-(.match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
+ (#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
 
 ; Literals
 
@@ -218,4 +218,4 @@
   "type"
   "readonly"
   "override"
-] @keyword
+] @keyword

crates/zed/src/main.rs 🔗

@@ -14,7 +14,7 @@ use futures::{
     channel::{mpsc, oneshot},
     FutureExt, SinkExt, StreamExt,
 };
-use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task, ViewContext};
+use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task};
 use isahc::{config::Configurable, Request};
 use language::{LanguageRegistry, Point};
 use log::LevelFilter;
@@ -43,8 +43,8 @@ use std::{
     time::{Duration, SystemTime, UNIX_EPOCH},
 };
 use sum_tree::Bias;
-use terminal_view::{get_working_directory, TerminalSettings, TerminalView};
 use util::{
+    channel::ReleaseChannel,
     http::{self, HttpClient},
     paths::PathLikeWithPosition,
 };
@@ -55,7 +55,7 @@ use fs::RealFs;
 #[cfg(debug_assertions)]
 use staff_mode::StaffMode;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
-use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace};
+use workspace::AppState;
 use zed::{
     assets::Assets,
     build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
@@ -415,22 +415,41 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
     panic::set_hook(Box::new(move |info| {
         let prior_panic_count = PANIC_COUNT.fetch_add(1, Ordering::SeqCst);
         if prior_panic_count > 0 {
-            std::panic::resume_unwind(Box::new(()));
+            // Give the panic-ing thread time to write the panic file
+            loop {
+                std::thread::yield_now();
+            }
         }
 
-        let app_version = ZED_APP_VERSION
-            .or_else(|| platform.app_version().ok())
-            .map_or("dev".to_string(), |v| v.to_string());
-
         let thread = thread::current();
-        let thread = thread.name().unwrap_or("<unnamed>");
+        let thread_name = thread.name().unwrap_or("<unnamed>");
 
-        let payload = info.payload();
-        let payload = None
-            .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
-            .or_else(|| payload.downcast_ref::<String>().map(|s| s.clone()))
+        let payload = info
+            .payload()
+            .downcast_ref::<&str>()
+            .map(|s| s.to_string())
+            .or_else(|| info.payload().downcast_ref::<String>().map(|s| s.clone()))
             .unwrap_or_else(|| "Box<Any>".to_string());
 
+        if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
+            let location = info.location().unwrap();
+            let backtrace = Backtrace::new();
+            eprintln!(
+                "Thread {:?} panicked with {:?} at {}:{}:{}\n{:?}",
+                thread_name,
+                payload,
+                location.file(),
+                location.line(),
+                location.column(),
+                backtrace,
+            );
+            std::process::exit(-1);
+        }
+
+        let app_version = ZED_APP_VERSION
+            .or_else(|| platform.app_version().ok())
+            .map_or("dev".to_string(), |v| v.to_string());
+
         let backtrace = Backtrace::new();
         let mut backtrace = backtrace
             .frames()
@@ -447,7 +466,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
         }
 
         let panic_data = Panic {
-            thread: thread.into(),
+            thread: thread_name.into(),
             payload: payload.into(),
             location_data: info.location().map(|location| LocationData {
                 file: location.file().into(),
@@ -635,6 +654,10 @@ fn load_embedded_fonts(app: &App) {
     let embedded_fonts = Mutex::new(Vec::new());
     smol::block_on(app.background().scoped(|scope| {
         for font_path in &font_paths {
+            if !font_path.ends_with(".ttf") {
+                continue;
+            }
+
             scope.spawn(async {
                 let font_path = &*font_path;
                 let font_bytes = Assets.load(font_path).unwrap().to_vec();
@@ -902,35 +925,6 @@ async fn handle_cli_connection(
     }
 }
 
-pub fn dock_default_item_factory(
-    workspace: &mut Workspace,
-    cx: &mut ViewContext<Workspace>,
-) -> Option<Box<dyn ItemHandle>> {
-    let strategy = settings::get::<TerminalSettings>(cx)
-        .working_directory
-        .clone();
-    let working_directory = get_working_directory(workspace, cx, strategy);
-
-    let window_id = cx.window_id();
-    let terminal = workspace
-        .project()
-        .update(cx, |project, cx| {
-            project.create_terminal(working_directory, window_id, cx)
-        })
-        .notify_err(workspace, cx)?;
-
-    let terminal_view = cx.add_view(|cx| {
-        TerminalView::new(
-            terminal,
-            workspace.weak_handle(),
-            workspace.database_id(),
-            cx,
-        )
-    });
-
-    Some(Box::new(terminal_view))
-}
-
 pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
     &[
         ("Go to file", &file_finder::Toggle),

crates/zed/src/zed.rs 🔗

@@ -179,13 +179,12 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
         move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
             let app_state = workspace.app_state().clone();
             let markdown = app_state.languages.language_for_name("JSON");
-            let window_id = cx.window_id();
+            let window = cx.window();
             cx.spawn(|workspace, mut cx| async move {
                 let markdown = markdown.await.log_err();
-                let content = to_string_pretty(
-                    &cx.debug_elements(window_id)
-                        .ok_or_else(|| anyhow!("could not debug elements for {window_id}"))?,
-                )
+                let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| {
+                    anyhow!("could not debug elements for window {}", window.id())
+                })?)
                 .unwrap();
                 workspace
                     .update(&mut cx, |workspace, cx| {
@@ -406,29 +405,22 @@ pub fn build_window_options(
 fn quit(_: &Quit, cx: &mut gpui::AppContext) {
     let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
     cx.spawn(|mut cx| async move {
-        let mut workspaces = cx
-            .window_ids()
+        let mut workspace_windows = cx
+            .windows()
             .into_iter()
-            .filter_map(|window_id| {
-                Some(
-                    cx.root_view(window_id)?
-                        .clone()
-                        .downcast::<Workspace>()?
-                        .downgrade(),
-                )
-            })
+            .filter_map(|window| window.downcast::<Workspace>())
             .collect::<Vec<_>>();
 
         // If multiple windows have unsaved changes, and need a save prompt,
         // prompt in the active window before switching to a different window.
-        workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
+        workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
 
-        if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
-            let answer = cx.prompt(
-                workspace.window_id(),
+        if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) {
+            let answer = window.prompt(
                 PromptLevel::Info,
                 "Are you sure you want to quit?",
                 &["Quit", "Cancel"],
+                &mut cx,
             );
 
             if let Some(mut answer) = answer {
@@ -440,14 +432,13 @@ fn quit(_: &Quit, cx: &mut gpui::AppContext) {
         }
 
         // If the user cancels any save prompt, then keep the app open.
-        for workspace in workspaces {
-            if !workspace
-                .update(&mut cx, |workspace, cx| {
-                    workspace.prepare_to_close(true, cx)
-                })?
-                .await?
-            {
-                return Ok(());
+        for window in workspace_windows {
+            if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
+                workspace.prepare_to_close(true, cx)
+            }) {
+                if !should_close.await? {
+                    return Ok(());
+                }
             }
         }
         cx.platform().quit();
@@ -545,7 +536,6 @@ pub fn handle_keymap_file_changes(
                             reload_keymaps(cx, &keymap_content);
                         }
                     })
-                    .detach();
                 }));
             }
         }
@@ -725,8 +715,8 @@ mod tests {
     use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
     use fs::{FakeFs, Fs};
     use gpui::{
-        actions, elements::Empty, executor::Deterministic, Action, AnyElement, AppContext,
-        AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
+        actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle,
+        AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
     };
     use language::LanguageRegistry;
     use node_runtime::NodeRuntime;
@@ -783,17 +773,13 @@ mod tests {
         })
         .await
         .unwrap();
-        assert_eq!(cx.window_ids().len(), 1);
+        assert_eq!(cx.windows().len(), 1);
 
         cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
             .await
             .unwrap();
-        assert_eq!(cx.window_ids().len(), 1);
-        let workspace_1 = cx
-            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
-            .unwrap()
-            .downcast::<Workspace>()
-            .unwrap();
+        assert_eq!(cx.windows().len(), 1);
+        let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
         workspace_1.update(cx, |workspace, cx| {
             assert_eq!(workspace.worktrees(cx).count(), 2);
             assert!(workspace.left_dock().read(cx).is_open());
@@ -810,27 +796,22 @@ mod tests {
         })
         .await
         .unwrap();
-        assert_eq!(cx.window_ids().len(), 2);
+        assert_eq!(cx.windows().len(), 2);
 
         // Replace existing windows
-        let window_id = cx.window_ids()[0];
+        let window = cx.windows()[0].downcast::<Workspace>().unwrap();
         cx.update(|cx| {
             open_paths(
                 &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
                 &app_state,
-                Some(window_id),
+                Some(window),
                 cx,
             )
         })
         .await
         .unwrap();
-        assert_eq!(cx.window_ids().len(), 2);
-        let workspace_1 = cx
-            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
-            .unwrap()
-            .clone()
-            .downcast::<Workspace>()
-            .unwrap();
+        assert_eq!(cx.windows().len(), 2);
+        let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
         workspace_1.update(cx, |workspace, cx| {
             assert_eq!(
                 workspace
@@ -856,14 +837,11 @@ mod tests {
         cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
             .await
             .unwrap();
-        assert_eq!(cx.window_ids().len(), 1);
+        assert_eq!(cx.windows().len(), 1);
 
         // When opening the workspace, the window is not in a edited state.
-        let workspace = cx
-            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
-            .unwrap()
-            .downcast::<Workspace>()
-            .unwrap();
+        let window = cx.windows()[0].downcast::<Workspace>().unwrap();
+        let workspace = window.root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
@@ -872,19 +850,19 @@ mod tests {
                 .downcast::<Editor>()
                 .unwrap()
         });
-        assert!(!cx.is_window_edited(workspace.window_id()));
+        assert!(!window.is_edited(cx));
 
         // Editing a buffer marks the window as edited.
         editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
-        assert!(cx.is_window_edited(workspace.window_id()));
+        assert!(window.is_edited(cx));
 
         // Undoing the edit restores the window's edited state.
         editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
-        assert!(!cx.is_window_edited(workspace.window_id()));
+        assert!(!window.is_edited(cx));
 
         // Redoing the edit marks the window as edited again.
         editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
-        assert!(cx.is_window_edited(workspace.window_id()));
+        assert!(window.is_edited(cx));
 
         // Closing the item restores the window's edited state.
         let close = pane.update(cx, |pane, cx| {
@@ -892,9 +870,10 @@ mod tests {
             pane.close_active_item(&Default::default(), cx).unwrap()
         });
         executor.run_until_parked();
-        cx.simulate_prompt_answer(workspace.window_id(), 1);
+
+        window.simulate_prompt_answer(1, cx);
         close.await.unwrap();
-        assert!(!cx.is_window_edited(workspace.window_id()));
+        assert!(!window.is_edited(cx));
 
         // Opening the buffer again doesn't impact the window's edited state.
         cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
@@ -907,22 +886,22 @@ mod tests {
                 .downcast::<Editor>()
                 .unwrap()
         });
-        assert!(!cx.is_window_edited(workspace.window_id()));
+        assert!(!window.is_edited(cx));
 
         // Editing the buffer marks the window as edited.
         editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
-        assert!(cx.is_window_edited(workspace.window_id()));
+        assert!(window.is_edited(cx));
 
         // Ensure closing the window via the mouse gets preempted due to the
         // buffer having unsaved changes.
-        assert!(!cx.simulate_window_close(workspace.window_id()));
+        assert!(!window.simulate_close(cx));
         executor.run_until_parked();
-        assert_eq!(cx.window_ids().len(), 1);
+        assert_eq!(cx.windows().len(), 1);
 
         // The window is successfully closed after the user dismisses the prompt.
-        cx.simulate_prompt_answer(workspace.window_id(), 1);
+        window.simulate_prompt_answer(1, cx);
         executor.run_until_parked();
-        assert_eq!(cx.window_ids().len(), 0);
+        assert_eq!(cx.windows().len(), 0);
     }
 
     #[gpui::test]
@@ -935,12 +914,13 @@ mod tests {
         })
         .await;
 
-        let window_id = *cx.window_ids().first().unwrap();
-        let workspace = cx
-            .read_window(window_id, |cx| cx.root_view().clone())
+        let window = cx
+            .windows()
+            .first()
             .unwrap()
             .downcast::<Workspace>()
             .unwrap();
+        let workspace = window.root(cx);
 
         let editor = workspace.update(cx, |workspace, cx| {
             workspace
@@ -983,7 +963,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1104,12 +1085,8 @@ mod tests {
         cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
             .await
             .unwrap();
-        assert_eq!(cx.window_ids().len(), 1);
-        let workspace = cx
-            .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
-            .unwrap()
-            .downcast::<Workspace>()
-            .unwrap();
+        assert_eq!(cx.windows().len(), 1);
+        let workspace = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
 
         #[track_caller]
         fn assert_project_panel_selection(
@@ -1295,7 +1272,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         // Open a file within an existing worktree.
         workspace
@@ -1321,7 +1299,7 @@ mod tests {
         cx.read(|cx| assert!(editor.is_dirty(cx)));
 
         let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
-        cx.simulate_prompt_answer(window_id, 0);
+        window.simulate_prompt_answer(0, cx);
         save_task.await.unwrap();
         editor.read_with(cx, |editor, cx| {
             assert!(!editor.is_dirty(cx));
@@ -1336,11 +1314,12 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(rust_lang()));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
         let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 
         // Create a new untitled buffer
-        cx.dispatch_action(window_id, NewFile);
+        cx.dispatch_action(window.into(), NewFile);
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
                 .active_item(cx)
@@ -1395,7 +1374,7 @@ mod tests {
 
         // Open the same newly-created file in another pane item. The new editor should reuse
         // the same buffer.
-        cx.dispatch_action(window_id, NewFile);
+        cx.dispatch_action(window.into(), NewFile);
         workspace
             .update(cx, |workspace, cx| {
                 workspace.split_and_clone(
@@ -1429,10 +1408,11 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
         project.update(cx, |project, _| project.languages().add(rust_lang()));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         // Create a new untitled buffer
-        cx.dispatch_action(window_id, NewFile);
+        cx.dispatch_action(window.into(), NewFile);
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
                 .active_item(cx)
@@ -1480,7 +1460,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1502,7 +1483,7 @@ mod tests {
             (editor.downgrade(), buffer)
         });
 
-        cx.dispatch_action(window_id, pane::SplitRight);
+        cx.dispatch_action(window.into(), pane::SplitRight);
         let editor_2 = cx.update(|cx| {
             let pane_2 = workspace.read(cx).active_pane().clone();
             assert_ne!(pane_1, pane_2);
@@ -1512,7 +1493,7 @@ mod tests {
 
             pane2_item.downcast::<Editor>().unwrap().downgrade()
         });
-        cx.dispatch_action(window_id, workspace::CloseActiveItem);
+        cx.dispatch_action(window.into(), workspace::CloseActiveItem);
 
         cx.foreground().run_until_parked();
         workspace.read_with(cx, |workspace, _| {
@@ -1520,9 +1501,9 @@ mod tests {
             assert_eq!(workspace.active_pane(), &pane_1);
         });
 
-        cx.dispatch_action(window_id, workspace::CloseActiveItem);
+        cx.dispatch_action(window.into(), workspace::CloseActiveItem);
         cx.foreground().run_until_parked();
-        cx.simulate_prompt_answer(window_id, 1);
+        window.simulate_prompt_answer(1, cx);
         cx.foreground().run_until_parked();
 
         workspace.read_with(cx, |workspace, cx| {
@@ -1554,7 +1535,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
@@ -1831,7 +1814,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project, cx))
+            .root(cx);
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
@@ -2073,11 +2058,11 @@ mod tests {
 
         cx.foreground().run_until_parked();
 
-        let (window_id, _view) = cx.add_window(|_| TestView);
+        let window = cx.add_window(|_| TestView);
 
         // Test loading the keymap base at all
         assert_key_bindings_for(
-            window_id,
+            window.into(),
             cx,
             vec![("backspace", &A), ("k", &ActivatePreviousPane)],
             line!(),
@@ -2104,7 +2089,7 @@ mod tests {
         cx.foreground().run_until_parked();
 
         assert_key_bindings_for(
-            window_id,
+            window.into(),
             cx,
             vec![("backspace", &B), ("k", &ActivatePreviousPane)],
             line!(),
@@ -2127,7 +2112,7 @@ mod tests {
         cx.foreground().run_until_parked();
 
         assert_key_bindings_for(
-            window_id,
+            window.into(),
             cx,
             vec![("backspace", &B), ("[", &ActivatePrevItem)],
             line!(),
@@ -2135,7 +2120,7 @@ mod tests {
 
         #[track_caller]
         fn assert_key_bindings_for<'a>(
-            window_id: usize,
+            window: AnyWindowHandle,
             cx: &TestAppContext,
             actions: Vec<(&'static str, &'a dyn Action)>,
             line: u32,
@@ -2143,7 +2128,7 @@ mod tests {
             for (key, action) in actions {
                 // assert that...
                 assert!(
-                    cx.available_actions(window_id, 0)
+                    cx.available_actions(window, 0)
                         .into_iter()
                         .any(|(_, bound_action, b)| {
                             // action names match...
@@ -2243,11 +2228,11 @@ mod tests {
 
         cx.foreground().run_until_parked();
 
-        let (window_id, _view) = cx.add_window(|_| TestView);
+        let window = cx.add_window(|_| TestView);
 
         // Test loading the keymap base at all
         assert_key_bindings_for(
-            window_id,
+            window.into(),
             cx,
             vec![("backspace", &A), ("k", &ActivatePreviousPane)],
             line!(),
@@ -2273,7 +2258,12 @@ mod tests {
 
         cx.foreground().run_until_parked();
 
-        assert_key_bindings_for(window_id, cx, vec![("k", &ActivatePreviousPane)], line!());
+        assert_key_bindings_for(
+            window.into(),
+            cx,
+            vec![("k", &ActivatePreviousPane)],
+            line!(),
+        );
 
         // Test modifying the base, while retaining the users keymap
         fs.save(
@@ -2291,11 +2281,11 @@ mod tests {
 
         cx.foreground().run_until_parked();
 
-        assert_key_bindings_for(window_id, cx, vec![("[", &ActivatePrevItem)], line!());
+        assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!());
 
         #[track_caller]
         fn assert_key_bindings_for<'a>(
-            window_id: usize,
+            window: AnyWindowHandle,
             cx: &TestAppContext,
             actions: Vec<(&'static str, &'a dyn Action)>,
             line: u32,
@@ -2303,7 +2293,7 @@ mod tests {
             for (key, action) in actions {
                 // assert that...
                 assert!(
-                    cx.available_actions(window_id, 0)
+                    cx.available_actions(window, 0)
                         .into_iter()
                         .any(|(_, bound_action, b)| {
                             // action names match...
@@ -2337,6 +2327,11 @@ mod tests {
                     .unwrap()
                     .to_vec()
                     .into(),
+                Assets
+                    .load("fonts/plex/IBMPlexSans-Regular.ttf")
+                    .unwrap()
+                    .to_vec()
+                    .into(),
             ])
             .unwrap();
         let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());

rust-toolchain.toml 🔗

@@ -1,4 +1,4 @@
 [toolchain]
-channel = "1.70"
+channel = "1.71"
 components = [ "rustfmt" ]
 targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ]

styles/package.json 🔗

@@ -8,7 +8,6 @@
         "build-licenses": "ts-node ./src/build_licenses.ts",
         "build-tokens": "ts-node ./src/build_tokens.ts",
         "build-types": "ts-node ./src/build_types.ts",
-        "generate-syntax": "ts-node ./src/types/extract_syntax_types.ts",
         "test": "vitest"
     },
     "author": "Zed Industries (https://github.com/zed-industries/)",

styles/src/build_themes.ts 🔗

@@ -21,7 +21,9 @@ function clear_themes(theme_directory: string) {
     }
 }
 
-const all_themes: Theme[] = themes.map((theme) => create_theme(theme))
+const all_themes: Theme[] = themes.map((theme) =>
+    create_theme(theme)
+)
 
 function write_themes(themes: Theme[], output_directory: string) {
     clear_themes(output_directory)
@@ -32,7 +34,10 @@ function write_themes(themes: Theme[], output_directory: string) {
         const style_tree = app()
         const style_tree_json = JSON.stringify(style_tree, null, 2)
         const temp_path = path.join(temp_directory, `${theme.name}.json`)
-        const out_path = path.join(output_directory, `${theme.name}.json`)
+        const out_path = path.join(
+            output_directory,
+            `${theme.name}.json`
+        )
         fs.writeFileSync(temp_path, style_tree_json)
         fs.renameSync(temp_path, out_path)
         console.log(`- ${out_path} created`)

styles/src/build_tokens.ts 🔗

@@ -83,6 +83,8 @@ function write_tokens(themes: Theme[], tokens_directory: string) {
     console.log(`- ${METADATA_FILE} created`)
 }
 
-const all_themes: Theme[] = themes.map((theme) => create_theme(theme))
+const all_themes: Theme[] = themes.map((theme) =>
+    create_theme(theme)
+)
 
 write_tokens(all_themes, TOKENS_DIRECTORY)

styles/src/common.ts 🔗

@@ -3,6 +3,7 @@ export * from "./theme"
 export { chroma }
 
 export const font_families = {
+    ui_sans: "IBM Plex Sans",
     sans: "Zed Sans",
     mono: "Zed Mono",
 }

styles/src/component/icon_button.ts 🔗

@@ -10,7 +10,10 @@ export type Margin = {
 }
 
 interface IconButtonOptions {
-    layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"]
+    layer?:
+    | Theme["lowest"]
+    | Theme["middle"]
+    | Theme["highest"]
     color?: keyof Theme["lowest"]
     margin?: Partial<Margin>
 }

styles/src/component/tab_bar_button.ts 🔗

@@ -12,47 +12,44 @@ type TabBarButtonProps = TabBarButtonOptions & {
     state?: Partial<Record<InteractiveState, Partial<TabBarButtonOptions>>>
 }
 
-export function tab_bar_button(
-    theme: Theme,
-    { icon, color = "base" }: TabBarButtonProps
-) {
+export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) {
     const button_spacing = 8
 
-    return interactive({
-        base: {
-            icon: {
-                color: foreground(theme.middle, color),
-                asset: icon,
-                dimensions: {
-                    width: 15,
-                    height: 15,
+    return (
+        interactive({
+            base: {
+                icon: {
+                    color: foreground(theme.middle, color),
+                    asset: icon,
+                    dimensions: {
+                        width: 15,
+                        height: 15,
+                    },
                 },
-            },
-            container: {
-                corner_radius: 4,
-                padding: {
-                    top: 4,
-                    bottom: 4,
-                    left: 4,
-                    right: 4,
-                },
-                margin: {
-                    left: button_spacing / 2,
-                    right: button_spacing / 2,
-                },
-            },
-        },
-        state: {
-            hovered: {
                 container: {
-                    background: background(theme.middle, color, "hovered"),
+                    corner_radius: 4,
+                    padding: {
+                        top: 4, bottom: 4, left: 4, right: 4
+                    },
+                    margin: {
+                        left: button_spacing / 2,
+                        right: button_spacing / 2,
+                    },
                 },
             },
-            clicked: {
-                container: {
-                    background: background(theme.middle, color, "pressed"),
+            state: {
+                hovered: {
+                    container: {
+                        background: background(theme.middle, color, "hovered"),
+
+                    }
+                },
+                clicked: {
+                    container: {
+                        background: background(theme.middle, color, "pressed"),
+                    }
                 },
             },
-        },
-    })
+        })
+    )
 }

styles/src/component/text_button.ts 🔗

@@ -9,7 +9,10 @@ import { useTheme, Theme } from "../theme"
 import { Margin } from "./icon_button"
 
 interface TextButtonOptions {
-    layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"]
+    layer?:
+    | Theme["lowest"]
+    | Theme["middle"]
+    | Theme["highest"]
     color?: keyof Theme["lowest"]
     margin?: Partial<Margin>
     text_properties?: TextProperties

styles/src/style_tree/app.ts 🔗

@@ -57,6 +57,6 @@ export default function app(): any {
         tooltip: tooltip(),
         terminal: terminal(),
         assistant: assistant(),
-        feedback: feedback(),
+        feedback: feedback()
     }
 }

styles/src/style_tree/assistant.ts 🔗

@@ -8,48 +8,50 @@ type RoleCycleButton = TextStyle & {
 }
 // TODO: Replace these with zed types
 type RemainingTokens = TextStyle & {
-    background: string
-    margin: { top: number; right: number }
+    background: string,
+    margin: { top: number, right: number },
     padding: {
-        right: number
-        left: number
-        top: number
-        bottom: number
-    }
-    corner_radius: number
+        right: number,
+        left: number,
+        top: number,
+        bottom: number,
+    },
+    corner_radius: number,
 }
 
 export default function assistant(): any {
     const theme = useTheme()
 
-    const interactive_role = (
-        color: StyleSets
-    ): Interactive<RoleCycleButton> => {
-        return interactive({
-            base: {
-                ...text(theme.highest, "sans", color, { size: "sm" }),
-            },
-            state: {
-                hovered: {
+    const interactive_role = (color: StyleSets): Interactive<RoleCycleButton> => {
+        return (
+            interactive({
+                base: {
                     ...text(theme.highest, "sans", color, { size: "sm" }),
-                    background: background(theme.highest, color, "hovered"),
                 },
-                clicked: {
-                    ...text(theme.highest, "sans", color, { size: "sm" }),
-                    background: background(theme.highest, color, "pressed"),
+                state: {
+                    hovered: {
+                        ...text(theme.highest, "sans", color, { size: "sm" }),
+                        background: background(theme.highest, color, "hovered"),
+                    },
+                    clicked: {
+                        ...text(theme.highest, "sans", color, { size: "sm" }),
+                        background: background(theme.highest, color, "pressed"),
+                    }
                 },
-            },
-        })
+            })
+        )
     }
 
     const tokens_remaining = (color: StyleSets): RemainingTokens => {
-        return {
-            ...text(theme.highest, "mono", color, { size: "xs" }),
-            background: background(theme.highest, "on", "default"),
-            margin: { top: 12, right: 20 },
-            padding: { right: 4, left: 4, top: 1, bottom: 1 },
-            corner_radius: 6,
-        }
+        return (
+            {
+                ...text(theme.highest, "mono", color, { size: "xs" }),
+                background: background(theme.highest, "on", "default"),
+                margin: { top: 12, right: 20 },
+                padding: { right: 4, left: 4, top: 1, bottom: 1 },
+                corner_radius: 6,
+            }
+        )
     }
 
     return {
@@ -91,10 +93,7 @@ export default function assistant(): any {
                 base: {
                     background: background(theme.middle),
                     padding: { top: 4, bottom: 4 },
-                    border: border(theme.middle, "default", {
-                        top: true,
-                        overlay: true,
-                    }),
+                    border: border(theme.middle, "default", { top: true, overlay: true }),
                 },
                 state: {
                     hovered: {
@@ -102,7 +101,7 @@ export default function assistant(): any {
                     },
                     clicked: {
                         background: background(theme.middle, "pressed"),
-                    },
+                    }
                 },
             }),
             saved_at: {

styles/src/style_tree/editor.ts 🔗

@@ -9,9 +9,9 @@ import {
 } from "./components"
 import hover_popover from "./hover_popover"
 
+import { build_syntax } from "../theme/syntax"
 import { interactive, toggleable } from "../element"
 import { useTheme } from "../theme"
-import chroma from "chroma-js"
 
 export default function editor(): any {
     const theme = useTheme()
@@ -48,28 +48,16 @@ export default function editor(): any {
         }
     }
 
+    const syntax = build_syntax()
+
     return {
-        text_color: theme.syntax.primary.color,
+        text_color: syntax.primary.color,
         background: background(layer),
         active_line_background: with_opacity(background(layer, "on"), 0.75),
         highlighted_line_background: background(layer, "on"),
         // Inline autocomplete suggestions, Co-pilot suggestions, etc.
-        hint: chroma
-            .mix(
-                theme.ramps.neutral(0.6).hex(),
-                theme.ramps.blue(0.4).hex(),
-                0.45,
-                "lch"
-            )
-            .hex(),
-        suggestion: chroma
-            .mix(
-                theme.ramps.neutral(0.4).hex(),
-                theme.ramps.blue(0.4).hex(),
-                0.45,
-                "lch"
-            )
-            .hex(),
+        hint: syntax.hint,
+        suggestion: syntax.predictive,
         code_actions: {
             indicator: toggleable({
                 base: interactive({
@@ -182,8 +170,8 @@ export default function editor(): any {
         line_number: with_opacity(foreground(layer), 0.35),
         line_number_active: foreground(layer),
         rename_fade: 0.6,
-        wrap_guide: with_opacity(foreground(layer), 0.1),
-        active_wrap_guide: with_opacity(foreground(layer), 0.2),
+        wrap_guide: with_opacity(foreground(layer), 0.05),
+        active_wrap_guide: with_opacity(foreground(layer), 0.1),
         unnecessary_code_fade: 0.5,
         selection: theme.players[0],
         whitespace: theme.ramps.neutral(0.5).hex(),
@@ -267,8 +255,8 @@ export default function editor(): any {
         invalid_warning_diagnostic: diagnostic(theme.middle, "base"),
         hover_popover: hover_popover(),
         link_definition: {
-            color: theme.syntax.link_uri.color,
-            underline: theme.syntax.link_uri.underline,
+            color: syntax.link_uri.color,
+            underline: syntax.link_uri.underline,
         },
         jump_icon: interactive({
             base: {
@@ -318,7 +306,7 @@ export default function editor(): any {
                     ? with_opacity(theme.ramps.green(0.5).hex(), 0.8)
                     : with_opacity(theme.ramps.green(0.4).hex(), 0.8),
             },
-            selections: foreground(layer, "accent"),
+            selections: foreground(layer, "accent")
         },
         composition_mark: {
             underline: {
@@ -326,6 +314,6 @@ export default function editor(): any {
                 color: border_color(layer),
             },
         },
-        syntax: theme.syntax,
+        syntax,
     }
 }

styles/src/style_tree/feedback.ts 🔗

@@ -37,7 +37,7 @@ export default function feedback(): any {
                     ...text(theme.highest, "mono", "on", "disabled"),
                     background: background(theme.highest, "on", "disabled"),
                     border: border(theme.highest, "on", "disabled"),
-                },
+                }
             },
         }),
         button_margin: 8,

styles/src/style_tree/project_panel.ts 🔗

@@ -64,17 +64,17 @@ export default function project_panel(): any {
         const unselected_default_style = merge(
             base_properties,
             unselected?.default ?? {},
-            {}
+            {},
         )
         const unselected_hovered_style = merge(
             base_properties,
             { background: background(theme.middle, "hovered") },
-            unselected?.hovered ?? {}
+            unselected?.hovered ?? {},
         )
         const unselected_clicked_style = merge(
             base_properties,
             { background: background(theme.middle, "pressed") },
-            unselected?.clicked ?? {}
+            unselected?.clicked ?? {},
         )
         const selected_default_style = merge(
             base_properties,
@@ -82,7 +82,7 @@ export default function project_panel(): any {
                 background: background(theme.lowest),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.default ?? {}
+            selected_style?.default ?? {},
         )
         const selected_hovered_style = merge(
             base_properties,
@@ -90,7 +90,7 @@ export default function project_panel(): any {
                 background: background(theme.lowest, "hovered"),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.hovered ?? {}
+            selected_style?.hovered ?? {},
         )
         const selected_clicked_style = merge(
             base_properties,
@@ -98,7 +98,7 @@ export default function project_panel(): any {
                 background: background(theme.lowest, "pressed"),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.clicked ?? {}
+            selected_style?.clicked ?? {},
         )
 
         return toggleable({
@@ -175,7 +175,7 @@ export default function project_panel(): any {
                 default: {
                     icon_color: foreground(theme.middle, "variant"),
                 },
-            }
+            },
         ),
         cut_entry: entry(
             {
@@ -190,7 +190,7 @@ export default function project_panel(): any {
                         size: "sm",
                     }),
                 },
-            }
+            },
         ),
         filename_editor: {
             background: background(theme.middle, "on"),

styles/src/style_tree/status_bar.ts 🔗

@@ -34,14 +34,10 @@ export default function status_bar(): any {
             ...text(layer, "mono", "variant", { size: "xs" }),
         },
         active_language: text_button({
-            color: "variant",
-        }),
-        auto_update_progress_message: text(layer, "sans", "variant", {
-            size: "xs",
-        }),
-        auto_update_done_message: text(layer, "sans", "variant", {
-            size: "xs",
+            color: "variant"
         }),
+        auto_update_progress_message: text(layer, "sans", "variant", { size: "xs" }),
+        auto_update_done_message: text(layer, "sans", "variant", { size: "xs" }),
         lsp_status: interactive({
             base: {
                 ...diagnostic_status_container,
@@ -53,7 +49,7 @@ export default function status_bar(): any {
             },
             state: {
                 hovered: {
-                    message: text(layer, "sans"),
+                    message: text(layer, "sans", { size: "xs" }),
                     icon_color: foreground(layer),
                     background: background(layer, "hovered"),
                 },

styles/src/style_tree/titlebar.ts 🔗

@@ -183,10 +183,10 @@ export function titlebar(): any {
         project_name_divider: text(theme.lowest, "sans", "variant"),
 
         project_menu_button: toggleable_text_button(theme, {
-            color: "base",
+            color: 'base',
         }),
         git_menu_button: toggleable_text_button(theme, {
-            color: "variant",
+            color: 'variant',
         }),
 
         // Collaborators

styles/src/theme/create_theme.ts 🔗

@@ -1,28 +1,28 @@
-import { Scale, Color } from "chroma-js"
+import chroma, { Scale, Color } from "chroma-js"
+import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
+export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
 import {
     ThemeConfig,
     ThemeAppearance,
     ThemeConfigInputColors,
 } from "./theme_config"
 import { get_ramps } from "./ramps"
-import { syntaxStyle } from "./syntax"
-import { Syntax } from "../types/syntax"
 
 export interface Theme {
     name: string
     is_light: boolean
 
     /**
-     * App background, other elements that should sit directly on top of the background.
-     */
+    * App background, other elements that should sit directly on top of the background.
+    */
     lowest: Layer
     /**
-     * Panels, tabs, other UI surfaces that sit on top of the background.
-     */
+    * Panels, tabs, other UI surfaces that sit on top of the background.
+    */
     middle: Layer
     /**
-     * Editors like code buffers, conversation editors, etc.
-     */
+    * Editors like code buffers, conversation editors, etc.
+    */
     highest: Layer
 
     ramps: RampSet
@@ -31,7 +31,8 @@ export interface Theme {
     modal_shadow: Shadow
 
     players: Players
-    syntax: Syntax
+    syntax?: Partial<ThemeSyntax>
+    color_family: ColorFamily
 }
 
 export interface Meta {
@@ -69,6 +70,15 @@ export interface Players {
     "7": Player
 }
 
+export type ColorFamily = Partial<{ [K in keyof RampSet]: ColorFamilyRange }>
+
+export interface ColorFamilyRange {
+    low: number
+    high: number
+    range: number
+    scaling_value: number
+}
+
 export interface Shadow {
     blur: number
     color: string
@@ -115,7 +125,12 @@ export interface Style {
 }
 
 export function create_theme(theme: ThemeConfig): Theme {
-    const { name, appearance, input_color } = theme
+    const {
+        name,
+        appearance,
+        input_color,
+        override: { syntax },
+    } = theme
 
     const is_light = appearance === ThemeAppearance.Light
     const color_ramps: ThemeConfigInputColors = input_color
@@ -157,10 +172,7 @@ export function create_theme(theme: ThemeConfig): Theme {
         "7": player(ramps.yellow),
     }
 
-    const syntax = syntaxStyle(
-        ramps,
-        theme.override.syntax ? theme.override.syntax : {}
-    )
+    const color_family = build_color_family(ramps)
 
     return {
         name,
@@ -177,6 +189,7 @@ export function create_theme(theme: ThemeConfig): Theme {
 
         players,
         syntax,
+        color_family,
     }
 }
 
@@ -187,6 +200,28 @@ function player(ramp: Scale): Player {
     }
 }
 
+function build_color_family(ramps: RampSet): ColorFamily {
+    const color_family: ColorFamily = {}
+
+    for (const ramp in ramps) {
+        const ramp_value = ramps[ramp as keyof RampSet]
+
+        const lightnessValues = [ramp_value(0).get('hsl.l') * 100, ramp_value(1).get('hsl.l') * 100]
+        const low = Math.min(...lightnessValues)
+        const high = Math.max(...lightnessValues)
+        const range = high - low
+
+        color_family[ramp as keyof RampSet] = {
+            low,
+            high,
+            range,
+            scaling_value: 100 / range,
+        }
+    }
+
+    return color_family
+}
+
 function lowest_layer(ramps: RampSet): Layer {
     return {
         base: build_style_set(ramps.neutral, 0.2, 1),

styles/src/theme/syntax.ts 🔗

@@ -1,45 +1,325 @@
 import deepmerge from "deepmerge"
-import { font_weights, ThemeConfigInputSyntax, RampSet } from "../common"
-import { Syntax, SyntaxHighlightStyle, allSyntaxKeys } from "../types/syntax"
-
-// Apply defaults to any missing syntax properties that are not defined manually
-function apply_defaults(
-    ramps: RampSet,
-    syntax_highlights: Partial<Syntax>
-): Syntax {
-    const restKeys: (keyof Syntax)[] = allSyntaxKeys.filter(
-        (key) => !syntax_highlights[key]
-    )
+import { FontWeight, font_weights, useTheme } from "../common"
+import chroma from "chroma-js"
 
-    const completeSyntax: Syntax = {} as Syntax
+export interface SyntaxHighlightStyle {
+    color?: string
+    weight?: FontWeight
+    underline?: boolean
+    italic?: boolean
+}
 
-    const defaults: SyntaxHighlightStyle = {
-        color: ramps.neutral(1).hex(),
-    }
+export interface Syntax {
+    // == Text Styles ====== /
+    comment: SyntaxHighlightStyle
+    // elixir: doc comment
+    "comment.doc": SyntaxHighlightStyle
+    primary: SyntaxHighlightStyle
+    predictive: SyntaxHighlightStyle
+    hint: SyntaxHighlightStyle
 
-    for (const key of restKeys) {
-        {
-            completeSyntax[key] = {
-                ...defaults,
-            }
+    // === Formatted Text ====== /
+    emphasis: SyntaxHighlightStyle
+    "emphasis.strong": SyntaxHighlightStyle
+    title: SyntaxHighlightStyle
+    link_uri: SyntaxHighlightStyle
+    link_text: SyntaxHighlightStyle
+    /** md: indented_code_block, fenced_code_block, code_span */
+    "text.literal": SyntaxHighlightStyle
+
+    // == Punctuation ====== /
+    punctuation: SyntaxHighlightStyle
+    /** Example: `(`, `[`, `{`...*/
+    "punctuation.bracket": SyntaxHighlightStyle
+    /**., ;*/
+    "punctuation.delimiter": SyntaxHighlightStyle
+    // js, ts: ${, } in a template literal
+    // yaml: *, &, ---, ...
+    "punctuation.special": SyntaxHighlightStyle
+    // md: list_marker_plus, list_marker_dot, etc
+    "punctuation.list_marker": SyntaxHighlightStyle
+
+    // == Strings ====== /
+
+    string: SyntaxHighlightStyle
+    // css: color_value
+    // js: this, super
+    // toml: offset_date_time, local_date_time...
+    "string.special": SyntaxHighlightStyle
+    // elixir: atom, quoted_atom, keyword, quoted_keyword
+    // ruby: simple_symbol, delimited_symbol...
+    "string.special.symbol"?: SyntaxHighlightStyle
+    // elixir, python, yaml...: escape_sequence
+    "string.escape"?: SyntaxHighlightStyle
+    // Regular expressions
+    "string.regex"?: SyntaxHighlightStyle
+
+    // == Types ====== /
+    // We allow Function here because all JS objects literals have this property
+    constructor: SyntaxHighlightStyle | Function // eslint-disable-line  @typescript-eslint/ban-types
+    variant: SyntaxHighlightStyle
+    type: SyntaxHighlightStyle
+    // js: predefined_type
+    "type.builtin"?: SyntaxHighlightStyle
+
+    // == Values
+    variable: SyntaxHighlightStyle
+    // this, ...
+    // css: -- (var(--foo))
+    // lua: self
+    "variable.special"?: SyntaxHighlightStyle
+    // c: statement_identifier,
+    label: SyntaxHighlightStyle
+    // css: tag_name, nesting_selector, universal_selector...
+    tag: SyntaxHighlightStyle
+    // css: attribute, pseudo_element_selector (tag_name),
+    attribute: SyntaxHighlightStyle
+    // css: class_name, property_name, namespace_name...
+    property: SyntaxHighlightStyle
+    // true, false, null, nullptr
+    constant: SyntaxHighlightStyle
+    // css: @media, @import, @supports...
+    // js: declare, implements, interface, keyof, public...
+    keyword: SyntaxHighlightStyle
+    // note: js enum is currently defined as a keyword
+    enum: SyntaxHighlightStyle
+    // -, --, ->, !=, &&, ||, <=...
+    operator: SyntaxHighlightStyle
+    number: SyntaxHighlightStyle
+    boolean: SyntaxHighlightStyle
+    // elixir: __MODULE__, __DIR__, __ENV__, etc
+    // go: nil, iota
+    "constant.builtin"?: SyntaxHighlightStyle
+
+    // == Functions ====== /
+
+    function: SyntaxHighlightStyle
+    // lua: assert, error, loadfile, tostring, unpack...
+    "function.builtin"?: SyntaxHighlightStyle
+    // go: call_expression, method_declaration
+    // js: call_expression, method_definition, pair (key, arrow function)
+    // rust: function_item name: (identifier)
+    "function.definition"?: SyntaxHighlightStyle
+    // rust: macro_definition name: (identifier)
+    "function.special.definition"?: SyntaxHighlightStyle
+    "function.method"?: SyntaxHighlightStyle
+    // ruby: identifier/"defined?" // Nate note: I don't fully understand this one.
+    "function.method.builtin"?: SyntaxHighlightStyle
+
+    // == Unsorted ====== /
+    // lua: hash_bang_line
+    preproc: SyntaxHighlightStyle
+    // elixir, python: interpolation (ex: foo in ${foo})
+    // js: template_substitution
+    embedded: SyntaxHighlightStyle
+}
+
+export type ThemeSyntax = Partial<Syntax>
+
+const default_syntax_highlight_style: Omit<SyntaxHighlightStyle, "color"> = {
+    weight: "normal",
+    underline: false,
+    italic: false,
+}
+
+function build_default_syntax(): Syntax {
+    const theme = useTheme()
+
+    // Make a temporary object that is allowed to be missing
+    // the "color" property for each style
+    const syntax: {
+        [key: string]: Omit<SyntaxHighlightStyle, "color">
+    } = {}
+
+    // then spread the default to each style
+    for (const key of Object.keys({} as Syntax)) {
+        syntax[key as keyof Syntax] = {
+            ...default_syntax_highlight_style,
         }
     }
 
-    const mergedBaseSyntax = Object.assign(completeSyntax, syntax_highlights)
+    // Mix the neutral and blue colors to get a
+    // predictive color distinct from any other color in the theme
+    const predictive = chroma
+        .mix(
+            theme.ramps.neutral(0.4).hex(),
+            theme.ramps.blue(0.4).hex(),
+            0.45,
+            "lch"
+        )
+        .hex()
+    // Mix the neutral and green colors to get a
+    // hint color distinct from any other color in the theme
+    const hint = chroma
+        .mix(
+            theme.ramps.neutral(0.6).hex(),
+            theme.ramps.blue(0.4).hex(),
+            0.45,
+            "lch"
+        )
+        .hex()
 
-    return mergedBaseSyntax
+    const color = {
+        primary: theme.ramps.neutral(1).hex(),
+        comment: theme.ramps.neutral(0.71).hex(),
+        punctuation: theme.ramps.neutral(0.86).hex(),
+        predictive: predictive,
+        hint: hint,
+        emphasis: theme.ramps.blue(0.5).hex(),
+        string: theme.ramps.orange(0.5).hex(),
+        function: theme.ramps.yellow(0.5).hex(),
+        type: theme.ramps.cyan(0.5).hex(),
+        constructor: theme.ramps.blue(0.5).hex(),
+        variant: theme.ramps.blue(0.5).hex(),
+        property: theme.ramps.blue(0.5).hex(),
+        enum: theme.ramps.orange(0.5).hex(),
+        operator: theme.ramps.orange(0.5).hex(),
+        number: theme.ramps.green(0.5).hex(),
+        boolean: theme.ramps.green(0.5).hex(),
+        constant: theme.ramps.green(0.5).hex(),
+        keyword: theme.ramps.blue(0.5).hex(),
+    }
+
+    // Then assign colors and use Syntax to enforce each style getting it's own color
+    const default_syntax: Syntax = {
+        ...syntax,
+        comment: {
+            color: color.comment,
+        },
+        "comment.doc": {
+            color: color.comment,
+        },
+        primary: {
+            color: color.primary,
+        },
+        predictive: {
+            color: color.predictive,
+            italic: true,
+        },
+        hint: {
+            color: color.hint,
+            weight: font_weights.bold,
+        },
+        emphasis: {
+            color: color.emphasis,
+        },
+        "emphasis.strong": {
+            color: color.emphasis,
+            weight: font_weights.bold,
+        },
+        title: {
+            color: color.primary,
+            weight: font_weights.bold,
+        },
+        link_uri: {
+            color: theme.ramps.green(0.5).hex(),
+            underline: true,
+        },
+        link_text: {
+            color: theme.ramps.orange(0.5).hex(),
+            italic: true,
+        },
+        "text.literal": {
+            color: color.string,
+        },
+        punctuation: {
+            color: color.punctuation,
+        },
+        "punctuation.bracket": {
+            color: color.punctuation,
+        },
+        "punctuation.delimiter": {
+            color: color.punctuation,
+        },
+        "punctuation.special": {
+            color: theme.ramps.neutral(0.86).hex(),
+        },
+        "punctuation.list_marker": {
+            color: color.punctuation,
+        },
+        string: {
+            color: color.string,
+        },
+        "string.special": {
+            color: color.string,
+        },
+        "string.special.symbol": {
+            color: color.string,
+        },
+        "string.escape": {
+            color: color.comment,
+        },
+        "string.regex": {
+            color: color.string,
+        },
+        constructor: {
+            color: theme.ramps.blue(0.5).hex(),
+        },
+        variant: {
+            color: theme.ramps.blue(0.5).hex(),
+        },
+        type: {
+            color: color.type,
+        },
+        variable: {
+            color: color.primary,
+        },
+        label: {
+            color: theme.ramps.blue(0.5).hex(),
+        },
+        tag: {
+            color: theme.ramps.blue(0.5).hex(),
+        },
+        attribute: {
+            color: theme.ramps.blue(0.5).hex(),
+        },
+        property: {
+            color: theme.ramps.blue(0.5).hex(),
+        },
+        constant: {
+            color: color.constant,
+        },
+        keyword: {
+            color: color.keyword,
+        },
+        enum: {
+            color: color.enum,
+        },
+        operator: {
+            color: color.operator,
+        },
+        number: {
+            color: color.number,
+        },
+        boolean: {
+            color: color.boolean,
+        },
+        function: {
+            color: color.function,
+        },
+        preproc: {
+            color: color.primary,
+        },
+        embedded: {
+            color: color.primary,
+        },
+    }
+
+    return default_syntax
 }
 
-// Merge the base syntax with the theme syntax overrides
-// This is a deep merge, so any nested properties will be merged as well
-// This allows for a theme to only override a single property of a syntax highlight style
-const merge_syntax = (
-    baseSyntax: Syntax,
-    theme_syntax_overrides: ThemeConfigInputSyntax
-): Syntax => {
-    return deepmerge<Syntax, ThemeConfigInputSyntax>(
-        baseSyntax,
-        theme_syntax_overrides,
+export function build_syntax(): Syntax {
+    const theme = useTheme()
+
+    const default_syntax: Syntax = build_default_syntax()
+
+    if (!theme.syntax) {
+        return default_syntax
+    }
+
+    const syntax = deepmerge<Syntax, Partial<ThemeSyntax>>(
+        default_syntax,
+        theme.syntax,
         {
             arrayMerge: (destinationArray, sourceArray) => [
                 ...destinationArray,
@@ -47,49 +327,6 @@ const merge_syntax = (
             ],
         }
     )
-}
-
-/** Returns a complete Syntax object of the combined styles of a theme's syntax overrides and the default syntax styles */
-export const syntaxStyle = (
-    ramps: RampSet,
-    theme_syntax_overrides: ThemeConfigInputSyntax
-): Syntax => {
-    const syntax_highlights: Partial<Syntax> = {
-        comment: { color: ramps.neutral(0.71).hex() },
-        "comment.doc": { color: ramps.neutral(0.71).hex() },
-        primary: { color: ramps.neutral(1).hex() },
-        emphasis: { color: ramps.blue(0.5).hex() },
-        "emphasis.strong": {
-            color: ramps.blue(0.5).hex(),
-            weight: font_weights.bold,
-        },
-        link_uri: { color: ramps.green(0.5).hex(), underline: true },
-        link_text: { color: ramps.orange(0.5).hex(), italic: true },
-        "text.literal": { color: ramps.orange(0.5).hex() },
-        punctuation: { color: ramps.neutral(0.86).hex() },
-        "punctuation.bracket": { color: ramps.neutral(0.86).hex() },
-        "punctuation.special": { color: ramps.neutral(0.86).hex() },
-        "punctuation.delimiter": { color: ramps.neutral(0.86).hex() },
-        "punctuation.list_marker": { color: ramps.neutral(0.86).hex() },
-        string: { color: ramps.orange(0.5).hex() },
-        "string.special": { color: ramps.orange(0.5).hex() },
-        "string.special.symbol": { color: ramps.orange(0.5).hex() },
-        "string.escape": { color: ramps.neutral(0.71).hex() },
-        "string.regex": { color: ramps.orange(0.5).hex() },
-        "method.constructor": { color: ramps.blue(0.5).hex() },
-        type: { color: ramps.cyan(0.5).hex() },
-        label: { color: ramps.blue(0.5).hex() },
-        attribute: { color: ramps.blue(0.5).hex() },
-        property: { color: ramps.blue(0.5).hex() },
-        constant: { color: ramps.green(0.5).hex() },
-        keyword: { color: ramps.blue(0.5).hex() },
-        operator: { color: ramps.orange(0.5).hex() },
-        number: { color: ramps.green(0.5).hex() },
-        boolean: { color: ramps.green(0.5).hex() },
-        function: { color: ramps.yellow(0.5).hex() },
-    }
 
-    const baseSyntax = apply_defaults(ramps, syntax_highlights)
-    const mergedSyntax = merge_syntax(baseSyntax, theme_syntax_overrides)
-    return mergedSyntax
+    return syntax
 }

styles/src/theme/theme_config.ts 🔗

@@ -1,5 +1,5 @@
 import { Scale, Color } from "chroma-js"
-import { SyntaxHighlightStyle, SyntaxProperty } from "../types/syntax"
+import { Syntax } from "./syntax"
 
 interface ThemeMeta {
     /** The name of the theme */
@@ -55,9 +55,7 @@ export type ThemeConfigInputColorsKeys = keyof ThemeConfigInputColors
  * }
  * ```
  */
-export type ThemeConfigInputSyntax = Partial<
-    Record<SyntaxProperty, Partial<SyntaxHighlightStyle>>
->
+export type ThemeConfigInputSyntax = Partial<Syntax>
 
 interface ThemeConfigOverrides {
     syntax: ThemeConfigInputSyntax

styles/src/theme/tokens/theme.ts 🔗

@@ -4,13 +4,17 @@ import {
     SingleOtherToken,
     TokenTypes,
 } from "@tokens-studio/types"
-import { Shadow } from "../create_theme"
+import {
+    Shadow,
+    SyntaxHighlightStyle,
+    ThemeSyntax,
+} from "../create_theme"
 import { LayerToken, layer_token } from "./layer"
 import { PlayersToken, players_token } from "./players"
 import { color_token } from "./token"
+import { Syntax } from "../syntax"
 import editor from "../../style_tree/editor"
 import { useTheme } from "../../../src/common"
-import { Syntax, SyntaxHighlightStyle } from "../../types/syntax"
 
 interface ThemeTokens {
     name: SingleOtherToken
@@ -47,7 +51,7 @@ const modal_shadow_token = (): SingleBoxShadowToken => {
     return create_shadow_token(shadow, "modal_shadow")
 }
 
-type ThemeSyntaxColorTokens = Record<keyof Syntax, SingleColorToken>
+type ThemeSyntaxColorTokens = Record<keyof ThemeSyntax, SingleColorToken>
 
 function syntax_highlight_style_color_tokens(
     syntax: Syntax

styles/src/themes/atelier/common.ts 🔗

@@ -1,8 +1,4 @@
-import {
-    ThemeLicenseType,
-    ThemeFamilyMeta,
-    ThemeConfigInputSyntax,
-} from "../../common"
+import { ThemeLicenseType, ThemeSyntax, ThemeFamilyMeta } from "../../common"
 
 export interface Variant {
     colors: {
@@ -33,7 +29,7 @@ export const meta: ThemeFamilyMeta = {
         "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/",
 }
 
-export const build_syntax = (variant: Variant): ThemeConfigInputSyntax => {
+export const build_syntax = (variant: Variant): ThemeSyntax => {
     const { colors } = variant
     return {
         primary: { color: colors.base06 },
@@ -54,6 +50,7 @@ export const build_syntax = (variant: Variant): ThemeConfigInputSyntax => {
         property: { color: colors.base08 },
         variable: { color: colors.base06 },
         "variable.special": { color: colors.base0E },
+        variant: { color: colors.base0A },
         keyword: { color: colors.base0E },
     }
 }

styles/src/themes/ayu/common.ts 🔗

@@ -3,8 +3,8 @@ import {
     chroma,
     color_ramp,
     ThemeLicenseType,
+    ThemeSyntax,
     ThemeFamilyMeta,
-    ThemeConfigInputSyntax,
 } from "../../common"
 
 export const ayu = {
@@ -27,7 +27,7 @@ export const build_theme = (t: typeof dark, light: boolean) => {
         purple: t.syntax.constant.hex(),
     }
 
-    const syntax: ThemeConfigInputSyntax = {
+    const syntax: ThemeSyntax = {
         constant: { color: t.syntax.constant.hex() },
         "string.regex": { color: t.syntax.regexp.hex() },
         string: { color: t.syntax.string.hex() },
@@ -61,7 +61,7 @@ export const build_theme = (t: typeof dark, light: boolean) => {
     }
 }
 
-export const build_syntax = (t: typeof dark): ThemeConfigInputSyntax => {
+export const build_syntax = (t: typeof dark): ThemeSyntax => {
     return {
         constant: { color: t.syntax.constant.hex() },
         "string.regex": { color: t.syntax.regexp.hex() },

styles/src/themes/gruvbox/gruvbox-common.ts 🔗

@@ -4,8 +4,8 @@ import {
     ThemeAppearance,
     ThemeLicenseType,
     ThemeConfig,
+    ThemeSyntax,
     ThemeFamilyMeta,
-    ThemeConfigInputSyntax,
 } from "../../common"
 
 const meta: ThemeFamilyMeta = {
@@ -214,7 +214,7 @@ const build_variant = (variant: Variant): ThemeConfig => {
         magenta: color_ramp(chroma(variant.colors.gray)),
     }
 
-    const syntax: ThemeConfigInputSyntax = {
+    const syntax: ThemeSyntax = {
         primary: { color: neutral[is_light ? 0 : 8] },
         "text.literal": { color: colors.blue },
         comment: { color: colors.gray },
@@ -229,7 +229,7 @@ const build_variant = (variant: Variant): ThemeConfig => {
         "string.special.symbol": { color: colors.aqua },
         "string.regex": { color: colors.orange },
         type: { color: colors.yellow },
-        // enum: { color: colors.orange },
+        enum: { color: colors.orange },
         tag: { color: colors.aqua },
         constant: { color: colors.yellow },
         keyword: { color: colors.red },

styles/src/themes/one/one-dark.ts 🔗

@@ -54,6 +54,7 @@ export const theme: ThemeConfig = {
         syntax: {
             boolean: { color: color.orange },
             comment: { color: color.grey },
+            enum: { color: color.red },
             "emphasis.strong": { color: color.orange },
             function: { color: color.blue },
             keyword: { color: color.purple },
@@ -72,7 +73,8 @@ export const theme: ThemeConfig = {
             "text.literal": { color: color.green },
             type: { color: color.teal },
             "variable.special": { color: color.orange },
-            "method.constructor": { color: color.blue },
+            variant: { color: color.blue },
+            constructor: { color: color.blue },
         },
     },
 }

styles/src/themes/one/one-light.ts 🔗

@@ -55,6 +55,7 @@ export const theme: ThemeConfig = {
         syntax: {
             boolean: { color: color.orange },
             comment: { color: color.grey },
+            enum: { color: color.red },
             "emphasis.strong": { color: color.orange },
             function: { color: color.blue },
             keyword: { color: color.purple },
@@ -72,6 +73,7 @@ export const theme: ThemeConfig = {
             "text.literal": { color: color.green },
             type: { color: color.teal },
             "variable.special": { color: color.orange },
+            variant: { color: color.blue },
         },
     },
 }

styles/src/themes/rose-pine/common.ts 🔗

@@ -1,4 +1,4 @@
-import { ThemeConfigInputSyntax } from "../../common"
+import { ThemeSyntax } from "../../common"
 
 export const color = {
     default: {
@@ -54,7 +54,7 @@ export const color = {
     },
 }
 
-export const syntax = (c: typeof color.default): ThemeConfigInputSyntax => {
+export const syntax = (c: typeof color.default): Partial<ThemeSyntax> => {
     return {
         comment: { color: c.muted },
         operator: { color: c.pine },

styles/src/types/extract_syntax_types.ts 🔗

@@ -1,111 +0,0 @@
-import fs from "fs"
-import path from "path"
-import readline from "readline"
-
-function escapeTypeName(name: string): string {
-    return `'${name.replace("@", "").toLowerCase()}'`
-}
-
-const generatedNote = `// This file is generated by extract_syntax_types.ts
-// Do not edit this file directly
-// It is generated from the highlight.scm files in the zed crate
-
-// To regenerate this file manually:
-//     'npm run extract-syntax-types' from ./styles`
-
-const defaultTextProperty = `    /** Default text color */
-    | 'primary'`
-
-const main = async () => {
-    const pathFromRoot = "crates/zed/src/languages"
-    const directoryPath = path.join(__dirname, "../../../", pathFromRoot)
-    const stylesMap: Record<string, Set<string>> = {}
-    const propertyLanguageMap: Record<string, Set<string>> = {}
-
-    const processFile = async (filePath: string, language: string) => {
-        const fileStream = fs.createReadStream(filePath)
-        const rl = readline.createInterface({
-            input: fileStream,
-            crlfDelay: Infinity,
-        })
-
-        for await (const line of rl) {
-            const cleanedLine = line.replace(/"@[a-zA-Z0-9_.]*"/g, "")
-            const match = cleanedLine.match(/@(\w+\.*)*/g)
-            if (match) {
-                match.forEach((property) => {
-                    const formattedProperty = escapeTypeName(property)
-                    // Only add non-empty properties
-                    if (formattedProperty !== "''") {
-                        if (!propertyLanguageMap[formattedProperty]) {
-                            propertyLanguageMap[formattedProperty] = new Set()
-                        }
-                        propertyLanguageMap[formattedProperty].add(language)
-                    }
-                })
-            }
-        }
-    }
-
-    const directories = fs
-        .readdirSync(directoryPath, { withFileTypes: true })
-        .filter((dirent) => dirent.isDirectory())
-        .map((dirent) => dirent.name)
-
-    for (const dir of directories) {
-        const highlightsFilePath = path.join(
-            directoryPath,
-            dir,
-            "highlights.scm"
-        )
-        if (fs.existsSync(highlightsFilePath)) {
-            await processFile(highlightsFilePath, dir)
-        }
-    }
-
-    for (const [language, properties] of Object.entries(stylesMap)) {
-        console.log(`${language}: ${Array.from(properties).join(", ")}`)
-    }
-
-    const sortedProperties = Object.entries(propertyLanguageMap).sort(
-        ([propA], [propB]) => propA.localeCompare(propB)
-    )
-
-    const outStream = fs.createWriteStream(path.join(__dirname, "syntax.ts"))
-    let allProperties = ""
-    const syntaxKeys = []
-    for (const [property, languages] of sortedProperties) {
-        let languagesArray = Array.from(languages)
-        const moreThanSeven = languagesArray.length > 7
-        // Limit to the first 7 languages, append "..." if more than 7
-        languagesArray = languagesArray.slice(0, 7)
-        if (moreThanSeven) {
-            languagesArray.push("...")
-        }
-        const languagesString = languagesArray.join(", ")
-        const comment = `/** ${languagesString} */`
-        allProperties += `    ${comment}\n    | ${property} \n`
-        syntaxKeys.push(property)
-    }
-    outStream.write(`${generatedNote}
-
-export type SyntaxHighlightStyle = {
-    color: string,
-    fade_out?: number,
-    italic?: boolean,
-    underline?: boolean,
-    weight?: string,
-}
-
-export type Syntax = Record<SyntaxProperty, SyntaxHighlightStyle>
-export type SyntaxOverride = Partial<Syntax>
-
-export type SyntaxProperty = \n${defaultTextProperty}\n\n${allProperties}
-
-export const allSyntaxKeys: SyntaxProperty[] = [\n    ${syntaxKeys.join(
-        ",\n    "
-    )}\n]`)
-    outStream.end()
-}
-
-main().catch(console.error)

styles/src/types/syntax.ts 🔗

@@ -1,202 +0,0 @@
-// This file is generated by extract_syntax_types.ts
-// Do not edit this file directly
-// It is generated from the highlight.scm files in the zed crate
-
-// To regenerate this file manually:
-//     'npm run extract-syntax-types' from ./styles
-
-export type SyntaxHighlightStyle = {
-    color: string
-    fade_out?: number
-    italic?: boolean
-    underline?: boolean
-    weight?: string
-}
-
-export type Syntax = Record<SyntaxProperty, SyntaxHighlightStyle>
-export type SyntaxOverride = Partial<Syntax>
-
-export type SyntaxProperty =
-    /** Default text color */
-    | "primary"
-
-    /** elixir */
-    | "__attribute__"
-    /** elixir */
-    | "__name__"
-    /** elixir */
-    | "_sigil_name"
-    /** css, heex, lua */
-    | "attribute"
-    /** javascript, lua, tsx, typescript, yaml */
-    | "boolean"
-    /** elixir */
-    | "comment.doc"
-    /** elixir */
-    | "comment.unused"
-    /** bash, c, cpp, css, elixir, elm, erb, ... */
-    | "comment"
-    /** elixir, go, javascript, lua, php, python, racket, ... */
-    | "constant.builtin"
-    /** bash, c, cpp, elixir, elm, glsl, heex, ... */
-    | "constant"
-    /** glsl */
-    | "delimiter"
-    /** bash, elixir, javascript, python, ruby, tsx, typescript */
-    | "embedded"
-    /** markdown */
-    | "emphasis.strong"
-    /** markdown */
-    | "emphasis"
-    /** go, python, racket, ruby, scheme */
-    | "escape"
-    /** lua */
-    | "field"
-    /** lua, php, python */
-    | "function.builtin"
-    /** elm, lua, rust */
-    | "function.definition"
-    /** ruby */
-    | "function.method.builtin"
-    /** go, javascript, php, python, ruby, rust, tsx, ... */
-    | "function.method"
-    /** rust */
-    | "function.special.definition"
-    /** c, cpp, glsl, rust */
-    | "function.special"
-    /** bash, c, cpp, css, elixir, elm, glsl, ... */
-    | "function"
-    /** elm */
-    | "identifier"
-    /** glsl */
-    | "keyword.function"
-    /** bash, c, cpp, css, elixir, elm, erb, ... */
-    | "keyword"
-    /** c, cpp, glsl */
-    | "label"
-    /** markdown */
-    | "link_text"
-    /** markdown */
-    | "link_uri"
-    /** lua, php, tsx, typescript */
-    | "method.constructor"
-    /** lua */
-    | "method"
-    /** heex */
-    | "module"
-    /** svelte */
-    | "none"
-    /** bash, c, cpp, css, elixir, glsl, go, ... */
-    | "number"
-    /** bash, c, cpp, css, elixir, elm, glsl, ... */
-    | "operator"
-    /** lua */
-    | "parameter"
-    /** lua */
-    | "preproc"
-    /** bash, c, cpp, css, glsl, go, html, ... */
-    | "property"
-    /** c, cpp, elixir, elm, heex, html, javascript, ... */
-    | "punctuation.bracket"
-    /** c, cpp, css, elixir, elm, heex, javascript, ... */
-    | "punctuation.delimiter"
-    /** markdown */
-    | "punctuation.list_marker"
-    /** elixir, javascript, python, ruby, tsx, typescript, yaml */
-    | "punctuation.special"
-    /** elixir */
-    | "punctuation"
-    /** glsl */
-    | "storageclass"
-    /** elixir, elm, yaml */
-    | "string.escape"
-    /** elixir, javascript, racket, ruby, tsx, typescript */
-    | "string.regex"
-    /** elixir, ruby */
-    | "string.special.symbol"
-    /** css, elixir, toml */
-    | "string.special"
-    /** bash, c, cpp, css, elixir, elm, glsl, ... */
-    | "string"
-    /** svelte */
-    | "tag.delimiter"
-    /** css, heex, php, svelte */
-    | "tag"
-    /** markdown */
-    | "text.literal"
-    /** markdown */
-    | "title"
-    /** javascript, php, rust, tsx, typescript */
-    | "type.builtin"
-    /** glsl */
-    | "type.qualifier"
-    /** c, cpp, css, elixir, elm, glsl, go, ... */
-    | "type"
-    /** glsl, php */
-    | "variable.builtin"
-    /** cpp, css, javascript, lua, racket, ruby, rust, ... */
-    | "variable.special"
-    /** c, cpp, elm, glsl, go, javascript, lua, ... */
-    | "variable"
-
-export const allSyntaxKeys: SyntaxProperty[] = [
-    "__attribute__",
-    "__name__",
-    "_sigil_name",
-    "attribute",
-    "boolean",
-    "comment.doc",
-    "comment.unused",
-    "comment",
-    "constant.builtin",
-    "constant",
-    "delimiter",
-    "embedded",
-    "emphasis.strong",
-    "emphasis",
-    "escape",
-    "field",
-    "function.builtin",
-    "function.definition",
-    "function.method.builtin",
-    "function.method",
-    "function.special.definition",
-    "function.special",
-    "function",
-    "identifier",
-    "keyword.function",
-    "keyword",
-    "label",
-    "link_text",
-    "link_uri",
-    "method.constructor",
-    "method",
-    "module",
-    "none",
-    "number",
-    "operator",
-    "parameter",
-    "preproc",
-    "property",
-    "punctuation.bracket",
-    "punctuation.delimiter",
-    "punctuation.list_marker",
-    "punctuation.special",
-    "punctuation",
-    "storageclass",
-    "string.escape",
-    "string.regex",
-    "string.special.symbol",
-    "string.special",
-    "string",
-    "tag.delimiter",
-    "tag",
-    "text.literal",
-    "title",
-    "type.builtin",
-    "type.qualifier",
-    "type",
-    "variable.builtin",
-    "variable.special",
-    "variable",
-]

styles/tsconfig.json 🔗

@@ -24,5 +24,7 @@
         "useUnknownInCatchVariables": false,
         "baseUrl": "."
     },
-    "exclude": ["node_modules"]
+    "exclude": [
+        "node_modules"
+    ]
 }