Merge branch 'main' into collab-panel

Max Brunsfeld created

Change summary

Cargo.lock                                                   |  12 
crates/collab/src/tests.rs                                   |   5 
crates/collab/src/tests/integration_tests.rs                 |  51 
crates/collab_ui/src/incoming_call_notification.rs           |   4 
crates/collab_ui/src/project_shared_notification.rs          |   4 
crates/command_palette/src/command_palette.rs                |   4 
crates/copilot/src/sign_in.rs                                |  55 
crates/diagnostics/src/diagnostics.rs                        |   8 
crates/editor/Cargo.toml                                     |   3 
crates/editor/src/editor.rs                                  | 106 
crates/editor/src/editor_tests.rs                            | 667 +++--
crates/editor/src/element.rs                                 |  30 
crates/editor/src/inlay_hint_cache.rs                        |  30 
crates/editor/src/items.rs                                   |   9 
crates/editor/src/test/editor_lsp_test_context.rs            |   5 
crates/editor/src/test/editor_test_context.rs                |  12 
crates/file_finder/src/file_finder.rs                        | 230 -
crates/gpui/src/app.rs                                       | 577 +++-
crates/gpui/src/app/test_app_context.rs                      |  23 
crates/gpui/src/app/window.rs                                |  20 
crates/language_tools/src/lsp_log_tests.rs                   |   4 
crates/project_panel/src/file_associations.rs                |  11 
crates/project_panel/src/project_panel.rs                    |  32 
crates/project_symbols/src/project_symbols.rs                |   4 
crates/recent_projects/src/highlighted_workspace_location.rs |   3 
crates/recent_projects/src/recent_projects.rs                |   3 
crates/search/src/buffer_search.rs                           |  19 
crates/search/src/project_search.rs                          |  16 
crates/terminal_view/src/terminal_view.rs                    |   4 
crates/util/src/paths.rs                                     | 118 
crates/workspace/src/dock.rs                                 |   8 
crates/workspace/src/pane.rs                                 |  30 
crates/workspace/src/workspace.rs                            | 210 
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/highlights.scm                  |  10 
crates/zed/src/languages/php/highlights.scm                  |  10 
crates/zed/src/languages/php/injections.scm                  |   4 
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/highlights.scm                 |   8 
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/typescript/highlights.scm           |  10 
crates/zed/src/zed.rs                                        |  34 
styles/package.json                                          |   1 
styles/src/build_themes.ts                                   |   9 
styles/src/build_tokens.ts                                   |   4 
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                              |  32 
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                             |  30 
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 -
92 files changed, 1,975 insertions(+), 1,561 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1651,6 +1651,15 @@ dependencies = [
  "theme",
 ]
 
+[[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"
@@ -2316,6 +2325,7 @@ dependencies = [
  "clock",
  "collections",
  "context_menu",
+ "convert_case",
  "copilot",
  "ctor",
  "db",
@@ -9817,7 +9827,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.98.0"
+version = "0.99.0"
 dependencies = [
  "activity_indicator",
  "ai",

crates/collab/src/tests.rs 🔗

@@ -570,8 +570,9 @@ impl TestClient {
 
         // 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| {
+        let window = cx.add_window(|_| WorkspaceContainer { workspace: None });
+        let container = window.root(cx);
+        let workspace = window.add_view(cx, |cx| {
             Workspace::new(0, project.clone(), self.app_state.clone(), cx)
         });
         container.update(cx, |container, 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,9 @@ 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) =
+    let window_b =
         cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.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,9 +1509,7 @@ 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()));
 
@@ -1525,7 +1523,7 @@ 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()));
@@ -3440,13 +3438,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_id: window_a.window_id(),
         editor: editor_a,
     };
 
@@ -3455,13 +3451,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_id: window_b.window_id(),
         editor: editor_b,
     };
 
@@ -4200,8 +4194,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)
     });
 
@@ -5312,8 +5306,9 @@ 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) =
+    let window_b =
         cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.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)
@@ -5537,8 +5532,9 @@ 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) =
+    let window_b =
         cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.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)
@@ -5569,6 +5565,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!(
@@ -7599,8 +7596,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)
     });
 
@@ -7728,8 +7725,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)
     });
 

crates/collab_ui/src/incoming_call_notification.rs 🔗

@@ -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.window_id());
                 }
             }
         }

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,7 +52,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
                 notification_windows
                     .entry(*project_id)
                     .or_insert(Vec::new())
-                    .push(window_id);
+                    .push(window.window_id());
             }
         }
         room::Event::RemoteProjectUnshared { project_id } => {

crates/command_palette/src/command_palette.rs 🔗

@@ -295,7 +295,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 window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let workspace = window.root(cx);
+        let window_id = window.window_id();
         let editor = cx.add_view(window_id, |cx| {
             let mut editor = Editor::single_line(None, cx);
             editor.set_text("abc", 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,9 @@ 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);
+        let window_id = window.window_id();
 
         // Create some diagnostics
         project.update(cx, |project, cx| {
@@ -1248,7 +1250,9 @@ 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 window_id = window.window_id();
 
         let view = cx.add_view(window_id, |cx| {
             ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)

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/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::*;
@@ -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);
@@ -4306,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;

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,7 +523,9 @@ 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 window_id = window.window_id();
     let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
     cx.add_view(window_id, |cx| {
         let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
@@ -618,10 +634,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 +679,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 +699,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 +772,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 +849,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 +956,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 +1008,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 +1173,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 +1227,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);
@@ -1530,10 +1563,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 +1601,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 +1626,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 +1638,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 +1856,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 +2370,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 +2395,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 +2695,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 +2805,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 +2834,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 +2935,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 +2965,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 +3255,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 +3274,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 +3323,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 +3396,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 +3686,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 +3849,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 +4412,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 +4560,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 +4650,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 +4780,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 +4892,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 +5006,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 +5784,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 +5854,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 +5930,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 +6017,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);
@@ -5956,7 +6091,7 @@ async fn test_extra_newline_insertion(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;
 
@@ -5992,10 +6127,12 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
 fn test_highlighted_ranges(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
-    let (_, editor) = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
-        build_editor(buffer.clone(), cx)
-    });
+    let editor = cx
+        .add_window(|cx| {
+            let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
+            build_editor(buffer.clone(), cx)
+        })
+        .root(cx);
 
     editor.update(cx, |editor, cx| {
         struct Type1;

crates/editor/src/element.rs 🔗

@@ -3002,10 +3002,12 @@ mod tests {
     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| {
@@ -3021,10 +3023,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);
@@ -3231,10 +3235,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/inlay_hint_cache.rs 🔗

@@ -1135,7 +1135,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()
@@ -1835,7 +1837,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()
@@ -1988,7 +1992,9 @@ mod tests {
         project.update(cx, |project, _| {
             project.languages().add(Arc::clone(&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()
@@ -2074,8 +2080,9 @@ mod tests {
 
         deterministic.run_until_parked();
         cx.foreground().run_until_parked();
-        let (_, editor) =
-            cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
+        let editor = cx
+            .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
+            .root(cx);
         let editor_edited = Arc::new(AtomicBool::new(false));
         let fake_server = fake_servers.next().await.unwrap();
         let closure_editor_edited = Arc::clone(&editor_edited);
@@ -2327,7 +2334,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
         project.update(cx, |project, _| {
             project.languages().add(Arc::clone(&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()
@@ -2372,8 +2381,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
 
         deterministic.run_until_parked();
         cx.foreground().run_until_parked();
-        let (_, editor) =
-            cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
+        let editor = cx
+            .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
+            .root(cx);
         let editor_edited = Arc::new(AtomicBool::new(false));
         let fake_server = fake_servers.next().await.unwrap();
         let closure_editor_edited = Arc::clone(&editor_edited);
@@ -2561,7 +2571,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
 
         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()

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/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_id: window.window_id(),
                 editor,
             },
             lsp,

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

@@ -32,16 +32,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_id: window.window_id(),
             editor,
         }
     }

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.window_id(), 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.window_id(), SelectNext);
+        cx.dispatch_action(window.window_id(), 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.window_id(), 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.window_id(), SelectNext);
+        cx.dispatch_action(window.window_id(), 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.window_id(), 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.window_id(), SelectNext);
+        cx.dispatch_action(window.window_id(), 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,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));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        let window_id = window.window_id();
         let worktree_id = cx.read(|cx| {
             let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
             assert_eq!(worktrees.len(), 1);
@@ -1404,7 +1374,9 @@ 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 window_id = window.window_id();
         let worktree_id = cx.read(|cx| {
             let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
             assert_eq!(worktrees.len(), 1,);

crates/gpui/src/app.rs 🔗

@@ -130,8 +130,14 @@ 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_with<T, F>(&self, window_id: usize, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&WindowContext) -> T;
+    fn update_window<T, F>(&mut self, window_id: usize, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&mut WindowContext) -> T;
 }
 
 #[derive(Clone)]
@@ -402,7 +408,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,
@@ -452,6 +458,26 @@ impl BorrowAppContext for AsyncAppContext {
     }
 }
 
+impl BorrowWindowContext for AsyncAppContext {
+    type Result<T> = Option<T>;
+
+    fn read_window_with<T, F>(&self, window_id: usize, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&WindowContext) -> T,
+    {
+        self.0.borrow().read_with(|cx| cx.read_window(window_id, f))
+    }
+
+    fn update_window<T, F>(&mut self, window_id: usize, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&mut WindowContext) -> T,
+    {
+        self.0
+            .borrow_mut()
+            .update(|cx| cx.update_window(window_id, f))
+    }
+}
+
 type ActionCallback = dyn FnMut(&mut dyn AnyView, &dyn Action, &mut WindowContext, usize);
 type GlobalActionCallback = dyn FnMut(&dyn Action, &mut AppContext);
 
@@ -494,8 +520,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_id: usize,
     next_subscription_id: usize,
     frame_count: usize,
 
@@ -554,8 +580,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(),
@@ -783,7 +808,7 @@ impl AppContext {
         result
     }
 
-    pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
+    fn read_window<T, F: FnOnce(&WindowContext) -> T>(
         &self,
         window_id: usize,
         callback: F,
@@ -1226,7 +1251,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);
@@ -1300,20 +1325,19 @@ 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 window_id = 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)
+            WindowHandle::new(window_id)
         })
     }
 
@@ -1323,7 +1347,7 @@ impl AppContext {
         F: FnOnce(&mut ViewContext<V>) -> V,
     {
         self.update(|this| {
-            let window_id = post_inc(&mut this.next_window_id);
+            let window_id = post_inc(&mut this.next_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();
@@ -1422,7 +1446,7 @@ impl AppContext {
         &mut self,
         window_id: usize,
         build_root_view: F,
-    ) -> Option<ViewHandle<V>>
+    ) -> Option<WindowHandle<V>>
     where
         V: View,
         F: FnOnce(&mut ViewContext<V>) -> V,
@@ -2158,6 +2182,24 @@ impl BorrowAppContext for AppContext {
     }
 }
 
+impl BorrowWindowContext for AppContext {
+    type Result<T> = Option<T>;
+
+    fn read_window_with<T, F>(&self, window_id: usize, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&WindowContext) -> T,
+    {
+        AppContext::read_window(self, window_id, f)
+    }
+
+    fn update_window<T, F>(&mut self, window_id: usize, f: F) -> Self::Result<T>
+    where
+        F: FnOnce(&mut WindowContext) -> T,
+    {
+        AppContext::update_window(self, window_id, f)
+    }
+}
+
 #[derive(Debug)]
 pub enum ParentId {
     View(usize),
@@ -3356,12 +3398,18 @@ impl<V> BorrowAppContext for ViewContext<'_, '_, V> {
 }
 
 impl<V> BorrowWindowContext for ViewContext<'_, '_, V> {
-    fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
-        BorrowWindowContext::read_with(&*self.window_context, window_id, f)
+    type Result<T> = T;
+
+    fn read_window_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
+        BorrowWindowContext::read_window_with(&*self.window_context, window_id, f)
     }
 
-    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
-        BorrowWindowContext::update(&mut *self.window_context, window_id, f)
+    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
+        &mut self,
+        window_id: usize,
+        f: F,
+    ) -> T {
+        BorrowWindowContext::update_window(&mut *self.window_context, window_id, f)
     }
 }
 
@@ -3461,12 +3509,18 @@ impl<V: View> BorrowAppContext for LayoutContext<'_, '_, '_, V> {
 }
 
 impl<V: View> BorrowWindowContext for LayoutContext<'_, '_, '_, V> {
-    fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
-        BorrowWindowContext::read_with(&*self.view_context, window_id, f)
+    type Result<T> = T;
+
+    fn read_window_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
+        BorrowWindowContext::read_window_with(&*self.view_context, window_id, f)
     }
 
-    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
-        BorrowWindowContext::update(&mut *self.view_context, window_id, f)
+    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
+        &mut self,
+        window_id: usize,
+        f: F,
+    ) -> T {
+        BorrowWindowContext::update_window(&mut *self.view_context, window_id, f)
     }
 }
 
@@ -3513,12 +3567,18 @@ impl<V: View> BorrowAppContext for EventContext<'_, '_, '_, V> {
 }
 
 impl<V: View> BorrowWindowContext for EventContext<'_, '_, '_, V> {
-    fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
-        BorrowWindowContext::read_with(&*self.view_context, window_id, f)
+    type Result<T> = T;
+
+    fn read_window_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
+        BorrowWindowContext::read_window_with(&*self.view_context, window_id, f)
     }
 
-    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
-        BorrowWindowContext::update(&mut *self.view_context, window_id, f)
+    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
+        &mut self,
+        window_id: usize,
+        f: F,
+    ) -> T {
+        BorrowWindowContext::update_window(&mut *self.view_context, window_id, f)
     }
 }
 
@@ -3802,6 +3862,89 @@ impl<T> Clone for WeakModelHandle<T> {
 
 impl<T> Copy for WeakModelHandle<T> {}
 
+pub struct WindowHandle<T> {
+    window_id: usize,
+    root_view_type: PhantomData<T>,
+}
+
+#[allow(dead_code)]
+impl<V: View> WindowHandle<V> {
+    fn new(window_id: usize) -> Self {
+        WindowHandle {
+            window_id,
+            root_view_type: PhantomData,
+        }
+    }
+
+    pub fn window_id(&self) -> usize {
+        self.window_id
+    }
+
+    pub fn root<C: BorrowWindowContext>(&self, cx: &C) -> C::Result<ViewHandle<V>> {
+        self.read_with(cx, |cx| cx.root_view().clone().downcast().unwrap())
+    }
+
+    pub fn read_with<C, F, R>(&self, cx: &C, read: F) -> C::Result<R>
+    where
+        C: BorrowWindowContext,
+        F: FnOnce(&WindowContext) -> R,
+    {
+        cx.read_window_with(self.window_id(), |cx| read(cx))
+    }
+
+    pub fn update<C, F, R>(&self, cx: &mut C, update: F) -> C::Result<R>
+    where
+        C: BorrowWindowContext,
+        F: FnOnce(&mut WindowContext) -> R,
+    {
+        cx.update_window(self.window_id(), update)
+    }
+
+    // pub fn update_root<C, F, R>(&self, cx: &mut C, update: F) -> C::Result<Option<R>>
+    // where
+    //     C: BorrowWindowContext,
+    //     F: FnOnce(&mut V, &mut ViewContext<V>) -> R,
+    // {
+    //     cx.update_window(self.window_id, |cx| {
+    //         cx.root_view()
+    //             .clone()
+    //             .downcast::<V>()
+    //             .map(|v| v.update(cx, update))
+    //     })
+    // }
+
+    pub fn read_root<'a>(&self, cx: &'a AppContext) -> &'a V {
+        let root_view = cx
+            .read_window(self.window_id(), |cx| {
+                cx.root_view().clone().downcast().unwrap()
+            })
+            .unwrap();
+        root_view.read(cx)
+    }
+
+    pub fn read_root_with<C, F, R>(&self, cx: &C, read: F) -> C::Result<R>
+    where
+        C: BorrowWindowContext,
+        F: FnOnce(&V, &ViewContext<V>) -> R,
+    {
+        self.read_with(cx, |cx| {
+            cx.root_view()
+                .downcast_ref::<V>()
+                .unwrap()
+                .read_with(cx, read)
+        })
+    }
+
+    pub fn add_view<C, U, F>(&self, cx: &mut C, build_view: F) -> C::Result<ViewHandle<U>>
+    where
+        C: BorrowWindowContext,
+        U: View,
+        F: FnOnce(&mut ViewContext<U>) -> U,
+    {
+        self.update(cx, |cx| cx.add_view(build_view))
+    }
+}
+
 #[repr(transparent)]
 pub struct ViewHandle<T> {
     any_handle: AnyViewHandle,
@@ -3849,25 +3992,25 @@ impl<T: View> ViewHandle<T> {
         cx.read_view(self)
     }
 
-    pub fn read_with<C, F, S>(&self, cx: &C, read: F) -> S
+    pub fn read_with<C, F, S>(&self, cx: &C, read: F) -> C::Result<S>
     where
         C: BorrowWindowContext,
         F: FnOnce(&T, &ViewContext<T>) -> S,
     {
-        cx.read_with(self.window_id, |cx| {
+        cx.read_window_with(self.window_id, |cx| {
             let cx = ViewContext::immutable(cx, self.view_id);
             read(cx.read_view(self), &cx)
         })
     }
 
-    pub fn update<C, F, S>(&self, cx: &mut C, update: F) -> S
+    pub fn update<C, F, S>(&self, cx: &mut C, update: F) -> C::Result<S>
     where
         C: BorrowWindowContext,
         F: FnOnce(&mut T, &mut ViewContext<T>) -> S,
     {
         let mut update = Some(update);
 
-        cx.update(self.window_id, |cx| {
+        cx.update_window(self.window_id, |cx| {
             cx.update_view(self, &mut |view, cx| {
                 let update = update.take().unwrap();
                 update(view, cx)
@@ -4684,11 +4827,11 @@ mod tests {
             }
         }
 
-        let (_, view) = cx.add_window(|_| View { render_count: 0 });
+        let window = cx.add_window(|_| View { render_count: 0 });
         let called_defer = Rc::new(AtomicBool::new(false));
         let called_after_window_update = Rc::new(AtomicBool::new(false));
 
-        view.update(cx, |this, cx| {
+        window.root(cx).update(cx, |this, cx| {
             assert_eq!(this.render_count, 1);
             cx.defer({
                 let called_defer = called_defer.clone();
@@ -4712,7 +4855,7 @@ mod tests {
 
         assert!(called_defer.load(SeqCst));
         assert!(called_after_window_update.load(SeqCst));
-        assert_eq!(view.read_with(cx, |view, _| view.render_count), 3);
+        assert_eq!(window.read_root_with(cx, |view, _| view.render_count), 3);
     }
 
     #[crate::test(self)]
@@ -4751,9 +4894,9 @@ mod tests {
             }
         }
 
-        let (window_id, _root_view) = cx.add_window(|cx| View::new(None, cx));
-        let handle_1 = cx.add_view(window_id, |cx| View::new(None, cx));
-        let handle_2 = cx.add_view(window_id, |cx| View::new(Some(handle_1.clone()), cx));
+        let window = cx.add_window(|cx| View::new(None, cx));
+        let handle_1 = window.add_view(cx, |cx| View::new(None, cx));
+        let handle_2 = window.add_view(cx, |cx| View::new(Some(handle_1.clone()), cx));
         assert_eq!(cx.read(|cx| cx.views.len()), 3);
 
         handle_1.update(cx, |view, cx| {
@@ -4813,11 +4956,11 @@ mod tests {
         }
 
         let mouse_down_count = Arc::new(AtomicUsize::new(0));
-        let (window_id, _) = cx.add_window(Default::default(), |_| View {
+        let window = cx.add_window(Default::default(), |_| View {
             mouse_down_count: mouse_down_count.clone(),
         });
 
-        cx.update_window(window_id, |cx| {
+        window.update(cx, |cx| {
             // Ensure window's root element is in a valid lifecycle state.
             cx.dispatch_event(
                 Event::MouseDown(MouseButtonEvent {
@@ -4833,7 +4976,7 @@ mod tests {
     }
 
     #[crate::test(self)]
-    fn test_entity_release_hooks(cx: &mut AppContext) {
+    fn test_entity_release_hooks(cx: &mut TestAppContext) {
         struct Model {
             released: Rc<Cell<bool>>,
         }
@@ -4876,22 +5019,26 @@ mod tests {
         let model = cx.add_model(|_| Model {
             released: model_released.clone(),
         });
-        let (window_id, view) = cx.add_window(Default::default(), |_| View {
+        let window = cx.add_window(|_| View {
             released: view_released.clone(),
         });
+        let view = window.root(cx);
+
         assert!(!model_released.get());
         assert!(!view_released.get());
 
-        cx.observe_release(&model, {
-            let model_release_observed = model_release_observed.clone();
-            move |_, _| model_release_observed.set(true)
-        })
-        .detach();
-        cx.observe_release(&view, {
-            let view_release_observed = view_release_observed.clone();
-            move |_, _| view_release_observed.set(true)
-        })
-        .detach();
+        cx.update(|cx| {
+            cx.observe_release(&model, {
+                let model_release_observed = model_release_observed.clone();
+                move |_, _| model_release_observed.set(true)
+            })
+            .detach();
+            cx.observe_release(&view, {
+                let view_release_observed = view_release_observed.clone();
+                move |_, _| view_release_observed.set(true)
+            })
+            .detach();
+        });
 
         cx.update(move |_| {
             drop(model);
@@ -4900,7 +5047,7 @@ mod tests {
         assert!(model_release_observed.get());
 
         drop(view);
-        cx.update_window(window_id, |cx| cx.remove_window());
+        window.update(cx, |cx| cx.remove_window());
         assert!(view_released.get());
         assert!(view_release_observed.get());
     }
@@ -4913,8 +5060,9 @@ mod tests {
             type Event = String;
         }
 
-        let (window_id, handle_1) = cx.add_window(|_| TestView::default());
-        let handle_2 = cx.add_view(window_id, |_| TestView::default());
+        let window = cx.add_window(|_| TestView::default());
+        let handle_1 = window.root(cx);
+        let handle_2 = window.add_view(cx, |_| TestView::default());
         let handle_3 = cx.add_model(|_| Model);
 
         handle_1.update(cx, |_, cx| {
@@ -5140,9 +5288,9 @@ mod tests {
             type Event = ();
         }
 
-        let (window_id, _root_view) = cx.add_window(|_| TestView::default());
-        let observing_view = cx.add_view(window_id, |_| TestView::default());
-        let emitting_view = cx.add_view(window_id, |_| TestView::default());
+        let window = cx.add_window(|_| TestView::default());
+        let observing_view = window.add_view(cx, |_| TestView::default());
+        let emitting_view = window.add_view(cx, |_| TestView::default());
         let observing_model = cx.add_model(|_| Model);
         let observed_model = cx.add_model(|_| Model);
 
@@ -5165,7 +5313,7 @@ mod tests {
 
     #[crate::test(self)]
     fn test_view_emit_before_subscribe_in_same_update_cycle(cx: &mut AppContext) {
-        let (_, view) = cx.add_window::<TestView, _>(Default::default(), |cx| {
+        let window = cx.add_window::<TestView, _>(Default::default(), |cx| {
             drop(cx.subscribe(&cx.handle(), {
                 move |this, _, _, _| this.events.push("dropped before flush".into())
             }));
@@ -5181,7 +5329,7 @@ mod tests {
             TestView { events: Vec::new() }
         });
 
-        assert_eq!(view.read(cx).events, ["before emit"]);
+        assert_eq!(window.read_root(cx).events, ["before emit"]);
     }
 
     #[crate::test(self)]
@@ -5195,7 +5343,8 @@ mod tests {
             type Event = ();
         }
 
-        let (_, view) = cx.add_window(|_| TestView::default());
+        let window = cx.add_window(|_| TestView::default());
+        let view = window.root(cx);
         let model = cx.add_model(|_| Model {
             state: "old-state".into(),
         });
@@ -5216,7 +5365,7 @@ mod tests {
 
     #[crate::test(self)]
     fn test_view_notify_before_observe_in_same_update_cycle(cx: &mut AppContext) {
-        let (_, view) = cx.add_window::<TestView, _>(Default::default(), |cx| {
+        let window = cx.add_window::<TestView, _>(Default::default(), |cx| {
             drop(cx.observe(&cx.handle(), {
                 move |this, _, _| this.events.push("dropped before flush".into())
             }));
@@ -5232,7 +5381,7 @@ mod tests {
             TestView { events: Vec::new() }
         });
 
-        assert_eq!(view.read(cx).events, ["before notify"]);
+        assert_eq!(window.read_root(cx).events, ["before notify"]);
     }
 
     #[crate::test(self)]
@@ -5243,7 +5392,8 @@ mod tests {
         }
 
         let model = cx.add_model(|_| Model);
-        let (_, view) = cx.add_window(|_| TestView::default());
+        let window = cx.add_window(|_| TestView::default());
+        let view = window.root(cx);
 
         view.update(cx, |_, cx| {
             model.update(cx, |_, cx| cx.notify());
@@ -5267,8 +5417,8 @@ mod tests {
             type Event = ();
         }
 
-        let (window_id, _root_view) = cx.add_window(|_| TestView::default());
-        let observing_view = cx.add_view(window_id, |_| TestView::default());
+        let window = cx.add_window(|_| TestView::default());
+        let observing_view = window.add_view(cx, |_| TestView::default());
         let observing_model = cx.add_model(|_| Model);
         let observed_model = cx.add_model(|_| Model);
 
@@ -5390,9 +5540,9 @@ mod tests {
             }
         }
 
-        let (window_id, _root_view) = cx.add_window(|_| View);
-        let observing_view = cx.add_view(window_id, |_| View);
-        let observed_view = cx.add_view(window_id, |_| View);
+        let window = cx.add_window(|_| View);
+        let observing_view = window.add_view(cx, |_| View);
+        let observed_view = window.add_view(cx, |_| View);
 
         let observation_count = Rc::new(RefCell::new(0));
         observing_view.update(cx, |_, cx| {
@@ -5474,25 +5624,24 @@ mod tests {
         }
 
         let view_events: Arc<Mutex<Vec<String>>> = Default::default();
-        let (window_id, view_1) = cx.add_window(|_| View {
+        let window = cx.add_window(|_| View {
             events: view_events.clone(),
             name: "view 1".to_string(),
             child: None,
         });
-        let view_2 = cx
-            .update_window(window_id, |cx| {
-                let view_2 = cx.add_view(|_| View {
-                    events: view_events.clone(),
-                    name: "view 2".to_string(),
-                    child: None,
-                });
-                view_1.update(cx, |view_1, cx| {
-                    view_1.child = Some(view_2.clone().into_any());
-                    cx.notify();
-                });
-                view_2
-            })
-            .unwrap();
+        let view_1 = window.root(cx);
+        let view_2 = window.update(cx, |cx| {
+            let view_2 = cx.add_view(|_| View {
+                events: view_events.clone(),
+                name: "view 2".to_string(),
+                child: None,
+            });
+            view_1.update(cx, |view_1, cx| {
+                view_1.child = Some(view_2.clone().into_any());
+                cx.notify();
+            });
+            view_2
+        });
 
         let observed_events: Arc<Mutex<Vec<String>>> = Default::default();
         view_1.update(cx, |_, cx| {
@@ -5619,7 +5768,7 @@ mod tests {
     }
 
     #[crate::test(self)]
-    fn test_dispatch_action(cx: &mut AppContext) {
+    fn test_dispatch_action(cx: &mut TestAppContext) {
         struct ViewA {
             id: usize,
             child: Option<AnyViewHandle>,
@@ -5670,101 +5819,97 @@ mod tests {
         impl_actions!(test, [Action]);
 
         let actions = Rc::new(RefCell::new(Vec::new()));
+        let observed_actions = Rc::new(RefCell::new(Vec::new()));
 
-        cx.add_global_action({
-            let actions = actions.clone();
-            move |_: &Action, _: &mut AppContext| {
-                actions.borrow_mut().push("global".to_string());
-            }
-        });
-
-        cx.add_action({
-            let actions = actions.clone();
-            move |view: &mut ViewA, action: &Action, cx| {
-                assert_eq!(action.0, "bar");
-                cx.propagate_action();
-                actions.borrow_mut().push(format!("{} a", view.id));
-            }
-        });
+        cx.update(|cx| {
+            cx.add_global_action({
+                let actions = actions.clone();
+                move |_: &Action, _: &mut AppContext| {
+                    actions.borrow_mut().push("global".to_string());
+                }
+            });
 
-        cx.add_action({
-            let actions = actions.clone();
-            move |view: &mut ViewA, _: &Action, cx| {
-                if view.id != 1 {
-                    cx.add_view(|cx| {
-                        cx.propagate_action(); // Still works on a nested ViewContext
-                        ViewB { id: 5, child: None }
-                    });
+            cx.add_action({
+                let actions = actions.clone();
+                move |view: &mut ViewA, action: &Action, cx| {
+                    assert_eq!(action.0, "bar");
+                    cx.propagate_action();
+                    actions.borrow_mut().push(format!("{} a", view.id));
                 }
-                actions.borrow_mut().push(format!("{} b", view.id));
-            }
-        });
+            });
 
-        cx.add_action({
-            let actions = actions.clone();
-            move |view: &mut ViewB, _: &Action, cx| {
-                cx.propagate_action();
-                actions.borrow_mut().push(format!("{} c", view.id));
-            }
-        });
+            cx.add_action({
+                let actions = actions.clone();
+                move |view: &mut ViewA, _: &Action, cx| {
+                    if view.id != 1 {
+                        cx.add_view(|cx| {
+                            cx.propagate_action(); // Still works on a nested ViewContext
+                            ViewB { id: 5, child: None }
+                        });
+                    }
+                    actions.borrow_mut().push(format!("{} b", view.id));
+                }
+            });
 
-        cx.add_action({
-            let actions = actions.clone();
-            move |view: &mut ViewB, _: &Action, cx| {
-                cx.propagate_action();
-                actions.borrow_mut().push(format!("{} d", view.id));
-            }
-        });
+            cx.add_action({
+                let actions = actions.clone();
+                move |view: &mut ViewB, _: &Action, cx| {
+                    cx.propagate_action();
+                    actions.borrow_mut().push(format!("{} c", view.id));
+                }
+            });
 
-        cx.capture_action({
-            let actions = actions.clone();
-            move |view: &mut ViewA, _: &Action, cx| {
-                cx.propagate_action();
-                actions.borrow_mut().push(format!("{} capture", view.id));
-            }
-        });
+            cx.add_action({
+                let actions = actions.clone();
+                move |view: &mut ViewB, _: &Action, cx| {
+                    cx.propagate_action();
+                    actions.borrow_mut().push(format!("{} d", view.id));
+                }
+            });
 
-        let observed_actions = Rc::new(RefCell::new(Vec::new()));
-        cx.observe_actions({
-            let observed_actions = observed_actions.clone();
-            move |action_id, _| observed_actions.borrow_mut().push(action_id)
-        })
-        .detach();
+            cx.capture_action({
+                let actions = actions.clone();
+                move |view: &mut ViewA, _: &Action, cx| {
+                    cx.propagate_action();
+                    actions.borrow_mut().push(format!("{} capture", view.id));
+                }
+            });
 
-        let (window_id, view_1) =
-            cx.add_window(Default::default(), |_| ViewA { id: 1, child: None });
-        let view_2 = cx
-            .update_window(window_id, |cx| {
-                let child = cx.add_view(|_| ViewB { id: 2, child: None });
-                view_1.update(cx, |view, cx| {
-                    view.child = Some(child.clone().into_any());
-                    cx.notify();
-                });
-                child
-            })
-            .unwrap();
-        let view_3 = cx
-            .update_window(window_id, |cx| {
-                let child = cx.add_view(|_| ViewA { id: 3, child: None });
-                view_2.update(cx, |view, cx| {
-                    view.child = Some(child.clone().into_any());
-                    cx.notify();
-                });
-                child
+            cx.observe_actions({
+                let observed_actions = observed_actions.clone();
+                move |action_id, _| observed_actions.borrow_mut().push(action_id)
             })
-            .unwrap();
-        let view_4 = cx
-            .update_window(window_id, |cx| {
-                let child = cx.add_view(|_| ViewB { id: 4, child: None });
-                view_3.update(cx, |view, cx| {
-                    view.child = Some(child.clone().into_any());
-                    cx.notify();
-                });
-                child
-            })
-            .unwrap();
+            .detach();
+        });
 
-        cx.update_window(window_id, |cx| {
+        let window = cx.add_window(|_| ViewA { id: 1, child: None });
+        let view_1 = window.root(cx);
+        let view_2 = window.update(cx, |cx| {
+            let child = cx.add_view(|_| ViewB { id: 2, child: None });
+            view_1.update(cx, |view, cx| {
+                view.child = Some(child.clone().into_any());
+                cx.notify();
+            });
+            child
+        });
+        let view_3 = window.update(cx, |cx| {
+            let child = cx.add_view(|_| ViewA { id: 3, child: None });
+            view_2.update(cx, |view, cx| {
+                view.child = Some(child.clone().into_any());
+                cx.notify();
+            });
+            child
+        });
+        let view_4 = window.update(cx, |cx| {
+            let child = cx.add_view(|_| ViewB { id: 4, child: None });
+            view_3.update(cx, |view, cx| {
+                view.child = Some(child.clone().into_any());
+                cx.notify();
+            });
+            child
+        });
+
+        window.update(cx, |cx| {
             cx.dispatch_action(Some(view_4.id()), &Action("bar".to_string()))
         });
 
@@ -5786,31 +5931,27 @@ mod tests {
 
         // Remove view_1, which doesn't propagate the action
 
-        let (window_id, view_2) =
-            cx.add_window(Default::default(), |_| ViewB { id: 2, child: None });
-        let view_3 = cx
-            .update_window(window_id, |cx| {
-                let child = cx.add_view(|_| ViewA { id: 3, child: None });
-                view_2.update(cx, |view, cx| {
-                    view.child = Some(child.clone().into_any());
-                    cx.notify();
-                });
-                child
-            })
-            .unwrap();
-        let view_4 = cx
-            .update_window(window_id, |cx| {
-                let child = cx.add_view(|_| ViewB { id: 4, child: None });
-                view_3.update(cx, |view, cx| {
-                    view.child = Some(child.clone().into_any());
-                    cx.notify();
-                });
-                child
-            })
-            .unwrap();
+        let window = cx.add_window(|_| ViewB { id: 2, child: None });
+        let view_2 = window.root(cx);
+        let view_3 = window.update(cx, |cx| {
+            let child = cx.add_view(|_| ViewA { id: 3, child: None });
+            view_2.update(cx, |view, cx| {
+                view.child = Some(child.clone().into_any());
+                cx.notify();
+            });
+            child
+        });
+        let view_4 = window.update(cx, |cx| {
+            let child = cx.add_view(|_| ViewB { id: 4, child: None });
+            view_3.update(cx, |view, cx| {
+                view.child = Some(child.clone().into_any());
+                cx.notify();
+            });
+            child
+        });
 
         actions.borrow_mut().clear();
-        cx.update_window(window_id, |cx| {
+        window.update(cx, |cx| {
             cx.dispatch_action(Some(view_4.id()), &Action("bar".to_string()))
         });
 
@@ -5887,7 +6028,7 @@ mod tests {
         view_3.keymap_context.add_identifier("b");
         view_3.keymap_context.add_identifier("c");
 
-        let (window_id, _view_1) = cx.add_window(Default::default(), |cx| {
+        let window = cx.add_window(Default::default(), |cx| {
             let view_2 = cx.add_view(|cx| {
                 let view_3 = cx.add_view(|cx| {
                     cx.focus_self();
@@ -5947,26 +6088,26 @@ mod tests {
             }
         });
 
-        cx.update_window(window_id, |cx| {
+        window.update(cx, |cx| {
             cx.dispatch_keystroke(&Keystroke::parse("a").unwrap())
         });
         assert_eq!(&*actions.borrow(), &["2 a"]);
         actions.borrow_mut().clear();
 
-        cx.update_window(window_id, |cx| {
+        window.update(cx, |cx| {
             cx.dispatch_keystroke(&Keystroke::parse("b").unwrap());
         });
 
         assert_eq!(&*actions.borrow(), &["3 b", "2 b", "1 b", "global b"]);
         actions.borrow_mut().clear();
 
-        cx.update_window(window_id, |cx| {
+        window.update(cx, |cx| {
             cx.dispatch_keystroke(&Keystroke::parse("c").unwrap());
         });
         assert_eq!(&*actions.borrow(), &["3 c"]);
         actions.borrow_mut().clear();
 
-        cx.update_window(window_id, |cx| {
+        window.update(cx, |cx| {
             cx.dispatch_keystroke(&Keystroke::parse("d").unwrap());
         });
         assert_eq!(&*actions.borrow(), &["2 d"]);
@@ -6006,13 +6147,14 @@ mod tests {
             }
         }
 
-        let (window_id, view_1) = cx.add_window(|cx| {
+        let window = cx.add_window(|cx| {
             let view_2 = cx.add_view(|cx| {
                 cx.focus_self();
                 View2 {}
             });
             View1 { child: view_2 }
         });
+        let view_1 = window.root(cx);
         let view_2 = view_1.read_with(cx, |view, _| view.child.clone());
 
         cx.update(|cx| {
@@ -6076,7 +6218,7 @@ mod tests {
 
         // Check that global actions do not have a binding, even if a binding does exist in another view
         assert_eq!(
-            &available_actions(window_id, view_1.id(), cx),
+            &available_actions(window.window_id(), view_1.id(), cx),
             &[
                 ("test::Action1", vec![Keystroke::parse("a").unwrap()]),
                 ("test::GlobalAction", vec![])
@@ -6085,7 +6227,7 @@ mod tests {
 
         // Check that view 1 actions and bindings are available even when called from view 2
         assert_eq!(
-            &available_actions(window_id, view_2.id(), cx),
+            &available_actions(window.window_id(), view_2.id(), cx),
             &[
                 ("test::Action1", vec![Keystroke::parse("a").unwrap()]),
                 ("test::Action2", vec![Keystroke::parse("b").unwrap()]),
@@ -6138,7 +6280,8 @@ mod tests {
 
         impl_actions!(test, [ActionWithArg]);
 
-        let (window_id, view) = cx.add_window(|_| View);
+        let window = cx.add_window(|_| View);
+        let view = window.root(cx);
         cx.update(|cx| {
             cx.add_global_action(|_: &ActionWithArg, _| {});
             cx.add_bindings(vec![
@@ -6147,7 +6290,7 @@ mod tests {
             ]);
         });
 
-        let actions = cx.available_actions(window_id, view.id());
+        let actions = cx.available_actions(window.window_id(), view.id());
         assert_eq!(
             actions[0].1.as_any().downcast_ref::<ActionWithArg>(),
             Some(&ActionWithArg { arg: false })
@@ -6250,7 +6393,8 @@ mod tests {
             }
         }
 
-        let (_, view) = cx.add_window(|_| Counter(0));
+        let window = cx.add_window(|_| Counter(0));
+        let view = window.root(cx);
 
         let condition1 = view.condition(cx, |view, _| view.0 == 2);
         let condition2 = view.condition(cx, |view, _| view.0 == 3);
@@ -6272,15 +6416,15 @@ mod tests {
     #[crate::test(self)]
     #[should_panic]
     async fn test_view_condition_timeout(cx: &mut TestAppContext) {
-        let (_, view) = cx.add_window(|_| TestView::default());
-        view.condition(cx, |_, _| false).await;
+        let window = cx.add_window(|_| TestView::default());
+        window.root(cx).condition(cx, |_, _| false).await;
     }
 
     #[crate::test(self)]
     #[should_panic(expected = "view dropped with pending condition")]
     async fn test_view_condition_panic_on_drop(cx: &mut TestAppContext) {
-        let (window_id, _root_view) = cx.add_window(|_| TestView::default());
-        let view = cx.add_view(window_id, |_| TestView::default());
+        let window = cx.add_window(|_| TestView::default());
+        let view = window.add_view(cx, |_| TestView::default());
 
         let condition = view.condition(cx, |_, _| false);
         cx.update(|_| drop(view));
@@ -6288,7 +6432,7 @@ mod tests {
     }
 
     #[crate::test(self)]
-    fn test_refresh_windows(cx: &mut AppContext) {
+    fn test_refresh_windows(cx: &mut TestAppContext) {
         struct View(usize);
 
         impl super::Entity for View {
@@ -6305,22 +6449,21 @@ mod tests {
             }
         }
 
-        let (window_id, root_view) = cx.add_window(Default::default(), |_| View(0));
-        cx.update_window(window_id, |cx| {
+        let window = cx.add_window(|_| View(0));
+        let root_view = window.root(cx);
+        window.update(cx, |cx| {
             assert_eq!(
                 cx.window.rendered_views[&root_view.id()].name(),
                 Some("render count: 0")
             );
         });
 
-        let view = cx
-            .update_window(window_id, |cx| {
-                cx.refresh_windows();
-                cx.add_view(|_| View(0))
-            })
-            .unwrap();
+        let view = window.update(cx, |cx| {
+            cx.refresh_windows();
+            cx.add_view(|_| View(0))
+        });
 
-        cx.update_window(window_id, |cx| {
+        window.update(cx, |cx| {
             assert_eq!(
                 cx.window.rendered_views[&root_view.id()].name(),
                 Some("render count: 1")

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

@@ -6,7 +6,7 @@ use crate::{
     platform::{Event, InputHandler, KeyDownEvent, Platform},
     Action, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache, Handle,
     ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakHandle,
-    WindowContext,
+    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,
@@ -148,17 +148,18 @@ 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<T, F>(&mut self, build_root_view: F) -> WindowHandle<T>
     where
         T: View,
         F: FnOnce(&mut ViewContext<T>) -> T,
     {
-        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)
+        self.simulate_window_activation(Some(window.window_id()));
+
+        WindowHandle::new(window.window_id())
     }
 
     pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
@@ -405,14 +406,20 @@ 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_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
         self.cx
             .borrow()
             .read_window(window_id, f)
             .expect("window was closed")
     }
 
-    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
+    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
+        &mut self,
+        window_id: usize,
+        f: F,
+    ) -> T {
         self.cx
             .borrow_mut()
             .update_window(window_id, f)

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

@@ -15,7 +15,7 @@ use crate::{
     util::post_inc,
     Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
     Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription,
-    View, ViewContext, ViewHandle, WindowInvalidation,
+    View, ViewContext, ViewHandle, WindowHandle, WindowInvalidation,
 };
 use anyhow::{anyhow, bail, Result};
 use collections::{HashMap, HashSet};
@@ -142,7 +142,9 @@ impl BorrowAppContext for WindowContext<'_> {
 }
 
 impl BorrowWindowContext for WindowContext<'_> {
-    fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
+    type Result<T> = T;
+
+    fn read_window_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
         if self.window_id == window_id {
             f(self)
         } else {
@@ -150,7 +152,11 @@ impl BorrowWindowContext for WindowContext<'_> {
         }
     }
 
-    fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
+    fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
+        &mut self,
+        window_id: usize,
+        f: F,
+    ) -> T {
         if self.window_id == window_id {
             f(self)
         } else {
@@ -1151,15 +1157,15 @@ 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>
+    pub fn replace_root_view<V, F>(&mut self, build_root_view: F) -> WindowHandle<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
+        self.window.root_view = Some(root_view.into_any());
+        WindowHandle::new(self.window_id)
     }
 
     pub fn add_view<T, F>(&mut self, build_view: F) -> ViewHandle<T>
@@ -1176,7 +1182,7 @@ impl<'a> WindowContext<'a> {
         F: FnOnce(&mut ViewContext<T>) -> Option<T>,
     {
         let window_id = self.window_id;
-        let view_id = post_inc(&mut self.next_entity_id);
+        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();

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/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 🔗

@@ -1756,7 +1756,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),
@@ -1844,7 +1846,9 @@ 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 window_id = window.window_id();
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         select_path(&panel, "root1", cx);
@@ -2195,7 +2199,9 @@ 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 window_id = window.window_id();
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         select_path(&panel, "root1", cx);
@@ -2295,7 +2301,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| {
@@ -2368,7 +2376,9 @@ 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 window_id = window.window_id();
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         toggle_expand_dir(&panel, "src/test", cx);
@@ -2457,7 +2467,9 @@ 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 window_id = window.window_id();
         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
 
         select_path(&panel, "src/", cx);
@@ -2603,7 +2615,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));
@@ -2690,7 +2704,9 @@ mod tests {
         .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));
+        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| {

crates/project_symbols/src/project_symbols.rs 🔗

@@ -326,7 +326,9 @@ 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);
+        let window_id = window.window_id();
 
         // Create the project symbols view.
         let symbols = cx.add_view(window_id, |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 🔗

@@ -849,11 +849,13 @@ mod tests {
                 cx,
             )
         });
-        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
+        let window = cx.add_window(|_| EmptyView);
 
-        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
+        let editor = cx.add_view(window.window_id(), |cx| {
+            Editor::for_buffer(buffer.clone(), None, cx)
+        });
 
-        let search_bar = cx.add_view(window_id, |cx| {
+        let search_bar = cx.add_view(window.window_id(), |cx| {
             let mut search_bar = BufferSearchBar::new(cx);
             search_bar.set_active_pane_item(Some(&editor), cx);
             search_bar.show(cx);
@@ -1229,7 +1231,8 @@ 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 window = cx.add_window(|_| EmptyView);
+        let window_id = window.window_id();
 
         let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
 
@@ -1416,11 +1419,13 @@ mod tests {
         "#
         .unindent();
         let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
-        let (window_id, _root_view) = cx.add_window(|_| EmptyView);
+        let window = cx.add_window(|_| EmptyView);
 
-        let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
+        let editor = cx.add_view(window.window_id(), |cx| {
+            Editor::for_buffer(buffer.clone(), None, cx)
+        });
 
-        let search_bar = cx.add_view(window_id, |cx| {
+        let search_bar = cx.add_view(window.window_id(), |cx| {
             let mut search_bar = BufferSearchBar::new(cx);
             search_bar.set_active_pane_item(Some(&editor), cx);
             search_bar.show(cx);

crates/search/src/project_search.rs 🔗

@@ -1447,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
@@ -1564,7 +1566,9 @@ 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 window_id = window.window_id();
 
         let active_item = cx.read(|cx| {
             workspace
@@ -1748,7 +1752,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
@@ -1866,7 +1872,9 @@ 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 window_id = window.window_id();
         workspace.update(cx, |workspace, cx| {
             ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
         });

crates/terminal_view/src/terminal_view.rs 🔗

@@ -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/util/src/paths.rs 🔗

@@ -30,49 +30,47 @@ 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>;
+}
+
+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()
         }
-    } else {
-        path.to_path_buf()
+    }
+
+    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(".");
+        }
+
+        self.as_ref()
+            .extension()
+            .map(|extension| extension.to_str())
+            .flatten()
     }
 }
 
@@ -279,4 +277,42 @@ 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_path_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"));
+    }
 }

crates/workspace/src/dock.rs 🔗

@@ -29,12 +29,8 @@ pub trait Panel: View {
     fn is_zoomed(&self, _cx: &WindowContext) -> bool {
         false
     }
-    fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {
-
-    }
-    fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {
-
-    }
+    fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
+    fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
     fn should_activate_on_event(_: &Self::Event) -> bool {
         false
     }

crates/workspace/src/pane.rs 🔗

@@ -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/workspace.rs 🔗

@@ -797,67 +797,59 @@ 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,
-                            )
-                        })
+            let window = 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_bounds_override = window_bounds_env_override(&cx);
-                    let (bounds, display) = if let Some(bounds) = window_bounds_override {
-                        (Some(bounds), None)
-                    } else {
-                        serialized_workspace
-                            .as_ref()
-                            .and_then(|serialized_workspace| {
-                                let display = serialized_workspace.display?;
-                                let mut bounds = serialized_workspace.bounds?;
-
-                                // Stored bounds are relative to the containing display.
-                                // So convert back to global coordinates if that screen still exists
-                                if let WindowBounds::Fixed(mut window_bounds) = bounds {
-                                    if let Some(screen) = cx.platform().screen_by_id(display) {
-                                        let screen_bounds = screen.bounds();
-                                        window_bounds.set_origin_x(
-                                            window_bounds.origin_x() + screen_bounds.origin_x(),
-                                        );
-                                        window_bounds.set_origin_y(
-                                            window_bounds.origin_y() + screen_bounds.origin_y(),
-                                        );
-                                        bounds = WindowBounds::Fixed(window_bounds);
-                                    } else {
-                                        // Screen no longer exists. Return none here.
-                                        return None;
-                                    }
+            });
+            let window = window.unwrap_or_else(|| {
+                let window_bounds_override = window_bounds_env_override(&cx);
+                let (bounds, display) = if let Some(bounds) = window_bounds_override {
+                    (Some(bounds), None)
+                } else {
+                    serialized_workspace
+                        .as_ref()
+                        .and_then(|serialized_workspace| {
+                            let display = serialized_workspace.display?;
+                            let mut bounds = serialized_workspace.bounds?;
+
+                            // Stored bounds are relative to the containing display.
+                            // So convert back to global coordinates if that screen still exists
+                            if let WindowBounds::Fixed(mut window_bounds) = bounds {
+                                if let Some(screen) = cx.platform().screen_by_id(display) {
+                                    let screen_bounds = screen.bounds();
+                                    window_bounds.set_origin_x(
+                                        window_bounds.origin_x() + screen_bounds.origin_x(),
+                                    );
+                                    window_bounds.set_origin_y(
+                                        window_bounds.origin_y() + screen_bounds.origin_y(),
+                                    );
+                                    bounds = WindowBounds::Fixed(window_bounds);
+                                } else {
+                                    // Screen no longer exists. Return none here.
+                                    return None;
                                 }
+                            }
 
-                                Some((bounds, display))
-                            })
-                            .unzip()
-                    };
-
-                    // Use the serialized workspace to construct the new window
-                    cx.add_window(
-                        (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
-                        |cx| {
-                            Workspace::new(
-                                workspace_id,
-                                project_handle.clone(),
-                                app_state.clone(),
-                                cx,
-                            )
-                        },
-                    )
-                    .1
-                });
+                            Some((bounds, display))
+                        })
+                        .unzip()
+                };
+
+                // Use the serialized workspace to construct the new window
+                cx.add_window(
+                    (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
+                    |cx| {
+                        Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
+                    },
+                )
+            });
+
+            // 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(),
@@ -868,7 +860,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);
@@ -3987,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,
@@ -3995,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,
@@ -4123,10 +4116,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
@@ -4138,7 +4132,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
@@ -4152,7 +4146,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
@@ -4187,16 +4181,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)])
         });
 
@@ -4211,14 +4206,14 @@ mod tests {
             );
         });
         assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
+            cx.current_window_title(window.window_id()).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(),
+            cx.current_window_title(window.window_id()).as_deref(),
             Some("two.txt — root1")
         );
         project.read_with(cx, |project, cx| {
@@ -4237,7 +4232,7 @@ mod tests {
         .await
         .unwrap();
         assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
+            cx.current_window_title(window.window_id()).as_deref(),
             Some("one.txt — root1")
         );
         project.read_with(cx, |project, cx| {
@@ -4257,14 +4252,14 @@ mod tests {
             .await
             .unwrap();
         assert_eq!(
-            cx.current_window_title(window_id).as_deref(),
+            cx.current_window_title(window.window_id()).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(),
+            cx.current_window_title(window.window_id()).as_deref(),
             Some("one.txt — root2")
         );
     }
@@ -4277,18 +4272,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)])
@@ -4299,9 +4295,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 */);
+        cx.simulate_prompt_answer(window.window_id(), 2 /* cancel */);
         cx.foreground().run_until_parked();
-        assert!(!cx.has_pending_prompt(window_id));
+        assert!(!cx.has_pending_prompt(window.window_id()));
         assert!(!task.await.unwrap());
     }
 
@@ -4312,26 +4308,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)])
@@ -4359,10 +4356,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!(cx.has_pending_prompt(window.window_id()));
 
         // Confirm saving item 1.
-        cx.simulate_prompt_answer(window_id, 0);
+        cx.simulate_prompt_answer(window.window_id(), 0);
         cx.foreground().run_until_parked();
 
         // Item 1 is saved. There's a prompt to save item 3.
@@ -4373,10 +4370,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!(cx.has_pending_prompt(window.window_id()));
 
         // Cancel saving item 3.
-        cx.simulate_prompt_answer(window_id, 1);
+        cx.simulate_prompt_answer(window.window_id(), 1);
         cx.foreground().run_until_parked();
 
         // Item 3 is reloaded. There's a prompt to save item 4.
@@ -4387,10 +4384,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!(cx.has_pending_prompt(window.window_id()));
 
         // Confirm saving item 4.
-        cx.simulate_prompt_answer(window_id, 0);
+        cx.simulate_prompt_answer(window.window_id(), 0);
         cx.foreground().run_until_parked();
 
         // There's a prompt for a path for item 4.
@@ -4414,13 +4411,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(
@@ -4431,7 +4429,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)
@@ -4440,7 +4438,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)
@@ -4492,7 +4490,7 @@ mod tests {
                 &[ProjectEntryId::from_proto(0)]
             );
         });
-        cx.simulate_prompt_answer(window_id, 0);
+        cx.simulate_prompt_answer(window.window_id(), 0);
 
         cx.foreground().run_until_parked();
         left_pane.read_with(cx, |pane, cx| {
@@ -4501,7 +4499,7 @@ mod tests {
                 &[ProjectEntryId::from_proto(2)]
             );
         });
-        cx.simulate_prompt_answer(window_id, 0);
+        cx.simulate_prompt_answer(window.window_id(), 0);
 
         cx.foreground().run_until_parked();
         close.await.unwrap();
@@ -4517,10 +4515,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();
@@ -4560,7 +4559,7 @@ 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));
+        cx.simulate_window_activation(Some(window.window_id()));
         item.update(cx, |item, cx| {
             cx.focus_self();
             item.is_dirty = true;
@@ -4602,7 +4601,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!(!cx.has_pending_prompt(window.window_id()));
         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.
@@ -4623,7 +4622,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!(cx.has_pending_prompt(window.window_id()));
         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
     }
 
@@ -4634,9 +4633,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());
@@ -4687,7 +4687,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));
@@ -4834,7 +4835,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.
@@ -4989,7 +4991,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.99.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"]
+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/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/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/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/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/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/zed.rs 🔗

@@ -981,7 +981,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 entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1293,7 +1295,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));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        let window_id = window.window_id();
 
         // Open a file within an existing worktree.
         workspace
@@ -1334,7 +1338,9 @@ 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 window_id = window.window_id();
         let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 
         // Create a new untitled buffer
@@ -1427,7 +1433,9 @@ 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);
+        let window_id = window.window_id();
 
         // Create a new untitled buffer
         cx.dispatch_action(window_id, NewFile);
@@ -1478,7 +1486,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));
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        let window_id = window.window_id();
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1552,7 +1562,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));
@@ -1829,7 +1841,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));
@@ -2071,7 +2085,8 @@ mod tests {
 
         cx.foreground().run_until_parked();
 
-        let (window_id, _view) = cx.add_window(|_| TestView);
+        let window = cx.add_window(|_| TestView);
+        let window_id = window.window_id();
 
         // Test loading the keymap base at all
         assert_key_bindings_for(
@@ -2241,7 +2256,8 @@ mod tests {
 
         cx.foreground().run_until_parked();
 
-        let (window_id, _view) = cx.add_window(|_| TestView);
+        let window = cx.add_window(|_| TestView);
+        let window_id = window.window_id();
 
         // Test loading the keymap base at all
         assert_key_bindings_for(

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/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 🔗

@@ -55,6 +55,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({
@@ -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 🔗

@@ -187,10 +187,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 { 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,7 @@ export interface Theme {
     modal_shadow: Shadow
 
     players: Players
-    syntax: Syntax
+    syntax?: Partial<ThemeSyntax>
 }
 
 export interface Meta {
@@ -115,7 +115,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,11 +162,6 @@ export function create_theme(theme: ThemeConfig): Theme {
         "7": player(ramps.yellow),
     }
 
-    const syntax = syntaxStyle(
-        ramps,
-        theme.override.syntax ? theme.override.syntax : {}
-    )
-
     return {
         name,
         is_light,

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",
-]