Merge branch 'main' into search2

Piotr Osiewicz created

Change summary

Cargo.lock                                          |    67 
Cargo.toml                                          |     2 
assets/keymaps/jetbrains.json                       |     3 
assets/settings/default.json                        |    14 
crates/Cargo.toml                                   |    38 
crates/ai/Cargo.toml                                |     2 
crates/ai2/Cargo.toml                               |     2 
crates/assistant/Cargo.toml                         |     2 
crates/assistant/src/assistant_settings.rs          |     7 
crates/client2/src/test.rs                          |     2 
crates/collab2/src/db/tests/db_tests.rs             |     2 
crates/collab2/src/tests.rs                         |     1 
crates/collab2/src/tests/channel_buffer_tests.rs    |  1011 
crates/collab2/src/tests/editor_tests.rs            |   835 +
crates/collab2/src/tests/integration_tests.rs       |   755 
crates/collab2/src/tests/test_server.rs             |     4 
crates/command_palette2/src/command_palette.rs      |   102 
crates/copilot2/Cargo.toml                          |     2 
crates/copilot2/src/copilot2.rs                     |    45 
crates/editor2/Cargo.toml                           |     1 
crates/editor2/src/display_map.rs                   |     7 
crates/editor2/src/display_map/block_map.rs         |    16 
crates/editor2/src/editor.rs                        |   851 
crates/editor2/src/editor_tests.rs                  | 11467 +++++++-------
crates/editor2/src/element.rs                       |   995 
crates/editor2/src/inlay_hint_cache.rs              |    46 
crates/editor2/src/items.rs                         |    16 
crates/editor2/src/scroll/scroll_amount.rs          |    27 
crates/editor2/src/selections_collection.rs         |     3 
crates/editor2/src/test.rs                          |   127 
crates/editor2/src/test/editor_lsp_test_context.rs  |   595 
crates/editor2/src/test/editor_test_context.rs      |   693 
crates/file_finder2/Cargo.toml                      |    37 
crates/file_finder2/src/file_finder.rs              |  1973 ++
crates/go_to_line2/src/go_to_line.rs                |    16 
crates/gpui/src/app.rs                              |     4 
crates/gpui/src/dispatch.rs                         |     1 
crates/gpui2/docs/contexts.md                       |    41 
crates/gpui2/docs/key_dispatch.md                   |   101 
crates/gpui2/src/action.rs                          |   409 
crates/gpui2/src/app.rs                             |    75 
crates/gpui2/src/app/async_context.rs               |    24 
crates/gpui2/src/app/entity_map.rs                  |     2 
crates/gpui2/src/app/model_context.rs               |    13 
crates/gpui2/src/app/test_context.rs                |   282 
crates/gpui2/src/color.rs                           |    73 
crates/gpui2/src/element.rs                         |    14 
crates/gpui2/src/elements/div.rs                    |  1560 +
crates/gpui2/src/elements/img.rs                    |   176 
crates/gpui2/src/elements/mod.rs                    |     0 
crates/gpui2/src/elements/svg.rs                    |   131 
crates/gpui2/src/elements/text.rs                   |    39 
crates/gpui2/src/elements/uniform_list.rs           |   225 
crates/gpui2/src/focusable.rs                       |   252 
crates/gpui2/src/gpui2.rs                           |    29 
crates/gpui2/src/input.rs                           |     2 
crates/gpui2/src/interactive.rs                     |   988 -
crates/gpui2/src/key_dispatch.rs                    |   204 
crates/gpui2/src/keymap/binding.rs                  |    26 
crates/gpui2/src/keymap/context.rs                  |   449 
crates/gpui2/src/keymap/keymap.rs                   |     4 
crates/gpui2/src/keymap/matcher.rs                  |    11 
crates/gpui2/src/keymap/mod.rs                      |     2 
crates/gpui2/src/platform.rs                        |     6 
crates/gpui2/src/platform/mac/text_system.rs        |    15 
crates/gpui2/src/platform/test/platform.rs          |    86 
crates/gpui2/src/platform/test/window.rs            |    91 
crates/gpui2/src/prelude.rs                         |     4 
crates/gpui2/src/style.rs                           |    48 
crates/gpui2/src/styled.rs                          |   291 
crates/gpui2/src/text_system.rs                     |    27 
crates/gpui2/src/text_system/line_layout.rs         |     3 
crates/gpui2/src/util.rs                            |    32 
crates/gpui2/src/view.rs                            |    94 
crates/gpui2/src/window.rs                          |   785 
crates/gpui2_macros/src/action.rs                   |    20 
crates/gpui2_macros/src/style_helpers.rs            |     4 
crates/install_cli2/Cargo.toml                      |     1 
crates/install_cli2/src/install_cli2.rs             |     5 
crates/language2/src/language2.rs                   |     4 
crates/live_kit_client2/examples/test_app.rs        |     2 
crates/menu2/src/menu2.rs                           |     8 
crates/node_runtime/src/node_runtime.rs             |    29 
crates/picker2/src/picker2.rs                       |   139 
crates/prettier2/src/prettier2.rs                   |    10 
crates/project2/src/project2.rs                     |     2 
crates/project2/src/project_tests.rs                |    68 
crates/project_panel2/Cargo.toml                    |    41 
crates/project_panel2/src/file_associations.rs      |    96 
crates/project_panel2/src/project_panel.rs          |  2880 +++
crates/project_panel2/src/project_panel_settings.rs |    45 
crates/rpc2/src/peer.rs                             |    20 
crates/search2/src/buffer_search.rs                 |     9 
crates/search2/src/search_bar.rs                    |    13 
crates/semantic_index/Cargo.toml                    |     2 
crates/settings2/src/keymap_file.rs                 |     6 
crates/storybook2/src/stories/colors.rs             |     2 
crates/storybook2/src/stories/focus.rs              |    34 
crates/storybook2/src/stories/kitchen_sink.rs       |     4 
crates/storybook2/src/stories/picker.rs             |    12 
crates/storybook2/src/stories/scroll.rs             |     7 
crates/storybook2/src/stories/text.rs               |     2 
crates/storybook2/src/storybook2.rs                 |     2 
crates/theme2/src/default_colors.rs                 |   324 
crates/theme2/src/default_theme.rs                  |    84 
crates/theme2/src/one_themes.rs                     |   199 
crates/theme2/src/registry.rs                       |    21 
crates/theme2/src/settings.rs                       |    10 
crates/theme2/src/story.rs                          |     2 
crates/theme2/src/styles.rs                         |    11 
crates/theme2/src/styles/colors.rs                  |    29 
crates/theme2/src/styles/players.rs                 |   105 
crates/theme2/src/styles/status.rs                  |   134 
crates/theme2/src/styles/syntax.rs                  |   170 
crates/theme2/src/styles/system.rs                  |    20 
crates/theme2/src/syntax.rs                         |    41 
crates/theme2/src/theme2.rs                         |     9 
crates/theme2/util/hex_to_hsla.py                   |    35 
crates/ui2/src/components/button.rs                 |    63 
crates/ui2/src/components/checkbox.rs               |    16 
crates/ui2/src/components/elevated_surface.rs       |     2 
crates/ui2/src/components/icon.rs                   |    54 
crates/ui2/src/components/icon_button.rs            |    31 
crates/ui2/src/components/input.rs                  |    13 
crates/ui2/src/components/keybinding.rs             |   161 
crates/ui2/src/components/label.rs                  |   119 
crates/ui2/src/components/list.rs                   |    22 
crates/ui2/src/components/modal.rs                  |     2 
crates/ui2/src/components/palette.rs                |    69 
crates/ui2/src/components/panel.rs                  |     6 
crates/ui2/src/components/tab.rs                    |    18 
crates/ui2/src/components/toast.rs                  |     7 
crates/ui2/src/components/toggle.rs                 |     8 
crates/ui2/src/components/tooltip.rs                |    62 
crates/ui2/src/prelude.rs                           |     6 
crates/ui2/src/static_data.rs                       |    88 
crates/ui2/src/styled_ext.rs                        |    53 
crates/ui2/src/to_extract/assistant_panel.rs        |     2 
crates/ui2/src/to_extract/breadcrumb.rs             |     6 
crates/ui2/src/to_extract/buffer_search.rs          |     4 
crates/ui2/src/to_extract/chat_panel.rs             |     7 
crates/ui2/src/to_extract/collab_panel.rs           |     5 
crates/ui2/src/to_extract/copilot.rs                |     4 
crates/ui2/src/to_extract/editor_pane.rs            |     8 
crates/ui2/src/to_extract/notifications_panel.rs    |    19 
crates/ui2/src/to_extract/panes.rs                  |     2 
crates/ui2/src/to_extract/project_panel.rs          |     5 
crates/ui2/src/to_extract/status_bar.rs             |    22 
crates/ui2/src/to_extract/tab_bar.rs                |     4 
crates/ui2/src/to_extract/title_bar.rs              |    20 
crates/ui2/src/to_extract/workspace.rs              |     3 
crates/workspace2/src/dock.rs                       |    28 
crates/workspace2/src/modal_layer.rs                |    37 
crates/workspace2/src/pane.rs                       |    69 
crates/workspace2/src/pane/dragged_item_receiver.rs |     2 
crates/workspace2/src/status_bar.rs                 |     2 
crates/workspace2/src/toolbar.rs                    |     8 
crates/workspace2/src/workspace2.rs                 |  1036 
crates/zed2/Cargo.toml                              |     6 
crates/zed2/src/main.rs                             |    28 
crates/zed2/src/open_listener.rs                    |     4 
crates/zed2/src/zed2.rs                             |   595 
crates/zed_actions2/src/lib.rs                      |    34 
163 files changed, 20,065 insertions(+), 13,741 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2012,7 +2012,7 @@ dependencies = [
  "serde_derive",
  "settings2",
  "smol",
- "theme",
+ "theme2",
  "util",
 ]
 
@@ -2768,7 +2768,6 @@ dependencies = [
  "copilot2",
  "ctor",
  "db2",
- "drag_and_drop",
  "env_logger 0.9.3",
  "futures 0.3.28",
  "fuzzy2",
@@ -3062,6 +3061,31 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "file_finder2"
+version = "0.1.0"
+dependencies = [
+ "collections",
+ "ctor",
+ "editor2",
+ "env_logger 0.9.3",
+ "fuzzy2",
+ "gpui2",
+ "language2",
+ "menu2",
+ "picker2",
+ "postage",
+ "project2",
+ "serde",
+ "serde_json",
+ "settings2",
+ "text2",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "filetime"
 version = "0.2.22"
@@ -4199,6 +4223,7 @@ dependencies = [
  "anyhow",
  "gpui2",
  "log",
+ "serde",
  "smol",
  "util",
 ]
@@ -6609,6 +6634,36 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "project_panel2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client2",
+ "collections",
+ "context_menu",
+ "db2",
+ "editor2",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "menu2",
+ "postage",
+ "pretty_assertions",
+ "project2",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings2",
+ "smallvec",
+ "theme2",
+ "ui2",
+ "unicase",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "project_symbols"
 version = "0.1.0"
@@ -9286,9 +9341,9 @@ dependencies = [
 
 [[package]]
 name = "tiktoken-rs"
-version = "0.5.4"
+version = "0.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9ae5a3c24361e5f038af22517ba7f8e3af4099e30e78a3d56f86b48238fce9d"
+checksum = "a4427b6b1c6b38215b92dd47a83a0ecc6735573d0a5a4c14acc0ac5b33b28adb"
 dependencies = [
  "anyhow",
  "base64 0.21.4",
@@ -11423,6 +11478,7 @@ dependencies = [
  "editor2",
  "env_logger 0.9.3",
  "feature_flags2",
+ "file_finder2",
  "fs2",
  "fsevent",
  "futures 0.3.28",
@@ -11432,7 +11488,7 @@ dependencies = [
  "ignore",
  "image",
  "indexmap 1.9.3",
- "install_cli",
+ "install_cli2",
  "isahc",
  "journal2",
  "language2",
@@ -11447,6 +11503,7 @@ dependencies = [
  "parking_lot 0.11.2",
  "postage",
  "project2",
+ "project_panel2",
  "rand 0.8.5",
  "regex",
  "rope2",

Cargo.toml 🔗

@@ -80,6 +80,7 @@ members = [
     "crates/project",
     "crates/project2",
     "crates/project_panel",
+    "crates/project_panel2",
     "crates/project_symbols",
     "crates/recent_projects",
     "crates/rope",
@@ -155,6 +156,7 @@ tempdir = { version = "0.3.7" }
 thiserror = { version = "1.0.29" }
 time = { version = "0.3", features = ["serde", "serde-well-known"] }
 toml = { version = "0.5" }
+tiktoken-rs = "0.5.7"
 tree-sitter = "0.20"
 unindent = { version = "0.1.7" }
 pretty_assertions = "1.3.0"

assets/keymaps/jetbrains.json 🔗

@@ -10,6 +10,7 @@
     "bindings": {
       "ctrl->": "zed::IncreaseBufferFontSize",
       "ctrl-<": "zed::DecreaseBufferFontSize",
+      "ctrl-shift-j": "editor::JoinLines",
       "cmd-d": "editor::DuplicateLine",
       "cmd-backspace": "editor::DeleteLine",
       "cmd-pagedown": "editor::MovePageDown",
@@ -18,7 +19,7 @@
       "cmd-alt-enter": "editor::NewlineAbove",
       "shift-enter": "editor::NewlineBelow",
       "cmd--": "editor::Fold",
-      "cmd-=": "editor::UnfoldLines",
+      "cmd-+": "editor::UnfoldLines",
       "alt-shift-g": "editor::SplitSelectionIntoLines",
       "ctrl-g": [
         "editor::SelectNext",

assets/settings/default.json 🔗

@@ -174,7 +174,8 @@
     //
     // 1. "gpt-3.5-turbo-0613""
     // 2. "gpt-4-0613""
-    "default_open_ai_model": "gpt-4-0613"
+    // 3. "gpt-4-1106-preview"
+    "default_open_ai_model": "gpt-4-1106-preview"
   },
   // Whether the screen sharing icon is shown in the os status bar.
   "show_call_status_icon": true,
@@ -270,9 +271,7 @@
   "copilot": {
     // The set of glob patterns for which copilot should be disabled
     // in any matching file.
-    "disabled_globs": [
-      ".env"
-    ]
+    "disabled_globs": [".env"]
   },
   // Settings specific to journaling
   "journal": {
@@ -381,12 +380,7 @@
         // Default directories to search for virtual environments, relative
         // to the current working directory. We recommend overriding this
         // in your project's settings, rather than globally.
-        "directories": [
-          ".env",
-          "env",
-          ".venv",
-          "venv"
-        ],
+        "directories": [".env", "env", ".venv", "venv"],
         // Can also be 'csh', 'fish', and `nushell`
         "activate_script": "default"
       }

crates/Cargo.toml 🔗

@@ -1,38 +0,0 @@
-[package]
-name = "ai"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/ai.rs"
-doctest = false
-
-[features]
-test-support = []
-
-[dependencies]
-gpui = { path = "../gpui" }
-util = { path = "../util" }
-language = { path = "../language" }
-async-trait.workspace = true
-anyhow.workspace = true
-futures.workspace = true
-lazy_static.workspace = true
-ordered-float.workspace = true
-parking_lot.workspace = true
-isahc.workspace = true
-regex.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-postage.workspace = true
-rand.workspace = true
-log.workspace = true
-parse_duration = "2.1.1"
-tiktoken-rs = "0.5.0"
-matrixmultiply = "0.3.7"
-rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
-bincode = "1.3.3"
-
-[dev-dependencies]
-gpui = { path = "../gpui", features = ["test-support"] }

crates/ai/Cargo.toml 🔗

@@ -29,7 +29,7 @@ postage.workspace = true
 rand.workspace = true
 log.workspace = true
 parse_duration = "2.1.1"
-tiktoken-rs = "0.5.0"
+tiktoken-rs.workspace = true
 matrixmultiply = "0.3.7"
 rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
 bincode = "1.3.3"

crates/ai2/Cargo.toml 🔗

@@ -29,7 +29,7 @@ postage.workspace = true
 rand.workspace = true
 log.workspace = true
 parse_duration = "2.1.1"
-tiktoken-rs = "0.5.0"
+tiktoken-rs.workspace = true
 matrixmultiply = "0.3.7"
 rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
 bincode = "1.3.3"

crates/assistant/Cargo.toml 🔗

@@ -40,7 +40,7 @@ schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 smol.workspace = true
-tiktoken-rs = "0.5"
+tiktoken-rs.workspace = true
 
 [dev-dependencies]
 editor = { path = "../editor", features = ["test-support"] }

crates/assistant/src/assistant_settings.rs 🔗

@@ -9,6 +9,8 @@ pub enum OpenAIModel {
     ThreePointFiveTurbo,
     #[serde(rename = "gpt-4-0613")]
     Four,
+    #[serde(rename = "gpt-4-1106-preview")]
+    FourTurbo,
 }
 
 impl OpenAIModel {
@@ -16,6 +18,7 @@ impl OpenAIModel {
         match self {
             OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613",
             OpenAIModel::Four => "gpt-4-0613",
+            OpenAIModel::FourTurbo => "gpt-4-1106-preview",
         }
     }
 
@@ -23,13 +26,15 @@ impl OpenAIModel {
         match self {
             OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo",
             OpenAIModel::Four => "gpt-4",
+            OpenAIModel::FourTurbo => "gpt-4-turbo",
         }
     }
 
     pub fn cycle(&self) -> Self {
         match self {
             OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four,
-            OpenAIModel::Four => OpenAIModel::ThreePointFiveTurbo,
+            OpenAIModel::Four => OpenAIModel::FourTurbo,
+            OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo,
         }
     }
 }

crates/client2/src/test.rs 🔗

@@ -36,7 +36,7 @@ impl FakeServer {
             peer: Peer::new(0),
             state: Default::default(),
             user_id: client_user_id,
-            executor: cx.executor().clone(),
+            executor: cx.executor(),
         };
 
         client

crates/collab2/src/db/tests/db_tests.rs 🔗

@@ -510,7 +510,7 @@ fn test_fuzzy_like_string() {
 
 #[gpui::test]
 async fn test_fuzzy_search_users(cx: &mut TestAppContext) {
-    let test_db = TestDb::postgres(cx.executor().clone());
+    let test_db = TestDb::postgres(cx.executor());
     let db = test_db.db();
     for (i, github_login) in [
         "California",

crates/collab2/src/tests.rs 🔗

@@ -4,6 +4,7 @@ use gpui::{Model, TestAppContext};
 mod channel_buffer_tests;
 mod channel_message_tests;
 mod channel_tests;
+mod editor_tests;
 mod following_tests;
 mod integration_tests;
 mod notification_tests;

crates/collab2/src/tests/channel_buffer_tests.rs 🔗

@@ -1,288 +1,291 @@
-use crate::{
-    rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
-    tests::TestServer,
-};
-use client::{Collaborator, UserId};
-use collections::HashMap;
-use futures::future;
-use gpui::{BackgroundExecutor, Model, TestAppContext};
-use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
-
-#[gpui::test]
-async fn test_core_channel_buffers(
-    executor: BackgroundExecutor,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
-) {
-    let mut server = TestServer::start(executor.clone()).await;
-    let client_a = server.create_client(cx_a, "user_a").await;
-    let client_b = server.create_client(cx_b, "user_b").await;
-
-    let channel_id = server
-        .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
-        .await;
-
-    // Client A joins the channel buffer
-    let channel_buffer_a = client_a
-        .channel_store()
-        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-
-    // Client A edits the buffer
-    let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer());
-    buffer_a.update(cx_a, |buffer, cx| {
-        buffer.edit([(0..0, "hello world")], None, cx)
-    });
-    buffer_a.update(cx_a, |buffer, cx| {
-        buffer.edit([(5..5, ", cruel")], None, cx)
-    });
-    buffer_a.update(cx_a, |buffer, cx| {
-        buffer.edit([(0..5, "goodbye")], None, cx)
-    });
-    buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx));
-    assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world");
-    executor.run_until_parked();
-
-    // Client B joins the channel buffer
-    let channel_buffer_b = client_b
-        .channel_store()
-        .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-    channel_buffer_b.read_with(cx_b, |buffer, _| {
-        assert_collaborators(
-            buffer.collaborators(),
-            &[client_a.user_id(), client_b.user_id()],
-        );
-    });
-
-    // Client B sees the correct text, and then edits it
-    let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer());
-    assert_eq!(
-        buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()),
-        buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id())
-    );
-    assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world");
-    buffer_b.update(cx_b, |buffer, cx| {
-        buffer.edit([(7..12, "beautiful")], None, cx)
-    });
-
-    // Both A and B see the new edit
-    executor.run_until_parked();
-    assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world");
-    assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world");
-
-    // Client A closes the channel buffer.
-    cx_a.update(|_| drop(channel_buffer_a));
-    executor.run_until_parked();
-
-    // Client B sees that client A is gone from the channel buffer.
-    channel_buffer_b.read_with(cx_b, |buffer, _| {
-        assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]);
-    });
-
-    // Client A rejoins the channel buffer
-    let _channel_buffer_a = client_a
-        .channel_store()
-        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-    executor.run_until_parked();
-
-    // Sanity test, make sure we saw A rejoining
-    channel_buffer_b.read_with(cx_b, |buffer, _| {
-        assert_collaborators(
-            &buffer.collaborators(),
-            &[client_a.user_id(), client_b.user_id()],
-        );
-    });
-
-    // Client A loses connection.
-    server.forbid_connections();
-    server.disconnect_client(client_a.peer_id().unwrap());
-    executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
-
-    // Client B observes A disconnect
-    channel_buffer_b.read_with(cx_b, |buffer, _| {
-        assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]);
-    });
-
-    // TODO:
-    // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects
-    // - Test interaction with channel deletion while buffer is open
-}
-
-// todo!("collab_ui")
+//todo(partially ported)
+// use std::ops::Range;
+
+// use crate::{
+//     rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
+//     tests::TestServer,
+// };
+// use client::{Collaborator, ParticipantIndex, UserId};
+// use collections::HashMap;
+// use editor::{Anchor, Editor, ToOffset};
+// use futures::future;
+// use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext};
+// use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
+
 // #[gpui::test]
-// async fn test_channel_notes_participant_indices(
+// async fn test_core_channel_buffers(
 //     executor: BackgroundExecutor,
-//     mut cx_a: &mut TestAppContext,
-//     mut cx_b: &mut TestAppContext,
-//     cx_c: &mut TestAppContext,
+//     cx_a: &mut TestAppContext,
+//     cx_b: &mut TestAppContext,
 // ) {
-//     let mut server = TestServer::start(&executor).await;
+//     let mut server = TestServer::start(executor.clone()).await;
 //     let client_a = server.create_client(cx_a, "user_a").await;
 //     let client_b = server.create_client(cx_b, "user_b").await;
-//     let client_c = server.create_client(cx_c, "user_c").await;
-
-//     let active_call_a = cx_a.read(ActiveCall::global);
-//     let active_call_b = cx_b.read(ActiveCall::global);
-
-//     cx_a.update(editor::init);
-//     cx_b.update(editor::init);
-//     cx_c.update(editor::init);
 
 //     let channel_id = server
-//         .make_channel(
-//             "the-channel",
-//             None,
-//             (&client_a, cx_a),
-//             &mut [(&client_b, cx_b), (&client_c, cx_c)],
-//         )
+//         .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
 //         .await;
 
-//     client_a
-//         .fs()
-//         .insert_tree("/root", json!({"file.txt": "123"}))
-//         .await;
-//     let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await;
-//     let project_b = client_b.build_empty_local_project(cx_b);
-//     let project_c = client_c.build_empty_local_project(cx_c);
-//     let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-//     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-//     let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
-
-//     // Clients A, B, and C open the channel notes
-//     let channel_view_a = cx_a
-//         .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx))
-//         .await
-//         .unwrap();
-//     let channel_view_b = cx_b
-//         .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
-//         .await
-//         .unwrap();
-//     let channel_view_c = cx_c
-//         .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
+//     // Client A joins the channel buffer
+//     let channel_buffer_a = client_a
+//         .channel_store()
+//         .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
 //         .await
 //         .unwrap();
 
-//     // Clients A, B, and C all insert and select some text
-//     channel_view_a.update(cx_a, |notes, cx| {
-//         notes.editor.update(cx, |editor, cx| {
-//             editor.insert("a", cx);
-//             editor.change_selections(None, cx, |selections| {
-//                 selections.select_ranges(vec![0..1]);
-//             });
-//         });
+//     // Client A edits the buffer
+//     let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer());
+//     buffer_a.update(cx_a, |buffer, cx| {
+//         buffer.edit([(0..0, "hello world")], None, cx)
 //     });
-//     executor.run_until_parked();
-//     channel_view_b.update(cx_b, |notes, cx| {
-//         notes.editor.update(cx, |editor, cx| {
-//             editor.move_down(&Default::default(), cx);
-//             editor.insert("b", cx);
-//             editor.change_selections(None, cx, |selections| {
-//                 selections.select_ranges(vec![1..2]);
-//             });
-//         });
+//     buffer_a.update(cx_a, |buffer, cx| {
+//         buffer.edit([(5..5, ", cruel")], None, cx)
+//     });
+//     buffer_a.update(cx_a, |buffer, cx| {
+//         buffer.edit([(0..5, "goodbye")], None, cx)
 //     });
+//     buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx));
+//     assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world");
 //     executor.run_until_parked();
-//     channel_view_c.update(cx_c, |notes, cx| {
-//         notes.editor.update(cx, |editor, cx| {
-//             editor.move_down(&Default::default(), cx);
-//             editor.insert("c", cx);
-//             editor.change_selections(None, cx, |selections| {
-//                 selections.select_ranges(vec![2..3]);
-//             });
-//         });
+
+//     // Client B joins the channel buffer
+//     let channel_buffer_b = client_b
+//         .channel_store()
+//         .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
+//         .await
+//         .unwrap();
+//     channel_buffer_b.read_with(cx_b, |buffer, _| {
+//         assert_collaborators(
+//             buffer.collaborators(),
+//             &[client_a.user_id(), client_b.user_id()],
+//         );
 //     });
 
-//     // Client A sees clients B and C without assigned colors, because they aren't
-//     // in a call together.
-//     executor.run_until_parked();
-//     channel_view_a.update(cx_a, |notes, cx| {
-//         notes.editor.update(cx, |editor, cx| {
-//             assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
-//         });
+//     // Client B sees the correct text, and then edits it
+//     let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer());
+//     assert_eq!(
+//         buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()),
+//         buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id())
+//     );
+//     assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world");
+//     buffer_b.update(cx_b, |buffer, cx| {
+//         buffer.edit([(7..12, "beautiful")], None, cx)
 //     });
 
-//     // Clients A and B join the same call.
-//     for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] {
-//         call.update(*cx, |call, cx| call.join_channel(channel_id, cx))
-//             .await
-//             .unwrap();
-//     }
+//     // Both A and B see the new edit
+//     executor.run_until_parked();
+//     assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world");
+//     assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world");
 
-//     // Clients A and B see each other with two different assigned colors. Client C
-//     // still doesn't have a color.
+//     // Client A closes the channel buffer.
+//     cx_a.update(|_| drop(channel_buffer_a));
 //     executor.run_until_parked();
-//     channel_view_a.update(cx_a, |notes, cx| {
-//         notes.editor.update(cx, |editor, cx| {
-//             assert_remote_selections(
-//                 editor,
-//                 &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
-//                 cx,
-//             );
-//         });
-//     });
-//     channel_view_b.update(cx_b, |notes, cx| {
-//         notes.editor.update(cx, |editor, cx| {
-//             assert_remote_selections(
-//                 editor,
-//                 &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
-//                 cx,
-//             );
-//         });
-//     });
 
-//     // Client A shares a project, and client B joins.
-//     let project_id = active_call_a
-//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-//         .await
-//         .unwrap();
-//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-//     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+//     // Client B sees that client A is gone from the channel buffer.
+//     channel_buffer_b.read_with(cx_b, |buffer, _| {
+//         assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]);
+//     });
 
-//     // Clients A and B open the same file.
-//     let editor_a = workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-//     let editor_b = workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
-//         })
+//     // Client A rejoins the channel buffer
+//     let _channel_buffer_a = client_a
+//         .channel_store()
+//         .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
 //         .await
-//         .unwrap()
-//         .downcast::<Editor>()
 //         .unwrap();
-
-//     editor_a.update(cx_a, |editor, cx| {
-//         editor.change_selections(None, cx, |selections| {
-//             selections.select_ranges(vec![0..1]);
-//         });
-//     });
-//     editor_b.update(cx_b, |editor, cx| {
-//         editor.change_selections(None, cx, |selections| {
-//             selections.select_ranges(vec![2..3]);
-//         });
-//     });
 //     executor.run_until_parked();
 
-//     // Clients A and B see each other with the same colors as in the channel notes.
-//     editor_a.update(cx_a, |editor, cx| {
-//         assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
+//     // Sanity test, make sure we saw A rejoining
+//     channel_buffer_b.read_with(cx_b, |buffer, _| {
+//         assert_collaborators(
+//             &buffer.collaborators(),
+//             &[client_a.user_id(), client_b.user_id()],
+//         );
 //     });
-//     editor_b.update(cx_b, |editor, cx| {
-//         assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
+
+//     // Client A loses connection.
+//     server.forbid_connections();
+//     server.disconnect_client(client_a.peer_id().unwrap());
+//     executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+
+//     // Client B observes A disconnect
+//     channel_buffer_b.read_with(cx_b, |buffer, _| {
+//         assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]);
 //     });
+
+//     // TODO:
+//     // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects
+//     // - Test interaction with channel deletion while buffer is open
 // }
 
-//todo!(editor)
+// // todo!("collab_ui")
+// // #[gpui::test]
+// // async fn test_channel_notes_participant_indices(
+// //     executor: BackgroundExecutor,
+// //     mut cx_a: &mut TestAppContext,
+// //     mut cx_b: &mut TestAppContext,
+// //     cx_c: &mut TestAppContext,
+// // ) {
+// //     let mut server = TestServer::start(&executor).await;
+// //     let client_a = server.create_client(cx_a, "user_a").await;
+// //     let client_b = server.create_client(cx_b, "user_b").await;
+// //     let client_c = server.create_client(cx_c, "user_c").await;
+
+// //     let active_call_a = cx_a.read(ActiveCall::global);
+// //     let active_call_b = cx_b.read(ActiveCall::global);
+
+// //     cx_a.update(editor::init);
+// //     cx_b.update(editor::init);
+// //     cx_c.update(editor::init);
+
+// //     let channel_id = server
+// //         .make_channel(
+// //             "the-channel",
+// //             None,
+// //             (&client_a, cx_a),
+// //             &mut [(&client_b, cx_b), (&client_c, cx_c)],
+// //         )
+// //         .await;
+
+// //     client_a
+// //         .fs()
+// //         .insert_tree("/root", json!({"file.txt": "123"}))
+// //         .await;
+// //     let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await;
+// //     let project_b = client_b.build_empty_local_project(cx_b);
+// //     let project_c = client_c.build_empty_local_project(cx_c);
+// //     let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+// //     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+// //     let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
+
+// //     // Clients A, B, and C open the channel notes
+// //     let channel_view_a = cx_a
+// //         .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx))
+// //         .await
+// //         .unwrap();
+// //     let channel_view_b = cx_b
+// //         .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
+// //         .await
+// //         .unwrap();
+// //     let channel_view_c = cx_c
+// //         .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
+// //         .await
+// //         .unwrap();
+
+// //     // Clients A, B, and C all insert and select some text
+// //     channel_view_a.update(cx_a, |notes, cx| {
+// //         notes.editor.update(cx, |editor, cx| {
+// //             editor.insert("a", cx);
+// //             editor.change_selections(None, cx, |selections| {
+// //                 selections.select_ranges(vec![0..1]);
+// //             });
+// //         });
+// //     });
+// //     executor.run_until_parked();
+// //     channel_view_b.update(cx_b, |notes, cx| {
+// //         notes.editor.update(cx, |editor, cx| {
+// //             editor.move_down(&Default::default(), cx);
+// //             editor.insert("b", cx);
+// //             editor.change_selections(None, cx, |selections| {
+// //                 selections.select_ranges(vec![1..2]);
+// //             });
+// //         });
+// //     });
+// //     executor.run_until_parked();
+// //     channel_view_c.update(cx_c, |notes, cx| {
+// //         notes.editor.update(cx, |editor, cx| {
+// //             editor.move_down(&Default::default(), cx);
+// //             editor.insert("c", cx);
+// //             editor.change_selections(None, cx, |selections| {
+// //                 selections.select_ranges(vec![2..3]);
+// //             });
+// //         });
+// //     });
+
+// //     // Client A sees clients B and C without assigned colors, because they aren't
+// //     // in a call together.
+// //     executor.run_until_parked();
+// //     channel_view_a.update(cx_a, |notes, cx| {
+// //         notes.editor.update(cx, |editor, cx| {
+// //             assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
+// //         });
+// //     });
+
+// //     // Clients A and B join the same call.
+// //     for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] {
+// //         call.update(*cx, |call, cx| call.join_channel(channel_id, cx))
+// //             .await
+// //             .unwrap();
+// //     }
+
+// //     // Clients A and B see each other with two different assigned colors. Client C
+// //     // still doesn't have a color.
+// //     executor.run_until_parked();
+// //     channel_view_a.update(cx_a, |notes, cx| {
+// //         notes.editor.update(cx, |editor, cx| {
+// //             assert_remote_selections(
+// //                 editor,
+// //                 &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
+// //                 cx,
+// //             );
+// //         });
+// //     });
+// //     channel_view_b.update(cx_b, |notes, cx| {
+// //         notes.editor.update(cx, |editor, cx| {
+// //             assert_remote_selections(
+// //                 editor,
+// //                 &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
+// //                 cx,
+// //             );
+// //         });
+// //     });
+
+// //     // Client A shares a project, and client B joins.
+// //     let project_id = active_call_a
+// //         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+// //         .await
+// //         .unwrap();
+// //     let project_b = client_b.build_remote_project(project_id, cx_b).await;
+// //     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+
+// //     // Clients A and B open the same file.
+// //     let editor_a = workspace_a
+// //         .update(cx_a, |workspace, cx| {
+// //             workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
+// //         })
+// //         .await
+// //         .unwrap()
+// //         .downcast::<Editor>()
+// //         .unwrap();
+// //     let editor_b = workspace_b
+// //         .update(cx_b, |workspace, cx| {
+// //             workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
+// //         })
+// //         .await
+// //         .unwrap()
+// //         .downcast::<Editor>()
+// //         .unwrap();
+
+// //     editor_a.update(cx_a, |editor, cx| {
+// //         editor.change_selections(None, cx, |selections| {
+// //             selections.select_ranges(vec![0..1]);
+// //         });
+// //     });
+// //     editor_b.update(cx_b, |editor, cx| {
+// //         editor.change_selections(None, cx, |selections| {
+// //             selections.select_ranges(vec![2..3]);
+// //         });
+// //     });
+// //     executor.run_until_parked();
+
+// //     // Clients A and B see each other with the same colors as in the channel notes.
+// //     editor_a.update(cx_a, |editor, cx| {
+// //         assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
+// //     });
+// //     editor_b.update(cx_b, |editor, cx| {
+// //         assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
+// //     });
+// // }
+
 // #[track_caller]
 // fn assert_remote_selections(
 //     editor: &mut Editor,
@@ -305,443 +308,135 @@ async fn test_core_channel_buffers(
 //     );
 // }
 
-#[gpui::test]
-async fn test_multiple_handles_to_channel_buffer(
-    deterministic: BackgroundExecutor,
-    cx_a: &mut TestAppContext,
-) {
-    let mut server = TestServer::start(deterministic.clone()).await;
-    let client_a = server.create_client(cx_a, "user_a").await;
-
-    let channel_id = server
-        .make_channel("the-channel", None, (&client_a, cx_a), &mut [])
-        .await;
-
-    let channel_buffer_1 = client_a
-        .channel_store()
-        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
-    let channel_buffer_2 = client_a
-        .channel_store()
-        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
-    let channel_buffer_3 = client_a
-        .channel_store()
-        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
-
-    // All concurrent tasks for opening a channel buffer return the same model handle.
-    let (channel_buffer, channel_buffer_2, channel_buffer_3) =
-        future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3)
-            .await
-            .unwrap();
-    let channel_buffer_model_id = channel_buffer.entity_id();
-    assert_eq!(channel_buffer, channel_buffer_2);
-    assert_eq!(channel_buffer, channel_buffer_3);
-
-    channel_buffer.update(cx_a, |buffer, cx| {
-        buffer.buffer().update(cx, |buffer, cx| {
-            buffer.edit([(0..0, "hello")], None, cx);
-        })
-    });
-    deterministic.run_until_parked();
-
-    cx_a.update(|_| {
-        drop(channel_buffer);
-        drop(channel_buffer_2);
-        drop(channel_buffer_3);
-    });
-    deterministic.run_until_parked();
-
-    // The channel buffer can be reopened after dropping it.
-    let channel_buffer = client_a
-        .channel_store()
-        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-    assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id);
-    channel_buffer.update(cx_a, |buffer, cx| {
-        buffer.buffer().update(cx, |buffer, _| {
-            assert_eq!(buffer.text(), "hello");
-        })
-    });
-}
-
-#[gpui::test]
-async fn test_channel_buffer_disconnect(
-    deterministic: BackgroundExecutor,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
-) {
-    let mut server = TestServer::start(deterministic.clone()).await;
-    let client_a = server.create_client(cx_a, "user_a").await;
-    let client_b = server.create_client(cx_b, "user_b").await;
-
-    let channel_id = server
-        .make_channel(
-            "the-channel",
-            None,
-            (&client_a, cx_a),
-            &mut [(&client_b, cx_b)],
-        )
-        .await;
-
-    let channel_buffer_a = client_a
-        .channel_store()
-        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-
-    let channel_buffer_b = client_b
-        .channel_store()
-        .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-
-    server.forbid_connections();
-    server.disconnect_client(client_a.peer_id().unwrap());
-    deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
-
-    channel_buffer_a.update(cx_a, |buffer, cx| {
-        assert_eq!(buffer.channel(cx).unwrap().name, "the-channel");
-        assert!(!buffer.is_connected());
-    });
-
-    deterministic.run_until_parked();
-
-    server.allow_connections();
-    deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
-
-    deterministic.run_until_parked();
-
-    client_a
-        .channel_store()
-        .update(cx_a, |channel_store, _| {
-            channel_store.remove_channel(channel_id)
-        })
-        .await
-        .unwrap();
-    deterministic.run_until_parked();
-
-    // Channel buffer observed the deletion
-    channel_buffer_b.update(cx_b, |buffer, cx| {
-        assert!(buffer.channel(cx).is_none());
-        assert!(!buffer.is_connected());
-    });
-}
-
-#[gpui::test]
-async fn test_rejoin_channel_buffer(
-    deterministic: BackgroundExecutor,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
-) {
-    let mut server = TestServer::start(deterministic.clone()).await;
-    let client_a = server.create_client(cx_a, "user_a").await;
-    let client_b = server.create_client(cx_b, "user_b").await;
-
-    let channel_id = server
-        .make_channel(
-            "the-channel",
-            None,
-            (&client_a, cx_a),
-            &mut [(&client_b, cx_b)],
-        )
-        .await;
-
-    let channel_buffer_a = client_a
-        .channel_store()
-        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-    let channel_buffer_b = client_b
-        .channel_store()
-        .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-
-    channel_buffer_a.update(cx_a, |buffer, cx| {
-        buffer.buffer().update(cx, |buffer, cx| {
-            buffer.edit([(0..0, "1")], None, cx);
-        })
-    });
-    deterministic.run_until_parked();
-
-    // Client A disconnects.
-    server.forbid_connections();
-    server.disconnect_client(client_a.peer_id().unwrap());
-
-    // Both clients make an edit.
-    channel_buffer_a.update(cx_a, |buffer, cx| {
-        buffer.buffer().update(cx, |buffer, cx| {
-            buffer.edit([(1..1, "2")], None, cx);
-        })
-    });
-    channel_buffer_b.update(cx_b, |buffer, cx| {
-        buffer.buffer().update(cx, |buffer, cx| {
-            buffer.edit([(0..0, "0")], None, cx);
-        })
-    });
-
-    // Both clients see their own edit.
-    deterministic.run_until_parked();
-    channel_buffer_a.read_with(cx_a, |buffer, cx| {
-        assert_eq!(buffer.buffer().read(cx).text(), "12");
-    });
-    channel_buffer_b.read_with(cx_b, |buffer, cx| {
-        assert_eq!(buffer.buffer().read(cx).text(), "01");
-    });
-
-    // Client A reconnects. Both clients see each other's edits, and see
-    // the same collaborators.
-    server.allow_connections();
-    deterministic.advance_clock(RECEIVE_TIMEOUT);
-    channel_buffer_a.read_with(cx_a, |buffer, cx| {
-        assert_eq!(buffer.buffer().read(cx).text(), "012");
-    });
-    channel_buffer_b.read_with(cx_b, |buffer, cx| {
-        assert_eq!(buffer.buffer().read(cx).text(), "012");
-    });
-
-    channel_buffer_a.read_with(cx_a, |buffer_a, _| {
-        channel_buffer_b.read_with(cx_b, |buffer_b, _| {
-            assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
-        });
-    });
-}
-
-#[gpui::test]
-async fn test_channel_buffers_and_server_restarts(
-    deterministic: BackgroundExecutor,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
-    cx_c: &mut TestAppContext,
-) {
-    let mut server = TestServer::start(deterministic.clone()).await;
-    let client_a = server.create_client(cx_a, "user_a").await;
-    let client_b = server.create_client(cx_b, "user_b").await;
-    let client_c = server.create_client(cx_c, "user_c").await;
-
-    let channel_id = server
-        .make_channel(
-            "the-channel",
-            None,
-            (&client_a, cx_a),
-            &mut [(&client_b, cx_b), (&client_c, cx_c)],
-        )
-        .await;
-
-    let channel_buffer_a = client_a
-        .channel_store()
-        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-    let channel_buffer_b = client_b
-        .channel_store()
-        .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-    let _channel_buffer_c = client_c
-        .channel_store()
-        .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-
-    channel_buffer_a.update(cx_a, |buffer, cx| {
-        buffer.buffer().update(cx, |buffer, cx| {
-            buffer.edit([(0..0, "1")], None, cx);
-        })
-    });
-    deterministic.run_until_parked();
-
-    // Client C can't reconnect.
-    client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
-
-    // Server stops.
-    server.reset().await;
-    deterministic.advance_clock(RECEIVE_TIMEOUT);
-
-    // While the server is down, both clients make an edit.
-    channel_buffer_a.update(cx_a, |buffer, cx| {
-        buffer.buffer().update(cx, |buffer, cx| {
-            buffer.edit([(1..1, "2")], None, cx);
-        })
-    });
-    channel_buffer_b.update(cx_b, |buffer, cx| {
-        buffer.buffer().update(cx, |buffer, cx| {
-            buffer.edit([(0..0, "0")], None, cx);
-        })
-    });
-
-    // Server restarts.
-    server.start().await.unwrap();
-    deterministic.advance_clock(CLEANUP_TIMEOUT);
-
-    // Clients reconnects. Clients A and B see each other's edits, and see
-    // that client C has disconnected.
-    channel_buffer_a.read_with(cx_a, |buffer, cx| {
-        assert_eq!(buffer.buffer().read(cx).text(), "012");
-    });
-    channel_buffer_b.read_with(cx_b, |buffer, cx| {
-        assert_eq!(buffer.buffer().read(cx).text(), "012");
-    });
-
-    channel_buffer_a.read_with(cx_a, |buffer_a, _| {
-        channel_buffer_b.read_with(cx_b, |buffer_b, _| {
-            assert_collaborators(
-                buffer_a.collaborators(),
-                &[client_a.user_id(), client_b.user_id()],
-            );
-            assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
-        });
-    });
-}
-
-//todo!(collab_ui)
-// #[gpui::test(iterations = 10)]
-// async fn test_following_to_channel_notes_without_a_shared_project(
+// #[gpui::test]
+// async fn test_multiple_handles_to_channel_buffer(
 //     deterministic: BackgroundExecutor,
-//     mut cx_a: &mut TestAppContext,
-//     mut cx_b: &mut TestAppContext,
-//     mut cx_c: &mut TestAppContext,
+//     cx_a: &mut TestAppContext,
 // ) {
-//     let mut server = TestServer::start(&deterministic).await;
+//     let mut server = TestServer::start(deterministic.clone()).await;
 //     let client_a = server.create_client(cx_a, "user_a").await;
-//     let client_b = server.create_client(cx_b, "user_b").await;
 
-//     let client_c = server.create_client(cx_c, "user_c").await;
+//     let channel_id = server
+//         .make_channel("the-channel", None, (&client_a, cx_a), &mut [])
+//         .await;
 
-//     cx_a.update(editor::init);
-//     cx_b.update(editor::init);
-//     cx_c.update(editor::init);
-//     cx_a.update(collab_ui::channel_view::init);
-//     cx_b.update(collab_ui::channel_view::init);
-//     cx_c.update(collab_ui::channel_view::init);
+//     let channel_buffer_1 = client_a
+//         .channel_store()
+//         .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
+//     let channel_buffer_2 = client_a
+//         .channel_store()
+//         .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
+//     let channel_buffer_3 = client_a
+//         .channel_store()
+//         .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx));
 
-//     let channel_1_id = server
-//         .make_channel(
-//             "channel-1",
-//             None,
-//             (&client_a, cx_a),
-//             &mut [(&client_b, cx_b), (&client_c, cx_c)],
-//         )
-//         .await;
-//     let channel_2_id = server
+//     // All concurrent tasks for opening a channel buffer return the same model handle.
+//     let (channel_buffer, channel_buffer_2, channel_buffer_3) =
+//         future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3)
+//             .await
+//             .unwrap();
+//     let channel_buffer_model_id = channel_buffer.entity_id();
+//     assert_eq!(channel_buffer, channel_buffer_2);
+//     assert_eq!(channel_buffer, channel_buffer_3);
+
+//     channel_buffer.update(cx_a, |buffer, cx| {
+//         buffer.buffer().update(cx, |buffer, cx| {
+//             buffer.edit([(0..0, "hello")], None, cx);
+//         })
+//     });
+//     deterministic.run_until_parked();
+
+//     cx_a.update(|_| {
+//         drop(channel_buffer);
+//         drop(channel_buffer_2);
+//         drop(channel_buffer_3);
+//     });
+//     deterministic.run_until_parked();
+
+//     // The channel buffer can be reopened after dropping it.
+//     let channel_buffer = client_a
+//         .channel_store()
+//         .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
+//         .await
+//         .unwrap();
+//     assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id);
+//     channel_buffer.update(cx_a, |buffer, cx| {
+//         buffer.buffer().update(cx, |buffer, _| {
+//             assert_eq!(buffer.text(), "hello");
+//         })
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_channel_buffer_disconnect(
+//     deterministic: BackgroundExecutor,
+//     cx_a: &mut TestAppContext,
+//     cx_b: &mut TestAppContext,
+// ) {
+//     let mut server = TestServer::start(deterministic.clone()).await;
+//     let client_a = server.create_client(cx_a, "user_a").await;
+//     let client_b = server.create_client(cx_b, "user_b").await;
+
+//     let channel_id = server
 //         .make_channel(
-//             "channel-2",
+//             "the-channel",
 //             None,
 //             (&client_a, cx_a),
-//             &mut [(&client_b, cx_b), (&client_c, cx_c)],
+//             &mut [(&client_b, cx_b)],
 //         )
 //         .await;
 
-//     // Clients A, B, and C join a channel.
-//     let active_call_a = cx_a.read(ActiveCall::global);
-//     let active_call_b = cx_b.read(ActiveCall::global);
-//     let active_call_c = cx_c.read(ActiveCall::global);
-//     for (call, cx) in [
-//         (&active_call_a, &mut cx_a),
-//         (&active_call_b, &mut cx_b),
-//         (&active_call_c, &mut cx_c),
-//     ] {
-//         call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
-//             .await
-//             .unwrap();
-//     }
-//     deterministic.run_until_parked();
-
-//     // Clients A, B, and C all open their own unshared projects.
-//     client_a.fs().insert_tree("/a", json!({})).await;
-//     client_b.fs().insert_tree("/b", json!({})).await;
-//     client_c.fs().insert_tree("/c", json!({})).await;
-//     let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
-//     let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
-//     let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
-//     let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-//     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-//     let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
-
-//     active_call_a
-//         .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+//     let channel_buffer_a = client_a
+//         .channel_store()
+//         .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
 //         .await
 //         .unwrap();
 
-//     // Client A opens the notes for channel 1.
-//     let channel_view_1_a = cx_a
-//         .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
+//     let channel_buffer_b = client_b
+//         .channel_store()
+//         .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
 //         .await
 //         .unwrap();
-//     channel_view_1_a.update(cx_a, |notes, cx| {
-//         assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
-//         notes.editor.update(cx, |editor, cx| {
-//             editor.insert("Hello from A.", cx);
-//             editor.change_selections(None, cx, |selections| {
-//                 selections.select_ranges(vec![3..4]);
-//             });
-//         });
+
+//     server.forbid_connections();
+//     server.disconnect_client(client_a.peer_id().unwrap());
+//     deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+
+//     channel_buffer_a.update(cx_a, |buffer, cx| {
+//         assert_eq!(buffer.channel(cx).unwrap().name, "the-channel");
+//         assert!(!buffer.is_connected());
 //     });
 
-//     // Client B follows client A.
-//     workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
+//     deterministic.run_until_parked();
+
+//     server.allow_connections();
+//     deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
 
-//     // Client B is taken to the notes for channel 1, with the same
-//     // text selected as client A.
 //     deterministic.run_until_parked();
-//     let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| {
-//         assert_eq!(
-//             workspace.leader_for_pane(workspace.active_pane()),
-//             Some(client_a.peer_id().unwrap())
-//         );
-//         workspace
-//             .active_item(cx)
-//             .expect("no active item")
-//             .downcast::<ChannelView>()
-//             .expect("active item is not a channel view")
-//     });
-//     channel_view_1_b.read_with(cx_b, |notes, cx| {
-//         assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
-//         let editor = notes.editor.read(cx);
-//         assert_eq!(editor.text(cx), "Hello from A.");
-//         assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
-//     });
 
-//     // Client A opens the notes for channel 2.
-//     let channel_view_2_a = cx_a
-//         .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
+//     client_a
+//         .channel_store()
+//         .update(cx_a, |channel_store, _| {
+//             channel_store.remove_channel(channel_id)
+//         })
 //         .await
 //         .unwrap();
-//     channel_view_2_a.read_with(cx_a, |notes, cx| {
-//         assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
-//     });
-
-//     // Client B is taken to the notes for channel 2.
 //     deterministic.run_until_parked();
-//     let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| {
-//         assert_eq!(
-//             workspace.leader_for_pane(workspace.active_pane()),
-//             Some(client_a.peer_id().unwrap())
-//         );
-//         workspace
-//             .active_item(cx)
-//             .expect("no active item")
-//             .downcast::<ChannelView>()
-//             .expect("active item is not a channel view")
-//     });
-//     channel_view_2_b.read_with(cx_b, |notes, cx| {
-//         assert_eq!(notes.channel(cx).unwrap().name, "channel-2");
+
+//     // Channel buffer observed the deletion
+//     channel_buffer_b.update(cx_b, |buffer, cx| {
+//         assert!(buffer.channel(cx).is_none());
+//         assert!(!buffer.is_connected());
 //     });
 // }
 
-//todo!(collab_ui)
 // #[gpui::test]
-// async fn test_channel_buffer_changes(
+// async fn test_rejoin_channel_buffer(
 //     deterministic: BackgroundExecutor,
 //     cx_a: &mut TestAppContext,
 //     cx_b: &mut TestAppContext,
 // ) {
-//     let mut server = TestServer::start(&deterministic).await;
+//     let mut server = TestServer::start(deterministic.clone()).await;
 //     let client_a = server.create_client(cx_a, "user_a").await;
 //     let client_b = server.create_client(cx_b, "user_b").await;
 

crates/collab2/src/tests/editor_tests.rs 🔗

@@ -1,9 +1,32 @@
+//todo(partially ported)
+// use std::{
+//     path::Path,
+//     sync::{
+//         atomic::{self, AtomicBool, AtomicUsize},
+//         Arc,
+//     },
+// };
+
+// use call::ActiveCall;
 // use editor::{
-//     test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
-//     ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo,
+//     test::editor_test_context::{AssertionContextManager, EditorTestContext},
+//     Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename,
+//     ToggleCodeActions, Undo,
 // };
+// use gpui::{BackgroundExecutor, TestAppContext, VisualContext, VisualTestContext};
+// use indoc::indoc;
+// use language::{
+//     language_settings::{AllLanguageSettings, InlayHintSettings},
+//     tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig,
+// };
+// use rpc::RECEIVE_TIMEOUT;
+// use serde_json::json;
+// use settings::SettingsStore;
+// use text::Point;
+// use workspace::Workspace;
+
+// use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
 
-//todo!(editor)
 // #[gpui::test(iterations = 10)]
 // async fn test_host_disconnect(
 //     executor: BackgroundExecutor,
@@ -11,7 +34,7 @@
 //     cx_b: &mut TestAppContext,
 //     cx_c: &mut TestAppContext,
 // ) {
-//     let mut server = TestServer::start(&executor).await;
+//     let mut server = TestServer::start(executor).await;
 //     let client_a = server.create_client(cx_a, "user_a").await;
 //     let client_b = server.create_client(cx_b, "user_b").await;
 //     let client_c = server.create_client(cx_c, "user_c").await;
@@ -25,7 +48,7 @@
 //         .fs()
 //         .insert_tree(
 //             "/a",
-//             json!({
+//             serde_json::json!({
 //                 "a.txt": "a-contents",
 //                 "b.txt": "b-contents",
 //             }),
@@ -35,7 +58,7 @@
 //     let active_call_a = cx_a.read(ActiveCall::global);
 //     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
 
-//     let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
+//     let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees().next().unwrap());
 //     let project_id = active_call_a
 //         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 //         .await
@@ -46,21 +69,25 @@
 
 //     assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
 
-//     let window_b =
+//     let workspace_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 cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
+
 //     let editor_b = workspace_b
 //         .update(cx_b, |workspace, cx| {
 //             workspace.open_path((worktree_id, "b.txt"), None, true, cx)
 //         })
+//         .unwrap()
 //         .await
 //         .unwrap()
 //         .downcast::<Editor>()
 //         .unwrap();
 
-//     assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx)));
+//     //TODO: focus
+//     assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
 //     editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
-//     assert!(window_b.is_edited(cx_b));
+//     //todo(is_edited)
+//     // assert!(workspace_b.is_edited(cx_b));
 
 //     // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
 //     server.forbid_connections();
@@ -77,10 +104,10 @@
 
 //     // Ensure client B's edited state is reset and that the whole window is blurred.
 
-//     window_b.read_with(cx_b, |cx| {
+//     workspace_b.update(cx_b, |_, cx| {
 //         assert_eq!(cx.focused_view_id(), None);
 //     });
-//     assert!(!window_b.is_edited(cx_b));
+//     // assert!(!workspace_b.is_edited(cx_b));
 
 //     // Ensure client B is not prompted to save edits when closing window after disconnecting.
 //     let can_close = workspace_b
@@ -120,7 +147,6 @@
 //     project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
 // }
 
-//todo!(editor)
 // #[gpui::test]
 // async fn test_newline_above_or_below_does_not_move_guest_cursor(
 //     executor: BackgroundExecutor,
@@ -152,12 +178,14 @@
 //         .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 = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
+//     let window_a = cx_a.add_empty_window();
+//     let editor_a =
+//         window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
 //     let mut editor_cx_a = EditorTestContext {
 //         cx: cx_a,
 //         window: window_a.into(),
 //         editor: editor_a,
+//         assertion_cx: AssertionContextManager::new(),
 //     };
 
 //     // Open a buffer as client B
@@ -165,12 +193,14 @@
 //         .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 = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
+//     let window_b = cx_b.add_empty_window();
+//     let editor_b =
+//         window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
 //     let mut editor_cx_b = EditorTestContext {
 //         cx: cx_b,
 //         window: window_b.into(),
 //         editor: editor_b,
+//         assertion_cx: AssertionContextManager::new(),
 //     };
 
 //     // Test newline above
@@ -214,7 +244,6 @@
 //     "});
 // }
 
-//todo!(editor)
 // #[gpui::test(iterations = 10)]
 // async fn test_collaborating_with_completion(
 //     executor: BackgroundExecutor,
@@ -275,8 +304,8 @@
 //         .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 = window_b.add_view(cx_b, |cx| {
+//     let window_b = cx_b.add_empty_window();
+//     let editor_b = window_b.build_view(cx_b, |cx| {
 //         Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
 //     });
 
@@ -384,7 +413,7 @@
 //     );
 
 //     // The additional edit is applied.
-//     cx_a.foreground().run_until_parked();
+//     cx_a.executor().run_until_parked();
 
 //     buffer_a.read_with(cx_a, |buffer, _| {
 //         assert_eq!(
@@ -400,7 +429,7 @@
 //         );
 //     });
 // }
-//todo!(editor)
+
 // #[gpui::test(iterations = 10)]
 // async fn test_collaborating_with_code_actions(
 //     executor: BackgroundExecutor,
@@ -619,7 +648,6 @@
 //     });
 // }
 
-//todo!(editor)
 // #[gpui::test(iterations = 10)]
 // async fn test_collaborating_with_renames(
 //     executor: BackgroundExecutor,
@@ -813,7 +841,6 @@
 //     })
 // }
 
-//todo!(editor)
 // #[gpui::test(iterations = 10)]
 // async fn test_language_server_statuses(
 //     executor: BackgroundExecutor,
@@ -937,8 +964,8 @@
 //     cx_b: &mut TestAppContext,
 //     cx_c: &mut TestAppContext,
 // ) {
-//     let window_b = cx_b.add_window(|_| EmptyView);
-//     let mut server = TestServer::start(&executor).await;
+//     let window_b = cx_b.add_empty_window();
+//     let mut server = TestServer::start(executor).await;
 //     let client_a = server.create_client(cx_a, "user_a").await;
 //     let client_b = server.create_client(cx_b, "user_b").await;
 //     let client_c = server.create_client(cx_c, "user_c").await;
@@ -1052,7 +1079,7 @@
 //         .await
 //         .unwrap();
 
-//     let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
+//     let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
 
 //     // Client A sees client B's selection
 //     executor.run_until_parked();
@@ -1106,3 +1133,757 @@
 //             == 0
 //     });
 // }
+
+// #[gpui::test(iterations = 10)]
+// async fn test_on_input_format_from_host_to_guest(
+//     executor: BackgroundExecutor,
+//     cx_a: &mut TestAppContext,
+//     cx_b: &mut TestAppContext,
+// ) {
+//     let mut server = TestServer::start(&executor).await;
+//     let client_a = server.create_client(cx_a, "user_a").await;
+//     let client_b = server.create_client(cx_b, "user_b").await;
+//     server
+//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+//         .await;
+//     let active_call_a = cx_a.read(ActiveCall::global);
+
+//     // Set up a fake language server.
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_rust::language()),
+//     );
+//     let mut fake_language_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             capabilities: lsp::ServerCapabilities {
+//                 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
+//                     first_trigger_character: ":".to_string(),
+//                     more_trigger_character: Some(vec![">".to_string()]),
+//                 }),
+//                 ..Default::default()
+//             },
+//             ..Default::default()
+//         }))
+//         .await;
+//     client_a.language_registry().add(Arc::new(language));
+
+//     client_a
+//         .fs()
+//         .insert_tree(
+//             "/a",
+//             json!({
+//                 "main.rs": "fn main() { a }",
+//                 "other.rs": "// Test file",
+//             }),
+//         )
+//         .await;
+//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+//     let project_id = active_call_a
+//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+//         .await
+//         .unwrap();
+//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
+
+//     // Open a file in an editor as the host.
+//     let buffer_a = project_a
+//         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+//         .await
+//         .unwrap();
+//     let window_a = cx_a.add_empty_window();
+//     let editor_a = window_a
+//         .update(cx_a, |_, cx| {
+//             cx.build_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx))
+//         })
+//         .unwrap();
+
+//     let fake_language_server = fake_language_servers.next().await.unwrap();
+//     executor.run_until_parked();
+
+//     // Receive an OnTypeFormatting request as the host's language server.
+//     // Return some formattings from the host's language server.
+//     fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
+//         |params, _| async move {
+//             assert_eq!(
+//                 params.text_document_position.text_document.uri,
+//                 lsp::Url::from_file_path("/a/main.rs").unwrap(),
+//             );
+//             assert_eq!(
+//                 params.text_document_position.position,
+//                 lsp::Position::new(0, 14),
+//             );
+
+//             Ok(Some(vec![lsp::TextEdit {
+//                 new_text: "~<".to_string(),
+//                 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
+//             }]))
+//         },
+//     );
+
+//     // Open the buffer on the guest and see that the formattings worked
+//     let buffer_b = project_b
+//         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+//         .await
+//         .unwrap();
+
+//     // Type a on type formatting trigger character as the guest.
+//     editor_a.update(cx_a, |editor, cx| {
+//         cx.focus(&editor_a);
+//         editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+//         editor.handle_input(">", cx);
+//     });
+
+//     executor.run_until_parked();
+
+//     buffer_b.read_with(cx_b, |buffer, _| {
+//         assert_eq!(buffer.text(), "fn main() { a>~< }")
+//     });
+
+//     // Undo should remove LSP edits first
+//     editor_a.update(cx_a, |editor, cx| {
+//         assert_eq!(editor.text(cx), "fn main() { a>~< }");
+//         editor.undo(&Undo, cx);
+//         assert_eq!(editor.text(cx), "fn main() { a> }");
+//     });
+//     executor.run_until_parked();
+
+//     buffer_b.read_with(cx_b, |buffer, _| {
+//         assert_eq!(buffer.text(), "fn main() { a> }")
+//     });
+
+//     editor_a.update(cx_a, |editor, cx| {
+//         assert_eq!(editor.text(cx), "fn main() { a> }");
+//         editor.undo(&Undo, cx);
+//         assert_eq!(editor.text(cx), "fn main() { a }");
+//     });
+//     executor.run_until_parked();
+
+//     buffer_b.read_with(cx_b, |buffer, _| {
+//         assert_eq!(buffer.text(), "fn main() { a }")
+//     });
+// }
+
+// #[gpui::test(iterations = 10)]
+// async fn test_on_input_format_from_guest_to_host(
+//     executor: BackgroundExecutor,
+//     cx_a: &mut TestAppContext,
+//     cx_b: &mut TestAppContext,
+// ) {
+//     let mut server = TestServer::start(&executor).await;
+//     let client_a = server.create_client(cx_a, "user_a").await;
+//     let client_b = server.create_client(cx_b, "user_b").await;
+//     server
+//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+//         .await;
+//     let active_call_a = cx_a.read(ActiveCall::global);
+
+//     // Set up a fake language server.
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_rust::language()),
+//     );
+//     let mut fake_language_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             capabilities: lsp::ServerCapabilities {
+//                 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
+//                     first_trigger_character: ":".to_string(),
+//                     more_trigger_character: Some(vec![">".to_string()]),
+//                 }),
+//                 ..Default::default()
+//             },
+//             ..Default::default()
+//         }))
+//         .await;
+//     client_a.language_registry().add(Arc::new(language));
+
+//     client_a
+//         .fs()
+//         .insert_tree(
+//             "/a",
+//             json!({
+//                 "main.rs": "fn main() { a }",
+//                 "other.rs": "// Test file",
+//             }),
+//         )
+//         .await;
+//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+//     let project_id = active_call_a
+//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+//         .await
+//         .unwrap();
+//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
+
+//     // Open a file in an editor as the guest.
+//     let buffer_b = project_b
+//         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+//         .await
+//         .unwrap();
+//     let window_b = cx_b.add_empty_window();
+//     let editor_b = window_b.build_view(cx_b, |cx| {
+//         Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
+//     });
+
+//     let fake_language_server = fake_language_servers.next().await.unwrap();
+//     executor.run_until_parked();
+//     // Type a on type formatting trigger character as the guest.
+//     editor_b.update(cx_b, |editor, cx| {
+//         editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+//         editor.handle_input(":", cx);
+//         cx.focus(&editor_b);
+//     });
+
+//     // Receive an OnTypeFormatting request as the host's language server.
+//     // Return some formattings from the host's language server.
+//     cx_a.foreground().start_waiting();
+//     fake_language_server
+//         .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
+//             assert_eq!(
+//                 params.text_document_position.text_document.uri,
+//                 lsp::Url::from_file_path("/a/main.rs").unwrap(),
+//             );
+//             assert_eq!(
+//                 params.text_document_position.position,
+//                 lsp::Position::new(0, 14),
+//             );
+
+//             Ok(Some(vec![lsp::TextEdit {
+//                 new_text: "~:".to_string(),
+//                 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
+//             }]))
+//         })
+//         .next()
+//         .await
+//         .unwrap();
+//     cx_a.foreground().finish_waiting();
+
+//     // Open the buffer on the host and see that the formattings worked
+//     let buffer_a = project_a
+//         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+//         .await
+//         .unwrap();
+//     executor.run_until_parked();
+
+//     buffer_a.read_with(cx_a, |buffer, _| {
+//         assert_eq!(buffer.text(), "fn main() { a:~: }")
+//     });
+
+//     // Undo should remove LSP edits first
+//     editor_b.update(cx_b, |editor, cx| {
+//         assert_eq!(editor.text(cx), "fn main() { a:~: }");
+//         editor.undo(&Undo, cx);
+//         assert_eq!(editor.text(cx), "fn main() { a: }");
+//     });
+//     executor.run_until_parked();
+
+//     buffer_a.read_with(cx_a, |buffer, _| {
+//         assert_eq!(buffer.text(), "fn main() { a: }")
+//     });
+
+//     editor_b.update(cx_b, |editor, cx| {
+//         assert_eq!(editor.text(cx), "fn main() { a: }");
+//         editor.undo(&Undo, cx);
+//         assert_eq!(editor.text(cx), "fn main() { a }");
+//     });
+//     executor.run_until_parked();
+
+//     buffer_a.read_with(cx_a, |buffer, _| {
+//         assert_eq!(buffer.text(), "fn main() { a }")
+//     });
+// }
+
+// #[gpui::test(iterations = 10)]
+// async fn test_mutual_editor_inlay_hint_cache_update(
+//     executor: BackgroundExecutor,
+//     cx_a: &mut TestAppContext,
+//     cx_b: &mut TestAppContext,
+// ) {
+//     let mut server = TestServer::start(&executor).await;
+//     let client_a = server.create_client(cx_a, "user_a").await;
+//     let client_b = server.create_client(cx_b, "user_b").await;
+//     server
+//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+//         .await;
+//     let active_call_a = cx_a.read(ActiveCall::global);
+//     let active_call_b = cx_b.read(ActiveCall::global);
+
+//     cx_a.update(editor::init);
+//     cx_b.update(editor::init);
+
+//     cx_a.update(|cx| {
+//         cx.update_global(|store: &mut SettingsStore, cx| {
+//             store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+//                 settings.defaults.inlay_hints = Some(InlayHintSettings {
+//                     enabled: true,
+//                     show_type_hints: true,
+//                     show_parameter_hints: false,
+//                     show_other_hints: true,
+//                 })
+//             });
+//         });
+//     });
+//     cx_b.update(|cx| {
+//         cx.update_global(|store: &mut SettingsStore, cx| {
+//             store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+//                 settings.defaults.inlay_hints = Some(InlayHintSettings {
+//                     enabled: true,
+//                     show_type_hints: true,
+//                     show_parameter_hints: false,
+//                     show_other_hints: true,
+//                 })
+//             });
+//         });
+//     });
+
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_rust::language()),
+//     );
+//     let mut fake_language_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             capabilities: lsp::ServerCapabilities {
+//                 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+//                 ..Default::default()
+//             },
+//             ..Default::default()
+//         }))
+//         .await;
+//     let language = Arc::new(language);
+//     client_a.language_registry().add(Arc::clone(&language));
+//     client_b.language_registry().add(language);
+
+//     // Client A opens a project.
+//     client_a
+//         .fs()
+//         .insert_tree(
+//             "/a",
+//             json!({
+//                 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
+//                 "other.rs": "// Test file",
+//             }),
+//         )
+//         .await;
+//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+//     active_call_a
+//         .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+//         .await
+//         .unwrap();
+//     let project_id = active_call_a
+//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+//         .await
+//         .unwrap();
+
+//     // Client B joins the project
+//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
+//     active_call_b
+//         .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+//         .await
+//         .unwrap();
+
+//     let workspace_a = client_a.build_workspace(&project_a, cx_a).root_view(cx_a);
+//     cx_a.foreground().start_waiting();
+
+//     // The host opens a rust file.
+//     let _buffer_a = project_a
+//         .update(cx_a, |project, cx| {
+//             project.open_local_buffer("/a/main.rs", cx)
+//         })
+//         .await
+//         .unwrap();
+//     let fake_language_server = fake_language_servers.next().await.unwrap();
+//     let editor_a = workspace_a
+//         .update(cx_a, |workspace, cx| {
+//             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+//         })
+//         .await
+//         .unwrap()
+//         .downcast::<Editor>()
+//         .unwrap();
+
+//     // Set up the language server to return an additional inlay hint on each request.
+//     let edits_made = Arc::new(AtomicUsize::new(0));
+//     let closure_edits_made = Arc::clone(&edits_made);
+//     fake_language_server
+//         .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+//             let task_edits_made = Arc::clone(&closure_edits_made);
+//             async move {
+//                 assert_eq!(
+//                     params.text_document.uri,
+//                     lsp::Url::from_file_path("/a/main.rs").unwrap(),
+//                 );
+//                 let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
+//                 Ok(Some(vec![lsp::InlayHint {
+//                     position: lsp::Position::new(0, edits_made as u32),
+//                     label: lsp::InlayHintLabel::String(edits_made.to_string()),
+//                     kind: None,
+//                     text_edits: None,
+//                     tooltip: None,
+//                     padding_left: None,
+//                     padding_right: None,
+//                     data: None,
+//                 }]))
+//             }
+//         })
+//         .next()
+//         .await
+//         .unwrap();
+
+//     executor.run_until_parked();
+
+//     let initial_edit = edits_made.load(atomic::Ordering::Acquire);
+//     editor_a.update(cx_a, |editor, _| {
+//         assert_eq!(
+//             vec![initial_edit.to_string()],
+//             extract_hint_labels(editor),
+//             "Host should get its first hints when opens an editor"
+//         );
+//         let inlay_cache = editor.inlay_hint_cache();
+//         assert_eq!(
+//             inlay_cache.version(),
+//             1,
+//             "Host editor update the cache version after every cache/view change",
+//         );
+//     });
+//     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+//     let editor_b = workspace_b
+//         .update(cx_b, |workspace, cx| {
+//             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+//         })
+//         .await
+//         .unwrap()
+//         .downcast::<Editor>()
+//         .unwrap();
+
+//     executor.run_until_parked();
+//     editor_b.update(cx_b, |editor, _| {
+//         assert_eq!(
+//             vec![initial_edit.to_string()],
+//             extract_hint_labels(editor),
+//             "Client should get its first hints when opens an editor"
+//         );
+//         let inlay_cache = editor.inlay_hint_cache();
+//         assert_eq!(
+//             inlay_cache.version(),
+//             1,
+//             "Guest editor update the cache version after every cache/view change"
+//         );
+//     });
+
+//     let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
+//     editor_b.update(cx_b, |editor, cx| {
+//         editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
+//         editor.handle_input(":", cx);
+//         cx.focus(&editor_b);
+//     });
+
+//     executor.run_until_parked();
+//     editor_a.update(cx_a, |editor, _| {
+//         assert_eq!(
+//             vec![after_client_edit.to_string()],
+//             extract_hint_labels(editor),
+//         );
+//         let inlay_cache = editor.inlay_hint_cache();
+//         assert_eq!(inlay_cache.version(), 2);
+//     });
+//     editor_b.update(cx_b, |editor, _| {
+//         assert_eq!(
+//             vec![after_client_edit.to_string()],
+//             extract_hint_labels(editor),
+//         );
+//         let inlay_cache = editor.inlay_hint_cache();
+//         assert_eq!(inlay_cache.version(), 2);
+//     });
+
+//     let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
+//     editor_a.update(cx_a, |editor, cx| {
+//         editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+//         editor.handle_input("a change to increment both buffers' versions", cx);
+//         cx.focus(&editor_a);
+//     });
+
+//     executor.run_until_parked();
+//     editor_a.update(cx_a, |editor, _| {
+//         assert_eq!(
+//             vec![after_host_edit.to_string()],
+//             extract_hint_labels(editor),
+//         );
+//         let inlay_cache = editor.inlay_hint_cache();
+//         assert_eq!(inlay_cache.version(), 3);
+//     });
+//     editor_b.update(cx_b, |editor, _| {
+//         assert_eq!(
+//             vec![after_host_edit.to_string()],
+//             extract_hint_labels(editor),
+//         );
+//         let inlay_cache = editor.inlay_hint_cache();
+//         assert_eq!(inlay_cache.version(), 3);
+//     });
+
+//     let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
+//     fake_language_server
+//         .request::<lsp::request::InlayHintRefreshRequest>(())
+//         .await
+//         .expect("inlay refresh request failed");
+
+//     executor.run_until_parked();
+//     editor_a.update(cx_a, |editor, _| {
+//         assert_eq!(
+//             vec![after_special_edit_for_refresh.to_string()],
+//             extract_hint_labels(editor),
+//             "Host should react to /refresh LSP request"
+//         );
+//         let inlay_cache = editor.inlay_hint_cache();
+//         assert_eq!(
+//             inlay_cache.version(),
+//             4,
+//             "Host should accepted all edits and bump its cache version every time"
+//         );
+//     });
+//     editor_b.update(cx_b, |editor, _| {
+//         assert_eq!(
+//             vec![after_special_edit_for_refresh.to_string()],
+//             extract_hint_labels(editor),
+//             "Guest should get a /refresh LSP request propagated by host"
+//         );
+//         let inlay_cache = editor.inlay_hint_cache();
+//         assert_eq!(
+//             inlay_cache.version(),
+//             4,
+//             "Guest should accepted all edits and bump its cache version every time"
+//         );
+//     });
+// }
+
+// #[gpui::test(iterations = 10)]
+// async fn test_inlay_hint_refresh_is_forwarded(
+//     executor: BackgroundExecutor,
+//     cx_a: &mut TestAppContext,
+//     cx_b: &mut TestAppContext,
+// ) {
+//     let mut server = TestServer::start(&executor).await;
+//     let client_a = server.create_client(cx_a, "user_a").await;
+//     let client_b = server.create_client(cx_b, "user_b").await;
+//     server
+//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+//         .await;
+//     let active_call_a = cx_a.read(ActiveCall::global);
+//     let active_call_b = cx_b.read(ActiveCall::global);
+
+//     cx_a.update(editor::init);
+//     cx_b.update(editor::init);
+
+//     cx_a.update(|cx| {
+//         cx.update_global(|store: &mut SettingsStore, cx| {
+//             store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+//                 settings.defaults.inlay_hints = Some(InlayHintSettings {
+//                     enabled: false,
+//                     show_type_hints: false,
+//                     show_parameter_hints: false,
+//                     show_other_hints: false,
+//                 })
+//             });
+//         });
+//     });
+//     cx_b.update(|cx| {
+//         cx.update_global(|store: &mut SettingsStore, cx| {
+//             store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+//                 settings.defaults.inlay_hints = Some(InlayHintSettings {
+//                     enabled: true,
+//                     show_type_hints: true,
+//                     show_parameter_hints: true,
+//                     show_other_hints: true,
+//                 })
+//             });
+//         });
+//     });
+
+//     let mut language = Language::new(
+//         LanguageConfig {
+//             name: "Rust".into(),
+//             path_suffixes: vec!["rs".to_string()],
+//             ..Default::default()
+//         },
+//         Some(tree_sitter_rust::language()),
+//     );
+//     let mut fake_language_servers = language
+//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+//             capabilities: lsp::ServerCapabilities {
+//                 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+//                 ..Default::default()
+//             },
+//             ..Default::default()
+//         }))
+//         .await;
+//     let language = Arc::new(language);
+//     client_a.language_registry().add(Arc::clone(&language));
+//     client_b.language_registry().add(language);
+
+//     client_a
+//         .fs()
+//         .insert_tree(
+//             "/a",
+//             json!({
+//                 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
+//                 "other.rs": "// Test file",
+//             }),
+//         )
+//         .await;
+//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+//     active_call_a
+//         .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+//         .await
+//         .unwrap();
+//     let project_id = active_call_a
+//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+//         .await
+//         .unwrap();
+
+//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
+//     active_call_b
+//         .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+//         .await
+//         .unwrap();
+
+//     let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+//     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+//     cx_a.foreground().start_waiting();
+//     cx_b.foreground().start_waiting();
+
+//     let editor_a = workspace_a
+//         .update(cx_a, |workspace, cx| {
+//             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+//         })
+//         .await
+//         .unwrap()
+//         .downcast::<Editor>()
+//         .unwrap();
+
+//     let editor_b = workspace_b
+//         .update(cx_b, |workspace, cx| {
+//             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+//         })
+//         .await
+//         .unwrap()
+//         .downcast::<Editor>()
+//         .unwrap();
+
+//     let other_hints = Arc::new(AtomicBool::new(false));
+//     let fake_language_server = fake_language_servers.next().await.unwrap();
+//     let closure_other_hints = Arc::clone(&other_hints);
+//     fake_language_server
+//         .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+//             let task_other_hints = Arc::clone(&closure_other_hints);
+//             async move {
+//                 assert_eq!(
+//                     params.text_document.uri,
+//                     lsp::Url::from_file_path("/a/main.rs").unwrap(),
+//                 );
+//                 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
+//                 let character = if other_hints { 0 } else { 2 };
+//                 let label = if other_hints {
+//                     "other hint"
+//                 } else {
+//                     "initial hint"
+//                 };
+//                 Ok(Some(vec![lsp::InlayHint {
+//                     position: lsp::Position::new(0, character),
+//                     label: lsp::InlayHintLabel::String(label.to_string()),
+//                     kind: None,
+//                     text_edits: None,
+//                     tooltip: None,
+//                     padding_left: None,
+//                     padding_right: None,
+//                     data: None,
+//                 }]))
+//             }
+//         })
+//         .next()
+//         .await
+//         .unwrap();
+//     cx_a.foreground().finish_waiting();
+//     cx_b.foreground().finish_waiting();
+
+//     executor.run_until_parked();
+//     editor_a.update(cx_a, |editor, _| {
+//         assert!(
+//             extract_hint_labels(editor).is_empty(),
+//             "Host should get no hints due to them turned off"
+//         );
+//         let inlay_cache = editor.inlay_hint_cache();
+//         assert_eq!(
+//             inlay_cache.version(),
+//             0,
+//             "Turned off hints should not generate version updates"
+//         );
+//     });
+
+//     executor.run_until_parked();
+//     editor_b.update(cx_b, |editor, _| {
+//         assert_eq!(
+//             vec!["initial hint".to_string()],
+//             extract_hint_labels(editor),
+//             "Client should get its first hints when opens an editor"
+//         );
+//         let inlay_cache = editor.inlay_hint_cache();
+//         assert_eq!(
+//             inlay_cache.version(),
+//             1,
+//             "Should update cache verison after first hints"
+//         );
+//     });
+
+//     other_hints.fetch_or(true, atomic::Ordering::Release);
+//     fake_language_server
+//         .request::<lsp::request::InlayHintRefreshRequest>(())
+//         .await
+//         .expect("inlay refresh request failed");
+//     executor.run_until_parked();
+//     editor_a.update(cx_a, |editor, _| {
+//         assert!(
+//             extract_hint_labels(editor).is_empty(),
+//             "Host should get nop hints due to them turned off, even after the /refresh"
+//         );
+//         let inlay_cache = editor.inlay_hint_cache();
+//         assert_eq!(
+//             inlay_cache.version(),
+//             0,
+//             "Turned off hints should not generate version updates, again"
+//         );
+//     });
+
+//     executor.run_until_parked();
+//     editor_b.update(cx_b, |editor, _| {
+//         assert_eq!(
+//             vec!["other hint".to_string()],
+//             extract_hint_labels(editor),
+//             "Guest should get a /refresh LSP request propagated by host despite host hints are off"
+//         );
+//         let inlay_cache = editor.inlay_hint_cache();
+//         assert_eq!(
+//             inlay_cache.version(),
+//             2,
+//             "Guest should accepted all edits and bump its cache version every time"
+//         );
+//     });
+// }
+
+// fn extract_hint_labels(editor: &Editor) -> Vec<String> {
+//     let mut labels = Vec::new();
+//     for hint in editor.inlay_hint_cache().hints() {
+//         match hint.label {
+//             project::InlayHintLabel::String(s) => labels.push(s),
+//             _ => unreachable!(),
+//         }
+//     }
+//     labels
+// }

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

@@ -5717,758 +5717,3 @@ async fn test_join_call_after_screen_was_shared(
         );
     });
 }
-
-//todo!(editor)
-// #[gpui::test(iterations = 10)]
-// async fn test_on_input_format_from_host_to_guest(
-//     executor: BackgroundExecutor,
-//     cx_a: &mut TestAppContext,
-//     cx_b: &mut TestAppContext,
-// ) {
-//     let mut server = TestServer::start(&executor).await;
-//     let client_a = server.create_client(cx_a, "user_a").await;
-//     let client_b = server.create_client(cx_b, "user_b").await;
-//     server
-//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-//         .await;
-//     let active_call_a = cx_a.read(ActiveCall::global);
-
-//     // Set up a fake language server.
-//     let mut language = Language::new(
-//         LanguageConfig {
-//             name: "Rust".into(),
-//             path_suffixes: vec!["rs".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     );
-//     let mut fake_language_servers = language
-//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-//             capabilities: lsp::ServerCapabilities {
-//                 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
-//                     first_trigger_character: ":".to_string(),
-//                     more_trigger_character: Some(vec![">".to_string()]),
-//                 }),
-//                 ..Default::default()
-//             },
-//             ..Default::default()
-//         }))
-//         .await;
-//     client_a.language_registry().add(Arc::new(language));
-
-//     client_a
-//         .fs()
-//         .insert_tree(
-//             "/a",
-//             json!({
-//                 "main.rs": "fn main() { a }",
-//                 "other.rs": "// Test file",
-//             }),
-//         )
-//         .await;
-//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-//     let project_id = active_call_a
-//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-//         .await
-//         .unwrap();
-//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-
-//     // Open a file in an editor as the host.
-//     let buffer_a = project_a
-//         .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 = window_a.add_view(cx_a, |cx| {
-//         Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)
-//     });
-
-//     let fake_language_server = fake_language_servers.next().await.unwrap();
-//     executor.run_until_parked();
-
-//     // Receive an OnTypeFormatting request as the host's language server.
-//     // Return some formattings from the host's language server.
-//     fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
-//         |params, _| async move {
-//             assert_eq!(
-//                 params.text_document_position.text_document.uri,
-//                 lsp::Url::from_file_path("/a/main.rs").unwrap(),
-//             );
-//             assert_eq!(
-//                 params.text_document_position.position,
-//                 lsp::Position::new(0, 14),
-//             );
-
-//             Ok(Some(vec![lsp::TextEdit {
-//                 new_text: "~<".to_string(),
-//                 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
-//             }]))
-//         },
-//     );
-
-//     // Open the buffer on the guest and see that the formattings worked
-//     let buffer_b = project_b
-//         .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
-//         .await
-//         .unwrap();
-
-//     // Type a on type formatting trigger character as the guest.
-//     editor_a.update(cx_a, |editor, cx| {
-//         cx.focus(&editor_a);
-//         editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
-//         editor.handle_input(">", cx);
-//     });
-
-//     executor.run_until_parked();
-
-//     buffer_b.read_with(cx_b, |buffer, _| {
-//         assert_eq!(buffer.text(), "fn main() { a>~< }")
-//     });
-
-//     // Undo should remove LSP edits first
-//     editor_a.update(cx_a, |editor, cx| {
-//         assert_eq!(editor.text(cx), "fn main() { a>~< }");
-//         editor.undo(&Undo, cx);
-//         assert_eq!(editor.text(cx), "fn main() { a> }");
-//     });
-//     executor.run_until_parked();
-
-//     buffer_b.read_with(cx_b, |buffer, _| {
-//         assert_eq!(buffer.text(), "fn main() { a> }")
-//     });
-
-//     editor_a.update(cx_a, |editor, cx| {
-//         assert_eq!(editor.text(cx), "fn main() { a> }");
-//         editor.undo(&Undo, cx);
-//         assert_eq!(editor.text(cx), "fn main() { a }");
-//     });
-//     executor.run_until_parked();
-
-//     buffer_b.read_with(cx_b, |buffer, _| {
-//         assert_eq!(buffer.text(), "fn main() { a }")
-//     });
-// }
-
-// #[gpui::test(iterations = 10)]
-// async fn test_on_input_format_from_guest_to_host(
-//     executor: BackgroundExecutor,
-//     cx_a: &mut TestAppContext,
-//     cx_b: &mut TestAppContext,
-// ) {
-//     let mut server = TestServer::start(&executor).await;
-//     let client_a = server.create_client(cx_a, "user_a").await;
-//     let client_b = server.create_client(cx_b, "user_b").await;
-//     server
-//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-//         .await;
-//     let active_call_a = cx_a.read(ActiveCall::global);
-
-//     // Set up a fake language server.
-//     let mut language = Language::new(
-//         LanguageConfig {
-//             name: "Rust".into(),
-//             path_suffixes: vec!["rs".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     );
-//     let mut fake_language_servers = language
-//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-//             capabilities: lsp::ServerCapabilities {
-//                 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
-//                     first_trigger_character: ":".to_string(),
-//                     more_trigger_character: Some(vec![">".to_string()]),
-//                 }),
-//                 ..Default::default()
-//             },
-//             ..Default::default()
-//         }))
-//         .await;
-//     client_a.language_registry().add(Arc::new(language));
-
-//     client_a
-//         .fs()
-//         .insert_tree(
-//             "/a",
-//             json!({
-//                 "main.rs": "fn main() { a }",
-//                 "other.rs": "// Test file",
-//             }),
-//         )
-//         .await;
-//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-//     let project_id = active_call_a
-//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-//         .await
-//         .unwrap();
-//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-
-//     // Open a file in an editor as the guest.
-//     let buffer_b = project_b
-//         .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 = window_b.add_view(cx_b, |cx| {
-//         Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
-//     });
-
-//     let fake_language_server = fake_language_servers.next().await.unwrap();
-//     executor.run_until_parked();
-//     // Type a on type formatting trigger character as the guest.
-//     editor_b.update(cx_b, |editor, cx| {
-//         editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
-//         editor.handle_input(":", cx);
-//         cx.focus(&editor_b);
-//     });
-
-//     // Receive an OnTypeFormatting request as the host's language server.
-//     // Return some formattings from the host's language server.
-//     cx_a.foreground().start_waiting();
-//     fake_language_server
-//         .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
-//             assert_eq!(
-//                 params.text_document_position.text_document.uri,
-//                 lsp::Url::from_file_path("/a/main.rs").unwrap(),
-//             );
-//             assert_eq!(
-//                 params.text_document_position.position,
-//                 lsp::Position::new(0, 14),
-//             );
-
-//             Ok(Some(vec![lsp::TextEdit {
-//                 new_text: "~:".to_string(),
-//                 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
-//             }]))
-//         })
-//         .next()
-//         .await
-//         .unwrap();
-//     cx_a.foreground().finish_waiting();
-
-//     // Open the buffer on the host and see that the formattings worked
-//     let buffer_a = project_a
-//         .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
-//         .await
-//         .unwrap();
-//     executor.run_until_parked();
-
-//     buffer_a.read_with(cx_a, |buffer, _| {
-//         assert_eq!(buffer.text(), "fn main() { a:~: }")
-//     });
-
-//     // Undo should remove LSP edits first
-//     editor_b.update(cx_b, |editor, cx| {
-//         assert_eq!(editor.text(cx), "fn main() { a:~: }");
-//         editor.undo(&Undo, cx);
-//         assert_eq!(editor.text(cx), "fn main() { a: }");
-//     });
-//     executor.run_until_parked();
-
-//     buffer_a.read_with(cx_a, |buffer, _| {
-//         assert_eq!(buffer.text(), "fn main() { a: }")
-//     });
-
-//     editor_b.update(cx_b, |editor, cx| {
-//         assert_eq!(editor.text(cx), "fn main() { a: }");
-//         editor.undo(&Undo, cx);
-//         assert_eq!(editor.text(cx), "fn main() { a }");
-//     });
-//     executor.run_until_parked();
-
-//     buffer_a.read_with(cx_a, |buffer, _| {
-//         assert_eq!(buffer.text(), "fn main() { a }")
-//     });
-// }
-
-//todo!(editor)
-// #[gpui::test(iterations = 10)]
-// async fn test_mutual_editor_inlay_hint_cache_update(
-//     executor: BackgroundExecutor,
-//     cx_a: &mut TestAppContext,
-//     cx_b: &mut TestAppContext,
-// ) {
-//     let mut server = TestServer::start(&executor).await;
-//     let client_a = server.create_client(cx_a, "user_a").await;
-//     let client_b = server.create_client(cx_b, "user_b").await;
-//     server
-//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-//         .await;
-//     let active_call_a = cx_a.read(ActiveCall::global);
-//     let active_call_b = cx_b.read(ActiveCall::global);
-
-//     cx_a.update(editor::init);
-//     cx_b.update(editor::init);
-
-//     cx_a.update(|cx| {
-//         cx.update_global(|store: &mut SettingsStore, cx| {
-//             store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-//                 settings.defaults.inlay_hints = Some(InlayHintSettings {
-//                     enabled: true,
-//                     show_type_hints: true,
-//                     show_parameter_hints: false,
-//                     show_other_hints: true,
-//                 })
-//             });
-//         });
-//     });
-//     cx_b.update(|cx| {
-//         cx.update_global(|store: &mut SettingsStore, cx| {
-//             store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-//                 settings.defaults.inlay_hints = Some(InlayHintSettings {
-//                     enabled: true,
-//                     show_type_hints: true,
-//                     show_parameter_hints: false,
-//                     show_other_hints: true,
-//                 })
-//             });
-//         });
-//     });
-
-//     let mut language = Language::new(
-//         LanguageConfig {
-//             name: "Rust".into(),
-//             path_suffixes: vec!["rs".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     );
-//     let mut fake_language_servers = language
-//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-//             capabilities: lsp::ServerCapabilities {
-//                 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
-//                 ..Default::default()
-//             },
-//             ..Default::default()
-//         }))
-//         .await;
-//     let language = Arc::new(language);
-//     client_a.language_registry().add(Arc::clone(&language));
-//     client_b.language_registry().add(language);
-
-//     // Client A opens a project.
-//     client_a
-//         .fs()
-//         .insert_tree(
-//             "/a",
-//             json!({
-//                 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
-//                 "other.rs": "// Test file",
-//             }),
-//         )
-//         .await;
-//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-//     active_call_a
-//         .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-//         .await
-//         .unwrap();
-//     let project_id = active_call_a
-//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-//         .await
-//         .unwrap();
-
-//     // Client B joins the project
-//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-//     active_call_b
-//         .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-//         .await
-//         .unwrap();
-
-//     let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-//     cx_a.foreground().start_waiting();
-
-//     // The host opens a rust file.
-//     let _buffer_a = project_a
-//         .update(cx_a, |project, cx| {
-//             project.open_local_buffer("/a/main.rs", cx)
-//         })
-//         .await
-//         .unwrap();
-//     let fake_language_server = fake_language_servers.next().await.unwrap();
-//     let editor_a = workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-
-//     // Set up the language server to return an additional inlay hint on each request.
-//     let edits_made = Arc::new(AtomicUsize::new(0));
-//     let closure_edits_made = Arc::clone(&edits_made);
-//     fake_language_server
-//         .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
-//             let task_edits_made = Arc::clone(&closure_edits_made);
-//             async move {
-//                 assert_eq!(
-//                     params.text_document.uri,
-//                     lsp::Url::from_file_path("/a/main.rs").unwrap(),
-//                 );
-//                 let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
-//                 Ok(Some(vec![lsp::InlayHint {
-//                     position: lsp::Position::new(0, edits_made as u32),
-//                     label: lsp::InlayHintLabel::String(edits_made.to_string()),
-//                     kind: None,
-//                     text_edits: None,
-//                     tooltip: None,
-//                     padding_left: None,
-//                     padding_right: None,
-//                     data: None,
-//                 }]))
-//             }
-//         })
-//         .next()
-//         .await
-//         .unwrap();
-
-//     executor.run_until_parked();
-
-//     let initial_edit = edits_made.load(atomic::Ordering::Acquire);
-//     editor_a.update(cx_a, |editor, _| {
-//         assert_eq!(
-//             vec![initial_edit.to_string()],
-//             extract_hint_labels(editor),
-//             "Host should get its first hints when opens an editor"
-//         );
-//         let inlay_cache = editor.inlay_hint_cache();
-//         assert_eq!(
-//             inlay_cache.version(),
-//             1,
-//             "Host editor update the cache version after every cache/view change",
-//         );
-//     });
-//     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-//     let editor_b = workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-
-//     executor.run_until_parked();
-//     editor_b.update(cx_b, |editor, _| {
-//         assert_eq!(
-//             vec![initial_edit.to_string()],
-//             extract_hint_labels(editor),
-//             "Client should get its first hints when opens an editor"
-//         );
-//         let inlay_cache = editor.inlay_hint_cache();
-//         assert_eq!(
-//             inlay_cache.version(),
-//             1,
-//             "Guest editor update the cache version after every cache/view change"
-//         );
-//     });
-
-//     let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
-//     editor_b.update(cx_b, |editor, cx| {
-//         editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
-//         editor.handle_input(":", cx);
-//         cx.focus(&editor_b);
-//     });
-
-//     executor.run_until_parked();
-//     editor_a.update(cx_a, |editor, _| {
-//         assert_eq!(
-//             vec![after_client_edit.to_string()],
-//             extract_hint_labels(editor),
-//         );
-//         let inlay_cache = editor.inlay_hint_cache();
-//         assert_eq!(inlay_cache.version(), 2);
-//     });
-//     editor_b.update(cx_b, |editor, _| {
-//         assert_eq!(
-//             vec![after_client_edit.to_string()],
-//             extract_hint_labels(editor),
-//         );
-//         let inlay_cache = editor.inlay_hint_cache();
-//         assert_eq!(inlay_cache.version(), 2);
-//     });
-
-//     let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
-//     editor_a.update(cx_a, |editor, cx| {
-//         editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
-//         editor.handle_input("a change to increment both buffers' versions", cx);
-//         cx.focus(&editor_a);
-//     });
-
-//     executor.run_until_parked();
-//     editor_a.update(cx_a, |editor, _| {
-//         assert_eq!(
-//             vec![after_host_edit.to_string()],
-//             extract_hint_labels(editor),
-//         );
-//         let inlay_cache = editor.inlay_hint_cache();
-//         assert_eq!(inlay_cache.version(), 3);
-//     });
-//     editor_b.update(cx_b, |editor, _| {
-//         assert_eq!(
-//             vec![after_host_edit.to_string()],
-//             extract_hint_labels(editor),
-//         );
-//         let inlay_cache = editor.inlay_hint_cache();
-//         assert_eq!(inlay_cache.version(), 3);
-//     });
-
-//     let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
-//     fake_language_server
-//         .request::<lsp::request::InlayHintRefreshRequest>(())
-//         .await
-//         .expect("inlay refresh request failed");
-
-//     executor.run_until_parked();
-//     editor_a.update(cx_a, |editor, _| {
-//         assert_eq!(
-//             vec![after_special_edit_for_refresh.to_string()],
-//             extract_hint_labels(editor),
-//             "Host should react to /refresh LSP request"
-//         );
-//         let inlay_cache = editor.inlay_hint_cache();
-//         assert_eq!(
-//             inlay_cache.version(),
-//             4,
-//             "Host should accepted all edits and bump its cache version every time"
-//         );
-//     });
-//     editor_b.update(cx_b, |editor, _| {
-//         assert_eq!(
-//             vec![after_special_edit_for_refresh.to_string()],
-//             extract_hint_labels(editor),
-//             "Guest should get a /refresh LSP request propagated by host"
-//         );
-//         let inlay_cache = editor.inlay_hint_cache();
-//         assert_eq!(
-//             inlay_cache.version(),
-//             4,
-//             "Guest should accepted all edits and bump its cache version every time"
-//         );
-//     });
-// }
-
-//todo!(editor)
-// #[gpui::test(iterations = 10)]
-// async fn test_inlay_hint_refresh_is_forwarded(
-//     executor: BackgroundExecutor,
-//     cx_a: &mut TestAppContext,
-//     cx_b: &mut TestAppContext,
-// ) {
-//     let mut server = TestServer::start(&executor).await;
-//     let client_a = server.create_client(cx_a, "user_a").await;
-//     let client_b = server.create_client(cx_b, "user_b").await;
-//     server
-//         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-//         .await;
-//     let active_call_a = cx_a.read(ActiveCall::global);
-//     let active_call_b = cx_b.read(ActiveCall::global);
-
-//     cx_a.update(editor::init);
-//     cx_b.update(editor::init);
-
-//     cx_a.update(|cx| {
-//         cx.update_global(|store: &mut SettingsStore, cx| {
-//             store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-//                 settings.defaults.inlay_hints = Some(InlayHintSettings {
-//                     enabled: false,
-//                     show_type_hints: false,
-//                     show_parameter_hints: false,
-//                     show_other_hints: false,
-//                 })
-//             });
-//         });
-//     });
-//     cx_b.update(|cx| {
-//         cx.update_global(|store: &mut SettingsStore, cx| {
-//             store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-//                 settings.defaults.inlay_hints = Some(InlayHintSettings {
-//                     enabled: true,
-//                     show_type_hints: true,
-//                     show_parameter_hints: true,
-//                     show_other_hints: true,
-//                 })
-//             });
-//         });
-//     });
-
-//     let mut language = Language::new(
-//         LanguageConfig {
-//             name: "Rust".into(),
-//             path_suffixes: vec!["rs".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     );
-//     let mut fake_language_servers = language
-//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-//             capabilities: lsp::ServerCapabilities {
-//                 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
-//                 ..Default::default()
-//             },
-//             ..Default::default()
-//         }))
-//         .await;
-//     let language = Arc::new(language);
-//     client_a.language_registry().add(Arc::clone(&language));
-//     client_b.language_registry().add(language);
-
-//     client_a
-//         .fs()
-//         .insert_tree(
-//             "/a",
-//             json!({
-//                 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
-//                 "other.rs": "// Test file",
-//             }),
-//         )
-//         .await;
-//     let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-//     active_call_a
-//         .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-//         .await
-//         .unwrap();
-//     let project_id = active_call_a
-//         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-//         .await
-//         .unwrap();
-
-//     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-//     active_call_b
-//         .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-//         .await
-//         .unwrap();
-
-//     let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-//     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-//     cx_a.foreground().start_waiting();
-//     cx_b.foreground().start_waiting();
-
-//     let editor_a = workspace_a
-//         .update(cx_a, |workspace, cx| {
-//             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-
-//     let editor_b = workspace_b
-//         .update(cx_b, |workspace, cx| {
-//             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
-//         })
-//         .await
-//         .unwrap()
-//         .downcast::<Editor>()
-//         .unwrap();
-
-//     let other_hints = Arc::new(AtomicBool::new(false));
-//     let fake_language_server = fake_language_servers.next().await.unwrap();
-//     let closure_other_hints = Arc::clone(&other_hints);
-//     fake_language_server
-//         .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
-//             let task_other_hints = Arc::clone(&closure_other_hints);
-//             async move {
-//                 assert_eq!(
-//                     params.text_document.uri,
-//                     lsp::Url::from_file_path("/a/main.rs").unwrap(),
-//                 );
-//                 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
-//                 let character = if other_hints { 0 } else { 2 };
-//                 let label = if other_hints {
-//                     "other hint"
-//                 } else {
-//                     "initial hint"
-//                 };
-//                 Ok(Some(vec![lsp::InlayHint {
-//                     position: lsp::Position::new(0, character),
-//                     label: lsp::InlayHintLabel::String(label.to_string()),
-//                     kind: None,
-//                     text_edits: None,
-//                     tooltip: None,
-//                     padding_left: None,
-//                     padding_right: None,
-//                     data: None,
-//                 }]))
-//             }
-//         })
-//         .next()
-//         .await
-//         .unwrap();
-//     cx_a.foreground().finish_waiting();
-//     cx_b.foreground().finish_waiting();
-
-//     executor.run_until_parked();
-//     editor_a.update(cx_a, |editor, _| {
-//         assert!(
-//             extract_hint_labels(editor).is_empty(),
-//             "Host should get no hints due to them turned off"
-//         );
-//         let inlay_cache = editor.inlay_hint_cache();
-//         assert_eq!(
-//             inlay_cache.version(),
-//             0,
-//             "Turned off hints should not generate version updates"
-//         );
-//     });
-
-//     executor.run_until_parked();
-//     editor_b.update(cx_b, |editor, _| {
-//         assert_eq!(
-//             vec!["initial hint".to_string()],
-//             extract_hint_labels(editor),
-//             "Client should get its first hints when opens an editor"
-//         );
-//         let inlay_cache = editor.inlay_hint_cache();
-//         assert_eq!(
-//             inlay_cache.version(),
-//             1,
-//             "Should update cache verison after first hints"
-//         );
-//     });
-
-//     other_hints.fetch_or(true, atomic::Ordering::Release);
-//     fake_language_server
-//         .request::<lsp::request::InlayHintRefreshRequest>(())
-//         .await
-//         .expect("inlay refresh request failed");
-//     executor.run_until_parked();
-//     editor_a.update(cx_a, |editor, _| {
-//         assert!(
-//             extract_hint_labels(editor).is_empty(),
-//             "Host should get nop hints due to them turned off, even after the /refresh"
-//         );
-//         let inlay_cache = editor.inlay_hint_cache();
-//         assert_eq!(
-//             inlay_cache.version(),
-//             0,
-//             "Turned off hints should not generate version updates, again"
-//         );
-//     });
-
-//     executor.run_until_parked();
-//     editor_b.update(cx_b, |editor, _| {
-//         assert_eq!(
-//             vec!["other hint".to_string()],
-//             extract_hint_labels(editor),
-//             "Guest should get a /refresh LSP request propagated by host despite host hints are off"
-//         );
-//         let inlay_cache = editor.inlay_hint_cache();
-//         assert_eq!(
-//             inlay_cache.version(),
-//             2,
-//             "Guest should accepted all edits and bump its cache version every time"
-//         );
-//     });
-// }
-
-// fn extract_hint_labels(editor: &Editor) -> Vec<String> {
-//     let mut labels = Vec::new();
-//     for hint in editor.inlay_hint_cache().hints() {
-//         match hint.label {
-//             project::InlayHintLabel::String(s) => labels.push(s),
-//             _ => unreachable!(),
-//         }
-//     }
-//     labels
-// }

crates/collab2/src/tests/test_server.rs 🔗

@@ -208,11 +208,11 @@ impl TestServer {
                 })
             });
 
-        let fs = FakeFs::new(cx.executor().clone());
+        let fs = FakeFs::new(cx.executor());
         let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
         let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
         let mut language_registry = LanguageRegistry::test();
-        language_registry.set_executor(cx.executor().clone());
+        language_registry.set_executor(cx.executor());
         let app_state = Arc::new(workspace::AppState {
             client: client.clone(),
             user_store: user_store.clone(),

crates/command_palette2/src/command_palette.rs 🔗

@@ -1,14 +1,17 @@
 use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke,
-    ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext,
-    WeakView, WindowContext,
+    actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
+    Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
+    WindowContext,
 };
 use picker::{Picker, PickerDelegate};
-use std::cmp::{self, Reverse};
+use std::{
+    cmp::{self, Reverse},
+    sync::Arc,
+};
 use theme::ActiveTheme;
-use ui::{v_stack, Label, StyledExt};
+use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt};
 use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,
@@ -127,16 +130,7 @@ impl CommandPaletteDelegate {
     ) -> Self {
         Self {
             command_palette,
-            matches: commands
-                .iter()
-                .enumerate()
-                .map(|(i, command)| StringMatch {
-                    candidate_id: i,
-                    string: command.name.clone(),
-                    positions: Vec::new(),
-                    score: 0.0,
-                })
-                .collect(),
+            matches: vec![],
             commands,
             selected_ix: 0,
             previous_focus_handle,
@@ -147,6 +141,10 @@ impl CommandPaletteDelegate {
 impl PickerDelegate for CommandPaletteDelegate {
     type ListItem = Div<Picker<Self>>;
 
+    fn placeholder_text(&self) -> Arc<str> {
+        "Execute a command...".into()
+    }
+
     fn match_count(&self) -> usize {
         self.matches.len()
     }
@@ -296,11 +294,10 @@ impl PickerDelegate for CommandPaletteDelegate {
         cx: &mut ViewContext<Picker<Self>>,
     ) -> Self::ListItem {
         let colors = cx.theme().colors();
-        let Some(command) = self
-            .matches
-            .get(ix)
-            .and_then(|m| self.commands.get(m.candidate_id))
-        else {
+        let Some(r#match) = self.matches.get(ix) else {
+            return div();
+        };
+        let Some(command) = self.commands.get(r#match.candidate_id) else {
             return div();
         };
 
@@ -312,63 +309,16 @@ impl PickerDelegate for CommandPaletteDelegate {
             .rounded_md()
             .when(selected, |this| this.bg(colors.ghost_element_selected))
             .hover(|this| this.bg(colors.ghost_element_hover))
-            .child(Label::new(command.name.clone()))
+            .child(
+                h_stack()
+                    .justify_between()
+                    .child(HighlightedLabel::new(
+                        command.name.clone(),
+                        r#match.positions.clone(),
+                    ))
+                    .children(KeyBinding::for_action(&*command.action, cx)),
+            )
     }
-
-    // fn render_match(
-    //     &self,
-    //     ix: usize,
-    //     mouse_state: &mut MouseState,
-    //     selected: bool,
-    //     cx: &gpui::AppContext,
-    // ) -> AnyElement<Picker<Self>> {
-    //     let mat = &self.matches[ix];
-    //     let command = &self.actions[mat.candidate_id];
-    //     let theme = theme::current(cx);
-    //     let style = theme.picker.item.in_state(selected).style_for(mouse_state);
-    //     let key_style = &theme.command_palette.key.in_state(selected);
-    //     let keystroke_spacing = theme.command_palette.keystroke_spacing;
-
-    //     Flex::row()
-    //         .with_child(
-    //             Label::new(mat.string.clone(), style.label.clone())
-    //                 .with_highlights(mat.positions.clone()),
-    //         )
-    //         .with_children(command.keystrokes.iter().map(|keystroke| {
-    //             Flex::row()
-    //                 .with_children(
-    //                     [
-    //                         (keystroke.ctrl, "^"),
-    //                         (keystroke.alt, "⌥"),
-    //                         (keystroke.cmd, "⌘"),
-    //                         (keystroke.shift, "⇧"),
-    //                     ]
-    //                     .into_iter()
-    //                     .filter_map(|(modifier, label)| {
-    //                         if modifier {
-    //                             Some(
-    //                                 Label::new(label, key_style.label.clone())
-    //                                     .contained()
-    //                                     .with_style(key_style.container),
-    //                             )
-    //                         } else {
-    //                             None
-    //                         }
-    //                     }),
-    //                 )
-    //                 .with_child(
-    //                     Label::new(keystroke.key.clone(), key_style.label.clone())
-    //                         .contained()
-    //                         .with_style(key_style.container),
-    //                 )
-    //                 .contained()
-    //                 .with_margin_left(keystroke_spacing)
-    //                 .flex_float()
-    //         }))
-    //         .contained()
-    //         .with_style(style.container)
-    //         .into_any()
-    // }
 }
 
 fn humanize_action_name(name: &str) -> String {

crates/copilot2/Cargo.toml 🔗

@@ -24,7 +24,7 @@ collections = { path = "../collections" }
 gpui = { package = "gpui2", path = "../gpui2" }
 language = { package = "language2", path = "../language2" }
 settings = { package = "settings2", path = "../settings2" }
-theme = { path = "../theme" }
+theme = { package = "theme2", path = "../theme2" }
 lsp = { package = "lsp2", path = "../lsp2" }
 node_runtime = { path = "../node_runtime"}
 util = { path = "../util" }

crates/copilot2/src/copilot2.rs 🔗

@@ -351,28 +351,29 @@ impl Copilot {
         }
     }
 
-    // #[cfg(any(test, feature = "test-support"))]
-    // pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle<Self>, lsp::FakeLanguageServer) {
-    //     use node_runtime::FakeNodeRuntime;
-
-    //     let (server, fake_server) =
-    //         LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
-    //     let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
-    //     let node_runtime = FakeNodeRuntime::new();
-    //     let this = cx.add_model(|_| Self {
-    //         server_id: LanguageServerId(0),
-    //         http: http.clone(),
-    //         node_runtime,
-    //         server: CopilotServer::Running(RunningCopilotServer {
-    //             name: LanguageServerName(Arc::from("copilot")),
-    //             lsp: Arc::new(server),
-    //             sign_in_status: SignInStatus::Authorized,
-    //             registered_buffers: Default::default(),
-    //         }),
-    //         buffers: Default::default(),
-    //     });
-    //     (this, fake_server)
-    // }
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn fake(cx: &mut gpui::TestAppContext) -> (Model<Self>, lsp::FakeLanguageServer) {
+        use node_runtime::FakeNodeRuntime;
+
+        let (server, fake_server) =
+            LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
+        let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
+        let node_runtime = FakeNodeRuntime::new();
+        let this = cx.build_model(|cx| Self {
+            server_id: LanguageServerId(0),
+            http: http.clone(),
+            node_runtime,
+            server: CopilotServer::Running(RunningCopilotServer {
+                name: LanguageServerName(Arc::from("copilot")),
+                lsp: Arc::new(server),
+                sign_in_status: SignInStatus::Authorized,
+                registered_buffers: Default::default(),
+            }),
+            _subscription: cx.on_app_quit(Self::shutdown_language_server),
+            buffers: Default::default(),
+        });
+        (this, fake_server)
+    }
 
     fn start_language_server(
         new_server_id: LanguageServerId,

crates/editor2/Cargo.toml 🔗

@@ -27,7 +27,6 @@ client = { package = "client2", path = "../client2" }
 clock = { path = "../clock" }
 copilot = { package="copilot2", path = "../copilot2" }
 db = { package="db2", path = "../db2" }
-drag_and_drop = { path = "../drag_and_drop" }
 collections = { path = "../collections" }
 # context_menu = { path = "../context_menu" }
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }

crates/editor2/src/display_map.rs 🔗

@@ -578,12 +578,7 @@ impl DisplaySnapshot {
             line.push_str(chunk.chunk);
 
             let text_style = if let Some(style) = chunk.style {
-                editor_style
-                    .text
-                    .clone()
-                    .highlight(style)
-                    .map(Cow::Owned)
-                    .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text))
+                Cow::Owned(editor_style.text.clone().highlight(style))
             } else {
                 Cow::Borrowed(&editor_style.text)
             };

crates/editor2/src/display_map/block_map.rs 🔗

@@ -2,9 +2,9 @@ use super::{
     wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
     Highlights,
 };
-use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _};
+use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _};
 use collections::{Bound, HashMap, HashSet};
-use gpui::{AnyElement, ViewContext};
+use gpui::{AnyElement, Pixels, ViewContext};
 use language::{BufferSnapshot, Chunk, Patch, Point};
 use parking_lot::Mutex;
 use std::{
@@ -82,13 +82,13 @@ pub enum BlockStyle {
 
 pub struct BlockContext<'a, 'b> {
     pub view_context: &'b mut ViewContext<'a, Editor>,
-    pub anchor_x: f32,
-    pub scroll_x: f32,
-    pub gutter_width: f32,
-    pub gutter_padding: f32,
-    pub em_width: f32,
-    pub line_height: f32,
+    pub anchor_x: Pixels,
+    pub gutter_width: Pixels,
+    pub gutter_padding: Pixels,
+    pub em_width: Pixels,
+    pub line_height: Pixels,
     pub block_id: usize,
+    pub editor_style: &'b EditorStyle,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]

crates/editor2/src/editor.rs 🔗

@@ -22,7 +22,7 @@ mod editor_tests;
 pub mod test;
 use ::git::diff::DiffHunk;
 use aho_corasick::AhoCorasick;
-use anyhow::{Context as _, Result};
+use anyhow::{anyhow, Context as _, Result};
 use blink_manager::BlinkManager;
 use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings};
 use clock::ReplicaId;
@@ -39,12 +39,12 @@ use futures::FutureExt;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use git::diff_hunk_to_display;
 use gpui::{
-    action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext,
-    AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
-    DispatchContext, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight,
-    HighlightStyle, Hsla, InputHandler, Model, MouseButton, ParentElement, Pixels, Render,
-    StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View,
-    ViewContext, VisualContext, WeakView, WindowContext,
+    action, actions, div, point, prelude::*, px, relative, rems, render_view, size, uniform_list,
+    AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem,
+    Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight,
+    HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels,
+    Render, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext,
+    VisualContext, WeakView, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -69,7 +69,7 @@ pub use multi_buffer::{
 };
 use ordered_float::OrderedFloat;
 use parking_lot::{Mutex, RwLock};
-use project::{FormatTrigger, Location, Project, ProjectTransaction};
+use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
 use rand::prelude::*;
 use rpc::proto::*;
 use scroll::{
@@ -97,10 +97,12 @@ use text::{OffsetUtf16, Rope};
 use theme::{
     ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
 };
-use ui::{IconButton, StyledExt};
+use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{
-    item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace,
+    item::{ItemEvent, ItemHandle},
+    searchable::SearchEvent,
+    ItemNavHistory, SplitDirection, ViewId, Workspace,
 };
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@@ -646,7 +648,7 @@ pub struct Editor {
     collapse_matches: bool,
     autoindent_mode: Option<AutoindentMode>,
     workspace: Option<(WeakView<Workspace>, i64)>,
-    keymap_context_layers: BTreeMap<TypeId, DispatchContext>,
+    keymap_context_layers: BTreeMap<TypeId, KeyContext>,
     input_enabled: bool,
     read_only: bool,
     leader_peer_id: Option<PeerId>,
@@ -1878,10 +1880,8 @@ impl Editor {
         );
 
         let focus_handle = cx.focus_handle();
-        cx.on_focus_in(&focus_handle, Self::handle_focus_in)
-            .detach();
-        cx.on_focus_out(&focus_handle, Self::handle_focus_out)
-            .detach();
+        cx.on_focus(&focus_handle, Self::handle_focus).detach();
+        cx.on_blur(&focus_handle, Self::handle_blur).detach();
 
         let mut this = Self {
             handle: cx.view().downgrade(),
@@ -1981,9 +1981,9 @@ impl Editor {
         this
     }
 
-    fn dispatch_context(&self, cx: &AppContext) -> DispatchContext {
-        let mut dispatch_context = DispatchContext::default();
-        dispatch_context.insert("Editor");
+    fn dispatch_context(&self, cx: &AppContext) -> KeyContext {
+        let mut dispatch_context = KeyContext::default();
+        dispatch_context.add("Editor");
         let mode = match self.mode {
             EditorMode::SingleLine => "single_line",
             EditorMode::AutoHeight { .. } => "auto_height",
@@ -1991,17 +1991,17 @@ impl Editor {
         };
         dispatch_context.set("mode", mode);
         if self.pending_rename.is_some() {
-            dispatch_context.insert("renaming");
+            dispatch_context.add("renaming");
         }
         if self.context_menu_visible() {
             match self.context_menu.read().as_ref() {
                 Some(ContextMenu::Completions(_)) => {
-                    dispatch_context.insert("menu");
-                    dispatch_context.insert("showing_completions")
+                    dispatch_context.add("menu");
+                    dispatch_context.add("showing_completions")
                 }
                 Some(ContextMenu::CodeActions(_)) => {
-                    dispatch_context.insert("menu");
-                    dispatch_context.insert("showing_code_actions")
+                    dispatch_context.add("menu");
+                    dispatch_context.add("showing_code_actions")
                 }
                 None => {}
             }
@@ -2023,24 +2023,24 @@ impl Editor {
         dispatch_context
     }
 
-    //     pub fn new_file(
-    //         workspace: &mut Workspace,
-    //         _: &workspace::NewFile,
-    //         cx: &mut ViewContext<Workspace>,
-    //     ) {
-    //         let project = workspace.project().clone();
-    //         if project.read(cx).is_remote() {
-    //             cx.propagate();
-    //         } else if let Some(buffer) = project
-    //             .update(cx, |project, cx| project.create_buffer("", None, cx))
-    //             .log_err()
-    //         {
-    //             workspace.add_item(
-    //                 Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
-    //                 cx,
-    //             );
-    //         }
-    //     }
+    pub fn new_file(
+        workspace: &mut Workspace,
+        _: &workspace::NewFile,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let project = workspace.project().clone();
+        if project.read(cx).is_remote() {
+            cx.propagate();
+        } else if let Some(buffer) = project
+            .update(cx, |project, cx| project.create_buffer("", None, cx))
+            .log_err()
+        {
+            workspace.add_item(
+                Box::new(cx.build_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
+                cx,
+            );
+        }
+    }
 
     //     pub fn new_file_in_direction(
     //         workspace: &mut Workspace,
@@ -2124,17 +2124,17 @@ impl Editor {
     //         )
     //     }
 
-    //     pub fn mode(&self) -> EditorMode {
-    //         self.mode
-    //     }
+    pub fn mode(&self) -> EditorMode {
+        self.mode
+    }
 
-    //     pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> {
-    //         self.collaboration_hub.as_deref()
-    //     }
+    pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> {
+        self.collaboration_hub.as_deref()
+    }
 
-    //     pub fn set_collaboration_hub(&mut self, hub: Box<dyn CollaborationHub>) {
-    //         self.collaboration_hub = Some(hub);
-    //     }
+    pub fn set_collaboration_hub(&mut self, hub: Box<dyn CollaborationHub>) {
+        self.collaboration_hub = Some(hub);
+    }
 
     pub fn set_placeholder_text(
         &mut self,
@@ -7588,53 +7588,47 @@ impl Editor {
         })
     }
 
-    //     pub fn find_all_references(
-    //         workspace: &mut Workspace,
-    //         _: &FindAllReferences,
-    //         cx: &mut ViewContext<Workspace>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let active_item = workspace.active_item(cx)?;
-    //         let editor_handle = active_item.act_as::<Self>(cx)?;
-
-    //         let editor = editor_handle.read(cx);
-    //         let buffer = editor.buffer.read(cx);
-    //         let head = editor.selections.newest::<usize>(cx).head();
-    //         let (buffer, head) = buffer.text_anchor_for_position(head, cx)?;
-    //         let replica_id = editor.replica_id(cx);
-
-    //         let project = workspace.project().clone();
-    //         let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
-    //         Some(cx.spawn_labeled(
-    //             "Finding All References...",
-    //             |workspace, mut cx| async move {
-    //                 let locations = references.await?;
-    //                 if locations.is_empty() {
-    //                     return Ok(());
-    //                 }
+    pub fn find_all_references(
+        &mut self,
+        _: &FindAllReferences,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let buffer = self.buffer.read(cx);
+        let head = self.selections.newest::<usize>(cx).head();
+        let (buffer, head) = buffer.text_anchor_for_position(head, cx)?;
+        let replica_id = self.replica_id(cx);
 
-    //                 workspace.update(&mut cx, |workspace, cx| {
-    //                     let title = locations
-    //                         .first()
-    //                         .as_ref()
-    //                         .map(|location| {
-    //                             let buffer = location.buffer.read(cx);
-    //                             format!(
-    //                                 "References to `{}`",
-    //                                 buffer
-    //                                     .text_for_range(location.range.clone())
-    //                                     .collect::<String>()
-    //                             )
-    //                         })
-    //                         .unwrap();
-    //                     Self::open_locations_in_multibuffer(
-    //                         workspace, locations, replica_id, title, false, cx,
-    //                     );
-    //                 })?;
+        let workspace = self.workspace()?;
+        let project = workspace.read(cx).project().clone();
+        let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
+        Some(cx.spawn(|_, mut cx| async move {
+            let locations = references.await?;
+            if locations.is_empty() {
+                return Ok(());
+            }
+
+            workspace.update(&mut cx, |workspace, cx| {
+                let title = locations
+                    .first()
+                    .as_ref()
+                    .map(|location| {
+                        let buffer = location.buffer.read(cx);
+                        format!(
+                            "References to `{}`",
+                            buffer
+                                .text_for_range(location.range.clone())
+                                .collect::<String>()
+                        )
+                    })
+                    .unwrap();
+                Self::open_locations_in_multibuffer(
+                    workspace, locations, replica_id, title, false, cx,
+                );
+            })?;
 
-    //                 Ok(())
-    //             },
-    //         ))
-    //     }
+            Ok(())
+        }))
+    }
 
     /// Opens a multibuffer with the given project locations in it
     pub fn open_locations_in_multibuffer(
@@ -7685,7 +7679,7 @@ impl Editor {
         editor.update(cx, |editor, cx| {
             editor.highlight_background::<Self>(
                 ranges_to_highlight,
-                |theme| todo!("theme.editor.highlighted_line_background"),
+                |theme| theme.editor_highlighted_line_background,
                 cx,
             );
         });
@@ -7696,183 +7690,210 @@ impl Editor {
         }
     }
 
-    // pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
-    //     use language::ToOffset as _;
+    pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+        use language::ToOffset as _;
 
-    //     let project = self.project.clone()?;
-    //     let selection = self.selections.newest_anchor().clone();
-    //     let (cursor_buffer, cursor_buffer_position) = self
-    //         .buffer
-    //         .read(cx)
-    //         .text_anchor_for_position(selection.head(), cx)?;
-    //     let (tail_buffer, _) = self
-    //         .buffer
-    //         .read(cx)
-    //         .text_anchor_for_position(selection.tail(), cx)?;
-    //     if tail_buffer != cursor_buffer {
-    //         return None;
-    //     }
+        let project = self.project.clone()?;
+        let selection = self.selections.newest_anchor().clone();
+        let (cursor_buffer, cursor_buffer_position) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(selection.head(), cx)?;
+        let (tail_buffer, _) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(selection.tail(), cx)?;
+        if tail_buffer != cursor_buffer {
+            return None;
+        }
 
-    //     let snapshot = cursor_buffer.read(cx).snapshot();
-    //     let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
-    //     let prepare_rename = project.update(cx, |project, cx| {
-    //         project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx)
-    //     });
+        let snapshot = cursor_buffer.read(cx).snapshot();
+        let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
+        let prepare_rename = project.update(cx, |project, cx| {
+            project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx)
+        });
 
-    //     Some(cx.spawn(|this, mut cx| async move {
-    //         let rename_range = if let Some(range) = prepare_rename.await? {
-    //             Some(range)
-    //         } else {
-    //             this.update(&mut cx, |this, cx| {
-    //                 let buffer = this.buffer.read(cx).snapshot(cx);
-    //                 let mut buffer_highlights = this
-    //                     .document_highlights_for_position(selection.head(), &buffer)
-    //                     .filter(|highlight| {
-    //                         highlight.start.excerpt_id == selection.head().excerpt_id
-    //                             && highlight.end.excerpt_id == selection.head().excerpt_id
-    //                     });
-    //                 buffer_highlights
-    //                     .next()
-    //                     .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor)
-    //             })?
-    //         };
-    //         if let Some(rename_range) = rename_range {
-    //             let rename_buffer_range = rename_range.to_offset(&snapshot);
-    //             let cursor_offset_in_rename_range =
-    //                 cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
-
-    //             this.update(&mut cx, |this, cx| {
-    //                 this.take_rename(false, cx);
-    //                 let buffer = this.buffer.read(cx).read(cx);
-    //                 let cursor_offset = selection.head().to_offset(&buffer);
-    //                 let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range);
-    //                 let rename_end = rename_start + rename_buffer_range.len();
-    //                 let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end);
-    //                 let mut old_highlight_id = None;
-    //                 let old_name: Arc<str> = buffer
-    //                     .chunks(rename_start..rename_end, true)
-    //                     .map(|chunk| {
-    //                         if old_highlight_id.is_none() {
-    //                             old_highlight_id = chunk.syntax_highlight_id;
-    //                         }
-    //                         chunk.text
-    //                     })
-    //                     .collect::<String>()
-    //                     .into();
-
-    //                 drop(buffer);
-
-    //                 // Position the selection in the rename editor so that it matches the current selection.
-    //                 this.show_local_selections = false;
-    //                 let rename_editor = cx.build_view(|cx| {
-    //                     let mut editor = Editor::single_line(cx);
-    //                     if let Some(old_highlight_id) = old_highlight_id {
-    //                         editor.override_text_style =
-    //                             Some(Box::new(move |style| old_highlight_id.style(&style.syntax)));
-    //                     }
-    //                     editor.buffer.update(cx, |buffer, cx| {
-    //                         buffer.edit([(0..0, old_name.clone())], None, cx)
-    //                     });
-    //                     editor.select_all(&SelectAll, cx);
-    //                     editor
-    //                 });
+        Some(cx.spawn(|this, mut cx| async move {
+            let rename_range = if let Some(range) = prepare_rename.await? {
+                Some(range)
+            } else {
+                this.update(&mut cx, |this, cx| {
+                    let buffer = this.buffer.read(cx).snapshot(cx);
+                    let mut buffer_highlights = this
+                        .document_highlights_for_position(selection.head(), &buffer)
+                        .filter(|highlight| {
+                            highlight.start.excerpt_id == selection.head().excerpt_id
+                                && highlight.end.excerpt_id == selection.head().excerpt_id
+                        });
+                    buffer_highlights
+                        .next()
+                        .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor)
+                })?
+            };
+            if let Some(rename_range) = rename_range {
+                let rename_buffer_range = rename_range.to_offset(&snapshot);
+                let cursor_offset_in_rename_range =
+                    cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
 
-    //                 let ranges = this
-    //                     .clear_background_highlights::<DocumentHighlightWrite>(cx)
-    //                     .into_iter()
-    //                     .flat_map(|(_, ranges)| ranges.into_iter())
-    //                     .chain(
-    //                         this.clear_background_highlights::<DocumentHighlightRead>(cx)
-    //                             .into_iter()
-    //                             .flat_map(|(_, ranges)| ranges.into_iter()),
-    //                     )
-    //                     .collect();
-
-    //                 this.highlight_text::<Rename>(
-    //                     ranges,
-    //                     HighlightStyle {
-    //                         fade_out: Some(style.rename_fade),
-    //                         ..Default::default()
-    //                     },
-    //                     cx,
-    //                 );
-    //                 cx.focus(&rename_editor);
-    //                 let block_id = this.insert_blocks(
-    //                     [BlockProperties {
-    //                         style: BlockStyle::Flex,
-    //                         position: range.start.clone(),
-    //                         height: 1,
-    //                         render: Arc::new({
-    //                             let editor = rename_editor.clone();
-    //                             move |cx: &mut BlockContext| {
-    //                                 ChildView::new(&editor, cx)
-    //                                     .contained()
-    //                                     .with_padding_left(cx.anchor_x)
-    //                                     .into_any()
-    //                             }
-    //                         }),
-    //                         disposition: BlockDisposition::Below,
-    //                     }],
-    //                     Some(Autoscroll::fit()),
-    //                     cx,
-    //                 )[0];
-    //                 this.pending_rename = Some(RenameState {
-    //                     range,
-    //                     old_name,
-    //                     editor: rename_editor,
-    //                     block_id,
-    //                 });
-    //             })?;
-    //         }
+                this.update(&mut cx, |this, cx| {
+                    this.take_rename(false, cx);
+                    let buffer = this.buffer.read(cx).read(cx);
+                    let cursor_offset = selection.head().to_offset(&buffer);
+                    let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range);
+                    let rename_end = rename_start + rename_buffer_range.len();
+                    let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end);
+                    let mut old_highlight_id = None;
+                    let old_name: Arc<str> = buffer
+                        .chunks(rename_start..rename_end, true)
+                        .map(|chunk| {
+                            if old_highlight_id.is_none() {
+                                old_highlight_id = chunk.syntax_highlight_id;
+                            }
+                            chunk.text
+                        })
+                        .collect::<String>()
+                        .into();
 
-    //         Ok(())
-    //     }))
-    // }
+                    drop(buffer);
 
-    //     pub fn confirm_rename(
-    //         workspace: &mut Workspace,
-    //         _: &ConfirmRename,
-    //         cx: &mut ViewContext<Workspace>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
-
-    //         let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| {
-    //             let rename = editor.take_rename(false, cx)?;
-    //             let buffer = editor.buffer.read(cx);
-    //             let (start_buffer, start) =
-    //                 buffer.text_anchor_for_position(rename.range.start.clone(), cx)?;
-    //             let (end_buffer, end) =
-    //                 buffer.text_anchor_for_position(rename.range.end.clone(), cx)?;
-    //             if start_buffer == end_buffer {
-    //                 let new_name = rename.editor.read(cx).text(cx);
-    //                 Some((start_buffer, start..end, rename.old_name, new_name))
-    //             } else {
-    //                 None
-    //             }
-    //         })?;
+                    // Position the selection in the rename editor so that it matches the current selection.
+                    this.show_local_selections = false;
+                    let rename_editor = cx.build_view(|cx| {
+                        let mut editor = Editor::single_line(cx);
+                        editor.buffer.update(cx, |buffer, cx| {
+                            buffer.edit([(0..0, old_name.clone())], None, cx)
+                        });
+                        editor.select_all(&SelectAll, cx);
+                        editor
+                    });
 
-    //         let rename = workspace.project().clone().update(cx, |project, cx| {
-    //             project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx)
-    //         });
+                    let ranges = this
+                        .clear_background_highlights::<DocumentHighlightWrite>(cx)
+                        .into_iter()
+                        .flat_map(|(_, ranges)| ranges.into_iter())
+                        .chain(
+                            this.clear_background_highlights::<DocumentHighlightRead>(cx)
+                                .into_iter()
+                                .flat_map(|(_, ranges)| ranges.into_iter()),
+                        )
+                        .collect();
 
-    //         let editor = editor.downgrade();
-    //         Some(cx.spawn(|workspace, mut cx| async move {
-    //             let project_transaction = rename.await?;
-    //             Self::open_project_transaction(
-    //                 &editor,
-    //                 workspace,
-    //                 project_transaction,
-    //                 format!("Rename: {} → {}", old_name, new_name),
-    //                 cx.clone(),
-    //             )
-    //             .await?;
+                    this.highlight_text::<Rename>(
+                        ranges,
+                        HighlightStyle {
+                            fade_out: Some(0.6),
+                            ..Default::default()
+                        },
+                        cx,
+                    );
+                    let rename_focus_handle = rename_editor.focus_handle(cx);
+                    cx.focus(&rename_focus_handle);
+                    let block_id = this.insert_blocks(
+                        [BlockProperties {
+                            style: BlockStyle::Flex,
+                            position: range.start.clone(),
+                            height: 1,
+                            render: Arc::new({
+                                let rename_editor = rename_editor.clone();
+                                move |cx: &mut BlockContext| {
+                                    let mut text_style = cx.editor_style.text.clone();
+                                    if let Some(highlight_style) = old_highlight_id
+                                        .and_then(|h| h.style(&cx.editor_style.syntax))
+                                    {
+                                        text_style = text_style.highlight(highlight_style);
+                                    }
+                                    div()
+                                        .pl(cx.anchor_x)
+                                        .child(render_view(
+                                            &rename_editor,
+                                            EditorElement::new(
+                                                &rename_editor,
+                                                EditorStyle {
+                                                    background: cx.theme().system().transparent,
+                                                    local_player: cx.editor_style.local_player,
+                                                    text: text_style,
+                                                    scrollbar_width: cx
+                                                        .editor_style
+                                                        .scrollbar_width,
+                                                    syntax: cx.editor_style.syntax.clone(),
+                                                    diagnostic_style: cx
+                                                        .editor_style
+                                                        .diagnostic_style
+                                                        .clone(),
+                                                },
+                                            ),
+                                        ))
+                                        .render()
+                                }
+                            }),
+                            disposition: BlockDisposition::Below,
+                        }],
+                        Some(Autoscroll::fit()),
+                        cx,
+                    )[0];
+                    this.pending_rename = Some(RenameState {
+                        range,
+                        old_name,
+                        editor: rename_editor,
+                        block_id,
+                    });
+                })?;
+            }
 
-    //             editor.update(&mut cx, |editor, cx| {
-    //                 editor.refresh_document_highlights(cx);
-    //             })?;
-    //             Ok(())
-    //         }))
-    //     }
+            Ok(())
+        }))
+    }
+
+    pub fn confirm_rename(
+        &mut self,
+        _: &ConfirmRename,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let rename = self.take_rename(false, cx)?;
+        let workspace = self.workspace()?;
+        let (start_buffer, start) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(rename.range.start.clone(), cx)?;
+        let (end_buffer, end) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(rename.range.end.clone(), cx)?;
+        if start_buffer != end_buffer {
+            return None;
+        }
+
+        let buffer = start_buffer;
+        let range = start..end;
+        let old_name = rename.old_name;
+        let new_name = rename.editor.read(cx).text(cx);
+
+        let rename = workspace
+            .read(cx)
+            .project()
+            .clone()
+            .update(cx, |project, cx| {
+                project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx)
+            });
+        let workspace = workspace.downgrade();
+
+        Some(cx.spawn(|editor, mut cx| async move {
+            let project_transaction = rename.await?;
+            Self::open_project_transaction(
+                &editor,
+                workspace,
+                project_transaction,
+                format!("Rename: {} → {}", old_name, new_name),
+                cx.clone(),
+            )
+            .await?;
+
+            editor.update(&mut cx, |editor, cx| {
+                editor.refresh_document_highlights(cx);
+            })?;
+            Ok(())
+        }))
+    }
 
     fn take_rename(
         &mut self,
@@ -7880,6 +7901,10 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) -> Option<RenameState> {
         let rename = self.pending_rename.take()?;
+        if rename.editor.focus_handle(cx).is_focused(cx) {
+            cx.focus(&self.focus_handle);
+        }
+
         self.remove_blocks(
             [rename.block_id].into_iter().collect(),
             Some(Autoscroll::fit()),
@@ -8869,46 +8894,50 @@ impl Editor {
     //         });
     //     }
 
-    //     fn jump(
-    //         workspace: &mut Workspace,
-    //         path: ProjectPath,
-    //         position: Point,
-    //         anchor: language::Anchor,
-    //         cx: &mut ViewContext<Workspace>,
-    //     ) {
-    //         let editor = workspace.open_path(path, None, true, cx);
-    //         cx.spawn(|_, mut cx| async move {
-    //             let editor = editor
-    //                 .await?
-    //                 .downcast::<Editor>()
-    //                 .ok_or_else(|| anyhow!("opened item was not an editor"))?
-    //                 .downgrade();
-    //             editor.update(&mut cx, |editor, cx| {
-    //                 let buffer = editor
-    //                     .buffer()
-    //                     .read(cx)
-    //                     .as_singleton()
-    //                     .ok_or_else(|| anyhow!("cannot jump in a multi-buffer"))?;
-    //                 let buffer = buffer.read(cx);
-    //                 let cursor = if buffer.can_resolve(&anchor) {
-    //                     language::ToPoint::to_point(&anchor, buffer)
-    //                 } else {
-    //                     buffer.clip_point(position, Bias::Left)
-    //                 };
+    fn jump(
+        &mut self,
+        path: ProjectPath,
+        position: Point,
+        anchor: language::Anchor,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let workspace = self.workspace();
+        cx.spawn(|_, mut cx| async move {
+            let workspace = workspace.ok_or_else(|| anyhow!("cannot jump without workspace"))?;
+            let editor = workspace.update(&mut cx, |workspace, cx| {
+                workspace.open_path(path, None, true, cx)
+            })?;
+            let editor = editor
+                .await?
+                .downcast::<Editor>()
+                .ok_or_else(|| anyhow!("opened item was not an editor"))?
+                .downgrade();
+            editor.update(&mut cx, |editor, cx| {
+                let buffer = editor
+                    .buffer()
+                    .read(cx)
+                    .as_singleton()
+                    .ok_or_else(|| anyhow!("cannot jump in a multi-buffer"))?;
+                let buffer = buffer.read(cx);
+                let cursor = if buffer.can_resolve(&anchor) {
+                    language::ToPoint::to_point(&anchor, buffer)
+                } else {
+                    buffer.clip_point(position, Bias::Left)
+                };
 
-    //                 let nav_history = editor.nav_history.take();
-    //                 editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
-    //                     s.select_ranges([cursor..cursor]);
-    //                 });
-    //                 editor.nav_history = nav_history;
+                let nav_history = editor.nav_history.take();
+                editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
+                    s.select_ranges([cursor..cursor]);
+                });
+                editor.nav_history = nav_history;
 
-    //                 anyhow::Ok(())
-    //             })??;
+                anyhow::Ok(())
+            })??;
 
-    //             anyhow::Ok(())
-    //         })
-    //         .detach_and_log_err(cx);
-    //     }
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
 
     fn marked_text_ranges(&self, cx: &AppContext) -> Option<Vec<Range<OffsetUtf16>>> {
         let snapshot = self.buffer.read(cx).read(cx);
@@ -9170,17 +9199,17 @@ impl Editor {
         cx.focus(&self.focus_handle)
     }
 
-    fn handle_focus_in(&mut self, cx: &mut ViewContext<Self>) {
-        if self.focus_handle.is_focused(cx) {
-            // todo!()
-            // let focused_event = EditorFocused(cx.handle());
-            // cx.emit_global(focused_event);
-            cx.emit(Event::Focused);
-        }
+    pub fn is_focused(&self, cx: &WindowContext) -> bool {
+        self.focus_handle.is_focused(cx)
+    }
+
+    fn handle_focus(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Focused);
+
         if let Some(rename) = self.pending_rename.as_ref() {
             let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone();
             cx.focus(&rename_editor_focus_handle);
-        } else if self.focus_handle.is_focused(cx) {
+        } else {
             self.blink_manager.update(cx, BlinkManager::enable);
             self.buffer.update(cx, |buffer, cx| {
                 buffer.finalize_last_transaction(cx);
@@ -9196,7 +9225,7 @@ impl Editor {
         }
     }
 
-    fn handle_focus_out(&mut self, cx: &mut ViewContext<Self>) {
+    fn handle_blur(&mut self, cx: &mut ViewContext<Self>) {
         // todo!()
         // let blurred_event = EditorBlurred(cx.handle());
         // cx.emit_global(blurred_event);
@@ -9381,9 +9410,9 @@ impl Render for Editor {
             EditorMode::SingleLine => {
                 TextStyle {
                     color: cx.theme().colors().text,
-                    font_family: "Zed Sans".into(), // todo!()
-                    font_features: FontFeatures::default(),
-                    font_size: rems(1.0).into(),
+                    font_family: settings.ui_font.family.clone(), // todo!()
+                    font_features: settings.ui_font.features,
+                    font_size: rems(0.875).into(),
                     font_weight: FontWeight::NORMAL,
                     font_style: FontStyle::Normal,
                     line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()),
@@ -9411,14 +9440,17 @@ impl Render for Editor {
             EditorMode::Full => cx.theme().colors().editor_background,
         };
 
-        EditorElement::new(EditorStyle {
-            background,
-            local_player: cx.theme().players().local(),
-            text: text_style,
-            scrollbar_width: px(12.),
-            syntax: cx.theme().syntax().clone(),
-            diagnostic_style: cx.theme().diagnostic_style(),
-        })
+        EditorElement::new(
+            cx.view(),
+            EditorStyle {
+                background,
+                local_player: cx.theme().players().local(),
+                text: text_style,
+                scrollbar_width: px(12.),
+                syntax: cx.theme().syntax().clone(),
+                diagnostic_style: cx.theme().diagnostic_style(),
+            },
+        )
     }
 }
 
@@ -9973,43 +10005,22 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
     }
     let message = diagnostic.message;
     Arc::new(move |cx: &mut BlockContext| {
-        todo!()
-        // let message = message.clone();
-        // let settings = ThemeSettings::get_global(cx);
-        // let tooltip_style = settings.theme.tooltip.clone();
-        // let theme = &settings.theme.editor;
-        // let style = diagnostic_style(diagnostic.severity, is_valid, theme);
-        // let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
-        // let anchor_x = cx.anchor_x;
-        // enum BlockContextToolip {}
-        // MouseEventHandler::new::<BlockContext, _>(cx.block_id, cx, |_, _| {
-        //     Flex::column()
-        //         .with_children(highlighted_lines.iter().map(|(line, highlights)| {
-        //             Label::new(
-        //                 line.clone(),
-        //                 style.message.clone().with_font_size(font_size),
-        //             )
-        //             .with_highlights(highlights.clone())
-        //             .contained()
-        //             .with_margin_left(anchor_x)
-        //         }))
-        //         .aligned()
-        //         .left()
-        //         .into_any()
-        // })
-        // .with_cursor_style(CursorStyle::PointingHand)
-        // .on_click(MouseButton::Left, move |_, _, cx| {
-        //     cx.write_to_clipboard(ClipboardItem::new(message.clone()));
-        // })
-        // // We really need to rethink this ID system...
-        // .with_tooltip::<BlockContextToolip>(
-        //     cx.block_id,
-        //     "Copy diagnostic message",
-        //     None,
-        //     tooltip_style,
-        //     cx,
-        // )
-        // .into_any()
+        let message = message.clone();
+        v_stack()
+            .id(cx.block_id)
+            .size_full()
+            .bg(gpui::red())
+            .children(highlighted_lines.iter().map(|(line, highlights)| {
+                div()
+                    .child(HighlightedLabel::new(line.clone(), highlights.clone()))
+                    .ml(cx.anchor_x)
+            }))
+            .cursor_pointer()
+            .on_click(move |_, _, cx| {
+                cx.write_to_clipboard(ClipboardItem::new(message.clone()));
+            })
+            .tooltip(|_, cx| cx.build_view(|cx| TextTooltip::new("Copy diagnostic message")))
+            .render()
     })
 }
 
@@ -10056,76 +10067,76 @@ pub fn diagnostic_style(
     }
 }
 
-// pub fn combine_syntax_and_fuzzy_match_highlights(
-//     text: &str,
-//     default_style: HighlightStyle,
-//     syntax_ranges: impl Iterator<Item = (Range<usize>, HighlightStyle)>,
-//     match_indices: &[usize],
-// ) -> Vec<(Range<usize>, HighlightStyle)> {
-//     let mut result = Vec::new();
-//     let mut match_indices = match_indices.iter().copied().peekable();
-
-//     for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())])
-//     {
-//         syntax_highlight.weight = None;
-
-//         // Add highlights for any fuzzy match characters before the next
-//         // syntax highlight range.
-//         while let Some(&match_index) = match_indices.peek() {
-//             if match_index >= range.start {
-//                 break;
-//             }
-//             match_indices.next();
-//             let end_index = char_ix_after(match_index, text);
-//             let mut match_style = default_style;
-//             match_style.weight = Some(FontWeight::BOLD);
-//             result.push((match_index..end_index, match_style));
-//         }
+pub fn combine_syntax_and_fuzzy_match_highlights(
+    text: &str,
+    default_style: HighlightStyle,
+    syntax_ranges: impl Iterator<Item = (Range<usize>, HighlightStyle)>,
+    match_indices: &[usize],
+) -> Vec<(Range<usize>, HighlightStyle)> {
+    let mut result = Vec::new();
+    let mut match_indices = match_indices.iter().copied().peekable();
 
-//         if range.start == usize::MAX {
-//             break;
-//         }
+    for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())])
+    {
+        syntax_highlight.font_weight = None;
 
-//         // Add highlights for any fuzzy match characters within the
-//         // syntax highlight range.
-//         let mut offset = range.start;
-//         while let Some(&match_index) = match_indices.peek() {
-//             if match_index >= range.end {
-//                 break;
-//             }
+        // Add highlights for any fuzzy match characters before the next
+        // syntax highlight range.
+        while let Some(&match_index) = match_indices.peek() {
+            if match_index >= range.start {
+                break;
+            }
+            match_indices.next();
+            let end_index = char_ix_after(match_index, text);
+            let mut match_style = default_style;
+            match_style.font_weight = Some(FontWeight::BOLD);
+            result.push((match_index..end_index, match_style));
+        }
 
-//             match_indices.next();
-//             if match_index > offset {
-//                 result.push((offset..match_index, syntax_highlight));
-//             }
+        if range.start == usize::MAX {
+            break;
+        }
 
-//             let mut end_index = char_ix_after(match_index, text);
-//             while let Some(&next_match_index) = match_indices.peek() {
-//                 if next_match_index == end_index && next_match_index < range.end {
-//                     end_index = char_ix_after(next_match_index, text);
-//                     match_indices.next();
-//                 } else {
-//                     break;
-//                 }
-//             }
+        // Add highlights for any fuzzy match characters within the
+        // syntax highlight range.
+        let mut offset = range.start;
+        while let Some(&match_index) = match_indices.peek() {
+            if match_index >= range.end {
+                break;
+            }
 
-//             let mut match_style = syntax_highlight;
-//             match_style.weight = Some(FontWeight::BOLD);
-//             result.push((match_index..end_index, match_style));
-//             offset = end_index;
-//         }
+            match_indices.next();
+            if match_index > offset {
+                result.push((offset..match_index, syntax_highlight));
+            }
 
-//         if offset < range.end {
-//             result.push((offset..range.end, syntax_highlight));
-//         }
-//     }
+            let mut end_index = char_ix_after(match_index, text);
+            while let Some(&next_match_index) = match_indices.peek() {
+                if next_match_index == end_index && next_match_index < range.end {
+                    end_index = char_ix_after(next_match_index, text);
+                    match_indices.next();
+                } else {
+                    break;
+                }
+            }
 
-//     fn char_ix_after(ix: usize, text: &str) -> usize {
-//         ix + text[ix..].chars().next().unwrap().len_utf8()
-//     }
+            let mut match_style = syntax_highlight;
+            match_style.font_weight = Some(FontWeight::BOLD);
+            result.push((match_index..end_index, match_style));
+            offset = end_index;
+        }
 
-//     result
-// }
+        if offset < range.end {
+            result.push((offset..range.end, syntax_highlight));
+        }
+    }
+
+    fn char_ix_after(ix: usize, text: &str) -> usize {
+        ix + text[ix..].chars().next().unwrap().len_utf8()
+    }
+
+    result
+}
 
 // pub fn styled_runs_for_code_label<'a>(
 //     label: &'a CodeLabel,

crates/editor2/src/editor_tests.rs 🔗

@@ -1,92 +1,80 @@
-use gpui::TestAppContext;
-use language::language_settings::{AllLanguageSettings, AllLanguageSettingsContent};
-use settings::SettingsStore;
-
-// use super::*;
-// use crate::{
-//     scroll::scroll_amount::ScrollAmount,
-//     test::{
-//         assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
-//         editor_test_context::EditorTestContext, select_ranges,
-//     },
-//     JoinLines,
-// };
-// use drag_and_drop::DragAndDrop;
-// use futures::StreamExt;
-// use gpui::{
-//     executor::Deterministic,
-//     geometry::{rect::RectF, vector::vec2f},
-//     platform::{WindowBounds, WindowOptions},
-//     serde_json::{self, json},
-//     TestAppContext,
-// };
-// use indoc::indoc;
-// use language::{
-//     language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
-//     BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
-//     Override, Point,
-// };
-// use parking_lot::Mutex;
-// use project::project_settings::{LspSettings, ProjectSettings};
-// use project::FakeFs;
-// use std::sync::atomic;
-// use std::sync::atomic::AtomicUsize;
-// use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
-// use unindent::Unindent;
-// use util::{
-//     assert_set_eq,
-//     test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
-// };
-// use workspace::{
-//     item::{FollowableItem, Item, ItemHandle},
-//     NavigationEntry, ViewId,
-// };
-
+use super::*;
+use crate::{
+    scroll::scroll_amount::ScrollAmount,
+    test::{
+        assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
+        editor_test_context::EditorTestContext, select_ranges,
+    },
+    JoinLines,
+};
+
+use futures::StreamExt;
+use gpui::{
+    div,
+    serde_json::{self, json},
+    Div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
+};
+use indoc::indoc;
+use language::{
+    language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
+    BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
+    Override, Point,
+};
+use parking_lot::Mutex;
+use project::project_settings::{LspSettings, ProjectSettings};
+use project::FakeFs;
+use std::sync::atomic;
+use std::sync::atomic::AtomicUsize;
+use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
+use unindent::Unindent;
+use util::{
+    assert_set_eq,
+    test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
+};
+use workspace::{
+    item::{FollowEvent, FollowableEvents, FollowableItem, Item, ItemHandle},
+    NavigationEntry, ViewId,
+};
+
+// todo(finish edit tests)
 // #[gpui::test]
 // fn test_edit_events(cx: &mut TestAppContext) {
 //     init_test(cx, |_| {});
 
-//     let buffer = cx.add_model(|cx| {
-//         let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456");
+//     let buffer = cx.build_model(|cx| {
+//         let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "123456");
 //         buffer.set_group_interval(Duration::from_secs(1));
 //         buffer
 //     });
 
 //     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)
-//             }
-//         })
-//         .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);
+//     let editor1 = cx.add_window({
+//         let events = events.clone();
+//         |cx| {
+//             let view = cx.view().clone();
+//             cx.subscribe(&view, move |_, _, event, _| {
+//                 if matches!(event, Event::Edited | Event::BufferEdited) {
+//                     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.view().clone(), move |_, _, event, _| {
+//                 if matches!(event, Event::Edited | Event::BufferEdited) {
+//                     events.borrow_mut().push(("editor2", event.clone()));
+//                 }
+//             })
+//             .detach();
+//             Editor::for_buffer(buffer.clone(), None, cx)
+//         }
+//     });
+
 //     assert_eq!(mem::take(&mut *events.borrow_mut()), []);
 
 //     // Mutating editor 1 will emit an `Edited` event only for that editor.
@@ -97,8 +85,6 @@ use settings::SettingsStore;
 //             ("editor1", Event::Edited),
 //             ("editor1", Event::BufferEdited),
 //             ("editor2", Event::BufferEdited),
-//             ("editor1", Event::DirtyChanged),
-//             ("editor2", Event::DirtyChanged)
 //         ]
 //     );
 
@@ -121,8 +107,6 @@ use settings::SettingsStore;
 //             ("editor1", Event::Edited),
 //             ("editor1", Event::BufferEdited),
 //             ("editor2", Event::BufferEdited),
-//             ("editor1", Event::DirtyChanged),
-//             ("editor2", Event::DirtyChanged),
 //         ]
 //     );
 
@@ -134,8 +118,6 @@ use settings::SettingsStore;
 //             ("editor1", Event::Edited),
 //             ("editor1", Event::BufferEdited),
 //             ("editor2", Event::BufferEdited),
-//             ("editor1", Event::DirtyChanged),
-//             ("editor2", Event::DirtyChanged),
 //         ]
 //     );
 
@@ -147,8 +129,6 @@ use settings::SettingsStore;
 //             ("editor2", Event::Edited),
 //             ("editor1", Event::BufferEdited),
 //             ("editor2", Event::BufferEdited),
-//             ("editor1", Event::DirtyChanged),
-//             ("editor2", Event::DirtyChanged),
 //         ]
 //     );
 
@@ -160,8 +140,6 @@ use settings::SettingsStore;
 //             ("editor2", Event::Edited),
 //             ("editor1", Event::BufferEdited),
 //             ("editor2", Event::BufferEdited),
-//             ("editor1", Event::DirtyChanged),
-//             ("editor2", Event::DirtyChanged),
 //         ]
 //     );
 
@@ -174,6095 +152,6198 @@ use settings::SettingsStore;
 //     assert_eq!(mem::take(&mut *events.borrow_mut()), []);
 // }
 
-// #[gpui::test]
-// fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let mut now = Instant::now();
-//     let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456"));
-//     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))
-//         .root(cx);
-
-//     editor.update(cx, |editor, cx| {
-//         editor.start_transaction_at(now, cx);
-//         editor.change_selections(None, cx, |s| s.select_ranges([2..4]));
-
-//         editor.insert("cd", cx);
-//         editor.end_transaction_at(now, cx);
-//         assert_eq!(editor.text(cx), "12cd56");
-//         assert_eq!(editor.selections.ranges(cx), vec![4..4]);
-
-//         editor.start_transaction_at(now, cx);
-//         editor.change_selections(None, cx, |s| s.select_ranges([4..5]));
-//         editor.insert("e", cx);
-//         editor.end_transaction_at(now, cx);
-//         assert_eq!(editor.text(cx), "12cde6");
-//         assert_eq!(editor.selections.ranges(cx), vec![5..5]);
-
-//         now += group_interval + Duration::from_millis(1);
-//         editor.change_selections(None, cx, |s| s.select_ranges([2..2]));
-
-//         // Simulate an edit in another editor
-//         buffer.update(cx, |buffer, cx| {
-//             buffer.start_transaction_at(now, cx);
-//             buffer.edit([(0..1, "a")], None, cx);
-//             buffer.edit([(1..1, "b")], None, cx);
-//             buffer.end_transaction_at(now, cx);
-//         });
+#[gpui::test]
+fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut now = Instant::now();
+    let buffer = cx.build_model(|cx| language::Buffer::new(0, cx.entity_id().as_u64(), "123456"));
+    let group_interval = buffer.update(cx, |buffer, _| buffer.transaction_group_interval());
+    let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let editor = cx.add_window(|cx| build_editor(buffer.clone(), cx));
+
+    editor.update(cx, |editor, cx| {
+        editor.start_transaction_at(now, cx);
+        editor.change_selections(None, cx, |s| s.select_ranges([2..4]));
+
+        editor.insert("cd", cx);
+        editor.end_transaction_at(now, cx);
+        assert_eq!(editor.text(cx), "12cd56");
+        assert_eq!(editor.selections.ranges(cx), vec![4..4]);
+
+        editor.start_transaction_at(now, cx);
+        editor.change_selections(None, cx, |s| s.select_ranges([4..5]));
+        editor.insert("e", cx);
+        editor.end_transaction_at(now, cx);
+        assert_eq!(editor.text(cx), "12cde6");
+        assert_eq!(editor.selections.ranges(cx), vec![5..5]);
+
+        now += group_interval + Duration::from_millis(1);
+        editor.change_selections(None, cx, |s| s.select_ranges([2..2]));
+
+        // Simulate an edit in another editor
+        buffer.update(cx, |buffer, cx| {
+            buffer.start_transaction_at(now, cx);
+            buffer.edit([(0..1, "a")], None, cx);
+            buffer.edit([(1..1, "b")], None, cx);
+            buffer.end_transaction_at(now, cx);
+        });
 
-//         assert_eq!(editor.text(cx), "ab2cde6");
-//         assert_eq!(editor.selections.ranges(cx), vec![3..3]);
+        assert_eq!(editor.text(cx), "ab2cde6");
+        assert_eq!(editor.selections.ranges(cx), vec![3..3]);
+
+        // Last transaction happened past the group interval in a different editor.
+        // Undo it individually and don't restore selections.
+        editor.undo(&Undo, cx);
+        assert_eq!(editor.text(cx), "12cde6");
+        assert_eq!(editor.selections.ranges(cx), vec![2..2]);
+
+        // First two transactions happened within the group interval in this editor.
+        // Undo them together and restore selections.
+        editor.undo(&Undo, cx);
+        editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op.
+        assert_eq!(editor.text(cx), "123456");
+        assert_eq!(editor.selections.ranges(cx), vec![0..0]);
+
+        // Redo the first two transactions together.
+        editor.redo(&Redo, cx);
+        assert_eq!(editor.text(cx), "12cde6");
+        assert_eq!(editor.selections.ranges(cx), vec![5..5]);
+
+        // Redo the last transaction on its own.
+        editor.redo(&Redo, cx);
+        assert_eq!(editor.text(cx), "ab2cde6");
+        assert_eq!(editor.selections.ranges(cx), vec![6..6]);
+
+        // Test empty transactions.
+        editor.start_transaction_at(now, cx);
+        editor.end_transaction_at(now, cx);
+        editor.undo(&Undo, cx);
+        assert_eq!(editor.text(cx), "12cde6");
+    });
+}
 
-//         // Last transaction happened past the group interval in a different editor.
-//         // Undo it individually and don't restore selections.
-//         editor.undo(&Undo, cx);
-//         assert_eq!(editor.text(cx), "12cde6");
-//         assert_eq!(editor.selections.ranges(cx), vec![2..2]);
+#[gpui::test]
+fn test_ime_composition(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//         // First two transactions happened within the group interval in this editor.
-//         // Undo them together and restore selections.
-//         editor.undo(&Undo, cx);
-//         editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op.
-//         assert_eq!(editor.text(cx), "123456");
-//         assert_eq!(editor.selections.ranges(cx), vec![0..0]);
-
-//         // Redo the first two transactions together.
-//         editor.redo(&Redo, cx);
-//         assert_eq!(editor.text(cx), "12cde6");
-//         assert_eq!(editor.selections.ranges(cx), vec![5..5]);
-
-//         // Redo the last transaction on its own.
-//         editor.redo(&Redo, cx);
-//         assert_eq!(editor.text(cx), "ab2cde6");
-//         assert_eq!(editor.selections.ranges(cx), vec![6..6]);
-
-//         // Test empty transactions.
-//         editor.start_transaction_at(now, cx);
-//         editor.end_transaction_at(now, cx);
-//         editor.undo(&Undo, cx);
-//         assert_eq!(editor.text(cx), "12cde6");
-//     });
-// }
+    let buffer = cx.build_model(|cx| {
+        let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "abcde");
+        // Ensure automatic grouping doesn't occur.
+        buffer.set_group_interval(Duration::ZERO);
+        buffer
+    });
 
-// #[gpui::test]
-// fn test_ime_composition(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
+    let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+    cx.add_window(|cx| {
+        let mut editor = build_editor(buffer.clone(), cx);
+
+        // Start a new IME composition.
+        editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx);
+        editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx);
+        editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx);
+        assert_eq!(editor.text(cx), "äbcde");
+        assert_eq!(
+            editor.marked_text_ranges(cx),
+            Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
+        );
+
+        // Finalize IME composition.
+        editor.replace_text_in_range(None, "ā", cx);
+        assert_eq!(editor.text(cx), "ābcde");
+        assert_eq!(editor.marked_text_ranges(cx), None);
+
+        // IME composition edits are grouped and are undone/redone at once.
+        editor.undo(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "abcde");
+        assert_eq!(editor.marked_text_ranges(cx), None);
+        editor.redo(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "ābcde");
+        assert_eq!(editor.marked_text_ranges(cx), None);
+
+        // Start a new IME composition.
+        editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx);
+        assert_eq!(
+            editor.marked_text_ranges(cx),
+            Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
+        );
+
+        // Undoing during an IME composition cancels it.
+        editor.undo(&Default::default(), cx);
+        assert_eq!(editor.text(cx), "ābcde");
+        assert_eq!(editor.marked_text_ranges(cx), None);
+
+        // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
+        editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx);
+        assert_eq!(editor.text(cx), "ābcdè");
+        assert_eq!(
+            editor.marked_text_ranges(cx),
+            Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
+        );
+
+        // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
+        editor.replace_text_in_range(Some(4..999), "ę", cx);
+        assert_eq!(editor.text(cx), "ābcdę");
+        assert_eq!(editor.marked_text_ranges(cx), None);
+
+        // Start a new IME composition with multiple cursors.
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([
+                OffsetUtf16(1)..OffsetUtf16(1),
+                OffsetUtf16(3)..OffsetUtf16(3),
+                OffsetUtf16(5)..OffsetUtf16(5),
+            ])
+        });
+        editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx);
+        assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
+        assert_eq!(
+            editor.marked_text_ranges(cx),
+            Some(vec![
+                OffsetUtf16(0)..OffsetUtf16(3),
+                OffsetUtf16(4)..OffsetUtf16(7),
+                OffsetUtf16(8)..OffsetUtf16(11)
+            ])
+        );
+
+        // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
+        editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx);
+        assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
+        assert_eq!(
+            editor.marked_text_ranges(cx),
+            Some(vec![
+                OffsetUtf16(1)..OffsetUtf16(2),
+                OffsetUtf16(5)..OffsetUtf16(6),
+                OffsetUtf16(9)..OffsetUtf16(10)
+            ])
+        );
+
+        // Finalize IME composition with multiple cursors.
+        editor.replace_text_in_range(Some(9..10), "2", cx);
+        assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
+        assert_eq!(editor.marked_text_ranges(cx), None);
+
+        editor
+    });
+}
 
-//     let buffer = cx.add_model(|cx| {
-//         let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde");
-//         // Ensure automatic grouping doesn't occur.
-//         buffer.set_group_interval(Duration::ZERO);
-//         buffer
-//     });
+#[gpui::test]
+fn test_selection_with_mouse(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//     let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-//     cx.add_window(|cx| {
-//         let mut editor = build_editor(buffer.clone(), cx);
+    let editor = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
+        build_editor(buffer, cx)
+    });
 
-//         // Start a new IME composition.
-//         editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx);
-//         editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx);
-//         editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx);
-//         assert_eq!(editor.text(cx), "äbcde");
-//         assert_eq!(
-//             editor.marked_text_ranges(cx),
-//             Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
-//         );
+    editor.update(cx, |view, cx| {
+        view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
+    });
+    assert_eq!(
+        editor
+            .update(cx, |view, cx| view.selections.display_ranges(cx))
+            .unwrap(),
+        [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
+    );
+
+    editor.update(cx, |view, cx| {
+        view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::<f32>::zero(), cx);
+    });
 
-//         // Finalize IME composition.
-//         editor.replace_text_in_range(None, "ā", cx);
-//         assert_eq!(editor.text(cx), "ābcde");
-//         assert_eq!(editor.marked_text_ranges(cx), None);
+    assert_eq!(
+        editor
+            .update(cx, |view, cx| view.selections.display_ranges(cx))
+            .unwrap(),
+        [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
+    );
 
-//         // IME composition edits are grouped and are undone/redone at once.
-//         editor.undo(&Default::default(), cx);
-//         assert_eq!(editor.text(cx), "abcde");
-//         assert_eq!(editor.marked_text_ranges(cx), None);
-//         editor.redo(&Default::default(), cx);
-//         assert_eq!(editor.text(cx), "ābcde");
-//         assert_eq!(editor.marked_text_ranges(cx), None);
-
-//         // Start a new IME composition.
-//         editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx);
-//         assert_eq!(
-//             editor.marked_text_ranges(cx),
-//             Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
-//         );
+    editor.update(cx, |view, cx| {
+        view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::<f32>::zero(), cx);
+    });
 
-//         // Undoing during an IME composition cancels it.
-//         editor.undo(&Default::default(), cx);
-//         assert_eq!(editor.text(cx), "ābcde");
-//         assert_eq!(editor.marked_text_ranges(cx), None);
+    assert_eq!(
+        editor
+            .update(cx, |view, cx| view.selections.display_ranges(cx))
+            .unwrap(),
+        [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
+    );
 
-//         // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
-//         editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx);
-//         assert_eq!(editor.text(cx), "ābcdè");
-//         assert_eq!(
-//             editor.marked_text_ranges(cx),
-//             Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
-//         );
+    editor.update(cx, |view, cx| {
+        view.end_selection(cx);
+        view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::<f32>::zero(), cx);
+    });
 
-//         // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
-//         editor.replace_text_in_range(Some(4..999), "ę", cx);
-//         assert_eq!(editor.text(cx), "ābcdę");
-//         assert_eq!(editor.marked_text_ranges(cx), None);
+    assert_eq!(
+        editor
+            .update(cx, |view, cx| view.selections.display_ranges(cx))
+            .unwrap(),
+        [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
+    );
 
-//         // Start a new IME composition with multiple cursors.
-//         editor.change_selections(None, cx, |s| {
-//             s.select_ranges([
-//                 OffsetUtf16(1)..OffsetUtf16(1),
-//                 OffsetUtf16(3)..OffsetUtf16(3),
-//                 OffsetUtf16(5)..OffsetUtf16(5),
-//             ])
-//         });
-//         editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx);
-//         assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
-//         assert_eq!(
-//             editor.marked_text_ranges(cx),
-//             Some(vec![
-//                 OffsetUtf16(0)..OffsetUtf16(3),
-//                 OffsetUtf16(4)..OffsetUtf16(7),
-//                 OffsetUtf16(8)..OffsetUtf16(11)
-//             ])
-//         );
+    editor.update(cx, |view, cx| {
+        view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx);
+        view.update_selection(DisplayPoint::new(0, 0), 0, gpui::Point::<f32>::zero(), cx);
+    });
 
-//         // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
-//         editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx);
-//         assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
-//         assert_eq!(
-//             editor.marked_text_ranges(cx),
-//             Some(vec![
-//                 OffsetUtf16(1)..OffsetUtf16(2),
-//                 OffsetUtf16(5)..OffsetUtf16(6),
-//                 OffsetUtf16(9)..OffsetUtf16(10)
-//             ])
-//         );
+    assert_eq!(
+        editor
+            .update(cx, |view, cx| view.selections.display_ranges(cx))
+            .unwrap(),
+        [
+            DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1),
+            DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)
+        ]
+    );
+
+    editor.update(cx, |view, cx| {
+        view.end_selection(cx);
+    });
 
-//         // Finalize IME composition with multiple cursors.
-//         editor.replace_text_in_range(Some(9..10), "2", cx);
-//         assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
-//         assert_eq!(editor.marked_text_ranges(cx), None);
+    assert_eq!(
+        editor
+            .update(cx, |view, cx| view.selections.display_ranges(cx))
+            .unwrap(),
+        [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)]
+    );
+}
 
-//         editor
-//     });
-// }
+#[gpui::test]
+fn test_canceling_pending_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-// #[gpui::test]
-// fn test_selection_with_mouse(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 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);
-//     });
-//     assert_eq!(
-//         editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
-//     );
+    view.update(cx, |view, cx| {
+        view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
+        );
+    });
 
-//     editor.update(cx, |view, cx| {
-//         view.update_selection(DisplayPoint::new(3, 3), 0, Point<Pixels>::zero(), cx);
-//     });
+    view.update(cx, |view, cx| {
+        view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::<f32>::zero(), cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
+        );
+    });
 
-//     assert_eq!(
-//         editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
-//     );
+    view.update(cx, |view, cx| {
+        view.cancel(&Cancel, cx);
+        view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::<f32>::zero(), cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
+        );
+    });
+}
 
-//     editor.update(cx, |view, cx| {
-//         view.update_selection(DisplayPoint::new(1, 1), 0, Point<Pixels>::zero(), cx);
-//     });
+#[gpui::test]
+fn test_clone(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let (text, selection_ranges) = marked_text_ranges(
+        indoc! {"
+            one
+            two
+            threeˇ
+            four
+            fiveˇ
+        "},
+        true,
+    );
+
+    let editor = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple(&text, cx);
+        build_editor(buffer, cx)
+    });
 
-//     assert_eq!(
-//         editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
-//     );
+    editor.update(cx, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
+        editor.fold_ranges(
+            [
+                Point::new(1, 0)..Point::new(2, 0),
+                Point::new(3, 0)..Point::new(4, 0),
+            ],
+            true,
+            cx,
+        );
+    });
 
-//     editor.update(cx, |view, cx| {
-//         view.end_selection(cx);
-//         view.update_selection(DisplayPoint::new(3, 3), 0, Point<Pixels>::zero(), cx);
-//     });
+    let cloned_editor = editor
+        .update(cx, |editor, cx| {
+            cx.open_window(Default::default(), |cx| {
+                cx.build_view(|cx| editor.clone(cx))
+            })
+        })
+        .unwrap();
+
+    let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)).unwrap();
+    let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)).unwrap();
+
+    assert_eq!(
+        cloned_editor
+            .update(cx, |e, cx| e.display_text(cx))
+            .unwrap(),
+        editor.update(cx, |e, cx| e.display_text(cx)).unwrap()
+    );
+    assert_eq!(
+        cloned_snapshot
+            .folds_in_range(0..text.len())
+            .collect::<Vec<_>>(),
+        snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
+    );
+    assert_set_eq!(
+        cloned_editor
+            .update(cx, |editor, cx| editor.selections.ranges::<Point>(cx))
+            .unwrap(),
+        editor
+            .update(cx, |editor, cx| editor.selections.ranges(cx))
+            .unwrap()
+    );
+    assert_set_eq!(
+        cloned_editor
+            .update(cx, |e, cx| e.selections.display_ranges(cx))
+            .unwrap(),
+        editor
+            .update(cx, |e, cx| e.selections.display_ranges(cx))
+            .unwrap()
+    );
+}
 
-//     assert_eq!(
-//         editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
-//     );
+//todo!(editor navigate)
+// #[gpui::test]
+// async fn test_navigation_history(cx: &mut TestAppContext) {
+//     init_test(cx, |_| {});
 
-//     editor.update(cx, |view, cx| {
-//         view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx);
-//         view.update_selection(DisplayPoint::new(0, 0), 0, Point<Pixels>::zero(), cx);
-//     });
+//     use workspace::item::Item;
 
-//     assert_eq!(
-//         editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         [
-//             DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1),
-//             DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)
-//         ]
-//     );
+//     let fs = FakeFs::new(cx.executor());
+//     let project = Project::test(fs, [], cx).await;
+//     let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
+//     let pane = workspace
+//         .update(cx, |workspace, _| workspace.active_pane().clone())
+//         .unwrap();
 
-//     editor.update(cx, |view, cx| {
-//         view.end_selection(cx);
-//     });
+//     workspace.update(cx, |v, cx| {
+//         cx.build_view(|cx| {
+//             let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
+//             let mut editor = build_editor(buffer.clone(), cx);
+//             let handle = cx.view();
+//             editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
 
-//     assert_eq!(
-//         editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)]
-//     );
-// }
+//             fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option<NavigationEntry> {
+//                 editor.nav_history.as_mut().unwrap().pop_backward(cx)
+//             }
 
-// #[gpui::test]
-// fn test_canceling_pending_selection(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
+//             // Move the cursor a small distance.
+//             // Nothing is added to the navigation history.
+//             editor.change_selections(None, cx, |s| {
+//                 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+//             });
+//             editor.change_selections(None, cx, |s| {
+//                 s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
+//             });
+//             assert!(pop_history(&mut editor, cx).is_none());
 
-//     let view = cx
-//         .add_window(|cx| {
-//             let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
-//             build_editor(buffer, cx)
-//         })
-//         .root(cx);
+//             // Move the cursor a large distance.
+//             // The history can jump back to the previous position.
+//             editor.change_selections(None, cx, |s| {
+//                 s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
+//             });
+//             let nav_entry = pop_history(&mut editor, cx).unwrap();
+//             editor.navigate(nav_entry.data.unwrap(), cx);
+//             assert_eq!(nav_entry.item.id(), cx.entity_id());
+//             assert_eq!(
+//                 editor.selections.display_ranges(cx),
+//                 &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
+//             );
+//             assert!(pop_history(&mut editor, cx).is_none());
 
-//     view.update(cx, |view, cx| {
-//         view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
-//         );
-//     });
+//             // Move the cursor a small distance via the mouse.
+//             // Nothing is added to the navigation history.
+//             editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
+//             editor.end_selection(cx);
+//             assert_eq!(
+//                 editor.selections.display_ranges(cx),
+//                 &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
+//             );
+//             assert!(pop_history(&mut editor, cx).is_none());
 
-//     view.update(cx, |view, cx| {
-//         view.update_selection(DisplayPoint::new(3, 3), 0, Point<Pixels>::zero(), cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
-//         );
-//     });
+//             // Move the cursor a large distance via the mouse.
+//             // The history can jump back to the previous position.
+//             editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
+//             editor.end_selection(cx);
+//             assert_eq!(
+//                 editor.selections.display_ranges(cx),
+//                 &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
+//             );
+//             let nav_entry = pop_history(&mut editor, cx).unwrap();
+//             editor.navigate(nav_entry.data.unwrap(), cx);
+//             assert_eq!(nav_entry.item.id(), cx.entity_id());
+//             assert_eq!(
+//                 editor.selections.display_ranges(cx),
+//                 &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
+//             );
+//             assert!(pop_history(&mut editor, cx).is_none());
+
+//             // Set scroll position to check later
+//             editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), cx);
+//             let original_scroll_position = editor.scroll_manager.anchor();
+
+//             // Jump to the end of the document and adjust scroll
+//             editor.move_to_end(&MoveToEnd, cx);
+//             editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), cx);
+//             assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
+
+//             let nav_entry = pop_history(&mut editor, cx).unwrap();
+//             editor.navigate(nav_entry.data.unwrap(), cx);
+//             assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
+
+//             // Ensure we don't panic when navigation data contains invalid anchors *and* points.
+//             let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
+//             invalid_anchor.text_anchor.buffer_id = Some(999);
+//             let invalid_point = Point::new(9999, 0);
+//             editor.navigate(
+//                 Box::new(NavigationData {
+//                     cursor_anchor: invalid_anchor,
+//                     cursor_position: invalid_point,
+//                     scroll_anchor: ScrollAnchor {
+//                         anchor: invalid_anchor,
+//                         offset: Default::default(),
+//                     },
+//                     scroll_top_row: invalid_point.row,
+//                 }),
+//                 cx,
+//             );
+//             assert_eq!(
+//                 editor.selections.display_ranges(cx),
+//                 &[editor.max_point(cx)..editor.max_point(cx)]
+//             );
+//             assert_eq!(
+//                 editor.scroll_position(cx),
+//                 gpui::Point::new(0., editor.max_point(cx).row() as f32)
+//             );
 
-//     view.update(cx, |view, cx| {
-//         view.cancel(&Cancel, cx);
-//         view.update_selection(DisplayPoint::new(1, 1), 0, Point<Pixels>::zero(), cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
-//         );
+//             editor
+//         })
 //     });
 // }
 
-// #[gpui::test]
-// fn test_clone(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+fn test_cancel(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//     let (text, selection_ranges) = marked_text_ranges(
-//         indoc! {"
-//             one
-//             two
-//             threeˇ
-//             four
-//             fiveˇ
-//         "},
-//         true,
-//     );
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
+        build_editor(buffer, cx)
+    });
 
-//     let editor = cx
-//         .add_window(|cx| {
-//             let buffer = MultiBuffer::build_simple(&text, cx);
-//             build_editor(buffer, cx)
-//         })
-//         .root(cx);
+    view.update(cx, |view, cx| {
+        view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
+        view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::<f32>::zero(), cx);
+        view.end_selection(cx);
+
+        view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx);
+        view.update_selection(DisplayPoint::new(0, 3), 0, gpui::Point::<f32>::zero(), cx);
+        view.end_selection(cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1),
+            ]
+        );
+    });
 
-//     editor.update(cx, |editor, cx| {
-//         editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
-//         editor.fold_ranges(
-//             [
-//                 Point::new(1, 0)..Point::new(2, 0),
-//                 Point::new(3, 0)..Point::new(4, 0),
-//             ],
-//             true,
-//             cx,
-//         );
-//     });
+    view.update(cx, |view, cx| {
+        view.cancel(&Cancel, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)]
+        );
+    });
 
-//     let cloned_editor = editor
-//         .update(cx, |editor, cx| {
-//             cx.add_window(Default::default(), |cx| editor.clone(cx))
-//         })
-//         .root(cx);
+    view.update(cx, |view, cx| {
+        view.cancel(&Cancel, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)]
+        );
+    });
+}
 
-//     let snapshot = editor.update(cx, |e, cx| e.snapshot(cx));
-//     let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx));
+#[gpui::test]
+fn test_fold_action(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple(
+            &"
+                impl Foo {
+                    // Hello!
+
+                    fn a() {
+                        1
+                    }
+
+                    fn b() {
+                        2
+                    }
+
+                    fn c() {
+                        3
+                    }
+                }
+            "
+            .unindent(),
+            cx,
+        );
+        build_editor(buffer.clone(), cx)
+    });
 
-//     assert_eq!(
-//         cloned_editor.update(cx, |e, cx| e.display_text(cx)),
-//         editor.update(cx, |e, cx| e.display_text(cx))
-//     );
-//     assert_eq!(
-//         cloned_snapshot
-//             .folds_in_range(0..text.len())
-//             .collect::<Vec<_>>(),
-//         snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
-//     );
-//     assert_set_eq!(
-//         cloned_editor.read_with(cx, |editor, cx| editor.selections.ranges::<Point>(cx)),
-//         editor.read_with(cx, |editor, cx| editor.selections.ranges(cx))
-//     );
-//     assert_set_eq!(
-//         cloned_editor.update(cx, |e, cx| e.selections.display_ranges(cx)),
-//         editor.update(cx, |e, cx| e.selections.display_ranges(cx))
-//     );
-// }
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]);
+        });
+        view.fold(&Fold, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "
+                impl Foo {
+                    // Hello!
+
+                    fn a() {
+                        1
+                    }
+
+                    fn b() {⋯
+                    }
+
+                    fn c() {⋯
+                    }
+                }
+            "
+            .unindent(),
+        );
+
+        view.fold(&Fold, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "
+                impl Foo {⋯
+                }
+            "
+            .unindent(),
+        );
+
+        view.unfold_lines(&UnfoldLines, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "
+                impl Foo {
+                    // Hello!
+
+                    fn a() {
+                        1
+                    }
+
+                    fn b() {⋯
+                    }
+
+                    fn c() {⋯
+                    }
+                }
+            "
+            .unindent(),
+        );
+
+        view.unfold_lines(&UnfoldLines, cx);
+        assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text());
+    });
+}
 
-// #[gpui::test]
-// async fn test_navigation_history(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+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));
+
+    buffer.update(cx, |buffer, cx| {
+        buffer.edit(
+            vec![
+                (Point::new(1, 0)..Point::new(1, 0), "\t"),
+                (Point::new(1, 1)..Point::new(1, 1), "\t"),
+            ],
+            None,
+            cx,
+        );
+    });
+    view.update(cx, |view, cx| {
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
+        );
+
+        view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
+        );
+
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)]
+        );
+
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
+        );
+
+        view.move_up(&MoveUp, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
+        );
+
+        view.move_to_end(&MoveToEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)]
+        );
+
+        view.move_to_beginning(&MoveToBeginning, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
+        );
+
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]);
+        });
+        view.select_to_beginning(&SelectToBeginning, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)]
+        );
+
+        view.select_to_end(&SelectToEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)]
+        );
+    });
+}
 
-//     cx.set_global(DragAndDrop::<Workspace>::default());
-//     use workspace::item::Item;
+#[gpui::test]
+fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//     let fs = FakeFs::new(cx.background());
-//     let project = Project::test(fs, [], cx).await;
-//     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());
-//     window.add_view(cx, |cx| {
-//         let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
-//         let mut editor = build_editor(buffer.clone(), cx);
-//         let handle = cx.handle();
-//         editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx);
+        build_editor(buffer.clone(), cx)
+    });
 
-//         fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option<NavigationEntry> {
-//             editor.nav_history.as_mut().unwrap().pop_backward(cx)
-//         }
+    assert_eq!('ⓐ'.len_utf8(), 3);
+    assert_eq!('α'.len_utf8(), 2);
+
+    view.update(cx, |view, cx| {
+        view.fold_ranges(
+            vec![
+                Point::new(0, 6)..Point::new(0, 12),
+                Point::new(1, 2)..Point::new(1, 4),
+                Point::new(2, 4)..Point::new(2, 8),
+            ],
+            true,
+            cx,
+        );
+        assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε");
+
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "ⓐ".len())]
+        );
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "ⓐⓑ".len())]
+        );
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "ⓐⓑ⋯".len())]
+        );
+
+        view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "ab⋯e".len())]
+        );
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "ab⋯".len())]
+        );
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "ab".len())]
+        );
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "a".len())]
+        );
+
+        view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(2, "α".len())]
+        );
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(2, "αβ".len())]
+        );
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(2, "αβ⋯".len())]
+        );
+        view.move_right(&MoveRight, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(2, "αβ⋯ε".len())]
+        );
+
+        view.move_up(&MoveUp, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "ab⋯e".len())]
+        );
+        view.move_down(&MoveDown, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(2, "αβ⋯ε".len())]
+        );
+        view.move_up(&MoveUp, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(1, "ab⋯e".len())]
+        );
+
+        view.move_up(&MoveUp, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "ⓐⓑ".len())]
+        );
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "ⓐ".len())]
+        );
+        view.move_left(&MoveLeft, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[empty_range(0, "".len())]
+        );
+    });
+}
 
-//         // Move the cursor a small distance.
-//         // Nothing is added to the navigation history.
-//         editor.change_selections(None, cx, |s| {
-//             s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
-//         });
-//         editor.change_selections(None, cx, |s| {
-//             s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
-//         });
-//         assert!(pop_history(&mut editor, cx).is_none());
+//todo!(finish editor tests)
+// #[gpui::test]
+// fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
+//     init_test(cx, |_| {});
 
-//         // Move the cursor a large distance.
-//         // The history can jump back to the previous position.
-//         editor.change_selections(None, cx, |s| {
-//             s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
+//     let view = cx.add_window(|cx| {
+//         let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
+//         build_editor(buffer.clone(), cx)
+//     });
+//     view.update(cx, |view, cx| {
+//         view.change_selections(None, cx, |s| {
+//             s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
 //         });
-//         let nav_entry = pop_history(&mut editor, cx).unwrap();
-//         editor.navigate(nav_entry.data.unwrap(), cx);
-//         assert_eq!(nav_entry.item.id(), cx.view_id());
+//         view.move_down(&MoveDown, cx);
 //         assert_eq!(
-//             editor.selections.display_ranges(cx),
-//             &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
+//             view.selections.display_ranges(cx),
+//             &[empty_range(1, "abcd".len())]
 //         );
-//         assert!(pop_history(&mut editor, cx).is_none());
 
-//         // Move the cursor a small distance via the mouse.
-//         // Nothing is added to the navigation history.
-//         editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
-//         editor.end_selection(cx);
+//         view.move_down(&MoveDown, cx);
 //         assert_eq!(
-//             editor.selections.display_ranges(cx),
-//             &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
+//             view.selections.display_ranges(cx),
+//             &[empty_range(2, "αβγ".len())]
 //         );
-//         assert!(pop_history(&mut editor, cx).is_none());
 
-//         // Move the cursor a large distance via the mouse.
-//         // The history can jump back to the previous position.
-//         editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
-//         editor.end_selection(cx);
-//         assert_eq!(
-//             editor.selections.display_ranges(cx),
-//             &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
-//         );
-//         let nav_entry = pop_history(&mut editor, cx).unwrap();
-//         editor.navigate(nav_entry.data.unwrap(), cx);
-//         assert_eq!(nav_entry.item.id(), cx.view_id());
-//         assert_eq!(
-//             editor.selections.display_ranges(cx),
-//             &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
-//         );
-//         assert!(pop_history(&mut editor, cx).is_none());
-
-//         // Set scroll position to check later
-//         editor.set_scroll_position(Point<Pixels>::new(5.5, 5.5), cx);
-//         let original_scroll_position = editor.scroll_manager.anchor();
-
-//         // Jump to the end of the document and adjust scroll
-//         editor.move_to_end(&MoveToEnd, cx);
-//         editor.set_scroll_position(Point<Pixels>::new(-2.5, -0.5), cx);
-//         assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
-
-//         let nav_entry = pop_history(&mut editor, cx).unwrap();
-//         editor.navigate(nav_entry.data.unwrap(), cx);
-//         assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
-
-//         // Ensure we don't panic when navigation data contains invalid anchors *and* points.
-//         let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
-//         invalid_anchor.text_anchor.buffer_id = Some(999);
-//         let invalid_point = Point::new(9999, 0);
-//         editor.navigate(
-//             Box::new(NavigationData {
-//                 cursor_anchor: invalid_anchor,
-//                 cursor_position: invalid_point,
-//                 scroll_anchor: ScrollAnchor {
-//                     anchor: invalid_anchor,
-//                     offset: Default::default(),
-//                 },
-//                 scroll_top_row: invalid_point.row,
-//             }),
-//             cx,
-//         );
-//         assert_eq!(
-//             editor.selections.display_ranges(cx),
-//             &[editor.max_point(cx)..editor.max_point(cx)]
-//         );
+//         view.move_down(&MoveDown, cx);
 //         assert_eq!(
-//             editor.scroll_position(cx),
-//             vec2f(0., editor.max_point(cx).row() as f32)
+//             view.selections.display_ranges(cx),
+//             &[empty_range(3, "abcd".len())]
 //         );
 
-//         editor
-//     });
-// }
-
-// #[gpui::test]
-// 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)
-//         })
-//         .root(cx);
-
-//     view.update(cx, |view, cx| {
-//         view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
-//         view.update_selection(DisplayPoint::new(1, 1), 0, Point<Pixels>::zero(), cx);
-//         view.end_selection(cx);
-
-//         view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx);
-//         view.update_selection(DisplayPoint::new(0, 3), 0, Point<Pixels>::zero(), cx);
-//         view.end_selection(cx);
+//         view.move_down(&MoveDown, cx);
 //         assert_eq!(
 //             view.selections.display_ranges(cx),
-//             [
-//                 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
-//                 DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1),
-//             ]
+//             &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
 //         );
-//     });
 
-//     view.update(cx, |view, cx| {
-//         view.cancel(&Cancel, cx);
+//         view.move_up(&MoveUp, cx);
 //         assert_eq!(
 //             view.selections.display_ranges(cx),
-//             [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)]
+//             &[empty_range(3, "abcd".len())]
 //         );
-//     });
 
-//     view.update(cx, |view, cx| {
-//         view.cancel(&Cancel, cx);
+//         view.move_up(&MoveUp, cx);
 //         assert_eq!(
 //             view.selections.display_ranges(cx),
-//             [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)]
+//             &[empty_range(2, "αβγ".len())]
 //         );
 //     });
 // }
 
-// #[gpui::test]
-// fn test_fold_action(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+fn test_beginning_end_of_line(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//     let view = cx
-//         .add_window(|cx| {
-//             let buffer = MultiBuffer::build_simple(
-//                 &"
-//                 impl Foo {
-//                     // Hello!
-
-//                     fn a() {
-//                         1
-//                     }
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("abc\n  def", cx);
+        build_editor(buffer, cx)
+    });
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
+            ]);
+        });
+    });
 
-//                     fn b() {
-//                         2
-//                     }
+    view.update(cx, |view, cx| {
+        view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+            ]
+        );
+    });
 
-//                     fn c() {
-//                         3
-//                     }
-//                 }
-//             "
-//                 .unindent(),
-//                 cx,
-//             );
-//             build_editor(buffer.clone(), cx)
-//         })
-//         .root(cx);
+    view.update(cx, |view, cx| {
+        view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+            ]
+        );
+    });
 
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]);
-//         });
-//         view.fold(&Fold, cx);
-//         assert_eq!(
-//             view.display_text(cx),
-//             "
-//                 impl Foo {
-//                     // Hello!
+    view.update(cx, |view, cx| {
+        view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+            ]
+        );
+    });
 
-//                     fn a() {
-//                         1
-//                     }
+    view.update(cx, |view, cx| {
+        view.move_to_end_of_line(&MoveToEndOfLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
+            ]
+        );
+    });
 
-//                     fn b() {⋯
-//                     }
+    // Moving to the end of line again is a no-op.
+    view.update(cx, |view, cx| {
+        view.move_to_end_of_line(&MoveToEndOfLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
+            ]
+        );
+    });
 
-//                     fn c() {⋯
-//                     }
-//                 }
-//             "
-//             .unindent(),
-//         );
+    view.update(cx, |view, cx| {
+        view.move_left(&MoveLeft, cx);
+        view.select_to_beginning_of_line(
+            &SelectToBeginningOfLine {
+                stop_at_soft_wraps: true,
+            },
+            cx,
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
+            ]
+        );
+    });
 
-//         view.fold(&Fold, cx);
-//         assert_eq!(
-//             view.display_text(cx),
-//             "
-//                 impl Foo {⋯
-//                 }
-//             "
-//             .unindent(),
-//         );
+    view.update(cx, |view, cx| {
+        view.select_to_beginning_of_line(
+            &SelectToBeginningOfLine {
+                stop_at_soft_wraps: true,
+            },
+            cx,
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0),
+            ]
+        );
+    });
 
-//         view.unfold_lines(&UnfoldLines, cx);
-//         assert_eq!(
-//             view.display_text(cx),
-//             "
-//                 impl Foo {
-//                     // Hello!
+    view.update(cx, |view, cx| {
+        view.select_to_beginning_of_line(
+            &SelectToBeginningOfLine {
+                stop_at_soft_wraps: true,
+            },
+            cx,
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
+            ]
+        );
+    });
 
-//                     fn a() {
-//                         1
-//                     }
+    view.update(cx, |view, cx| {
+        view.select_to_end_of_line(
+            &SelectToEndOfLine {
+                stop_at_soft_wraps: true,
+            },
+            cx,
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5),
+            ]
+        );
+    });
 
-//                     fn b() {⋯
-//                     }
+    view.update(cx, |view, cx| {
+        view.delete_to_end_of_line(&DeleteToEndOfLine, cx);
+        assert_eq!(view.display_text(cx), "ab\n  de");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
+            ]
+        );
+    });
 
-//                     fn c() {⋯
-//                     }
-//                 }
-//             "
-//             .unindent(),
-//         );
+    view.update(cx, |view, cx| {
+        view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
+        assert_eq!(view.display_text(cx), "\n");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+            ]
+        );
+    });
+}
 
-//         view.unfold_lines(&UnfoldLines, cx);
-//         assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text());
-//     });
-// }
+#[gpui::test]
+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)
+    });
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11),
+                DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
+            ])
+        });
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n  {ˇbaz.qux()}", view, cx);
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n  ˇ{baz.qux()}", view, cx);
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ  {baz.qux()}", view, cx);
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n  {baz.qux()}", view, cx);
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n  {baz.qux()}", view, cx);
+
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n  {baz.qux()}", view, cx);
 
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n  {baz.qux()}", view, cx);
+
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n  {ˇbaz.qux()}", view, cx);
+
+        view.move_right(&MoveRight, cx);
+        view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
+        assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n  {«ˇb»az.qux()}", view, cx);
+
+        view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
+        assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n  «ˇ{b»az.qux()}", view, cx);
+
+        view.select_to_next_word_end(&SelectToNextWordEnd, cx);
+        assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n  {«ˇb»az.qux()}", view, cx);
+    });
+}
+
+//todo!(finish editor tests)
 // #[gpui::test]
-// fn test_move_cursor(cx: &mut TestAppContext) {
+// fn test_prev_next_word_bounds_with_soft_wrap(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))
-//         .root(cx);
-
-//     buffer.update(cx, |buffer, cx| {
-//         buffer.edit(
-//             vec![
-//                 (Point::new(1, 0)..Point::new(1, 0), "\t"),
-//                 (Point::new(1, 1)..Point::new(1, 1), "\t"),
-//             ],
-//             None,
-//             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)
 //     });
-//     view.update(cx, |view, cx| {
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
-//         );
 
-//         view.move_down(&MoveDown, cx);
+//     view.update(cx, |view, cx| {
+//         view.set_wrap_width(Some(140.0.into()), cx);
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
+//             view.display_text(cx),
+//             "use one::{\n    two::three::\n    four::five\n};"
 //         );
 
-//         view.move_right(&MoveRight, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)]
-//         );
+//         view.change_selections(None, cx, |s| {
+//             s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]);
+//         });
 
-//         view.move_left(&MoveLeft, cx);
+//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
 //         assert_eq!(
 //             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
+//             &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)]
 //         );
 
-//         view.move_up(&MoveUp, cx);
+//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
 //         assert_eq!(
 //             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
+//             &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
 //         );
 
-//         view.move_to_end(&MoveToEnd, cx);
+//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
 //         assert_eq!(
 //             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)]
+//             &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
 //         );
 
-//         view.move_to_beginning(&MoveToBeginning, cx);
+//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
 //         assert_eq!(
 //             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
+//             &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)]
 //         );
 
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]);
-//         });
-//         view.select_to_beginning(&SelectToBeginning, cx);
+//         view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
 //         assert_eq!(
 //             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)]
+//             &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
 //         );
 
-//         view.select_to_end(&SelectToEnd, cx);
+//         view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
 //         assert_eq!(
 //             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)]
+//             &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
 //         );
 //     });
 // }
 
+//todo!(simulate_resize)
 // #[gpui::test]
-// fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
+// async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
 //     init_test(cx, |_| {});
+//     let mut cx = EditorTestContext::new(cx).await;
 
-//     let view = cx
-//         .add_window(|cx| {
-//             let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx);
-//             build_editor(buffer.clone(), cx)
-//         })
-//         .root(cx);
+//     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+//     let window = cx.window;
+//     window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx);
 
-//     assert_eq!('ⓐ'.len_utf8(), 3);
-//     assert_eq!('α'.len_utf8(), 2);
+//     cx.set_state(
+//         &r#"ˇone
+//         two
 
-//     view.update(cx, |view, cx| {
-//         view.fold_ranges(
-//             vec![
-//                 Point::new(0, 6)..Point::new(0, 12),
-//                 Point::new(1, 2)..Point::new(1, 4),
-//                 Point::new(2, 4)..Point::new(2, 8),
-//             ],
-//             true,
-//             cx,
-//         );
-//         assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε");
+//         three
+//         fourˇ
+//         five
 
-//         view.move_right(&MoveRight, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(0, "ⓐ".len())]
-//         );
-//         view.move_right(&MoveRight, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(0, "ⓐⓑ".len())]
-//         );
-//         view.move_right(&MoveRight, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(0, "ⓐⓑ⋯".len())]
-//         );
+//         six"#
+//             .unindent(),
+//     );
 
-//         view.move_down(&MoveDown, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(1, "ab⋯e".len())]
-//         );
-//         view.move_left(&MoveLeft, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(1, "ab⋯".len())]
-//         );
-//         view.move_left(&MoveLeft, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(1, "ab".len())]
-//         );
-//         view.move_left(&MoveLeft, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(1, "a".len())]
-//         );
+//     cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+//     cx.assert_editor_state(
+//         &r#"one
+//         two
+//         ˇ
+//         three
+//         four
+//         five
+//         ˇ
+//         six"#
+//             .unindent(),
+//     );
 
-//         view.move_down(&MoveDown, cx);
+//     cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+//     cx.assert_editor_state(
+//         &r#"one
+//         two
+
+//         three
+//         four
+//         five
+//         ˇ
+//         sixˇ"#
+//             .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+//     cx.assert_editor_state(
+//         &r#"one
+//         two
+
+//         three
+//         four
+//         five
+
+//         sixˇ"#
+//             .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+//     cx.assert_editor_state(
+//         &r#"one
+//         two
+
+//         three
+//         four
+//         five
+//         ˇ
+//         six"#
+//             .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+//     cx.assert_editor_state(
+//         &r#"one
+//         two
+//         ˇ
+//         three
+//         four
+//         five
+
+//         six"#
+//             .unindent(),
+//     );
+
+//     cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+//     cx.assert_editor_state(
+//         &r#"ˇone
+//         two
+
+//         three
+//         four
+//         five
+
+//         six"#
+//             .unindent(),
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
+//     init_test(cx, |_| {});
+//     let mut cx = EditorTestContext::new(cx).await;
+//     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+//     let window = cx.window;
+//     window.simulate_resize(Point::new(1000., 4. * line_height + 0.5), &mut cx);
+
+//     cx.set_state(
+//         &r#"ˇone
+//         two
+//         three
+//         four
+//         five
+//         six
+//         seven
+//         eight
+//         nine
+//         ten
+//         "#,
+//     );
+
+//     cx.update_editor(|editor, cx| {
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(2, "α".len())]
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 0.)
 //         );
-//         view.move_right(&MoveRight, cx);
+//         editor.scroll_screen(&ScrollAmount::Page(1.), cx);
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(2, "αβ".len())]
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 3.)
 //         );
-//         view.move_right(&MoveRight, cx);
+//         editor.scroll_screen(&ScrollAmount::Page(1.), cx);
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(2, "αβ⋯".len())]
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 6.)
 //         );
-//         view.move_right(&MoveRight, cx);
+//         editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(2, "αβ⋯ε".len())]
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 3.)
 //         );
 
-//         view.move_up(&MoveUp, cx);
+//         editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(1, "ab⋯e".len())]
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 1.)
 //         );
-//         view.move_down(&MoveDown, cx);
+//         editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(2, "αβ⋯ε".len())]
-//         );
-//         view.move_up(&MoveUp, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(1, "ab⋯e".len())]
-//         );
-
-//         view.move_up(&MoveUp, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(0, "ⓐⓑ".len())]
-//         );
-//         view.move_left(&MoveLeft, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(0, "ⓐ".len())]
-//         );
-//         view.move_left(&MoveLeft, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(0, "".len())]
-//         );
-//     });
-// }
-
-// #[gpui::test]
-// 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)
-//         })
-//         .root(cx);
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
-//         });
-//         view.move_down(&MoveDown, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(1, "abcd".len())]
-//         );
-
-//         view.move_down(&MoveDown, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(2, "αβγ".len())]
-//         );
-
-//         view.move_down(&MoveDown, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(3, "abcd".len())]
-//         );
-
-//         view.move_down(&MoveDown, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
-//         );
-
-//         view.move_up(&MoveUp, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(3, "abcd".len())]
-//         );
-
-//         view.move_up(&MoveUp, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[empty_range(2, "αβγ".len())]
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 3.)
 //         );
 //     });
 // }
 
 // #[gpui::test]
-// fn test_beginning_end_of_line(cx: &mut TestAppContext) {
+// async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
 //     init_test(cx, |_| {});
+//     let mut cx = EditorTestContext::new(cx).await;
 
-//     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([
-//                 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
-//             ]);
-//         });
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
-//                 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
-//             ]
-//         );
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
-//                 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
-//             ]
-//         );
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
-//                 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
-//             ]
-//         );
+//     let line_height = cx.update_editor(|editor, cx| {
+//         editor.set_vertical_scroll_margin(2, cx);
+//         editor.style(cx).text.line_height(cx.font_cache())
 //     });
 
-//     view.update(cx, |view, cx| {
-//         view.move_to_end_of_line(&MoveToEndOfLine, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[
-//                 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
-//                 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
-//             ]
-//         );
-//     });
+//     let window = cx.window;
+//     window.simulate_resize(gpui::Point::new(1000., 6.0 * line_height), &mut cx);
 
-//     // Moving to the end of line again is a no-op.
-//     view.update(cx, |view, cx| {
-//         view.move_to_end_of_line(&MoveToEndOfLine, cx);
+//     cx.set_state(
+//         &r#"ˇone
+//             two
+//             three
+//             four
+//             five
+//             six
+//             seven
+//             eight
+//             nine
+//             ten
+//         "#,
+//     );
+//     cx.update_editor(|editor, cx| {
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[
-//                 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
-//                 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
-//             ]
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 0.0)
 //         );
 //     });
 
-//     view.update(cx, |view, cx| {
-//         view.move_left(&MoveLeft, cx);
-//         view.select_to_beginning_of_line(
-//             &SelectToBeginningOfLine {
-//                 stop_at_soft_wraps: true,
-//             },
-//             cx,
-//         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[
-//                 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
-//                 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
-//             ]
-//         );
+//     // Add a cursor below the visible area. Since both cursors cannot fit
+//     // on screen, the editor autoscrolls to reveal the newest cursor, and
+//     // allows the vertical scroll margin below that cursor.
+//     cx.update_editor(|editor, cx| {
+//         editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
+//             selections.select_ranges([
+//                 Point::new(0, 0)..Point::new(0, 0),
+//                 Point::new(6, 0)..Point::new(6, 0),
+//             ]);
+//         })
 //     });
-
-//     view.update(cx, |view, cx| {
-//         view.select_to_beginning_of_line(
-//             &SelectToBeginningOfLine {
-//                 stop_at_soft_wraps: true,
-//             },
-//             cx,
-//         );
+//     cx.update_editor(|editor, cx| {
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[
-//                 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
-//                 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0),
-//             ]
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 3.0)
 //         );
 //     });
 
-//     view.update(cx, |view, cx| {
-//         view.select_to_beginning_of_line(
-//             &SelectToBeginningOfLine {
-//                 stop_at_soft_wraps: true,
-//             },
-//             cx,
-//         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[
-//                 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
-//                 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
-//             ]
-//         );
+//     // Move down. The editor cursor scrolls down to track the newest cursor.
+//     cx.update_editor(|editor, cx| {
+//         editor.move_down(&Default::default(), cx);
 //     });
-
-//     view.update(cx, |view, cx| {
-//         view.select_to_end_of_line(
-//             &SelectToEndOfLine {
-//                 stop_at_soft_wraps: true,
-//             },
-//             cx,
-//         );
+//     cx.update_editor(|editor, cx| {
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[
-//                 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
-//                 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5),
-//             ]
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 4.0)
 //         );
 //     });
 
-//     view.update(cx, |view, cx| {
-//         view.delete_to_end_of_line(&DeleteToEndOfLine, cx);
-//         assert_eq!(view.display_text(cx), "ab\n  de");
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[
-//                 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-//                 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
-//             ]
-//         );
+//     // Add a cursor above the visible area. Since both cursors fit on screen,
+//     // the editor scrolls to show both.
+//     cx.update_editor(|editor, cx| {
+//         editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
+//             selections.select_ranges([
+//                 Point::new(1, 0)..Point::new(1, 0),
+//                 Point::new(6, 0)..Point::new(6, 0),
+//             ]);
+//         })
 //     });
-
-//     view.update(cx, |view, cx| {
-//         view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
-//         assert_eq!(view.display_text(cx), "\n");
+//     cx.update_editor(|editor, cx| {
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
-//                 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
-//             ]
+//             editor.snapshot(cx).scroll_position(),
+//             gpui::Point::new(0., 1.0)
 //         );
 //     });
 // }
 
 // #[gpui::test]
-// fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
+// async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
 //     init_test(cx, |_| {});
+//     let mut cx = EditorTestContext::new(cx).await;
 
-//     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([
-//                 DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11),
-//                 DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
-//             ])
-//         });
-
-//         view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
-//         assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n  {ˇbaz.qux()}", view, cx);
-
-//         view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
-//         assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n  ˇ{baz.qux()}", view, cx);
-
-//         view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
-//         assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ  {baz.qux()}", view, cx);
+//     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+//     let window = cx.window;
+//     window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx);
 
-//         view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
-//         assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n  {baz.qux()}", view, cx);
+//     cx.set_state(
+//         &r#"
+//         ˇone
+//         two
+//         threeˇ
+//         four
+//         five
+//         six
+//         seven
+//         eight
+//         nine
+//         ten
+//         "#
+//         .unindent(),
+//     );
 
-//         view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
-//         assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n  {baz.qux()}", view, cx);
+//     cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
+//     cx.assert_editor_state(
+//         &r#"
+//         one
+//         two
+//         three
+//         ˇfour
+//         five
+//         sixˇ
+//         seven
+//         eight
+//         nine
+//         ten
+//         "#
+//         .unindent(),
+//     );
 
-//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-//         assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n  {baz.qux()}", view, cx);
+//     cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
+//     cx.assert_editor_state(
+//         &r#"
+//         one
+//         two
+//         three
+//         four
+//         five
+//         six
+//         ˇseven
+//         eight
+//         nineˇ
+//         ten
+//         "#
+//         .unindent(),
+//     );
 
-//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-//         assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n  {baz.qux()}", view, cx);
-
-//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-//         assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n  {ˇbaz.qux()}", view, cx);
-
-//         view.move_right(&MoveRight, cx);
-//         view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
-//         assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n  {«ˇb»az.qux()}", view, cx);
+//     cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
+//     cx.assert_editor_state(
+//         &r#"
+//         one
+//         two
+//         three
+//         ˇfour
+//         five
+//         sixˇ
+//         seven
+//         eight
+//         nine
+//         ten
+//         "#
+//         .unindent(),
+//     );
 
-//         view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
-//         assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n  «ˇ{b»az.qux()}", view, cx);
+//     cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
+//     cx.assert_editor_state(
+//         &r#"
+//         ˇone
+//         two
+//         threeˇ
+//         four
+//         five
+//         six
+//         seven
+//         eight
+//         nine
+//         ten
+//         "#
+//         .unindent(),
+//     );
 
-//         view.select_to_next_word_end(&SelectToNextWordEnd, cx);
-//         assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n  {«ˇb»az.qux()}", view, cx);
+//     // Test select collapsing
+//     cx.update_editor(|editor, cx| {
+//         editor.move_page_down(&MovePageDown::default(), cx);
+//         editor.move_page_down(&MovePageDown::default(), cx);
+//         editor.move_page_down(&MovePageDown::default(), cx);
 //     });
+//     cx.assert_editor_state(
+//         &r#"
+//         one
+//         two
+//         three
+//         four
+//         five
+//         six
+//         seven
+//         eight
+//         nine
+//         ˇten
+//         ˇ"#
+//         .unindent(),
+//     );
 // }
 
-// #[gpui::test]
-// fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state("one «two threeˇ» four");
+    cx.update_editor(|editor, cx| {
+        editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
+        assert_eq!(editor.text(cx), " four");
+    });
+}
 
-//     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);
+#[gpui::test]
+fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//     view.update(cx, |view, cx| {
-//         view.set_wrap_width(Some(140.), cx);
-//         assert_eq!(
-//             view.display_text(cx),
-//             "use one::{\n    two::three::\n    four::five\n};"
-//         );
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("one two three four", cx);
+        build_editor(buffer.clone(), cx)
+    });
 
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]);
-//         });
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                // an empty selection - the preceding word fragment is deleted
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                // characters selected - they are deleted
+                DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12),
+            ])
+        });
+        view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx);
+        assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four");
+    });
 
-//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)]
-//         );
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                // an empty selection - the following word fragment is deleted
+                DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+                // characters selected - they are deleted
+                DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10),
+            ])
+        });
+        view.delete_to_next_word_end(&DeleteToNextWordEnd, cx);
+        assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our");
+    });
+}
 
-//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
-//         );
+#[gpui::test]
+fn test_newline(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
-//         );
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("aaaa\n    bbbb\n", cx);
+        build_editor(buffer.clone(), cx)
+    });
 
-//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)]
-//         );
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+                DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6),
+            ])
+        });
 
-//         view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
-//         );
+        view.newline(&Newline, cx);
+        assert_eq!(view.text(cx), "aa\naa\n  \n    bb\n    bb\n");
+    });
+}
 
-//         view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
-//         );
-//     });
-// }
+#[gpui::test]
+fn test_newline_with_old_selections(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let editor = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple(
+            "
+                a
+                b(
+                    X
+                )
+                c(
+                    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
+    });
 
-// #[gpui::test]
-// async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-//     let mut cx = EditorTestContext::new(cx).await;
+    editor.update(cx, |editor, cx| {
+        // Edit the buffer directly, deleting ranges surrounding the editor's selections
+        editor.buffer.update(cx, |buffer, cx| {
+            buffer.edit(
+                [
+                    (Point::new(1, 2)..Point::new(3, 0), ""),
+                    (Point::new(4, 2)..Point::new(6, 0), ""),
+                ],
+                None,
+                cx,
+            );
+            assert_eq!(
+                buffer.read(cx).text(),
+                "
+                    a
+                    b()
+                    c()
+                "
+                .unindent()
+            );
+        });
+        assert_eq!(
+            editor.selections.ranges(cx),
+            &[
+                Point::new(1, 2)..Point::new(1, 2),
+                Point::new(2, 2)..Point::new(2, 2),
+            ],
+        );
+
+        editor.newline(&Newline, cx);
+        assert_eq!(
+            editor.text(cx),
+            "
+                a
+                b(
+                )
+                c(
+                )
+            "
+            .unindent()
+        );
+
+        // The selections are moved after the inserted newlines
+        assert_eq!(
+            editor.selections.ranges(cx),
+            &[
+                Point::new(2, 0)..Point::new(2, 0),
+                Point::new(4, 0)..Point::new(4, 0),
+            ],
+        );
+    });
+}
 
-//     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-//     let window = cx.window;
-//     window.simulate_resize(vec2f(100., 4. * line_height), &mut cx);
+#[gpui::test]
+async fn test_newline_above(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
+    });
 
-//     cx.set_state(
-//         &r#"ˇone
-//         two
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig::default(),
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
+        .unwrap(),
+    );
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+    cx.set_state(indoc! {"
+        const a: ˇA = (
+            (ˇ
+                «const_functionˇ»(ˇ),
+                so«mˇ»et«hˇ»ing_ˇelse,ˇ
+            )ˇ
+        ˇ);ˇ
+    "});
+
+    cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx));
+    cx.assert_editor_state(indoc! {"
+        ˇ
+        const a: A = (
+            ˇ
+            (
+                ˇ
+                ˇ
+                const_function(),
+                ˇ
+                ˇ
+                ˇ
+                ˇ
+                something_else,
+                ˇ
+            )
+            ˇ
+            ˇ
+        );
+    "});
+}
 
-//         three
-//         fourˇ
-//         five
+#[gpui::test]
+async fn test_newline_below(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
+    });
 
-//         six"#
-//             .unindent(),
-//     );
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig::default(),
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
+        .unwrap(),
+    );
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+    cx.set_state(indoc! {"
+        const a: ˇA = (
+            (ˇ
+                «const_functionˇ»(ˇ),
+                so«mˇ»et«hˇ»ing_ˇelse,ˇ
+            )ˇ
+        ˇ);ˇ
+    "});
+
+    cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx));
+    cx.assert_editor_state(indoc! {"
+        const a: A = (
+            ˇ
+            (
+                ˇ
+                const_function(),
+                ˇ
+                ˇ
+                something_else,
+                ˇ
+                ˇ
+                ˇ
+                ˇ
+            )
+            ˇ
+        );
+        ˇ
+        ˇ
+    "});
+}
 
-//     cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
-//     cx.assert_editor_state(
-//         &r#"one
-//         two
-//         ˇ
-//         three
-//         four
-//         five
-//         ˇ
-//         six"#
-//             .unindent(),
-//     );
+#[gpui::test]
+async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
+    });
 
-//     cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
-//     cx.assert_editor_state(
-//         &r#"one
-//         two
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            line_comment: Some("//".into()),
+            ..LanguageConfig::default()
+        },
+        None,
+    ));
+    {
+        let mut cx = EditorTestContext::new(cx).await;
+        cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+        cx.set_state(indoc! {"
+        // Fooˇ
+    "});
+
+        cx.update_editor(|e, cx| e.newline(&Newline, cx));
+        cx.assert_editor_state(indoc! {"
+        // Foo
+        //ˇ
+    "});
+        // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
+        cx.set_state(indoc! {"
+        ˇ// Foo
+    "});
+        cx.update_editor(|e, cx| e.newline(&Newline, cx));
+        cx.assert_editor_state(indoc! {"
+
+        ˇ// Foo
+    "});
+    }
+    // Ensure that comment continuations can be disabled.
+    update_test_language_settings(cx, |settings| {
+        settings.defaults.extend_comment_on_newline = Some(false);
+    });
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state(indoc! {"
+        // Fooˇ
+    "});
+    cx.update_editor(|e, cx| e.newline(&Newline, cx));
+    cx.assert_editor_state(indoc! {"
+        // Foo
+        ˇ
+    "});
+}
 
-//         three
-//         four
-//         five
-//         ˇ
-//         sixˇ"#
-//             .unindent(),
-//     );
+#[gpui::test]
+fn test_insert_with_old_selections(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//     cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
-//     cx.assert_editor_state(
-//         &r#"one
-//         two
+    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
+    });
 
-//         three
-//         four
-//         five
+    editor.update(cx, |editor, cx| {
+        // Edit the buffer directly, deleting ranges surrounding the editor's selections
+        editor.buffer.update(cx, |buffer, cx| {
+            buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
+            assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
+        });
+        assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
 
-//         sixˇ"#
-//             .unindent(),
-//     );
+        editor.insert("Z", cx);
+        assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
 
-//     cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
-//     cx.assert_editor_state(
-//         &r#"one
-//         two
+        // The selections are moved after the inserted characters
+        assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
+    });
+}
 
-//         three
-//         four
-//         five
-//         ˇ
-//         six"#
-//             .unindent(),
-//     );
+#[gpui::test]
+async fn test_tab(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(3)
+    });
 
-//     cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
-//     cx.assert_editor_state(
-//         &r#"one
-//         two
-//         ˇ
-//         three
-//         four
-//         five
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state(indoc! {"
+        ˇabˇc
+        ˇ🏀ˇ🏀ˇefg
+        dˇ
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+           ˇab ˇc
+           ˇ🏀  ˇ🏀  ˇefg
+        d  ˇ
+    "});
+
+    cx.set_state(indoc! {"
+        a
+        «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        a
+           «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
+    "});
+}
 
-//         six"#
-//             .unindent(),
-//     );
+#[gpui::test]
+async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig::default(),
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
+        .unwrap(),
+    );
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+    // cursors that are already at the suggested indent level insert
+    // a soft tab. cursors that are to the left of the suggested indent
+    // auto-indent their line.
+    cx.set_state(indoc! {"
+        ˇ
+        const a: B = (
+            c(
+                d(
+        ˇ
+                )
+        ˇ
+        ˇ    )
+        );
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+            ˇ
+        const a: B = (
+            c(
+                d(
+                    ˇ
+                )
+                ˇ
+            ˇ)
+        );
+    "});
+
+    // handle auto-indent when there are multiple cursors on the same line
+    cx.set_state(indoc! {"
+        const a: B = (
+            c(
+        ˇ    ˇ
+        ˇ    )
+        );
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        const a: B = (
+            c(
+                ˇ
+            ˇ)
+        );
+    "});
+}
 
-//     cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
-//     cx.assert_editor_state(
-//         &r#"ˇone
-//         two
+#[gpui::test]
+async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
+    });
 
-//         three
-//         four
-//         five
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig::default(),
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
+        .unwrap(),
+    );
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+    cx.set_state(indoc! {"
+        fn a() {
+            if b {
+        \t ˇc
+            }
+        }
+    "});
+
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        fn a() {
+            if b {
+                ˇc
+            }
+        }
+    "});
+}
 
-//         six"#
-//             .unindent(),
-//     );
-// }
+#[gpui::test]
+async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4);
+    });
 
-// #[gpui::test]
-// async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-//     let mut cx = EditorTestContext::new(cx).await;
-//     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-//     let window = cx.window;
-//     window.simulate_resize(vec2f(1000., 4. * line_height + 0.5), &mut cx);
+    let mut cx = EditorTestContext::new(cx).await;
+
+    cx.set_state(indoc! {"
+          «oneˇ» «twoˇ»
+        three
+         four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+            «oneˇ» «twoˇ»
+        three
+         four
+    "});
+
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        «oneˇ» «twoˇ»
+        three
+         four
+    "});
+
+    // select across line ending
+    cx.set_state(indoc! {"
+        one two
+        t«hree
+        ˇ» four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+            t«hree
+        ˇ» four
+    "});
+
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        t«hree
+        ˇ» four
+    "});
+
+    // Ensure that indenting/outdenting works when the cursor is at column 0.
+    cx.set_state(indoc! {"
+        one two
+        ˇthree
+            four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+            ˇthree
+            four
+    "});
+
+    cx.set_state(indoc! {"
+        one two
+        ˇ    three
+            four
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        ˇthree
+            four
+    "});
+}
 
-//     cx.set_state(
-//         &r#"ˇone
-//         two
-//         three
-//         four
-//         five
-//         six
-//         seven
-//         eight
-//         nine
-//         ten
-//         "#,
-//     );
+#[gpui::test]
+async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.hard_tabs = Some(true);
+    });
 
-//     cx.update_editor(|editor, cx| {
-//         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.));
-//         editor.scroll_screen(&ScrollAmount::Page(1.), cx);
-//         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
-//         editor.scroll_screen(&ScrollAmount::Page(1.), cx);
-//         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.));
-//         editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
-//         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // select two ranges on one line
+    cx.set_state(indoc! {"
+        «oneˇ» «twoˇ»
+        three
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        \t«oneˇ» «twoˇ»
+        three
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        \t\t«oneˇ» «twoˇ»
+        three
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        \t«oneˇ» «twoˇ»
+        three
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        «oneˇ» «twoˇ»
+        three
+        four
+    "});
+
+    // select across a line ending
+    cx.set_state(indoc! {"
+        one two
+        t«hree
+        ˇ»four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        \tt«hree
+        ˇ»four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        \t\tt«hree
+        ˇ»four
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        \tt«hree
+        ˇ»four
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        t«hree
+        ˇ»four
+    "});
+
+    // Ensure that indenting/outdenting works when the cursor is at column 0.
+    cx.set_state(indoc! {"
+        one two
+        ˇthree
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        ˇthree
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab(&Tab, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        \tˇthree
+        four
+    "});
+    cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+    cx.assert_editor_state(indoc! {"
+        one two
+        ˇthree
+        four
+    "});
+}
 
-//         editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
-//         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.));
-//         editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
-//         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
-//     });
-// }
+#[gpui::test]
+fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
+    init_test(cx, |settings| {
+        settings.languages.extend([
+            (
+                "TOML".into(),
+                LanguageSettingsContent {
+                    tab_size: NonZeroU32::new(2),
+                    ..Default::default()
+                },
+            ),
+            (
+                "Rust".into(),
+                LanguageSettingsContent {
+                    tab_size: NonZeroU32::new(4),
+                    ..Default::default()
+                },
+            ),
+        ]);
+    });
 
-// #[gpui::test]
-// async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-//     let mut cx = EditorTestContext::new(cx).await;
+    let toml_language = Arc::new(Language::new(
+        LanguageConfig {
+            name: "TOML".into(),
+            ..Default::default()
+        },
+        None,
+    ));
+    let rust_language = Arc::new(Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            ..Default::default()
+        },
+        None,
+    ));
+
+    let toml_buffer = cx.build_model(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n").with_language(toml_language, cx)
+    });
+    let rust_buffer = cx.build_model(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), "const c: usize = 3;\n")
+            .with_language(rust_language, cx)
+    });
+    let multibuffer = cx.build_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        multibuffer.push_excerpts(
+            toml_buffer.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(2, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multibuffer.push_excerpts(
+            rust_buffer.clone(),
+            [ExcerptRange {
+                context: Point::new(0, 0)..Point::new(1, 0),
+                primary: None,
+            }],
+            cx,
+        );
+        multibuffer
+    });
 
-//     let line_height = cx.update_editor(|editor, cx| {
-//         editor.set_vertical_scroll_margin(2, cx);
-//         editor.style(cx).text.line_height(cx.font_cache())
-//     });
-
-//     let window = cx.window;
-//     window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx);
-
-//     cx.set_state(
-//         &r#"ˇone
-//             two
-//             three
-//             four
-//             five
-//             six
-//             seven
-//             eight
-//             nine
-//             ten
-//         "#,
-//     );
-//     cx.update_editor(|editor, cx| {
-//         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0));
-//     });
-
-//     // Add a cursor below the visible area. Since both cursors cannot fit
-//     // on screen, the editor autoscrolls to reveal the newest cursor, and
-//     // allows the vertical scroll margin below that cursor.
-//     cx.update_editor(|editor, cx| {
-//         editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
-//             selections.select_ranges([
-//                 Point::new(0, 0)..Point::new(0, 0),
-//                 Point::new(6, 0)..Point::new(6, 0),
-//             ]);
-//         })
-//     });
-//     cx.update_editor(|editor, cx| {
-//         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
-//     });
-
-//     // Move down. The editor cursor scrolls down to track the newest cursor.
-//     cx.update_editor(|editor, cx| {
-//         editor.move_down(&Default::default(), cx);
-//     });
-//     cx.update_editor(|editor, cx| {
-//         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0));
-//     });
-
-//     // Add a cursor above the visible area. Since both cursors fit on screen,
-//     // the editor scrolls to show both.
-//     cx.update_editor(|editor, cx| {
-//         editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
-//             selections.select_ranges([
-//                 Point::new(1, 0)..Point::new(1, 0),
-//                 Point::new(6, 0)..Point::new(6, 0),
-//             ]);
-//         })
-//     });
-//     cx.update_editor(|editor, cx| {
-//         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0));
-//     });
-// }
-
-// #[gpui::test]
-// async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-//     let mut cx = EditorTestContext::new(cx).await;
-
-//     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-//     let window = cx.window;
-//     window.simulate_resize(vec2f(100., 4. * line_height), &mut cx);
-
-//     cx.set_state(
-//         &r#"
-//         ˇone
-//         two
-//         threeˇ
-//         four
-//         five
-//         six
-//         seven
-//         eight
-//         nine
-//         ten
-//         "#
-//         .unindent(),
-//     );
-
-//     cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
-//     cx.assert_editor_state(
-//         &r#"
-//         one
-//         two
-//         three
-//         ˇfour
-//         five
-//         sixˇ
-//         seven
-//         eight
-//         nine
-//         ten
-//         "#
-//         .unindent(),
-//     );
-
-//     cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
-//     cx.assert_editor_state(
-//         &r#"
-//         one
-//         two
-//         three
-//         four
-//         five
-//         six
-//         ˇseven
-//         eight
-//         nineˇ
-//         ten
-//         "#
-//         .unindent(),
-//     );
-
-//     cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
-//     cx.assert_editor_state(
-//         &r#"
-//         one
-//         two
-//         three
-//         ˇfour
-//         five
-//         sixˇ
-//         seven
-//         eight
-//         nine
-//         ten
-//         "#
-//         .unindent(),
-//     );
-
-//     cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
-//     cx.assert_editor_state(
-//         &r#"
-//         ˇone
-//         two
-//         threeˇ
-//         four
-//         five
-//         six
-//         seven
-//         eight
-//         nine
-//         ten
-//         "#
-//         .unindent(),
-//     );
-
-//     // Test select collapsing
-//     cx.update_editor(|editor, cx| {
-//         editor.move_page_down(&MovePageDown::default(), cx);
-//         editor.move_page_down(&MovePageDown::default(), cx);
-//         editor.move_page_down(&MovePageDown::default(), cx);
-//     });
-//     cx.assert_editor_state(
-//         &r#"
-//         one
-//         two
-//         three
-//         four
-//         five
-//         six
-//         seven
-//         eight
-//         nine
-//         ˇten
-//         ˇ"#
-//         .unindent(),
-//     );
-// }
-
-// #[gpui::test]
-// async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-//     let mut cx = EditorTestContext::new(cx).await;
-//     cx.set_state("one «two threeˇ» four");
-//     cx.update_editor(|editor, cx| {
-//         editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
-//         assert_eq!(editor.text(cx), " four");
-//     });
-// }
-
-// #[gpui::test]
-// 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)
-//         })
-//         .root(cx);
-
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([
-//                 // an empty selection - the preceding word fragment is deleted
-//                 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-//                 // characters selected - they are deleted
-//                 DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12),
-//             ])
-//         });
-//         view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx);
-//         assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four");
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([
-//                 // an empty selection - the following word fragment is deleted
-//                 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
-//                 // characters selected - they are deleted
-//                 DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10),
-//             ])
-//         });
-//         view.delete_to_next_word_end(&DeleteToNextWordEnd, cx);
-//         assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our");
-//     });
-// }
-
-// #[gpui::test]
-// 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)
-//         })
-//         .root(cx);
-
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([
-//                 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-//                 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
-//                 DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6),
-//             ])
-//         });
-
-//         view.newline(&Newline, cx);
-//         assert_eq!(view.text(cx), "aa\naa\n  \n    bb\n    bb\n");
-//     });
-// }
-
-// #[gpui::test]
-// fn test_newline_with_old_selections(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let editor = cx
-//         .add_window(|cx| {
-//             let buffer = MultiBuffer::build_simple(
-//                 "
-//                 a
-//                 b(
-//                     X
-//                 )
-//                 c(
-//                     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
-//         })
-//         .root(cx);
-
-//     editor.update(cx, |editor, cx| {
-//         // Edit the buffer directly, deleting ranges surrounding the editor's selections
-//         editor.buffer.update(cx, |buffer, cx| {
-//             buffer.edit(
-//                 [
-//                     (Point::new(1, 2)..Point::new(3, 0), ""),
-//                     (Point::new(4, 2)..Point::new(6, 0), ""),
-//                 ],
-//                 None,
-//                 cx,
-//             );
-//             assert_eq!(
-//                 buffer.read(cx).text(),
-//                 "
-//                     a
-//                     b()
-//                     c()
-//                 "
-//                 .unindent()
-//             );
-//         });
-//         assert_eq!(
-//             editor.selections.ranges(cx),
-//             &[
-//                 Point::new(1, 2)..Point::new(1, 2),
-//                 Point::new(2, 2)..Point::new(2, 2),
-//             ],
-//         );
-
-//         editor.newline(&Newline, cx);
-//         assert_eq!(
-//             editor.text(cx),
-//             "
-//                 a
-//                 b(
-//                 )
-//                 c(
-//                 )
-//             "
-//             .unindent()
-//         );
-
-//         // The selections are moved after the inserted newlines
-//         assert_eq!(
-//             editor.selections.ranges(cx),
-//             &[
-//                 Point::new(2, 0)..Point::new(2, 0),
-//                 Point::new(4, 0)..Point::new(4, 0),
-//             ],
-//         );
-//     });
-// }
-
-// #[gpui::test]
-// async fn test_newline_above(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |settings| {
-//         settings.defaults.tab_size = NonZeroU32::new(4)
-//     });
-
-//     let language = Arc::new(
-//         Language::new(
-//             LanguageConfig::default(),
-//             Some(tree_sitter_rust::language()),
-//         )
-//         .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
-//         .unwrap(),
-//     );
-
-//     let mut cx = EditorTestContext::new(cx).await;
-//     cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
-//     cx.set_state(indoc! {"
-//         const a: ˇA = (
-//             (ˇ
-//                 «const_functionˇ»(ˇ),
-//                 so«mˇ»et«hˇ»ing_ˇelse,ˇ
-//             )ˇ
-//         ˇ);ˇ
-//     "});
-
-//     cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx));
-//     cx.assert_editor_state(indoc! {"
-//         ˇ
-//         const a: A = (
-//             ˇ
-//             (
-//                 ˇ
-//                 ˇ
-//                 const_function(),
-//                 ˇ
-//                 ˇ
-//                 ˇ
-//                 ˇ
-//                 something_else,
-//                 ˇ
-//             )
-//             ˇ
-//             ˇ
-//         );
-//     "});
-// }
-
-// #[gpui::test]
-// async fn test_newline_below(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |settings| {
-//         settings.defaults.tab_size = NonZeroU32::new(4)
-//     });
-
-//     let language = Arc::new(
-//         Language::new(
-//             LanguageConfig::default(),
-//             Some(tree_sitter_rust::language()),
-//         )
-//         .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
-//         .unwrap(),
-//     );
-
-//     let mut cx = EditorTestContext::new(cx).await;
-//     cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
-//     cx.set_state(indoc! {"
-//         const a: ˇA = (
-//             (ˇ
-//                 «const_functionˇ»(ˇ),
-//                 so«mˇ»et«hˇ»ing_ˇelse,ˇ
-//             )ˇ
-//         ˇ);ˇ
-//     "});
-
-//     cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx));
-//     cx.assert_editor_state(indoc! {"
-//         const a: A = (
-//             ˇ
-//             (
-//                 ˇ
-//                 const_function(),
-//                 ˇ
-//                 ˇ
-//                 something_else,
-//                 ˇ
-//                 ˇ
-//                 ˇ
-//                 ˇ
-//             )
-//             ˇ
-//         );
-//         ˇ
-//         ˇ
-//     "});
-// }
-
-// #[gpui::test]
-// async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |settings| {
-//         settings.defaults.tab_size = NonZeroU32::new(4)
-//     });
-
-//     let language = Arc::new(Language::new(
-//         LanguageConfig {
-//             line_comment: Some("//".into()),
-//             ..LanguageConfig::default()
-//         },
-//         None,
-//     ));
-//     {
-//         let mut cx = EditorTestContext::new(cx).await;
-//         cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
-//         cx.set_state(indoc! {"
-//         // Fooˇ
-//     "});
-
-//         cx.update_editor(|e, cx| e.newline(&Newline, cx));
-//         cx.assert_editor_state(indoc! {"
-//         // Foo
-//         //ˇ
-//     "});
-//         // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
-//         cx.set_state(indoc! {"
-//         ˇ// Foo
-//     "});
-//         cx.update_editor(|e, cx| e.newline(&Newline, cx));
-//         cx.assert_editor_state(indoc! {"
-
-//         ˇ// Foo
-//     "});
-//     }
-//     // Ensure that comment continuations can be disabled.
-//     update_test_language_settings(cx, |settings| {
-//         settings.defaults.extend_comment_on_newline = Some(false);
-//     });
-//     let mut cx = EditorTestContext::new(cx).await;
-//     cx.set_state(indoc! {"
-//         // Fooˇ
-//     "});
-//     cx.update_editor(|e, cx| e.newline(&Newline, cx));
-//     cx.assert_editor_state(indoc! {"
-//         // Foo
-//         ˇ
-//     "});
-// }
-
-// #[gpui::test]
-// 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
-//         })
-//         .root(cx);
-
-//     editor.update(cx, |editor, cx| {
-//         // Edit the buffer directly, deleting ranges surrounding the editor's selections
-//         editor.buffer.update(cx, |buffer, cx| {
-//             buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
-//             assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
-//         });
-//         assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
-
-//         editor.insert("Z", cx);
-//         assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
-
-//         // The selections are moved after the inserted characters
-//         assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
-//     });
-// }
-
-// #[gpui::test]
-// async fn test_tab(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |settings| {
-//         settings.defaults.tab_size = NonZeroU32::new(3)
-//     });
-
-//     let mut cx = EditorTestContext::new(cx).await;
-//     cx.set_state(indoc! {"
-//         ˇabˇc
-//         ˇ🏀ˇ🏀ˇefg
-//         dˇ
-//     "});
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//            ˇab ˇc
-//            ˇ🏀  ˇ🏀  ˇefg
-//         d  ˇ
-//     "});
-
-//     cx.set_state(indoc! {"
-//         a
-//         «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
-//     "});
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//         a
-//            «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
-//     "});
-// }
-
-// #[gpui::test]
-// async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let mut cx = EditorTestContext::new(cx).await;
-//     let language = Arc::new(
-//         Language::new(
-//             LanguageConfig::default(),
-//             Some(tree_sitter_rust::language()),
-//         )
-//         .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
-//         .unwrap(),
-//     );
-//     cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
-
-//     // cursors that are already at the suggested indent level insert
-//     // a soft tab. cursors that are to the left of the suggested indent
-//     // auto-indent their line.
-//     cx.set_state(indoc! {"
-//         ˇ
-//         const a: B = (
-//             c(
-//                 d(
-//         ˇ
-//                 )
-//         ˇ
-//         ˇ    )
-//         );
-//     "});
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//             ˇ
-//         const a: B = (
-//             c(
-//                 d(
-//                     ˇ
-//                 )
-//                 ˇ
-//             ˇ)
-//         );
-//     "});
-
-//     // handle auto-indent when there are multiple cursors on the same line
-//     cx.set_state(indoc! {"
-//         const a: B = (
-//             c(
-//         ˇ    ˇ
-//         ˇ    )
-//         );
-//     "});
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//         const a: B = (
-//             c(
-//                 ˇ
-//             ˇ)
-//         );
-//     "});
-// }
-
-// #[gpui::test]
-// async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |settings| {
-//         settings.defaults.tab_size = NonZeroU32::new(4)
-//     });
-
-//     let language = Arc::new(
-//         Language::new(
-//             LanguageConfig::default(),
-//             Some(tree_sitter_rust::language()),
-//         )
-//         .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
-//         .unwrap(),
-//     );
-
-//     let mut cx = EditorTestContext::new(cx).await;
-//     cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
-//     cx.set_state(indoc! {"
-//         fn a() {
-//             if b {
-//         \t ˇc
-//             }
-//         }
-//     "});
-
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//         fn a() {
-//             if b {
-//                 ˇc
-//             }
-//         }
-//     "});
-// }
-
-// #[gpui::test]
-// async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |settings| {
-//         settings.defaults.tab_size = NonZeroU32::new(4);
-//     });
-
-//     let mut cx = EditorTestContext::new(cx).await;
-
-//     cx.set_state(indoc! {"
-//           «oneˇ» «twoˇ»
-//         three
-//          four
-//     "});
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//             «oneˇ» «twoˇ»
-//         three
-//          four
-//     "});
-
-//     cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
-//     cx.assert_editor_state(indoc! {"
-//         «oneˇ» «twoˇ»
-//         three
-//          four
-//     "});
-
-//     // select across line ending
-//     cx.set_state(indoc! {"
-//         one two
-//         t«hree
-//         ˇ» four
-//     "});
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//         one two
-//             t«hree
-//         ˇ» four
-//     "});
-
-//     cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
-//     cx.assert_editor_state(indoc! {"
-//         one two
-//         t«hree
-//         ˇ» four
-//     "});
-
-//     // Ensure that indenting/outdenting works when the cursor is at column 0.
-//     cx.set_state(indoc! {"
-//         one two
-//         ˇthree
-//             four
-//     "});
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//         one two
-//             ˇthree
-//             four
-//     "});
-
-//     cx.set_state(indoc! {"
-//         one two
-//         ˇ    three
-//             four
-//     "});
-//     cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
-//     cx.assert_editor_state(indoc! {"
-//         one two
-//         ˇthree
-//             four
-//     "});
-// }
-
-// #[gpui::test]
-// async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |settings| {
-//         settings.defaults.hard_tabs = Some(true);
-//     });
-
-//     let mut cx = EditorTestContext::new(cx).await;
-
-//     // select two ranges on one line
-//     cx.set_state(indoc! {"
-//         «oneˇ» «twoˇ»
-//         three
-//         four
-//     "});
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//         \t«oneˇ» «twoˇ»
-//         three
-//         four
-//     "});
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//         \t\t«oneˇ» «twoˇ»
-//         three
-//         four
-//     "});
-//     cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
-//     cx.assert_editor_state(indoc! {"
-//         \t«oneˇ» «twoˇ»
-//         three
-//         four
-//     "});
-//     cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
-//     cx.assert_editor_state(indoc! {"
-//         «oneˇ» «twoˇ»
-//         three
-//         four
-//     "});
-
-//     // select across a line ending
-//     cx.set_state(indoc! {"
-//         one two
-//         t«hree
-//         ˇ»four
-//     "});
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//         one two
-//         \tt«hree
-//         ˇ»four
-//     "});
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//         one two
-//         \t\tt«hree
-//         ˇ»four
-//     "});
-//     cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
-//     cx.assert_editor_state(indoc! {"
-//         one two
-//         \tt«hree
-//         ˇ»four
-//     "});
-//     cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
-//     cx.assert_editor_state(indoc! {"
-//         one two
-//         t«hree
-//         ˇ»four
-//     "});
-
-//     // Ensure that indenting/outdenting works when the cursor is at column 0.
-//     cx.set_state(indoc! {"
-//         one two
-//         ˇthree
-//         four
-//     "});
-//     cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
-//     cx.assert_editor_state(indoc! {"
-//         one two
-//         ˇthree
-//         four
-//     "});
-//     cx.update_editor(|e, cx| e.tab(&Tab, cx));
-//     cx.assert_editor_state(indoc! {"
-//         one two
-//         \tˇthree
-//         four
-//     "});
-//     cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
-//     cx.assert_editor_state(indoc! {"
-//         one two
-//         ˇthree
-//         four
-//     "});
-// }
-
-// #[gpui::test]
-// fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
-//     init_test(cx, |settings| {
-//         settings.languages.extend([
-//             (
-//                 "TOML".into(),
-//                 LanguageSettingsContent {
-//                     tab_size: NonZeroU32::new(2),
-//                     ..Default::default()
-//                 },
-//             ),
-//             (
-//                 "Rust".into(),
-//                 LanguageSettingsContent {
-//                     tab_size: NonZeroU32::new(4),
-//                     ..Default::default()
-//                 },
-//             ),
-//         ]);
-//     });
-
-//     let toml_language = Arc::new(Language::new(
-//         LanguageConfig {
-//             name: "TOML".into(),
-//             ..Default::default()
-//         },
-//         None,
-//     ));
-//     let rust_language = Arc::new(Language::new(
-//         LanguageConfig {
-//             name: "Rust".into(),
-//             ..Default::default()
-//         },
-//         None,
-//     ));
-
-//     let toml_buffer = cx.add_model(|cx| {
-//         Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx)
-//     });
-//     let rust_buffer = cx.add_model(|cx| {
-//         Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n")
-//             .with_language(rust_language, cx)
-//     });
-//     let multibuffer = cx.add_model(|cx| {
-//         let mut multibuffer = MultiBuffer::new(0);
-//         multibuffer.push_excerpts(
-//             toml_buffer.clone(),
-//             [ExcerptRange {
-//                 context: Point::new(0, 0)..Point::new(2, 0),
-//                 primary: None,
-//             }],
-//             cx,
-//         );
-//         multibuffer.push_excerpts(
-//             rust_buffer.clone(),
-//             [ExcerptRange {
-//                 context: Point::new(0, 0)..Point::new(1, 0),
-//                 primary: None,
-//             }],
-//             cx,
-//         );
-//         multibuffer
-//     });
-
-//     cx.add_window(|cx| {
-//         let mut editor = build_editor(multibuffer, cx);
-
-//         assert_eq!(
-//             editor.text(cx),
-//             indoc! {"
-//                 a = 1
-//                 b = 2
-
-//                 const c: usize = 3;
-//             "}
-//         );
-
-//         select_ranges(
-//             &mut editor,
-//             indoc! {"
-//                 «aˇ» = 1
-//                 b = 2
-
-//                 «const c:ˇ» usize = 3;
-//             "},
-//             cx,
-//         );
-
-//         editor.tab(&Tab, cx);
-//         assert_text_with_selections(
-//             &mut editor,
-//             indoc! {"
-//                   «aˇ» = 1
-//                 b = 2
-
-//                     «const c:ˇ» usize = 3;
-//             "},
-//             cx,
-//         );
-//         editor.tab_prev(&TabPrev, cx);
-//         assert_text_with_selections(
-//             &mut editor,
-//             indoc! {"
-//                 «aˇ» = 1
-//                 b = 2
-
-//                 «const c:ˇ» usize = 3;
-//             "},
-//             cx,
-//         );
-
-//         editor
-//     });
-// }
-
-// #[gpui::test]
-// async fn test_backspace(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let mut cx = EditorTestContext::new(cx).await;
-
-//     // Basic backspace
-//     cx.set_state(indoc! {"
-//         onˇe two three
-//         fou«rˇ» five six
-//         seven «ˇeight nine
-//         »ten
-//     "});
-//     cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
-//     cx.assert_editor_state(indoc! {"
-//         oˇe two three
-//         fouˇ five six
-//         seven ˇten
-//     "});
-
-//     // Test backspace inside and around indents
-//     cx.set_state(indoc! {"
-//         zero
-//             ˇone
-//                 ˇtwo
-//             ˇ ˇ ˇ  three
-//         ˇ  ˇ  four
-//     "});
-//     cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
-//     cx.assert_editor_state(indoc! {"
-//         zero
-//         ˇone
-//             ˇtwo
-//         ˇ  threeˇ  four
-//     "});
-
-//     // Test backspace with line_mode set to true
-//     cx.update_editor(|e, _| e.selections.line_mode = true);
-//     cx.set_state(indoc! {"
-//         The ˇquick ˇbrown
-//         fox jumps over
-//         the lazy dog
-//         ˇThe qu«ick bˇ»rown"});
-//     cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
-//     cx.assert_editor_state(indoc! {"
-//         ˇfox jumps over
-//         the lazy dogˇ"});
-// }
-
-// #[gpui::test]
-// async fn test_delete(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let mut cx = EditorTestContext::new(cx).await;
-//     cx.set_state(indoc! {"
-//         onˇe two three
-//         fou«rˇ» five six
-//         seven «ˇeight nine
-//         »ten
-//     "});
-//     cx.update_editor(|e, cx| e.delete(&Delete, cx));
-//     cx.assert_editor_state(indoc! {"
-//         onˇ two three
-//         fouˇ five six
-//         seven ˇten
-//     "});
-
-//     // Test backspace with line_mode set to true
-//     cx.update_editor(|e, _| e.selections.line_mode = true);
-//     cx.set_state(indoc! {"
-//         The ˇquick ˇbrown
-//         fox «ˇjum»ps over
-//         the lazy dog
-//         ˇThe qu«ick bˇ»rown"});
-//     cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
-//     cx.assert_editor_state("ˇthe lazy dogˇ");
-// }
-
-// #[gpui::test]
-// 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)
-//         })
-//         .root(cx);
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([
-//                 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
-//                 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
-//             ])
-//         });
-//         view.delete_line(&DeleteLine, cx);
-//         assert_eq!(view.display_text(cx), "ghi");
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
-//                 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)
-//             ]
-//         );
-//     });
-
-//     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)])
-//         });
-//         view.delete_line(&DeleteLine, cx);
-//         assert_eq!(view.display_text(cx), "ghi\n");
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]
-//         );
-//     });
-// }
-
-// #[gpui::test]
-// fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     cx.add_window(|cx| {
-//         let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
-//         let mut editor = build_editor(buffer.clone(), cx);
-//         let buffer = buffer.read(cx).as_singleton().unwrap();
-
-//         assert_eq!(
-//             editor.selections.ranges::<Point>(cx),
-//             &[Point::new(0, 0)..Point::new(0, 0)]
-//         );
-
-//         // When on single line, replace newline at end by space
-//         editor.join_lines(&JoinLines, cx);
-//         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
-//         assert_eq!(
-//             editor.selections.ranges::<Point>(cx),
-//             &[Point::new(0, 3)..Point::new(0, 3)]
-//         );
-
-//         // When multiple lines are selected, remove newlines that are spanned by the selection
-//         editor.change_selections(None, cx, |s| {
-//             s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
-//         });
-//         editor.join_lines(&JoinLines, cx);
-//         assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
-//         assert_eq!(
-//             editor.selections.ranges::<Point>(cx),
-//             &[Point::new(0, 11)..Point::new(0, 11)]
-//         );
-
-//         // Undo should be transactional
-//         editor.undo(&Undo, cx);
-//         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
-//         assert_eq!(
-//             editor.selections.ranges::<Point>(cx),
-//             &[Point::new(0, 5)..Point::new(2, 2)]
-//         );
-
-//         // When joining an empty line don't insert a space
-//         editor.change_selections(None, cx, |s| {
-//             s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
-//         });
-//         editor.join_lines(&JoinLines, cx);
-//         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
-//         assert_eq!(
-//             editor.selections.ranges::<Point>(cx),
-//             [Point::new(2, 3)..Point::new(2, 3)]
-//         );
-
-//         // We can remove trailing newlines
-//         editor.join_lines(&JoinLines, cx);
-//         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
-//         assert_eq!(
-//             editor.selections.ranges::<Point>(cx),
-//             [Point::new(2, 3)..Point::new(2, 3)]
-//         );
-
-//         // We don't blow up on the last line
-//         editor.join_lines(&JoinLines, cx);
-//         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
-//         assert_eq!(
-//             editor.selections.ranges::<Point>(cx),
-//             [Point::new(2, 3)..Point::new(2, 3)]
-//         );
-
-//         // reset to test indentation
-//         editor.buffer.update(cx, |buffer, cx| {
-//             buffer.edit(
-//                 [
-//                     (Point::new(1, 0)..Point::new(1, 2), "  "),
-//                     (Point::new(2, 0)..Point::new(2, 3), "  \n\td"),
-//                 ],
-//                 None,
-//                 cx,
-//             )
-//         });
-
-//         // We remove any leading spaces
-//         assert_eq!(buffer.read(cx).text(), "aaa bbb\n  c\n  \n\td");
-//         editor.change_selections(None, cx, |s| {
-//             s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
-//         });
-//         editor.join_lines(&JoinLines, cx);
-//         assert_eq!(buffer.read(cx).text(), "aaa bbb c\n  \n\td");
-
-//         // We don't insert a space for a line containing only spaces
-//         editor.join_lines(&JoinLines, cx);
-//         assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
-
-//         // We ignore any leading tabs
-//         editor.join_lines(&JoinLines, cx);
-//         assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
-
-//         editor
-//     });
-// }
-
-// #[gpui::test]
-// fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     cx.add_window(|cx| {
-//         let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
-//         let mut editor = build_editor(buffer.clone(), cx);
-//         let buffer = buffer.read(cx).as_singleton().unwrap();
-
-//         editor.change_selections(None, cx, |s| {
-//             s.select_ranges([
-//                 Point::new(0, 2)..Point::new(1, 1),
-//                 Point::new(1, 2)..Point::new(1, 2),
-//                 Point::new(3, 1)..Point::new(3, 2),
-//             ])
-//         });
-
-//         editor.join_lines(&JoinLines, cx);
-//         assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
-
-//         assert_eq!(
-//             editor.selections.ranges::<Point>(cx),
-//             [
-//                 Point::new(0, 7)..Point::new(0, 7),
-//                 Point::new(1, 3)..Point::new(1, 3)
-//             ]
-//         );
-//         editor
-//     });
-// }
-
-// #[gpui::test]
-// async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let mut cx = EditorTestContext::new(cx).await;
-
-//     // Test sort_lines_case_insensitive()
-//     cx.set_state(indoc! {"
-//         «z
-//         y
-//         x
-//         Z
-//         Y
-//         Xˇ»
-//     "});
-//     cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx));
-//     cx.assert_editor_state(indoc! {"
-//         «x
-//         X
-//         y
-//         Y
-//         z
-//         Zˇ»
-//     "});
-
-//     // Test reverse_lines()
-//     cx.set_state(indoc! {"
-//         «5
-//         4
-//         3
-//         2
-//         1ˇ»
-//     "});
-//     cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx));
-//     cx.assert_editor_state(indoc! {"
-//         «1
-//         2
-//         3
-//         4
-//         5ˇ»
-//     "});
-
-//     // Skip testing shuffle_line()
-
-//     // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
-//     // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
-
-//     // Don't manipulate when cursor is on single line, but expand the selection
-//     cx.set_state(indoc! {"
-//         ddˇdd
-//         ccc
-//         bb
-//         a
-//     "});
-//     cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
-//     cx.assert_editor_state(indoc! {"
-//         «ddddˇ»
-//         ccc
-//         bb
-//         a
-//     "});
-
-//     // Basic manipulate case
-//     // Start selection moves to column 0
-//     // End of selection shrinks to fit shorter line
-//     cx.set_state(indoc! {"
-//         dd«d
-//         ccc
-//         bb
-//         aaaaaˇ»
-//     "});
-//     cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
-//     cx.assert_editor_state(indoc! {"
-//         «aaaaa
-//         bb
-//         ccc
-//         dddˇ»
-//     "});
-
-//     // Manipulate case with newlines
-//     cx.set_state(indoc! {"
-//         dd«d
-//         ccc
-
-//         bb
-//         aaaaa
-
-//         ˇ»
-//     "});
-//     cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
-//     cx.assert_editor_state(indoc! {"
-//         «
-
-//         aaaaa
-//         bb
-//         ccc
-//         dddˇ»
-
-//     "});
-// }
-
-// #[gpui::test]
-// async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let mut cx = EditorTestContext::new(cx).await;
-
-//     // Manipulate with multiple selections on a single line
-//     cx.set_state(indoc! {"
-//         dd«dd
-//         cˇ»c«c
-//         bb
-//         aaaˇ»aa
-//     "});
-//     cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
-//     cx.assert_editor_state(indoc! {"
-//         «aaaaa
-//         bb
-//         ccc
-//         ddddˇ»
-//     "});
-
-//     // Manipulate with multiple disjoin selections
-//     cx.set_state(indoc! {"
-//         5«
-//         4
-//         3
-//         2
-//         1ˇ»
-
-//         dd«dd
-//         ccc
-//         bb
-//         aaaˇ»aa
-//     "});
-//     cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
-//     cx.assert_editor_state(indoc! {"
-//         «1
-//         2
-//         3
-//         4
-//         5ˇ»
-
-//         «aaaaa
-//         bb
-//         ccc
-//         ddddˇ»
-//     "});
-// }
-
-// #[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ˇ»
-//     "});
-
-//     // Test multiple line, single selection case
-//     // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
-//     cx.set_state(indoc! {"
-//         «The quick brown
-//         fox jumps over
-//         the lazy dogˇ»
-//     "});
-//     cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
-//     cx.assert_editor_state(indoc! {"
-//         «The Quick Brown
-//         Fox Jumps Over
-//         The Lazy Dogˇ»
-//     "});
-
-//     // Test multiple line, single selection case
-//     // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
-//     cx.set_state(indoc! {"
-//         «The quick brown
-//         fox jumps over
-//         the lazy dogˇ»
-//     "});
-//     cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
-//     cx.assert_editor_state(indoc! {"
-//         «TheQuickBrown
-//         FoxJumpsOver
-//         TheLazyDogˇ»
-//     "});
-
-//     // 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)
-//         })
-//         .root(cx);
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-//                 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
-//                 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
-//             ])
-//         });
-//         view.duplicate_line(&DuplicateLine, cx);
-//         assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
-//                 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
-//                 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
-//                 DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
-//             ]
-//         );
-//     });
-
-//     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(0, 1)..DisplayPoint::new(1, 1),
-//                 DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
-//             ])
-//         });
-//         view.duplicate_line(&DuplicateLine, cx);
-//         assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1),
-//                 DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1),
-//             ]
-//         );
-//     });
-// }
-
-// #[gpui::test]
-// 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)
-//         })
-//         .root(cx);
-//     view.update(cx, |view, cx| {
-//         view.fold_ranges(
-//             vec![
-//                 Point::new(0, 2)..Point::new(1, 2),
-//                 Point::new(2, 3)..Point::new(4, 1),
-//                 Point::new(7, 0)..Point::new(8, 4),
-//             ],
-//             true,
-//             cx,
-//         );
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([
-//                 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
-//                 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
-//                 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2),
-//             ])
-//         });
-//         assert_eq!(
-//             view.display_text(cx),
-//             "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
-//         );
-
-//         view.move_line_up(&MoveLineUp, cx);
-//         assert_eq!(
-//             view.display_text(cx),
-//             "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
-//         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
-//                 DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
-//                 DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
-//             ]
-//         );
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.move_line_down(&MoveLineDown, cx);
-//         assert_eq!(
-//             view.display_text(cx),
-//             "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
-//         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
-//                 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
-//                 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
-//                 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
-//             ]
-//         );
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.move_line_down(&MoveLineDown, cx);
-//         assert_eq!(
-//             view.display_text(cx),
-//             "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
-//         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
-//                 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
-//                 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
-//                 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
-//             ]
-//         );
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.move_line_up(&MoveLineUp, cx);
-//         assert_eq!(
-//             view.display_text(cx),
-//             "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
-//         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
-//                 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
-//                 DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
-//                 DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
-//             ]
-//         );
-//     });
-// }
-
-// #[gpui::test]
-// 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)
-//         })
-//         .root(cx);
-//     editor.update(cx, |editor, cx| {
-//         let snapshot = editor.buffer.read(cx).snapshot(cx);
-//         editor.insert_blocks(
-//             [BlockProperties {
-//                 style: BlockStyle::Fixed,
-//                 position: snapshot.anchor_after(Point::new(2, 0)),
-//                 disposition: BlockDisposition::Below,
-//                 height: 1,
-//                 render: Arc::new(|_| Empty::new().into_any()),
-//             }],
-//             Some(Autoscroll::fit()),
-//             cx,
-//         );
-//         editor.change_selections(None, cx, |s| {
-//             s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
-//         });
-//         editor.move_line_down(&MoveLineDown, cx);
-//     });
-// }
-
-// #[gpui::test]
-// fn test_transpose(cx: &mut TestAppContext) {
-//     init_test(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.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
-//     });
-
-//     _ = 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([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), "acbd\ne");
-//         assert_eq!(editor.selections.ranges(cx), [6..6]);
-
-//         editor
-//     });
-
-//     _ = 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.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), "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
-//     });
-
-//     _ = 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.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
-//     });
-// }
-
-// #[gpui::test]
-// async fn test_clipboard(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let mut cx = EditorTestContext::new(cx).await;
-
-//     cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
-//     cx.update_editor(|e, cx| e.cut(&Cut, cx));
-//     cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
-
-//     // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
-//     cx.set_state("two ˇfour ˇsix ˇ");
-//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
-//     cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
-
-//     // Paste again but with only two cursors. Since the number of cursors doesn't
-//     // match the number of slices in the clipboard, the entire clipboard text
-//     // is pasted at each cursor.
-//     cx.set_state("ˇtwo one✅ four three six five ˇ");
-//     cx.update_editor(|e, cx| {
-//         e.handle_input("( ", cx);
-//         e.paste(&Paste, cx);
-//         e.handle_input(") ", cx);
-//     });
-//     cx.assert_editor_state(
-//         &([
-//             "( one✅ ",
-//             "three ",
-//             "five ) ˇtwo one✅ four three six five ( one✅ ",
-//             "three ",
-//             "five ) ˇ",
-//         ]
-//         .join("\n")),
-//     );
-
-//     // Cut with three selections, one of which is full-line.
-//     cx.set_state(indoc! {"
-//         1«2ˇ»3
-//         4ˇ567
-//         «8ˇ»9"});
-//     cx.update_editor(|e, cx| e.cut(&Cut, cx));
-//     cx.assert_editor_state(indoc! {"
-//         1ˇ3
-//         ˇ9"});
-
-//     // Paste with three selections, noticing how the copied selection that was full-line
-//     // gets inserted before the second cursor.
-//     cx.set_state(indoc! {"
-//         1ˇ3
-//         9ˇ
-//         «oˇ»ne"});
-//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
-//     cx.assert_editor_state(indoc! {"
-//         12ˇ3
-//         4567
-//         9ˇ
-//         8ˇne"});
-
-//     // Copy with a single cursor only, which writes the whole line into the clipboard.
-//     cx.set_state(indoc! {"
-//         The quick brown
-//         fox juˇmps over
-//         the lazy dog"});
-//     cx.update_editor(|e, cx| e.copy(&Copy, cx));
-//     cx.cx.assert_clipboard_content(Some("fox jumps over\n"));
-
-//     // Paste with three selections, noticing how the copied full-line selection is inserted
-//     // before the empty selections but replaces the selection that is non-empty.
-//     cx.set_state(indoc! {"
-//         Tˇhe quick brown
-//         «foˇ»x jumps over
-//         tˇhe lazy dog"});
-//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
-//     cx.assert_editor_state(indoc! {"
-//         fox jumps over
-//         Tˇhe quick brown
-//         fox jumps over
-//         ˇx jumps over
-//         fox jumps over
-//         tˇhe lazy dog"});
-// }
-
-// #[gpui::test]
-// async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let mut cx = EditorTestContext::new(cx).await;
-//     let language = Arc::new(Language::new(
-//         LanguageConfig::default(),
-//         Some(tree_sitter_rust::language()),
-//     ));
-//     cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
-
-//     // Cut an indented block, without the leading whitespace.
-//     cx.set_state(indoc! {"
-//         const a: B = (
-//             c(),
-//             «d(
-//                 e,
-//                 f
-//             )ˇ»
-//         );
-//     "});
-//     cx.update_editor(|e, cx| e.cut(&Cut, cx));
-//     cx.assert_editor_state(indoc! {"
-//         const a: B = (
-//             c(),
-//             ˇ
-//         );
-//     "});
-
-//     // Paste it at the same position.
-//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
-//     cx.assert_editor_state(indoc! {"
-//         const a: B = (
-//             c(),
-//             d(
-//                 e,
-//                 f
-//             )ˇ
-//         );
-//     "});
-
-//     // Paste it at a line with a lower indent level.
-//     cx.set_state(indoc! {"
-//         ˇ
-//         const a: B = (
-//             c(),
-//         );
-//     "});
-//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
-//     cx.assert_editor_state(indoc! {"
-//         d(
-//             e,
-//             f
-//         )ˇ
-//         const a: B = (
-//             c(),
-//         );
-//     "});
-
-//     // Cut an indented block, with the leading whitespace.
-//     cx.set_state(indoc! {"
-//         const a: B = (
-//             c(),
-//         «    d(
-//                 e,
-//                 f
-//             )
-//         ˇ»);
-//     "});
-//     cx.update_editor(|e, cx| e.cut(&Cut, cx));
-//     cx.assert_editor_state(indoc! {"
-//         const a: B = (
-//             c(),
-//         ˇ);
-//     "});
-
-//     // Paste it at the same position.
-//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
-//     cx.assert_editor_state(indoc! {"
-//         const a: B = (
-//             c(),
-//             d(
-//                 e,
-//                 f
-//             )
-//         ˇ);
-//     "});
-
-//     // Paste it at a line with a higher indent level.
-//     cx.set_state(indoc! {"
-//         const a: B = (
-//             c(),
-//             d(
-//                 e,
-//                 fˇ
-//             )
-//         );
-//     "});
-//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
-//     cx.assert_editor_state(indoc! {"
-//         const a: B = (
-//             c(),
-//             d(
-//                 e,
-//                 f    d(
-//                     e,
-//                     f
-//                 )
-//         ˇ
-//             )
-//         );
-//     "});
-// }
-
-// #[gpui::test]
-// 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)
-//         })
-//         .root(cx);
-//     view.update(cx, |view, cx| {
-//         view.select_all(&SelectAll, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)]
-//         );
-//     });
-// }
-
-// #[gpui::test]
-// 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)
-//         })
-//         .root(cx);
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-//                 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
-//                 DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2),
-//             ])
-//         });
-//         view.select_line(&SelectLine, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0),
-//                 DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0),
-//             ]
-//         );
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.select_line(&SelectLine, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0),
-//                 DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5),
-//             ]
-//         );
-//     });
+    cx.add_window(|cx| {
+        let mut editor = build_editor(multibuffer, cx);
+
+        assert_eq!(
+            editor.text(cx),
+            indoc! {"
+                a = 1
+                b = 2
+
+                const c: usize = 3;
+            "}
+        );
+
+        select_ranges(
+            &mut editor,
+            indoc! {"
+                «aˇ» = 1
+                b = 2
+
+                «const c:ˇ» usize = 3;
+            "},
+            cx,
+        );
+
+        editor.tab(&Tab, cx);
+        assert_text_with_selections(
+            &mut editor,
+            indoc! {"
+                  «aˇ» = 1
+                b = 2
+
+                    «const c:ˇ» usize = 3;
+            "},
+            cx,
+        );
+        editor.tab_prev(&TabPrev, cx);
+        assert_text_with_selections(
+            &mut editor,
+            indoc! {"
+                «aˇ» = 1
+                b = 2
+
+                «const c:ˇ» usize = 3;
+            "},
+            cx,
+        );
+
+        editor
+    });
+}
 
-//     view.update(cx, |view, cx| {
-//         view.select_line(&SelectLine, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)]
-//         );
-//     });
-// }
+#[gpui::test]
+async fn test_backspace(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // Basic backspace
+    cx.set_state(indoc! {"
+        onˇe two three
+        fou«rˇ» five six
+        seven «ˇeight nine
+        »ten
+    "});
+    cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+    cx.assert_editor_state(indoc! {"
+        oˇe two three
+        fouˇ five six
+        seven ˇten
+    "});
+
+    // Test backspace inside and around indents
+    cx.set_state(indoc! {"
+        zero
+            ˇone
+                ˇtwo
+            ˇ ˇ ˇ  three
+        ˇ  ˇ  four
+    "});
+    cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+    cx.assert_editor_state(indoc! {"
+        zero
+        ˇone
+            ˇtwo
+        ˇ  threeˇ  four
+    "});
+
+    // Test backspace with line_mode set to true
+    cx.update_editor(|e, _| e.selections.line_mode = true);
+    cx.set_state(indoc! {"
+        The ˇquick ˇbrown
+        fox jumps over
+        the lazy dog
+        ˇThe qu«ick bˇ»rown"});
+    cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+    cx.assert_editor_state(indoc! {"
+        ˇfox jumps over
+        the lazy dogˇ"});
+}
 
-// #[gpui::test]
-// fn test_split_selection_into_lines(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+async fn test_delete(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state(indoc! {"
+        onˇe two three
+        fou«rˇ» five six
+        seven «ˇeight nine
+        »ten
+    "});
+    cx.update_editor(|e, cx| e.delete(&Delete, cx));
+    cx.assert_editor_state(indoc! {"
+        onˇ two three
+        fouˇ five six
+        seven ˇten
+    "});
+
+    // Test backspace with line_mode set to true
+    cx.update_editor(|e, _| e.selections.line_mode = true);
+    cx.set_state(indoc! {"
+        The ˇquick ˇbrown
+        fox «ˇjum»ps over
+        the lazy dog
+        ˇThe qu«ick bˇ»rown"});
+    cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+    cx.assert_editor_state("ˇthe lazy dogˇ");
+}
 
-//     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![
-//                 Point::new(0, 2)..Point::new(1, 2),
-//                 Point::new(2, 3)..Point::new(4, 1),
-//                 Point::new(7, 0)..Point::new(8, 4),
-//             ],
-//             true,
-//             cx,
-//         );
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-//                 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
-//                 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
-//             ])
-//         });
-//         assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i");
-//     });
+#[gpui::test]
+fn test_delete_line(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//     view.update(cx, |view, cx| {
-//         view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
-//         assert_eq!(
-//             view.display_text(cx),
-//             "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
-//         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             [
-//                 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
-//                 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
-//                 DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4)
-//             ]
-//         );
-//     });
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+        build_editor(buffer, cx)
+    });
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
+            ])
+        });
+        view.delete_line(&DeleteLine, cx);
+        assert_eq!(view.display_text(cx), "ghi");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)
+            ]
+        );
+    });
 
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)])
-//         });
-//         view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
-//         assert_eq!(
-//             view.display_text(cx),
-//             "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
-//         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             [
-//                 DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5),
-//                 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
-//                 DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
-//                 DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
-//                 DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5),
-//                 DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5),
-//                 DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5),
-//                 DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0)
-//             ]
-//         );
-//     });
-// }
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+        build_editor(buffer, cx)
+    });
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)])
+        });
+        view.delete_line(&DeleteLine, cx);
+        assert_eq!(view.display_text(cx), "ghi\n");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]
+        );
+    });
+}
 
+//todo!(select_anchor_ranges)
 // #[gpui::test]
-// fn test_add_selection_above_below(cx: &mut TestAppContext) {
+// fn test_join_lines_with_single_selection(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)
-//         })
-//         .root(cx);
-
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)])
-//         });
-//     });
-//     view.update(cx, |view, cx| {
-//         view.add_selection_above(&AddSelectionAbove, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
-//                 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)
-//             ]
-//         );
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.add_selection_above(&AddSelectionAbove, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
-//                 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)
-//             ]
-//         );
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.add_selection_below(&AddSelectionBelow, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]
-//         );
-
-//         view.undo_selection(&UndoSelection, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
-//                 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)
-//             ]
-//         );
-
-//         view.redo_selection(&RedoSelection, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]
-//         );
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.add_selection_below(&AddSelectionBelow, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3),
-//                 DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3)
-//             ]
-//         );
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.add_selection_below(&AddSelectionBelow, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3),
-//                 DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3)
-//             ]
-//         );
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)])
-//         });
-//     });
-//     view.update(cx, |view, cx| {
-//         view.add_selection_below(&AddSelectionBelow, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
-//                 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3)
-//             ]
-//         );
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.add_selection_below(&AddSelectionBelow, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
-//                 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3)
-//             ]
-//         );
-//     });
+//     cx.add_window(|cx| {
+//         let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
+//         let mut editor = build_editor(buffer.clone(), cx);
+//         let buffer = buffer.read(cx).as_singleton().unwrap();
 
-//     view.update(cx, |view, cx| {
-//         view.add_selection_above(&AddSelectionAbove, cx);
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]
+//             editor.selections.ranges::<Point>(cx),
+//             &[Point::new(0, 0)..Point::new(0, 0)]
 //         );
-//     });
 
-//     view.update(cx, |view, cx| {
-//         view.add_selection_above(&AddSelectionAbove, cx);
+//         // When on single line, replace newline at end by space
+//         editor.join_lines(&JoinLines, cx);
+//         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]
+//             editor.selections.ranges::<Point>(cx),
+//             &[Point::new(0, 3)..Point::new(0, 3)]
 //         );
-//     });
 
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)])
+//         // When multiple lines are selected, remove newlines that are spanned by the selection
+//         editor.change_selections(None, cx, |s| {
+//             s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
 //         });
-//         view.add_selection_below(&AddSelectionBelow, cx);
+//         editor.join_lines(&JoinLines, cx);
+//         assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
-//                 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4),
-//                 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2),
-//             ]
+//             editor.selections.ranges::<Point>(cx),
+//             &[Point::new(0, 11)..Point::new(0, 11)]
 //         );
-//     });
 
-//     view.update(cx, |view, cx| {
-//         view.add_selection_below(&AddSelectionBelow, cx);
+//         // Undo should be transactional
+//         editor.undo(&Undo, cx);
+//         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
-//                 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4),
-//                 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2),
-//                 DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4),
-//             ]
+//             editor.selections.ranges::<Point>(cx),
+//             &[Point::new(0, 5)..Point::new(2, 2)]
 //         );
-//     });
 
-//     view.update(cx, |view, cx| {
-//         view.add_selection_above(&AddSelectionAbove, cx);
+//         // When joining an empty line don't insert a space
+//         editor.change_selections(None, cx, |s| {
+//             s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
+//         });
+//         editor.join_lines(&JoinLines, cx);
+//         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
-//                 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4),
-//                 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2),
-//             ]
+//             editor.selections.ranges::<Point>(cx),
+//             [Point::new(2, 3)..Point::new(2, 3)]
 //         );
-//     });
 
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)])
-//         });
-//     });
-//     view.update(cx, |view, cx| {
-//         view.add_selection_above(&AddSelectionAbove, cx);
+//         // We can remove trailing newlines
+//         editor.join_lines(&JoinLines, cx);
+//         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1),
-//                 DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1),
-//                 DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1),
-//             ]
+//             editor.selections.ranges::<Point>(cx),
+//             [Point::new(2, 3)..Point::new(2, 3)]
 //         );
-//     });
 
-//     view.update(cx, |view, cx| {
-//         view.add_selection_below(&AddSelectionBelow, cx);
+//         // We don't blow up on the last line
+//         editor.join_lines(&JoinLines, cx);
+//         assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
 //         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             vec![
-//                 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1),
-//                 DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1),
-//                 DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1),
-//             ]
+//             editor.selections.ranges::<Point>(cx),
+//             [Point::new(2, 3)..Point::new(2, 3)]
 //         );
-//     });
-// }
 
-// #[gpui::test]
-// async fn test_select_next(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let mut cx = EditorTestContext::new(cx).await;
-//     cx.set_state("abc\nˇabc abc\ndefabc\nabc");
-
-//     cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
-//         .unwrap();
-//     cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
-
-//     cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
-//         .unwrap();
-//     cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
-
-//     cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
-//     cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
-
-//     cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
-//     cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
-
-//     cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
-//         .unwrap();
-//     cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
-
-//     cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
-//         .unwrap();
-//     cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
-// }
-
-// #[gpui::test]
-// async fn test_select_previous(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-//     {
-//         // `Select previous` without a selection (selects wordwise)
-//         let mut cx = EditorTestContext::new(cx).await;
-//         cx.set_state("abc\nˇabc abc\ndefabc\nabc");
-
-//         cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-//             .unwrap();
-//         cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
-
-//         cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-//             .unwrap();
-//         cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
-
-//         cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
-//         cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
-
-//         cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
-//         cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
-
-//         cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-//             .unwrap();
-//         cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
-
-//         cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-//             .unwrap();
-//         cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
-//     }
-//     {
-//         // `Select previous` with a selection
-//         let mut cx = EditorTestContext::new(cx).await;
-//         cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
-
-//         cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-//             .unwrap();
-//         cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
-
-//         cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-//             .unwrap();
-//         cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
-
-//         cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
-//         cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
-
-//         cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
-//         cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
-
-//         cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-//             .unwrap();
-//         cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
-
-//         cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-//             .unwrap();
-//         cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
-//     }
-// }
-
-// #[gpui::test]
-// async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let language = Arc::new(Language::new(
-//         LanguageConfig::default(),
-//         Some(tree_sitter_rust::language()),
-//     ));
-
-//     let text = r#"
-//         use mod1::mod2::{mod3, mod4};
-
-//         fn fn_1(param1: bool, param2: &str) {
-//             let var1 = "text";
-//         }
-//     "#
-//     .unindent();
-
-//     let buffer =
-//         cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
-//     let buffer = cx.add_model(|cx| MultiBuffer::singleton(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;
-
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([
-//                 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
-//                 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
-//                 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
-//             ]);
+//         // reset to test indentation
+//         editor.buffer.update(cx, |buffer, cx| {
+//             buffer.edit(
+//                 [
+//                     (Point::new(1, 0)..Point::new(1, 2), "  "),
+//                     (Point::new(2, 0)..Point::new(2, 3), "  \n\td"),
+//                 ],
+//                 None,
+//                 cx,
+//             )
 //         });
-//         view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
-//     });
-//     assert_eq!(
-//         view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
-//         &[
-//             DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
-//             DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
-//             DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
-//         ]
-//     );
-
-//     view.update(cx, |view, cx| {
-//         view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
-//     });
-//     assert_eq!(
-//         view.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         &[
-//             DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
-//             DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
-//         ]
-//     );
-
-//     view.update(cx, |view, cx| {
-//         view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
-//     });
-//     assert_eq!(
-//         view.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
-//     );
-
-//     // Trying to expand the selected syntax node one more time has no effect.
-//     view.update(cx, |view, cx| {
-//         view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
-//     });
-//     assert_eq!(
-//         view.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
-//     );
-
-//     view.update(cx, |view, cx| {
-//         view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
-//     });
-//     assert_eq!(
-//         view.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         &[
-//             DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
-//             DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
-//         ]
-//     );
 
-//     view.update(cx, |view, cx| {
-//         view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
-//     });
-//     assert_eq!(
-//         view.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         &[
-//             DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
-//             DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
-//             DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
-//         ]
-//     );
+//         // We remove any leading spaces
+//         assert_eq!(buffer.read(cx).text(), "aaa bbb\n  c\n  \n\td");
+//         editor.change_selections(None, cx, |s| {
+//             s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
+//         });
+//         editor.join_lines(&JoinLines, cx);
+//         assert_eq!(buffer.read(cx).text(), "aaa bbb c\n  \n\td");
 
-//     view.update(cx, |view, cx| {
-//         view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
-//     });
-//     assert_eq!(
-//         view.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         &[
-//             DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
-//             DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
-//             DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
-//         ]
-//     );
+//         // We don't insert a space for a line containing only spaces
+//         editor.join_lines(&JoinLines, cx);
+//         assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
 
-//     // Trying to shrink the selected syntax node one more time has no effect.
-//     view.update(cx, |view, cx| {
-//         view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
-//     });
-//     assert_eq!(
-//         view.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         &[
-//             DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
-//             DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
-//             DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
-//         ]
-//     );
+//         // We ignore any leading tabs
+//         editor.join_lines(&JoinLines, cx);
+//         assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
 
-//     // Ensure that we keep expanding the selection if the larger selection starts or ends within
-//     // a fold.
-//     view.update(cx, |view, cx| {
-//         view.fold_ranges(
-//             vec![
-//                 Point::new(0, 21)..Point::new(0, 24),
-//                 Point::new(3, 20)..Point::new(3, 22),
-//             ],
-//             true,
-//             cx,
-//         );
-//         view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+//         editor
 //     });
-//     assert_eq!(
-//         view.update(cx, |view, cx| view.selections.display_ranges(cx)),
-//         &[
-//             DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
-//             DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
-//             DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23),
-//         ]
-//     );
 // }
 
 // #[gpui::test]
-// async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
+// fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
 //     init_test(cx, |_| {});
 
-//     let language = Arc::new(
-//         Language::new(
-//             LanguageConfig {
-//                 brackets: BracketPairConfig {
-//                     pairs: vec![
-//                         BracketPair {
-//                             start: "{".to_string(),
-//                             end: "}".to_string(),
-//                             close: false,
-//                             newline: true,
-//                         },
-//                         BracketPair {
-//                             start: "(".to_string(),
-//                             end: ")".to_string(),
-//                             close: false,
-//                             newline: true,
-//                         },
-//                     ],
-//                     ..Default::default()
-//                 },
-//                 ..Default::default()
-//             },
-//             Some(tree_sitter_rust::language()),
-//         )
-//         .with_indents_query(
-//             r#"
-//                 (_ "(" ")" @end) @indent
-//                 (_ "{" "}" @end) @indent
-//             "#,
-//         )
-//         .unwrap(),
-//     );
+//     cx.add_window(|cx| {
+//         let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
+//         let mut editor = build_editor(buffer.clone(), cx);
+//         let buffer = buffer.read(cx).as_singleton().unwrap();
 
-//     let text = "fn a() {}";
+//         editor.change_selections(None, cx, |s| {
+//             s.select_ranges([
+//                 Point::new(0, 2)..Point::new(1, 1),
+//                 Point::new(1, 2)..Point::new(1, 2),
+//                 Point::new(3, 1)..Point::new(3, 2),
+//             ])
+//         });
 
-//     let buffer =
-//         cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
-//     let buffer = cx.add_model(|cx| MultiBuffer::singleton(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;
+//         editor.join_lines(&JoinLines, cx);
+//         assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
 
-//     editor.update(cx, |editor, cx| {
-//         editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
-//         editor.newline(&Newline, cx);
-//         assert_eq!(editor.text(cx), "fn a(\n    \n) {\n    \n}\n");
 //         assert_eq!(
-//             editor.selections.ranges(cx),
-//             &[
-//                 Point::new(1, 4)..Point::new(1, 4),
-//                 Point::new(3, 4)..Point::new(3, 4),
-//                 Point::new(5, 0)..Point::new(5, 0)
+//             editor.selections.ranges::<Point>(cx),
+//             [
+//                 Point::new(0, 7)..Point::new(0, 7),
+//                 Point::new(1, 3)..Point::new(1, 3)
 //             ]
 //         );
+//         editor
 //     });
 // }
 
-// #[gpui::test]
-// async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // Test sort_lines_case_insensitive()
+    cx.set_state(indoc! {"
+        «z
+        y
+        x
+        Z
+        Y
+        Xˇ»
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        «x
+        X
+        y
+        Y
+        z
+        Zˇ»
+    "});
+
+    // Test reverse_lines()
+    cx.set_state(indoc! {"
+        «5
+        4
+        3
+        2
+        1ˇ»
+    "});
+    cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx));
+    cx.assert_editor_state(indoc! {"
+        «1
+        2
+        3
+        4
+        5ˇ»
+    "});
+
+    // Skip testing shuffle_line()
+
+    // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
+    // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
+
+    // Don't manipulate when cursor is on single line, but expand the selection
+    cx.set_state(indoc! {"
+        ddˇdd
+        ccc
+        bb
+        a
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        «ddddˇ»
+        ccc
+        bb
+        a
+    "});
+
+    // Basic manipulate case
+    // Start selection moves to column 0
+    // End of selection shrinks to fit shorter line
+    cx.set_state(indoc! {"
+        dd«d
+        ccc
+        bb
+        aaaaaˇ»
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        «aaaaa
+        bb
+        ccc
+        dddˇ»
+    "});
+
+    // Manipulate case with newlines
+    cx.set_state(indoc! {"
+        dd«d
+        ccc
+
+        bb
+        aaaaa
+
+        ˇ»
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        «
+
+        aaaaa
+        bb
+        ccc
+        dddˇ»
+
+    "});
+}
 
-//     let mut cx = EditorTestContext::new(cx).await;
+#[gpui::test]
+async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // Manipulate with multiple selections on a single line
+    cx.set_state(indoc! {"
+        dd«dd
+        cˇ»c«c
+        bb
+        aaaˇ»aa
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        «aaaaa
+        bb
+        ccc
+        ddddˇ»
+    "});
+
+    // Manipulate with multiple disjoin selections
+    cx.set_state(indoc! {"
+        5«
+        4
+        3
+        2
+        1ˇ»
+
+        dd«dd
+        ccc
+        bb
+        aaaˇ»aa
+    "});
+    cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+    cx.assert_editor_state(indoc! {"
+        «1
+        2
+        3
+        4
+        5ˇ»
+
+        «aaaaa
+        bb
+        ccc
+        ddddˇ»
+    "});
+}
 
-//     let language = Arc::new(Language::new(
-//         LanguageConfig {
-//             brackets: BracketPairConfig {
-//                 pairs: vec![
-//                     BracketPair {
-//                         start: "{".to_string(),
-//                         end: "}".to_string(),
-//                         close: true,
-//                         newline: true,
-//                     },
-//                     BracketPair {
-//                         start: "(".to_string(),
-//                         end: ")".to_string(),
-//                         close: true,
-//                         newline: true,
-//                     },
-//                     BracketPair {
-//                         start: "/*".to_string(),
-//                         end: " */".to_string(),
-//                         close: true,
-//                         newline: true,
-//                     },
-//                     BracketPair {
-//                         start: "[".to_string(),
-//                         end: "]".to_string(),
-//                         close: false,
-//                         newline: true,
-//                     },
-//                     BracketPair {
-//                         start: "\"".to_string(),
-//                         end: "\"".to_string(),
-//                         close: true,
-//                         newline: false,
-//                     },
-//                 ],
-//                 ..Default::default()
-//             },
-//             autoclose_before: "})]".to_string(),
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     ));
+#[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ˇ»
+    "});
+
+    // Test multiple line, single selection case
+    // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
+    cx.set_state(indoc! {"
+        «The quick brown
+        fox jumps over
+        the lazy dogˇ»
+    "});
+    cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «The Quick Brown
+        Fox Jumps Over
+        The Lazy Dogˇ»
+    "});
+
+    // Test multiple line, single selection case
+    // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
+    cx.set_state(indoc! {"
+        «The quick brown
+        fox jumps over
+        the lazy dogˇ»
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
+    cx.assert_editor_state(indoc! {"
+        «TheQuickBrown
+        FoxJumpsOver
+        TheLazyDogˇ»
+    "});
+
+    // 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ˇ»
+    "});
+}
 
-//     let registry = Arc::new(LanguageRegistry::test());
-//     registry.add(language.clone());
-//     cx.update_buffer(|buffer, cx| {
-//         buffer.set_language_registry(registry);
-//         buffer.set_language(Some(language), cx);
-//     });
+#[gpui::test]
+fn test_duplicate_line(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//     cx.set_state(
-//         &r#"
-//             🏀ˇ
-//             εˇ
-//             ❤️ˇ
-//         "#
-//         .unindent(),
-//     );
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+        build_editor(buffer, cx)
+    });
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+                DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
+            ])
+        });
+        view.duplicate_line(&DuplicateLine, cx);
+        assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+                DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
+                DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
+            ]
+        );
+    });
+
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+        build_editor(buffer, cx)
+    });
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
+            ])
+        });
+        view.duplicate_line(&DuplicateLine, cx);
+        assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1),
+                DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1),
+            ]
+        );
+    });
+}
 
-//     // autoclose multiple nested brackets at multiple cursors
-//     cx.update_editor(|view, cx| {
-//         view.handle_input("{", cx);
-//         view.handle_input("{", cx);
-//         view.handle_input("{", cx);
-//     });
-//     cx.assert_editor_state(
-//         &"
-//             🏀{{{ˇ}}}
-//             ε{{{ˇ}}}
-//             ❤️{{{ˇ}}}
-//         "
-//         .unindent(),
-//     );
+#[gpui::test]
+fn test_move_line_up_down(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//     // insert a different closing bracket
-//     cx.update_editor(|view, cx| {
-//         view.handle_input(")", cx);
-//     });
-//     cx.assert_editor_state(
-//         &"
-//             🏀{{{)ˇ}}}
-//             ε{{{)ˇ}}}
-//             ❤️{{{)ˇ}}}
-//         "
-//         .unindent(),
-//     );
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
+        build_editor(buffer, cx)
+    });
+    view.update(cx, |view, cx| {
+        view.fold_ranges(
+            vec![
+                Point::new(0, 2)..Point::new(1, 2),
+                Point::new(2, 3)..Point::new(4, 1),
+                Point::new(7, 0)..Point::new(8, 4),
+            ],
+            true,
+            cx,
+        );
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
+                DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
+                DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2),
+            ])
+        });
+        assert_eq!(
+            view.display_text(cx),
+            "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
+        );
+
+        view.move_line_up(&MoveLineUp, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
+                DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
+                DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
+            ]
+        );
+    });
 
-//     // skip over the auto-closed brackets when typing a closing bracket
-//     cx.update_editor(|view, cx| {
-//         view.move_right(&MoveRight, cx);
-//         view.handle_input("}", cx);
-//         view.handle_input("}", cx);
-//         view.handle_input("}", cx);
-//     });
-//     cx.assert_editor_state(
-//         &"
-//             🏀{{{)}}}}ˇ
-//             ε{{{)}}}}ˇ
-//             ❤️{{{)}}}}ˇ
-//         "
-//         .unindent(),
-//     );
+    view.update(cx, |view, cx| {
+        view.move_line_down(&MoveLineDown, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
+                DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
+                DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
+            ]
+        );
+    });
 
-//     // autoclose multi-character pairs
-//     cx.set_state(
-//         &"
-//             ˇ
-//             ˇ
-//         "
-//         .unindent(),
-//     );
-//     cx.update_editor(|view, cx| {
-//         view.handle_input("/", cx);
-//         view.handle_input("*", cx);
-//     });
-//     cx.assert_editor_state(
-//         &"
-//             /*ˇ */
-//             /*ˇ */
-//         "
-//         .unindent(),
-//     );
+    view.update(cx, |view, cx| {
+        view.move_line_down(&MoveLineDown, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
+                DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
+                DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
+                DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
+            ]
+        );
+    });
 
-//     // one cursor autocloses a multi-character pair, one cursor
-//     // does not autoclose.
-//     cx.set_state(
-//         &"
-//             /ˇ
-//             ˇ
-//         "
-//         .unindent(),
-//     );
-//     cx.update_editor(|view, cx| view.handle_input("*", cx));
-//     cx.assert_editor_state(
-//         &"
-//             /*ˇ */
-//             *ˇ
-//         "
-//         .unindent(),
-//     );
+    view.update(cx, |view, cx| {
+        view.move_line_up(&MoveLineUp, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
+                DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
+                DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
+            ]
+        );
+    });
+}
 
-//     // Don't autoclose if the next character isn't whitespace and isn't
-//     // listed in the language's "autoclose_before" section.
-//     cx.set_state("ˇa b");
-//     cx.update_editor(|view, cx| view.handle_input("{", cx));
-//     cx.assert_editor_state("{ˇa b");
-
-//     // Don't autoclose if `close` is false for the bracket pair
-//     cx.set_state("ˇ");
-//     cx.update_editor(|view, cx| view.handle_input("[", cx));
-//     cx.assert_editor_state("[ˇ");
-
-//     // Surround with brackets if text is selected
-//     cx.set_state("«aˇ» b");
-//     cx.update_editor(|view, cx| view.handle_input("{", cx));
-//     cx.assert_editor_state("{«aˇ»} b");
-
-//     // Autclose pair where the start and end characters are the same
-//     cx.set_state("aˇ");
-//     cx.update_editor(|view, cx| view.handle_input("\"", cx));
-//     cx.assert_editor_state("a\"ˇ\"");
-//     cx.update_editor(|view, cx| view.handle_input("\"", cx));
-//     cx.assert_editor_state("a\"\"ˇ");
-// }
+#[gpui::test]
+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)
+    });
+    editor.update(cx, |editor, cx| {
+        let snapshot = editor.buffer.read(cx).snapshot(cx);
+        editor.insert_blocks(
+            [BlockProperties {
+                style: BlockStyle::Fixed,
+                position: snapshot.anchor_after(Point::new(2, 0)),
+                disposition: BlockDisposition::Below,
+                height: 1,
+                render: Arc::new(|_| div().render()),
+            }],
+            Some(Autoscroll::fit()),
+            cx,
+        );
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
+        });
+        editor.move_line_down(&MoveLineDown, cx);
+    });
+}
 
+//todo!(test_transpose)
 // #[gpui::test]
-// async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
+// fn test_transpose(cx: &mut TestAppContext) {
 //     init_test(cx, |_| {});
 
-//     let mut cx = EditorTestContext::new(cx).await;
+//     _ = cx.add_window(|cx| {
+//         let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
 
-//     let html_language = Arc::new(
-//         Language::new(
-//             LanguageConfig {
-//                 name: "HTML".into(),
-//                 brackets: BracketPairConfig {
-//                     pairs: vec![
-//                         BracketPair {
-//                             start: "<".into(),
-//                             end: ">".into(),
-//                             close: true,
-//                             ..Default::default()
-//                         },
-//                         BracketPair {
-//                             start: "{".into(),
-//                             end: "}".into(),
-//                             close: true,
-//                             ..Default::default()
-//                         },
-//                         BracketPair {
-//                             start: "(".into(),
-//                             end: ")".into(),
-//                             close: true,
-//                             ..Default::default()
-//                         },
-//                     ],
-//                     ..Default::default()
-//                 },
-//                 autoclose_before: "})]>".into(),
-//                 ..Default::default()
-//             },
-//             Some(tree_sitter_html::language()),
-//         )
-//         .with_injection_query(
-//             r#"
-//             (script_element
-//                 (raw_text) @content
-//                 (#set! "language" "javascript"))
-//             "#,
-//         )
-//         .unwrap(),
-//     );
+//         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]);
 
-//     let javascript_language = Arc::new(Language::new(
-//         LanguageConfig {
-//             name: "JavaScript".into(),
-//             brackets: BracketPairConfig {
-//                 pairs: vec![
-//                     BracketPair {
-//                         start: "/*".into(),
-//                         end: " */".into(),
-//                         close: true,
-//                         ..Default::default()
-//                     },
-//                     BracketPair {
-//                         start: "{".into(),
-//                         end: "}".into(),
-//                         close: true,
-//                         ..Default::default()
-//                     },
-//                     BracketPair {
-//                         start: "(".into(),
-//                         end: ")".into(),
-//                         close: true,
-//                         ..Default::default()
-//                     },
-//                 ],
-//                 ..Default::default()
-//             },
-//             autoclose_before: "})]>".into(),
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_typescript::language_tsx()),
-//     ));
+//         editor.transpose(&Default::default(), cx);
+//         assert_eq!(editor.text(cx), "bca");
+//         assert_eq!(editor.selections.ranges(cx), [3..3]);
 
-//     let registry = Arc::new(LanguageRegistry::test());
-//     registry.add(html_language.clone());
-//     registry.add(javascript_language.clone());
+//         editor.transpose(&Default::default(), cx);
+//         assert_eq!(editor.text(cx), "bac");
+//         assert_eq!(editor.selections.ranges(cx), [3..3]);
 
-//     cx.update_buffer(|buffer, cx| {
-//         buffer.set_language_registry(registry);
-//         buffer.set_language(Some(html_language), cx);
+//         editor
 //     });
 
-//     cx.set_state(
-//         &r#"
-//             <body>ˇ
-//                 <script>
-//                     var x = 1;ˇ
-//                 </script>
-//             </body>ˇ
-//         "#
-//         .unindent(),
-//     );
+//     _ = cx.add_window(|cx| {
+//         let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
 
-//     // Precondition: different languages are active at different locations.
-//     cx.update_editor(|editor, cx| {
-//         let snapshot = editor.snapshot(cx);
-//         let cursors = editor.selections.ranges::<usize>(cx);
-//         let languages = cursors
-//             .iter()
-//             .map(|c| snapshot.language_at(c.start).unwrap().name())
-//             .collect::<Vec<_>>();
-//         assert_eq!(
-//             languages,
-//             &["HTML".into(), "JavaScript".into(), "HTML".into()]
-//         );
-//     });
+//         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]);
 
-//     // Angle brackets autoclose in HTML, but not JavaScript.
-//     cx.update_editor(|editor, cx| {
-//         editor.handle_input("<", cx);
-//         editor.handle_input("a", cx);
-//     });
-//     cx.assert_editor_state(
-//         &r#"
-//             <body><aˇ>
-//                 <script>
-//                     var x = 1;<aˇ
-//                 </script>
-//             </body><aˇ>
-//         "#
-//         .unindent(),
-//     );
+//         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]);
 
-//     // Curly braces and parens autoclose in both HTML and JavaScript.
-//     cx.update_editor(|editor, cx| {
-//         editor.handle_input(" b=", cx);
-//         editor.handle_input("{", cx);
-//         editor.handle_input("c", cx);
-//         editor.handle_input("(", cx);
-//     });
-//     cx.assert_editor_state(
-//         &r#"
-//             <body><a b={c(ˇ)}>
-//                 <script>
-//                     var x = 1;<a b={c(ˇ)}
-//                 </script>
-//             </body><a b={c(ˇ)}>
-//         "#
-//         .unindent(),
-//     );
+//         editor.transpose(&Default::default(), cx);
+//         assert_eq!(editor.text(cx), "acbde\n");
+//         assert_eq!(editor.selections.ranges(cx), [6..6]);
 
-//     // Brackets that were already autoclosed are skipped.
-//     cx.update_editor(|editor, cx| {
-//         editor.handle_input(")", cx);
-//         editor.handle_input("d", cx);
-//         editor.handle_input("}", cx);
-//     });
-//     cx.assert_editor_state(
-//         &r#"
-//             <body><a b={c()d}ˇ>
-//                 <script>
-//                     var x = 1;<a b={c()d}ˇ
-//                 </script>
-//             </body><a b={c()d}ˇ>
-//         "#
-//         .unindent(),
-//     );
-//     cx.update_editor(|editor, cx| {
-//         editor.handle_input(">", cx);
+//         editor.transpose(&Default::default(), cx);
+//         assert_eq!(editor.text(cx), "acbd\ne");
+//         assert_eq!(editor.selections.ranges(cx), [6..6]);
+
+//         editor
 //     });
-//     cx.assert_editor_state(
-//         &r#"
-//             <body><a b={c()d}>ˇ
-//                 <script>
-//                     var x = 1;<a b={c()d}>ˇ
-//                 </script>
-//             </body><a b={c()d}>ˇ
-//         "#
-//         .unindent(),
-//     );
 
-//     // Reset
-//     cx.set_state(
-//         &r#"
-//             <body>ˇ
-//                 <script>
-//                     var x = 1;ˇ
-//                 </script>
-//             </body>ˇ
-//         "#
-//         .unindent(),
-//     );
+//     _ = cx.add_window(|cx| {
+//         let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
 
-//     cx.update_editor(|editor, cx| {
-//         editor.handle_input("<", cx);
-//     });
-//     cx.assert_editor_state(
-//         &r#"
-//             <body><ˇ>
-//                 <script>
-//                     var x = 1;<ˇ
-//                 </script>
-//             </body><ˇ>
-//         "#
-//         .unindent(),
-//     );
+//         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]);
 
-//     // When backspacing, the closing angle brackets are removed.
-//     cx.update_editor(|editor, cx| {
-//         editor.backspace(&Backspace, cx);
+//         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), "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
 //     });
-//     cx.assert_editor_state(
-//         &r#"
-//             <body>ˇ
-//                 <script>
-//                     var x = 1;ˇ
-//                 </script>
-//             </body>ˇ
-//         "#
-//         .unindent(),
-//     );
 
-//     // Block comments autoclose in JavaScript, but not HTML.
-//     cx.update_editor(|editor, cx| {
-//         editor.handle_input("/", cx);
-//         editor.handle_input("*", 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.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
 //     });
-//     cx.assert_editor_state(
-//         &r#"
-//             <body>/*ˇ
-//                 <script>
-//                     var x = 1;/*ˇ */
-//                 </script>
-//             </body>/*ˇ
-//         "#
-//         .unindent(),
-//     );
 // }
 
+//todo!(clipboard)
 // #[gpui::test]
-// async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
+// async fn test_clipboard(cx: &mut gpui::TestAppContext) {
 //     init_test(cx, |_| {});
 
 //     let mut cx = EditorTestContext::new(cx).await;
 
-//     let rust_language = Arc::new(
-//         Language::new(
-//             LanguageConfig {
-//                 name: "Rust".into(),
-//                 brackets: serde_json::from_value(json!([
-//                     { "start": "{", "end": "}", "close": true, "newline": true },
-//                     { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
-//                 ]))
-//                 .unwrap(),
-//                 autoclose_before: "})]>".into(),
-//                 ..Default::default()
-//             },
-//             Some(tree_sitter_rust::language()),
-//         )
-//         .with_override_query("(string_literal) @string")
-//         .unwrap(),
-//     );
-
-//     let registry = Arc::new(LanguageRegistry::test());
-//     registry.add(rust_language.clone());
-
-//     cx.update_buffer(|buffer, cx| {
-//         buffer.set_language_registry(registry);
-//         buffer.set_language(Some(rust_language), cx);
-//     });
+//     cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
+//     cx.update_editor(|e, cx| e.cut(&Cut, cx));
+//     cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
 
-//     cx.set_state(
-//         &r#"
-//             let x = ˇ
-//         "#
-//         .unindent(),
-//     );
+//     // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
+//     cx.set_state("two ˇfour ˇsix ˇ");
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
 
-//     // Inserting a quotation mark. A closing quotation mark is automatically inserted.
-//     cx.update_editor(|editor, cx| {
-//         editor.handle_input("\"", cx);
+//     // Paste again but with only two cursors. Since the number of cursors doesn't
+//     // match the number of slices in the clipboard, the entire clipboard text
+//     // is pasted at each cursor.
+//     cx.set_state("ˇtwo one✅ four three six five ˇ");
+//     cx.update_editor(|e, cx| {
+//         e.handle_input("( ", cx);
+//         e.paste(&Paste, cx);
+//         e.handle_input(") ", cx);
 //     });
 //     cx.assert_editor_state(
-//         &r#"
-//             let x = "ˇ"
-//         "#
-//         .unindent(),
+//         &([
+//             "( one✅ ",
+//             "three ",
+//             "five ) ˇtwo one✅ four three six five ( one✅ ",
+//             "three ",
+//             "five ) ˇ",
+//         ]
+//         .join("\n")),
 //     );
 
-//     // Inserting another quotation mark. The cursor moves across the existing
-//     // automatically-inserted quotation mark.
-//     cx.update_editor(|editor, cx| {
-//         editor.handle_input("\"", cx);
-//     });
-//     cx.assert_editor_state(
-//         &r#"
-//             let x = ""ˇ
-//         "#
-//         .unindent(),
-//     );
+//     // Cut with three selections, one of which is full-line.
+//     cx.set_state(indoc! {"
+//         1«2ˇ»3
+//         4ˇ567
+//         «8ˇ»9"});
+//     cx.update_editor(|e, cx| e.cut(&Cut, cx));
+//     cx.assert_editor_state(indoc! {"
+//         1ˇ3
+//         ˇ9"});
 
-//     // Reset
-//     cx.set_state(
-//         &r#"
-//             let x = ˇ
-//         "#
-//         .unindent(),
-//     );
+//     // Paste with three selections, noticing how the copied selection that was full-line
+//     // gets inserted before the second cursor.
+//     cx.set_state(indoc! {"
+//         1ˇ3
+//         9ˇ
+//         «oˇ»ne"});
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state(indoc! {"
+//         12ˇ3
+//         4567
+//         9ˇ
+//         8ˇne"});
 
-//     // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
-//     cx.update_editor(|editor, cx| {
-//         editor.handle_input("\"", cx);
-//         editor.handle_input(" ", cx);
-//         editor.move_left(&Default::default(), cx);
-//         editor.handle_input("\\", cx);
-//         editor.handle_input("\"", cx);
-//     });
-//     cx.assert_editor_state(
-//         &r#"
-//             let x = "\"ˇ "
-//         "#
-//         .unindent(),
-//     );
+//     // Copy with a single cursor only, which writes the whole line into the clipboard.
+//     cx.set_state(indoc! {"
+//         The quick brown
+//         fox juˇmps over
+//         the lazy dog"});
+//     cx.update_editor(|e, cx| e.copy(&Copy, cx));
+//     cx.cx.assert_clipboard_content(Some("fox jumps over\n"));
 
-//     // Inserting a closing quotation mark at the position of an automatically-inserted quotation
-//     // mark. Nothing is inserted.
-//     cx.update_editor(|editor, cx| {
-//         editor.move_right(&Default::default(), cx);
-//         editor.handle_input("\"", cx);
-//     });
-//     cx.assert_editor_state(
-//         &r#"
-//             let x = "\" "ˇ
-//         "#
-//         .unindent(),
-//     );
+//     // Paste with three selections, noticing how the copied full-line selection is inserted
+//     // before the empty selections but replaces the selection that is non-empty.
+//     cx.set_state(indoc! {"
+//         Tˇhe quick brown
+//         «foˇ»x jumps over
+//         tˇhe lazy dog"});
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state(indoc! {"
+//         fox jumps over
+//         Tˇhe quick brown
+//         fox jumps over
+//         ˇx jumps over
+//         fox jumps over
+//         tˇhe lazy dog"});
 // }
 
 // #[gpui::test]
-// async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
+// async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
 //     init_test(cx, |_| {});
 
+//     let mut cx = EditorTestContext::new(cx).await;
 //     let language = Arc::new(Language::new(
-//         LanguageConfig {
-//             brackets: BracketPairConfig {
-//                 pairs: vec![
-//                     BracketPair {
-//                         start: "{".to_string(),
-//                         end: "}".to_string(),
-//                         close: true,
-//                         newline: true,
-//                     },
-//                     BracketPair {
-//                         start: "/* ".to_string(),
-//                         end: "*/".to_string(),
-//                         close: true,
-//                         ..Default::default()
-//                     },
-//                 ],
-//                 ..Default::default()
-//             },
-//             ..Default::default()
-//         },
+//         LanguageConfig::default(),
 //         Some(tree_sitter_rust::language()),
 //     ));
+//     cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
 
-//     let text = r#"
-//         a
-//         b
-//         c
-//     "#
-//     .unindent();
-
-//     let buffer =
-//         cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
-//     let buffer = cx.add_model(|cx| MultiBuffer::singleton(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;
-
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
-//                 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1),
-//             ])
-//         });
-
-//         view.handle_input("{", cx);
-//         view.handle_input("{", cx);
-//         view.handle_input("{", cx);
-//         assert_eq!(
-//             view.text(cx),
-//             "
-//                 {{{a}}}
-//                 {{{b}}}
-//                 {{{c}}}
-//             "
-//             .unindent()
+//     // Cut an indented block, without the leading whitespace.
+//     cx.set_state(indoc! {"
+//         const a: B = (
+//             c(),
+//             «d(
+//                 e,
+//                 f
+//             )ˇ»
 //         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             [
-//                 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4),
-//                 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4),
-//                 DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4)
-//             ]
+//     "});
+//     cx.update_editor(|e, cx| e.cut(&Cut, cx));
+//     cx.assert_editor_state(indoc! {"
+//         const a: B = (
+//             c(),
+//             ˇ
 //         );
+//     "});
 
-//         view.undo(&Undo, cx);
-//         view.undo(&Undo, cx);
-//         view.undo(&Undo, cx);
-//         assert_eq!(
-//             view.text(cx),
-//             "
-//                 a
-//                 b
-//                 c
-//             "
-//             .unindent()
-//         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             [
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
-//                 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
-//             ]
+//     // Paste it at the same position.
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state(indoc! {"
+//         const a: B = (
+//             c(),
+//             d(
+//                 e,
+//                 f
+//             )ˇ
 //         );
+//     "});
 
-//         // Ensure inserting the first character of a multi-byte bracket pair
-//         // doesn't surround the selections with the bracket.
-//         view.handle_input("/", cx);
-//         assert_eq!(
-//             view.text(cx),
-//             "
-//                 /
-//                 /
-//                 /
-//             "
-//             .unindent()
+//     // Paste it at a line with a lower indent level.
+//     cx.set_state(indoc! {"
+//         ˇ
+//         const a: B = (
+//             c(),
 //         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             [
-//                 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
-//                 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
-//             ]
+//     "});
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state(indoc! {"
+//         d(
+//             e,
+//             f
+//         )ˇ
+//         const a: B = (
+//             c(),
 //         );
+//     "});
 
-//         view.undo(&Undo, cx);
-//         assert_eq!(
-//             view.text(cx),
-//             "
-//                 a
-//                 b
-//                 c
-//             "
-//             .unindent()
-//         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             [
-//                 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
-//                 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
-//             ]
-//         );
+//     // Cut an indented block, with the leading whitespace.
+//     cx.set_state(indoc! {"
+//         const a: B = (
+//             c(),
+//         «    d(
+//                 e,
+//                 f
+//             )
+//         ˇ»);
+//     "});
+//     cx.update_editor(|e, cx| e.cut(&Cut, cx));
+//     cx.assert_editor_state(indoc! {"
+//         const a: B = (
+//             c(),
+//         ˇ);
+//     "});
 
-//         // Ensure inserting the last character of a multi-byte bracket pair
-//         // doesn't surround the selections with the bracket.
-//         view.handle_input("*", cx);
-//         assert_eq!(
-//             view.text(cx),
-//             "
-//                 *
-//                 *
-//                 *
-//             "
-//             .unindent()
+//     // Paste it at the same position.
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state(indoc! {"
+//         const a: B = (
+//             c(),
+//             d(
+//                 e,
+//                 f
+//             )
+//         ˇ);
+//     "});
+
+//     // Paste it at a line with a higher indent level.
+//     cx.set_state(indoc! {"
+//         const a: B = (
+//             c(),
+//             d(
+//                 e,
+//                 fˇ
+//             )
 //         );
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             [
-//                 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
-//                 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
-//                 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
-//             ]
+//     "});
+//     cx.update_editor(|e, cx| e.paste(&Paste, cx));
+//     cx.assert_editor_state(indoc! {"
+//         const a: B = (
+//             c(),
+//             d(
+//                 e,
+//                 f    d(
+//                     e,
+//                     f
+//                 )
+//         ˇ
+//             )
 //         );
-//     });
+//     "});
 // }
 
-// #[gpui::test]
-// async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+fn test_select_all(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//     let language = Arc::new(Language::new(
-//         LanguageConfig {
-//             brackets: BracketPairConfig {
-//                 pairs: vec![BracketPair {
-//                     start: "{".to_string(),
-//                     end: "}".to_string(),
-//                     close: true,
-//                     newline: true,
-//                 }],
-//                 ..Default::default()
-//             },
-//             autoclose_before: "}".to_string(),
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     ));
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
+        build_editor(buffer, cx)
+    });
+    view.update(cx, |view, cx| {
+        view.select_all(&SelectAll, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)]
+        );
+    });
+}
 
-//     let text = r#"
-//         a
-//         b
-//         c
-//     "#
-//     .unindent();
-
-//     let buffer =
-//         cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
-//     let buffer = cx.add_model(|cx| MultiBuffer::singleton(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;
+#[gpui::test]
+fn test_select_line(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-//     editor.update(cx, |editor, cx| {
-//         editor.change_selections(None, cx, |s| {
-//             s.select_ranges([
-//                 Point::new(0, 1)..Point::new(0, 1),
-//                 Point::new(1, 1)..Point::new(1, 1),
-//                 Point::new(2, 1)..Point::new(2, 1),
-//             ])
-//         });
+    let view = cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
+        build_editor(buffer, cx)
+    });
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+                DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2),
+            ])
+        });
+        view.select_line(&SelectLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0),
+                DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0),
+            ]
+        );
+    });
 
-//         editor.handle_input("{", cx);
-//         editor.handle_input("{", cx);
-//         editor.handle_input("_", cx);
-//         assert_eq!(
-//             editor.text(cx),
-//             "
-//                 a{{_}}
-//                 b{{_}}
-//                 c{{_}}
-//             "
-//             .unindent()
-//         );
-//         assert_eq!(
-//             editor.selections.ranges::<Point>(cx),
-//             [
-//                 Point::new(0, 4)..Point::new(0, 4),
-//                 Point::new(1, 4)..Point::new(1, 4),
-//                 Point::new(2, 4)..Point::new(2, 4)
-//             ]
-//         );
+    view.update(cx, |view, cx| {
+        view.select_line(&SelectLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![
+                DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0),
+                DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5),
+            ]
+        );
+    });
 
-//         editor.backspace(&Default::default(), cx);
-//         editor.backspace(&Default::default(), cx);
-//         assert_eq!(
-//             editor.text(cx),
-//             "
-//                 a{}
-//                 b{}
-//                 c{}
-//             "
-//             .unindent()
-//         );
-//         assert_eq!(
-//             editor.selections.ranges::<Point>(cx),
-//             [
-//                 Point::new(0, 2)..Point::new(0, 2),
-//                 Point::new(1, 2)..Point::new(1, 2),
-//                 Point::new(2, 2)..Point::new(2, 2)
-//             ]
-//         );
+    view.update(cx, |view, cx| {
+        view.select_line(&SelectLine, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)]
+        );
+    });
+}
 
-//         editor.delete_to_previous_word_start(&Default::default(), cx);
-//         assert_eq!(
-//             editor.text(cx),
-//             "
-//                 a
-//                 b
-//                 c
-//             "
-//             .unindent()
-//         );
-//         assert_eq!(
-//             editor.selections.ranges::<Point>(cx),
-//             [
-//                 Point::new(0, 1)..Point::new(0, 1),
-//                 Point::new(1, 1)..Point::new(1, 1),
-//                 Point::new(2, 1)..Point::new(2, 1)
-//             ]
-//         );
-//     });
-// }
+#[gpui::test]
+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)
+    });
+    view.update(cx, |view, cx| {
+        view.fold_ranges(
+            vec![
+                Point::new(0, 2)..Point::new(1, 2),
+                Point::new(2, 3)..Point::new(4, 1),
+                Point::new(7, 0)..Point::new(8, 4),
+            ],
+            true,
+            cx,
+        );
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+                DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
+            ])
+        });
+        assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i");
+    });
+
+    view.update(cx, |view, cx| {
+        view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+                DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
+                DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4)
+            ]
+        );
+    });
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)])
+        });
+        view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
+        assert_eq!(
+            view.display_text(cx),
+            "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5),
+                DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
+                DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
+                DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
+                DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5),
+                DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5),
+                DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5),
+                DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0)
+            ]
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_add_selection_above_below(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
+    cx.set_state(indoc!(
+        r#"abc
+           defˇghi
+
+           jk
+           nlmo
+           "#
+    ));
+
+    cx.update_editor(|editor, cx| {
+        editor.add_selection_above(&Default::default(), cx);
+    });
 
-// #[gpui::test]
-// async fn test_snippets(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+    cx.assert_editor_state(indoc!(
+        r#"abcˇ
+           defˇghi
 
-//     let (text, insertion_ranges) = marked_text_ranges(
-//         indoc! {"
-//             a.ˇ b
-//             a.ˇ b
-//             a.ˇ b
-//         "},
-//         false,
-//     );
+           jk
+           nlmo
+           "#
+    ));
 
-//     let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
-//     let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+    cx.update_editor(|editor, cx| {
+        editor.add_selection_above(&Default::default(), cx);
+    });
 
-//     editor.update(cx, |editor, cx| {
-//         let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
+    cx.assert_editor_state(indoc!(
+        r#"abcˇ
+            defˇghi
 
-//         editor
-//             .insert_snippet(&insertion_ranges, snippet, cx)
-//             .unwrap();
+            jk
+            nlmo
+            "#
+    ));
 
-//         fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
-//             let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
-//             assert_eq!(editor.text(cx), expected_text);
-//             assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
-//         }
+    cx.update_editor(|view, cx| {
+        view.add_selection_below(&Default::default(), cx);
+    });
 
-//         assert(
-//             editor,
-//             cx,
-//             indoc! {"
-//                 a.f(«one», two, «three») b
-//                 a.f(«one», two, «three») b
-//                 a.f(«one», two, «three») b
-//             "},
-//         );
+    cx.assert_editor_state(indoc!(
+        r#"abc
+           defˇghi
 
-//         // Can't move earlier than the first tab stop
-//         assert!(!editor.move_to_prev_snippet_tabstop(cx));
-//         assert(
-//             editor,
-//             cx,
-//             indoc! {"
-//                 a.f(«one», two, «three») b
-//                 a.f(«one», two, «three») b
-//                 a.f(«one», two, «three») b
-//             "},
-//         );
+           jk
+           nlmo
+           "#
+    ));
 
-//         assert!(editor.move_to_next_snippet_tabstop(cx));
-//         assert(
-//             editor,
-//             cx,
-//             indoc! {"
-//                 a.f(one, «two», three) b
-//                 a.f(one, «two», three) b
-//                 a.f(one, «two», three) b
-//             "},
-//         );
+    cx.update_editor(|view, cx| {
+        view.undo_selection(&Default::default(), cx);
+    });
 
-//         editor.move_to_prev_snippet_tabstop(cx);
-//         assert(
-//             editor,
-//             cx,
-//             indoc! {"
-//                 a.f(«one», two, «three») b
-//                 a.f(«one», two, «three») b
-//                 a.f(«one», two, «three») b
-//             "},
-//         );
+    cx.assert_editor_state(indoc!(
+        r#"abcˇ
+           defˇghi
 
-//         assert!(editor.move_to_next_snippet_tabstop(cx));
-//         assert(
-//             editor,
-//             cx,
-//             indoc! {"
-//                 a.f(one, «two», three) b
-//                 a.f(one, «two», three) b
-//                 a.f(one, «two», three) b
-//             "},
-//         );
-//         assert!(editor.move_to_next_snippet_tabstop(cx));
-//         assert(
-//             editor,
-//             cx,
-//             indoc! {"
-//                 a.f(one, two, three)ˇ b
-//                 a.f(one, two, three)ˇ b
-//                 a.f(one, two, three)ˇ b
-//             "},
-//         );
+           jk
+           nlmo
+           "#
+    ));
 
-//         // As soon as the last tab stop is reached, snippet state is gone
-//         editor.move_to_prev_snippet_tabstop(cx);
-//         assert(
-//             editor,
-//             cx,
-//             indoc! {"
-//                 a.f(one, two, three)ˇ b
-//                 a.f(one, two, three)ˇ b
-//                 a.f(one, two, three)ˇ b
-//             "},
-//         );
-//     });
-// }
+    cx.update_editor(|view, cx| {
+        view.redo_selection(&Default::default(), cx);
+    });
 
-// #[gpui::test]
-// async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+    cx.assert_editor_state(indoc!(
+        r#"abc
+           defˇghi
 
-//     let mut language = Language::new(
-//         LanguageConfig {
-//             name: "Rust".into(),
-//             path_suffixes: vec!["rs".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     );
-//     let mut fake_servers = language
-//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-//             capabilities: lsp::ServerCapabilities {
-//                 document_formatting_provider: Some(lsp::OneOf::Left(true)),
-//                 ..Default::default()
-//             },
-//             ..Default::default()
-//         }))
-//         .await;
+           jk
+           nlmo
+           "#
+    ));
 
-//     let fs = FakeFs::new(cx.background());
-//     fs.insert_file("/file.rs", Default::default()).await;
+    cx.update_editor(|view, cx| {
+        view.add_selection_below(&Default::default(), cx);
+    });
 
-//     let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
-//     project.update(cx, |project, _| project.languages().add(Arc::new(language)));
-//     let buffer = project
-//         .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
-//         .await
-//         .unwrap();
+    cx.assert_editor_state(indoc!(
+        r#"abc
+           defˇghi
 
-//     cx.foreground().start_waiting();
-//     let fake_server = fake_servers.next().await.unwrap();
+           jk
+           nlmˇo
+           "#
+    ));
 
-//     let buffer = cx.add_model(|cx| MultiBuffer::singleton(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)));
+    cx.update_editor(|view, cx| {
+        view.add_selection_below(&Default::default(), cx);
+    });
 
-//     let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
-//     fake_server
-//         .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
-//             assert_eq!(
-//                 params.text_document.uri,
-//                 lsp::Url::from_file_path("/file.rs").unwrap()
-//             );
-//             assert_eq!(params.options.tab_size, 4);
-//             Ok(Some(vec![lsp::TextEdit::new(
-//                 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
-//                 ", ".to_string(),
-//             )]))
-//         })
-//         .next()
-//         .await;
-//     cx.foreground().start_waiting();
-//     save.await.unwrap();
-//     assert_eq!(
-//         editor.read_with(cx, |editor, cx| editor.text(cx)),
-//         "one, two\nthree\n"
-//     );
-//     assert!(!cx.read(|cx| editor.is_dirty(cx)));
+    cx.assert_editor_state(indoc!(
+        r#"abc
+           defˇghi
 
-//     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
-//     assert!(cx.read(|cx| editor.is_dirty(cx)));
+           jk
+           nlmˇo
+           "#
+    ));
 
-//     // Ensure we can still save even if formatting hangs.
-//     fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
-//         assert_eq!(
-//             params.text_document.uri,
-//             lsp::Url::from_file_path("/file.rs").unwrap()
-//         );
-//         futures::future::pending::<()>().await;
-//         unreachable!()
-//     });
-//     let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
-//     cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
-//     cx.foreground().start_waiting();
-//     save.await.unwrap();
-//     assert_eq!(
-//         editor.read_with(cx, |editor, cx| editor.text(cx)),
-//         "one\ntwo\nthree\n"
-//     );
-//     assert!(!cx.read(|cx| editor.is_dirty(cx)));
-
-//     // Set rust language override and assert overridden tabsize is sent to language server
-//     update_test_language_settings(cx, |settings| {
-//         settings.languages.insert(
-//             "Rust".into(),
-//             LanguageSettingsContent {
-//                 tab_size: NonZeroU32::new(8),
-//                 ..Default::default()
-//             },
-//         );
-//     });
+    // change selections
+    cx.set_state(indoc!(
+        r#"abc
+           def«ˇg»hi
 
-//     let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
-//     fake_server
-//         .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
-//             assert_eq!(
-//                 params.text_document.uri,
-//                 lsp::Url::from_file_path("/file.rs").unwrap()
-//             );
-//             assert_eq!(params.options.tab_size, 8);
-//             Ok(Some(vec![]))
-//         })
-//         .next()
-//         .await;
-//     cx.foreground().start_waiting();
-//     save.await.unwrap();
-// }
+           jk
+           nlmo
+           "#
+    ));
 
-// #[gpui::test]
-// async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+    cx.update_editor(|view, cx| {
+        view.add_selection_below(&Default::default(), cx);
+    });
 
-//     let mut language = Language::new(
-//         LanguageConfig {
-//             name: "Rust".into(),
-//             path_suffixes: vec!["rs".to_string()],
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     );
-//     let mut fake_servers = language
-//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-//             capabilities: lsp::ServerCapabilities {
-//                 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
-//                 ..Default::default()
-//             },
-//             ..Default::default()
-//         }))
-//         .await;
+    cx.assert_editor_state(indoc!(
+        r#"abc
+           def«ˇg»hi
 
-//     let fs = FakeFs::new(cx.background());
-//     fs.insert_file("/file.rs", Default::default()).await;
+           jk
+           nlm«ˇo»
+           "#
+    ));
 
-//     let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
-//     project.update(cx, |project, _| project.languages().add(Arc::new(language)));
-//     let buffer = project
-//         .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
-//         .await
-//         .unwrap();
+    cx.update_editor(|view, cx| {
+        view.add_selection_below(&Default::default(), cx);
+    });
 
-//     cx.foreground().start_waiting();
-//     let fake_server = fake_servers.next().await.unwrap();
+    cx.assert_editor_state(indoc!(
+        r#"abc
+           def«ˇg»hi
 
-//     let buffer = cx.add_model(|cx| MultiBuffer::singleton(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)));
+           jk
+           nlm«ˇo»
+           "#
+    ));
 
-//     let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
-//     fake_server
-//         .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
-//             assert_eq!(
-//                 params.text_document.uri,
-//                 lsp::Url::from_file_path("/file.rs").unwrap()
-//             );
-//             assert_eq!(params.options.tab_size, 4);
-//             Ok(Some(vec![lsp::TextEdit::new(
-//                 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
-//                 ", ".to_string(),
-//             )]))
-//         })
-//         .next()
-//         .await;
-//     cx.foreground().start_waiting();
-//     save.await.unwrap();
-//     assert_eq!(
-//         editor.read_with(cx, |editor, cx| editor.text(cx)),
-//         "one, two\nthree\n"
-//     );
-//     assert!(!cx.read(|cx| editor.is_dirty(cx)));
+    cx.update_editor(|view, cx| {
+        view.add_selection_above(&Default::default(), cx);
+    });
 
-//     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
-//     assert!(cx.read(|cx| editor.is_dirty(cx)));
+    cx.assert_editor_state(indoc!(
+        r#"abc
+           def«ˇg»hi
 
-//     // Ensure we can still save even if formatting hangs.
-//     fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
-//         move |params, _| async move {
-//             assert_eq!(
-//                 params.text_document.uri,
-//                 lsp::Url::from_file_path("/file.rs").unwrap()
-//             );
-//             futures::future::pending::<()>().await;
-//             unreachable!()
-//         },
-//     );
-//     let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
-//     cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
-//     cx.foreground().start_waiting();
-//     save.await.unwrap();
-//     assert_eq!(
-//         editor.read_with(cx, |editor, cx| editor.text(cx)),
-//         "one\ntwo\nthree\n"
-//     );
-//     assert!(!cx.read(|cx| editor.is_dirty(cx)));
-
-//     // Set rust language override and assert overridden tabsize is sent to language server
-//     update_test_language_settings(cx, |settings| {
-//         settings.languages.insert(
-//             "Rust".into(),
-//             LanguageSettingsContent {
-//                 tab_size: NonZeroU32::new(8),
-//                 ..Default::default()
-//             },
-//         );
-//     });
+           jk
+           nlmo
+           "#
+    ));
 
-//     let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
-//     fake_server
-//         .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
-//             assert_eq!(
-//                 params.text_document.uri,
-//                 lsp::Url::from_file_path("/file.rs").unwrap()
-//             );
-//             assert_eq!(params.options.tab_size, 8);
-//             Ok(Some(vec![]))
-//         })
-//         .next()
-//         .await;
-//     cx.foreground().start_waiting();
-//     save.await.unwrap();
-// }
+    cx.update_editor(|view, cx| {
+        view.add_selection_above(&Default::default(), cx);
+    });
 
-// #[gpui::test]
-// async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |settings| {
-//         settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
-//     });
+    cx.assert_editor_state(indoc!(
+        r#"abc
+           def«ˇg»hi
 
-//     let mut language = Language::new(
-//         LanguageConfig {
-//             name: "Rust".into(),
-//             path_suffixes: vec!["rs".to_string()],
-//             // Enable Prettier formatting for the same buffer, and ensure
-//             // LSP is called instead of Prettier.
-//             prettier_parser_name: Some("test_parser".to_string()),
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     );
-//     let mut fake_servers = language
-//         .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-//             capabilities: lsp::ServerCapabilities {
-//                 document_formatting_provider: Some(lsp::OneOf::Left(true)),
-//                 ..Default::default()
-//             },
-//             ..Default::default()
-//         }))
-//         .await;
+           jk
+           nlmo
+           "#
+    ));
 
-//     let fs = FakeFs::new(cx.background());
-//     fs.insert_file("/file.rs", Default::default()).await;
+    // Change selections again
+    cx.set_state(indoc!(
+        r#"a«bc
+           defgˇ»hi
 
-//     let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
-//     project.update(cx, |project, _| {
-//         project.languages().add(Arc::new(language));
-//     });
-//     let buffer = project
-//         .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
-//         .await
-//         .unwrap();
+           jk
+           nlmo
+           "#
+    ));
 
-//     cx.foreground().start_waiting();
-//     let fake_server = fake_servers.next().await.unwrap();
+    cx.update_editor(|view, cx| {
+        view.add_selection_below(&Default::default(), cx);
+    });
 
-//     let buffer = cx.add_model(|cx| MultiBuffer::singleton(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));
+    cx.assert_editor_state(indoc!(
+        r#"a«bcˇ»
+           d«efgˇ»hi
 
-//     let format = editor.update(cx, |editor, cx| {
-//         editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
-//     });
-//     fake_server
-//         .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
-//             assert_eq!(
-//                 params.text_document.uri,
-//                 lsp::Url::from_file_path("/file.rs").unwrap()
-//             );
-//             assert_eq!(params.options.tab_size, 4);
-//             Ok(Some(vec![lsp::TextEdit::new(
-//                 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
-//                 ", ".to_string(),
-//             )]))
-//         })
-//         .next()
-//         .await;
-//     cx.foreground().start_waiting();
-//     format.await.unwrap();
-//     assert_eq!(
-//         editor.read_with(cx, |editor, cx| editor.text(cx)),
-//         "one, two\nthree\n"
-//     );
+           j«kˇ»
+           nlmo
+           "#
+    ));
 
-//     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
-//     // Ensure we don't lock if formatting hangs.
-//     fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
-//         assert_eq!(
-//             params.text_document.uri,
-//             lsp::Url::from_file_path("/file.rs").unwrap()
-//         );
-//         futures::future::pending::<()>().await;
-//         unreachable!()
-//     });
-//     let format = editor.update(cx, |editor, cx| {
-//         editor.perform_format(project, FormatTrigger::Manual, cx)
-//     });
-//     cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
-//     cx.foreground().start_waiting();
-//     format.await.unwrap();
-//     assert_eq!(
-//         editor.read_with(cx, |editor, cx| editor.text(cx)),
-//         "one\ntwo\nthree\n"
-//     );
-// }
+    cx.update_editor(|view, cx| {
+        view.add_selection_below(&Default::default(), cx);
+    });
+    cx.assert_editor_state(indoc!(
+        r#"a«bcˇ»
+           d«efgˇ»hi
+
+           j«kˇ»
+           n«lmoˇ»
+           "#
+    ));
+    cx.update_editor(|view, cx| {
+        view.add_selection_above(&Default::default(), cx);
+    });
 
-// #[gpui::test]
-// async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+    cx.assert_editor_state(indoc!(
+        r#"a«bcˇ»
+           d«efgˇ»hi
 
-//     let mut cx = EditorLspTestContext::new_rust(
-//         lsp::ServerCapabilities {
-//             document_formatting_provider: Some(lsp::OneOf::Left(true)),
-//             ..Default::default()
-//         },
-//         cx,
-//     )
-//     .await;
+           j«kˇ»
+           nlmo
+           "#
+    ));
 
-//     cx.set_state(indoc! {"
-//         one.twoˇ
-//     "});
+    // Change selections again
+    cx.set_state(indoc!(
+        r#"abc
+           d«ˇefghi
 
-//     // The format request takes a long time. When it completes, it inserts
-//     // a newline and an indent before the `.`
-//     cx.lsp
-//         .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
-//             let executor = cx.background();
-//             async move {
-//                 executor.timer(Duration::from_millis(100)).await;
-//                 Ok(Some(vec![lsp::TextEdit {
-//                     range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
-//                     new_text: "\n    ".into(),
-//                 }]))
-//             }
-//         });
+           jk
+           nlm»o
+           "#
+    ));
 
-//     // Submit a format request.
-//     let format_1 = cx
-//         .update_editor(|editor, cx| editor.format(&Format, cx))
-//         .unwrap();
-//     cx.foreground().run_until_parked();
+    cx.update_editor(|view, cx| {
+        view.add_selection_above(&Default::default(), cx);
+    });
 
-//     // Submit a second format request.
-//     let format_2 = cx
-//         .update_editor(|editor, cx| editor.format(&Format, cx))
-//         .unwrap();
-//     cx.foreground().run_until_parked();
+    cx.assert_editor_state(indoc!(
+        r#"a«ˇbc»
+           d«ˇef»ghi
 
-//     // Wait for both format requests to complete
-//     cx.foreground().advance_clock(Duration::from_millis(200));
-//     cx.foreground().start_waiting();
-//     format_1.await.unwrap();
-//     cx.foreground().start_waiting();
-//     format_2.await.unwrap();
+           j«ˇk»
+           n«ˇlm»o
+           "#
+    ));
 
-//     // The formatting edits only happens once.
-//     cx.assert_editor_state(indoc! {"
-//         one
-//             .twoˇ
-//     "});
-// }
+    cx.update_editor(|view, cx| {
+        view.add_selection_below(&Default::default(), cx);
+    });
 
-// #[gpui::test]
-// async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |settings| {
-//         settings.defaults.formatter = Some(language_settings::Formatter::Auto)
-//     });
+    cx.assert_editor_state(indoc!(
+        r#"abc
+           d«ˇef»ghi
 
-//     let mut cx = EditorLspTestContext::new_rust(
-//         lsp::ServerCapabilities {
-//             document_formatting_provider: Some(lsp::OneOf::Left(true)),
-//             ..Default::default()
-//         },
-//         cx,
-//     )
-//     .await;
+           j«ˇk»
+           n«ˇlm»o
+           "#
+    ));
+}
 
-//     // Set up a buffer white some trailing whitespace and no trailing newline.
-//     cx.set_state(
-//         &[
-//             "one ",   //
-//             "twoˇ",   //
-//             "three ", //
-//             "four",   //
-//         ]
-//         .join("\n"),
-//     );
+#[gpui::test]
+async fn test_select_next(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
 
-//     // Submit a format request.
-//     let format = cx
-//         .update_editor(|editor, cx| editor.format(&Format, cx))
-//         .unwrap();
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state("abc\nˇabc abc\ndefabc\nabc");
 
-//     // Record which buffer changes have been sent to the language server
-//     let buffer_changes = Arc::new(Mutex::new(Vec::new()));
-//     cx.lsp
-//         .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
-//             let buffer_changes = buffer_changes.clone();
-//             move |params, _| {
-//                 buffer_changes.lock().extend(
-//                     params
-//                         .content_changes
-//                         .into_iter()
-//                         .map(|e| (e.range.unwrap(), e.text)),
-//                 );
-//             }
-//         });
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
 
-//     // Handle formatting requests to the language server.
-//     cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
-//         let buffer_changes = buffer_changes.clone();
-//         move |_, _| {
-//             // When formatting is requested, trailing whitespace has already been stripped,
-//             // and the trailing newline has already been added.
-//             assert_eq!(
-//                 &buffer_changes.lock()[1..],
-//                 &[
-//                     (
-//                         lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
-//                         "".into()
-//                     ),
-//                     (
-//                         lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
-//                         "".into()
-//                     ),
-//                     (
-//                         lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
-//                         "\n".into()
-//                     ),
-//                 ]
-//             );
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
 
-//             // Insert blank lines between each line of the buffer.
-//             async move {
-//                 Ok(Some(vec![
-//                     lsp::TextEdit {
-//                         range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
-//                         new_text: "\n".into(),
-//                     },
-//                     lsp::TextEdit {
-//                         range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
-//                         new_text: "\n".into(),
-//                     },
-//                 ]))
-//             }
-//         }
-//     });
+    cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
+    cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
+
+    cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
+    cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
+
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
+
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
+}
+
+#[gpui::test]
+async fn test_select_previous(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    {
+        // `Select previous` without a selection (selects wordwise)
+        let mut cx = EditorTestContext::new(cx).await;
+        cx.set_state("abc\nˇabc abc\ndefabc\nabc");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
+
+        cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
+        cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
+
+        cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
+        cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
+    }
+    {
+        // `Select previous` with a selection
+        let mut cx = EditorTestContext::new(cx).await;
+        cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
+
+        cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
+        cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
+
+        cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
+        cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
+
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
+        cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
+    }
+}
 
-//     // After formatting the buffer, the trailing whitespace is stripped,
-//     // a newline is appended, and the edits provided by the language server
-//     // have been applied.
-//     format.await.unwrap();
-//     cx.assert_editor_state(
-//         &[
-//             "one",   //
-//             "",      //
-//             "twoˇ",  //
-//             "",      //
-//             "three", //
-//             "four",  //
-//             "",      //
-//         ]
-//         .join("\n"),
-//     );
+#[gpui::test]
+async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
 
-//     // Undoing the formatting undoes the trailing whitespace removal, the
-//     // trailing newline, and the LSP edits.
-//     cx.update_buffer(|buffer, cx| buffer.undo(cx));
-//     cx.assert_editor_state(
-//         &[
-//             "one ",   //
-//             "twoˇ",   //
-//             "three ", //
-//             "four",   //
-//         ]
-//         .join("\n"),
-//     );
-// }
+    let language = Arc::new(Language::new(
+        LanguageConfig::default(),
+        Some(tree_sitter_rust::language()),
+    ));
 
-// #[gpui::test]
-// async fn test_completion(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+    let text = r#"
+        use mod1::mod2::{mod3, mod4};
 
-//     let mut cx = EditorLspTestContext::new_rust(
-//         lsp::ServerCapabilities {
-//             completion_provider: Some(lsp::CompletionOptions {
-//                 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
-//                 resolve_provider: Some(true),
-//                 ..Default::default()
-//             }),
-//             ..Default::default()
-//         },
-//         cx,
-//     )
-//     .await;
+        fn fn_1(param1: bool, param2: &str) {
+            let var1 = "text";
+        }
+    "#
+    .unindent();
 
-//     cx.set_state(indoc! {"
-//         oneˇ
-//         two
-//         three
-//     "});
-//     cx.simulate_keystroke(".");
-//     handle_completion_request(
-//         &mut cx,
-//         indoc! {"
-//             one.|<>
-//             two
-//             three
-//         "},
-//         vec!["first_completion", "second_completion"],
-//     )
-//     .await;
-//     cx.condition(|editor, _| editor.context_menu_visible())
-//         .await;
-//     let apply_additional_edits = cx.update_editor(|editor, cx| {
-//         editor.context_menu_next(&Default::default(), cx);
-//         editor
-//             .confirm_completion(&ConfirmCompletion::default(), cx)
-//             .unwrap()
-//     });
-//     cx.assert_editor_state(indoc! {"
-//         one.second_completionˇ
-//         two
-//         three
-//     "});
+    let buffer = cx.build_model(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+    });
+    let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
+
+    view.condition::<crate::Event>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+        .await;
+
+    view.update(&mut cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
+                DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
+                DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
+            ]);
+        });
+        view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(&mut cx, |view, cx| { view.selections.display_ranges(cx) }),
+        &[
+            DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
+            DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
+            DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
+        ]
+    );
+
+    view.update(&mut cx, |view, cx| {
+        view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        &[
+            DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
+            DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
+        ]
+    );
+
+    view.update(&mut cx, |view, cx| {
+        view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
+    );
+
+    // Trying to expand the selected syntax node one more time has no effect.
+    view.update(&mut cx, |view, cx| {
+        view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
+    );
 
-//     handle_resolve_completion_request(
-//         &mut cx,
-//         Some(vec![
-//             (
-//                 //This overlaps with the primary completion edit which is
-//                 //misbehavior from the LSP spec, test that we filter it out
-//                 indoc! {"
-//                     one.second_ˇcompletion
-//                     two
-//                     threeˇ
-//                 "},
-//                 "overlapping additional edit",
-//             ),
-//             (
-//                 indoc! {"
-//                     one.second_completion
-//                     two
-//                     threeˇ
-//                 "},
-//                 "\nadditional edit",
-//             ),
-//         ]),
-//     )
-//     .await;
-//     apply_additional_edits.await.unwrap();
-//     cx.assert_editor_state(indoc! {"
-//         one.second_completionˇ
-//         two
-//         three
-//         additional edit
-//     "});
+    view.update(&mut cx, |view, cx| {
+        view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        &[
+            DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
+            DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
+        ]
+    );
+
+    view.update(&mut cx, |view, cx| {
+        view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        &[
+            DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
+            DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
+            DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
+        ]
+    );
+
+    view.update(&mut cx, |view, cx| {
+        view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        &[
+            DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
+            DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
+            DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
+        ]
+    );
+
+    // Trying to shrink the selected syntax node one more time has no effect.
+    view.update(&mut cx, |view, cx| {
+        view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        &[
+            DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
+            DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
+            DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
+        ]
+    );
+
+    // Ensure that we keep expanding the selection if the larger selection starts or ends within
+    // a fold.
+    view.update(&mut cx, |view, cx| {
+        view.fold_ranges(
+            vec![
+                Point::new(0, 21)..Point::new(0, 24),
+                Point::new(3, 20)..Point::new(3, 22),
+            ],
+            true,
+            cx,
+        );
+        view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+    });
+    assert_eq!(
+        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        &[
+            DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
+            DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
+            DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23),
+        ]
+    );
+}
 
-//     cx.set_state(indoc! {"
-//         one.second_completion
-//         twoˇ
-//         threeˇ
-//         additional edit
-//     "});
-//     cx.simulate_keystroke(" ");
-//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
-//     cx.simulate_keystroke("s");
-//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+#[gpui::test]
+async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "{".to_string(),
+                            end: "}".to_string(),
+                            close: false,
+                            newline: true,
+                        },
+                        BracketPair {
+                            start: "(".to_string(),
+                            end: ")".to_string(),
+                            close: false,
+                            newline: true,
+                        },
+                    ],
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query(
+            r#"
+                (_ "(" ")" @end) @indent
+                (_ "{" "}" @end) @indent
+            "#,
+        )
+        .unwrap(),
+    );
+
+    let text = "fn a() {}";
+
+    let buffer = cx.build_model(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+    });
+    let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
+    let cx = &mut cx;
+    editor
+        .condition::<crate::Event>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
+        .await;
+
+    editor.update(cx, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
+        editor.newline(&Newline, cx);
+        assert_eq!(editor.text(cx), "fn a(\n    \n) {\n    \n}\n");
+        assert_eq!(
+            editor.selections.ranges(cx),
+            &[
+                Point::new(1, 4)..Point::new(1, 4),
+                Point::new(3, 4)..Point::new(3, 4),
+                Point::new(5, 0)..Point::new(5, 0)
+            ]
+        );
+    });
+}
 
-//     cx.assert_editor_state(indoc! {"
-//         one.second_completion
-//         two sˇ
-//         three sˇ
-//         additional edit
-//     "});
-//     handle_completion_request(
-//         &mut cx,
-//         indoc! {"
-//             one.second_completion
-//             two s
-//             three <s|>
-//             additional edit
-//         "},
-//         vec!["fourth_completion", "fifth_completion", "sixth_completion"],
-//     )
-//     .await;
-//     cx.condition(|editor, _| editor.context_menu_visible())
-//         .await;
+#[gpui::test]
+async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            brackets: BracketPairConfig {
+                pairs: vec![
+                    BracketPair {
+                        start: "{".to_string(),
+                        end: "}".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "(".to_string(),
+                        end: ")".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "/*".to_string(),
+                        end: " */".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "[".to_string(),
+                        end: "]".to_string(),
+                        close: false,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "\"".to_string(),
+                        end: "\"".to_string(),
+                        close: true,
+                        newline: false,
+                    },
+                ],
+                ..Default::default()
+            },
+            autoclose_before: "})]".to_string(),
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ));
+
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(language.clone());
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(registry);
+        buffer.set_language(Some(language), cx);
+    });
 
-//     cx.simulate_keystroke("i");
+    cx.set_state(
+        &r#"
+            🏀ˇ
+            εˇ
+            ❤️ˇ
+        "#
+        .unindent(),
+    );
+
+    // autoclose multiple nested brackets at multiple cursors
+    cx.update_editor(|view, cx| {
+        view.handle_input("{", cx);
+        view.handle_input("{", cx);
+        view.handle_input("{", cx);
+    });
+    cx.assert_editor_state(
+        &"
+            🏀{{{ˇ}}}
+            ε{{{ˇ}}}
+            ❤️{{{ˇ}}}
+        "
+        .unindent(),
+    );
+
+    // insert a different closing bracket
+    cx.update_editor(|view, cx| {
+        view.handle_input(")", cx);
+    });
+    cx.assert_editor_state(
+        &"
+            🏀{{{)ˇ}}}
+            ε{{{)ˇ}}}
+            ❤️{{{)ˇ}}}
+        "
+        .unindent(),
+    );
+
+    // skip over the auto-closed brackets when typing a closing bracket
+    cx.update_editor(|view, cx| {
+        view.move_right(&MoveRight, cx);
+        view.handle_input("}", cx);
+        view.handle_input("}", cx);
+        view.handle_input("}", cx);
+    });
+    cx.assert_editor_state(
+        &"
+            🏀{{{)}}}}ˇ
+            ε{{{)}}}}ˇ
+            ❤️{{{)}}}}ˇ
+        "
+        .unindent(),
+    );
+
+    // autoclose multi-character pairs
+    cx.set_state(
+        &"
+            ˇ
+            ˇ
+        "
+        .unindent(),
+    );
+    cx.update_editor(|view, cx| {
+        view.handle_input("/", cx);
+        view.handle_input("*", cx);
+    });
+    cx.assert_editor_state(
+        &"
+            /*ˇ */
+            /*ˇ */
+        "
+        .unindent(),
+    );
+
+    // one cursor autocloses a multi-character pair, one cursor
+    // does not autoclose.
+    cx.set_state(
+        &"
+            /ˇ
+            ˇ
+        "
+        .unindent(),
+    );
+    cx.update_editor(|view, cx| view.handle_input("*", cx));
+    cx.assert_editor_state(
+        &"
+            /*ˇ */
+            *ˇ
+        "
+        .unindent(),
+    );
+
+    // Don't autoclose if the next character isn't whitespace and isn't
+    // listed in the language's "autoclose_before" section.
+    cx.set_state("ˇa b");
+    cx.update_editor(|view, cx| view.handle_input("{", cx));
+    cx.assert_editor_state("{ˇa b");
+
+    // Don't autoclose if `close` is false for the bracket pair
+    cx.set_state("ˇ");
+    cx.update_editor(|view, cx| view.handle_input("[", cx));
+    cx.assert_editor_state("[ˇ");
+
+    // Surround with brackets if text is selected
+    cx.set_state("«aˇ» b");
+    cx.update_editor(|view, cx| view.handle_input("{", cx));
+    cx.assert_editor_state("{«aˇ»} b");
+
+    // Autclose pair where the start and end characters are the same
+    cx.set_state("aˇ");
+    cx.update_editor(|view, cx| view.handle_input("\"", cx));
+    cx.assert_editor_state("a\"ˇ\"");
+    cx.update_editor(|view, cx| view.handle_input("\"", cx));
+    cx.assert_editor_state("a\"\"ˇ");
+}
 
-//     handle_completion_request(
-//         &mut cx,
-//         indoc! {"
-//             one.second_completion
-//             two si
-//             three <si|>
-//             additional edit
-//         "},
-//         vec!["fourth_completion", "fifth_completion", "sixth_completion"],
-//     )
-//     .await;
-//     cx.condition(|editor, _| editor.context_menu_visible())
-//         .await;
+#[gpui::test]
+async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let html_language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "HTML".into(),
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "<".into(),
+                            end: ">".into(),
+                            close: true,
+                            ..Default::default()
+                        },
+                        BracketPair {
+                            start: "{".into(),
+                            end: "}".into(),
+                            close: true,
+                            ..Default::default()
+                        },
+                        BracketPair {
+                            start: "(".into(),
+                            end: ")".into(),
+                            close: true,
+                            ..Default::default()
+                        },
+                    ],
+                    ..Default::default()
+                },
+                autoclose_before: "})]>".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_html::language()),
+        )
+        .with_injection_query(
+            r#"
+            (script_element
+                (raw_text) @content
+                (#set! "language" "javascript"))
+            "#,
+        )
+        .unwrap(),
+    );
+
+    let javascript_language = Arc::new(Language::new(
+        LanguageConfig {
+            name: "JavaScript".into(),
+            brackets: BracketPairConfig {
+                pairs: vec![
+                    BracketPair {
+                        start: "/*".into(),
+                        end: " */".into(),
+                        close: true,
+                        ..Default::default()
+                    },
+                    BracketPair {
+                        start: "{".into(),
+                        end: "}".into(),
+                        close: true,
+                        ..Default::default()
+                    },
+                    BracketPair {
+                        start: "(".into(),
+                        end: ")".into(),
+                        close: true,
+                        ..Default::default()
+                    },
+                ],
+                ..Default::default()
+            },
+            autoclose_before: "})]>".into(),
+            ..Default::default()
+        },
+        Some(tree_sitter_typescript::language_tsx()),
+    ));
+
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(html_language.clone());
+    registry.add(javascript_language.clone());
+
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(registry);
+        buffer.set_language(Some(html_language), cx);
+    });
 
-//     let apply_additional_edits = cx.update_editor(|editor, cx| {
-//         editor
-//             .confirm_completion(&ConfirmCompletion::default(), cx)
-//             .unwrap()
-//     });
-//     cx.assert_editor_state(indoc! {"
-//         one.second_completion
-//         two sixth_completionˇ
-//         three sixth_completionˇ
-//         additional edit
-//     "});
+    cx.set_state(
+        &r#"
+            <body>ˇ
+                <script>
+                    var x = 1;ˇ
+                </script>
+            </body>ˇ
+        "#
+        .unindent(),
+    );
+
+    // Precondition: different languages are active at different locations.
+    cx.update_editor(|editor, cx| {
+        let snapshot = editor.snapshot(cx);
+        let cursors = editor.selections.ranges::<usize>(cx);
+        let languages = cursors
+            .iter()
+            .map(|c| snapshot.language_at(c.start).unwrap().name())
+            .collect::<Vec<_>>();
+        assert_eq!(
+            languages,
+            &["HTML".into(), "JavaScript".into(), "HTML".into()]
+        );
+    });
 
-//     handle_resolve_completion_request(&mut cx, None).await;
-//     apply_additional_edits.await.unwrap();
+    // Angle brackets autoclose in HTML, but not JavaScript.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("<", cx);
+        editor.handle_input("a", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body><aˇ>
+                <script>
+                    var x = 1;<aˇ
+                </script>
+            </body><aˇ>
+        "#
+        .unindent(),
+    );
+
+    // Curly braces and parens autoclose in both HTML and JavaScript.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input(" b=", cx);
+        editor.handle_input("{", cx);
+        editor.handle_input("c", cx);
+        editor.handle_input("(", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body><a b={c(ˇ)}>
+                <script>
+                    var x = 1;<a b={c(ˇ)}
+                </script>
+            </body><a b={c(ˇ)}>
+        "#
+        .unindent(),
+    );
+
+    // Brackets that were already autoclosed are skipped.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input(")", cx);
+        editor.handle_input("d", cx);
+        editor.handle_input("}", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body><a b={c()d}ˇ>
+                <script>
+                    var x = 1;<a b={c()d}ˇ
+                </script>
+            </body><a b={c()d}ˇ>
+        "#
+        .unindent(),
+    );
+    cx.update_editor(|editor, cx| {
+        editor.handle_input(">", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body><a b={c()d}>ˇ
+                <script>
+                    var x = 1;<a b={c()d}>ˇ
+                </script>
+            </body><a b={c()d}>ˇ
+        "#
+        .unindent(),
+    );
+
+    // Reset
+    cx.set_state(
+        &r#"
+            <body>ˇ
+                <script>
+                    var x = 1;ˇ
+                </script>
+            </body>ˇ
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("<", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body><ˇ>
+                <script>
+                    var x = 1;<ˇ
+                </script>
+            </body><ˇ>
+        "#
+        .unindent(),
+    );
+
+    // When backspacing, the closing angle brackets are removed.
+    cx.update_editor(|editor, cx| {
+        editor.backspace(&Backspace, cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body>ˇ
+                <script>
+                    var x = 1;ˇ
+                </script>
+            </body>ˇ
+        "#
+        .unindent(),
+    );
+
+    // Block comments autoclose in JavaScript, but not HTML.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("/", cx);
+        editor.handle_input("*", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            <body>/*ˇ
+                <script>
+                    var x = 1;/*ˇ */
+                </script>
+            </body>/*ˇ
+        "#
+        .unindent(),
+    );
+}
 
-//     cx.update(|cx| {
-//         cx.update_global::<SettingsStore, _, _>(|settings, cx| {
-//             settings.update_user_settings::<EditorSettings>(cx, |settings| {
-//                 settings.show_completions_on_input = Some(false);
-//             });
-//         })
-//     });
-//     cx.set_state("editorˇ");
-//     cx.simulate_keystroke(".");
-//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
-//     cx.simulate_keystroke("c");
-//     cx.simulate_keystroke("l");
-//     cx.simulate_keystroke("o");
-//     cx.assert_editor_state("editor.cloˇ");
-//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
-//     cx.update_editor(|editor, cx| {
-//         editor.show_completions(&ShowCompletions, cx);
-//     });
-//     handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
-//     cx.condition(|editor, _| editor.context_menu_visible())
-//         .await;
-//     let apply_additional_edits = cx.update_editor(|editor, cx| {
-//         editor
-//             .confirm_completion(&ConfirmCompletion::default(), cx)
-//             .unwrap()
-//     });
-//     cx.assert_editor_state("editor.closeˇ");
-//     handle_resolve_completion_request(&mut cx, None).await;
-//     apply_additional_edits.await.unwrap();
-// }
+#[gpui::test]
+async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let rust_language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                brackets: serde_json::from_value(json!([
+                    { "start": "{", "end": "}", "close": true, "newline": true },
+                    { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
+                ]))
+                .unwrap(),
+                autoclose_before: "})]>".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_override_query("(string_literal) @string")
+        .unwrap(),
+    );
+
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(rust_language.clone());
+
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(registry);
+        buffer.set_language(Some(rust_language), cx);
+    });
 
-// #[gpui::test]
-// async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-//     let mut cx = EditorTestContext::new(cx).await;
-//     let language = Arc::new(Language::new(
-//         LanguageConfig {
-//             line_comment: Some("// ".into()),
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     ));
-//     cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+    cx.set_state(
+        &r#"
+            let x = ˇ
+        "#
+        .unindent(),
+    );
 
-//     // If multiple selections intersect a line, the line is only toggled once.
-//     cx.set_state(indoc! {"
-//         fn a() {
-//             «//b();
-//             ˇ»// «c();
-//             //ˇ»  d();
-//         }
-//     "});
+    // Inserting a quotation mark. A closing quotation mark is automatically inserted.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("\"", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            let x = "ˇ"
+        "#
+        .unindent(),
+    );
+
+    // Inserting another quotation mark. The cursor moves across the existing
+    // automatically-inserted quotation mark.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("\"", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            let x = ""ˇ
+        "#
+        .unindent(),
+    );
+
+    // Reset
+    cx.set_state(
+        &r#"
+            let x = ˇ
+        "#
+        .unindent(),
+    );
+
+    // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("\"", cx);
+        editor.handle_input(" ", cx);
+        editor.move_left(&Default::default(), cx);
+        editor.handle_input("\\", cx);
+        editor.handle_input("\"", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            let x = "\"ˇ "
+        "#
+        .unindent(),
+    );
+
+    // Inserting a closing quotation mark at the position of an automatically-inserted quotation
+    // mark. Nothing is inserted.
+    cx.update_editor(|editor, cx| {
+        editor.move_right(&Default::default(), cx);
+        editor.handle_input("\"", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            let x = "\" "ˇ
+        "#
+        .unindent(),
+    );
+}
 
-//     cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+#[gpui::test]
+async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            brackets: BracketPairConfig {
+                pairs: vec![
+                    BracketPair {
+                        start: "{".to_string(),
+                        end: "}".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "/* ".to_string(),
+                        end: "*/".to_string(),
+                        close: true,
+                        ..Default::default()
+                    },
+                ],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ));
+
+    let text = r#"
+        a
+        b
+        c
+    "#
+    .unindent();
+
+    let buffer = cx.build_model(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+    });
+    let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
+    let cx = &mut cx;
+    view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+        .await;
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1),
+            ])
+        });
 
-//     cx.assert_editor_state(indoc! {"
-//         fn a() {
-//             «b();
-//             c();
-//             ˇ» d();
-//         }
-//     "});
+        view.handle_input("{", cx);
+        view.handle_input("{", cx);
+        view.handle_input("{", cx);
+        assert_eq!(
+            view.text(cx),
+            "
+                {{{a}}}
+                {{{b}}}
+                {{{c}}}
+            "
+            .unindent()
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4),
+                DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4),
+                DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4)
+            ]
+        );
+
+        view.undo(&Undo, cx);
+        view.undo(&Undo, cx);
+        view.undo(&Undo, cx);
+        assert_eq!(
+            view.text(cx),
+            "
+                a
+                b
+                c
+            "
+            .unindent()
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
+            ]
+        );
+
+        // Ensure inserting the first character of a multi-byte bracket pair
+        // doesn't surround the selections with the bracket.
+        view.handle_input("/", cx);
+        assert_eq!(
+            view.text(cx),
+            "
+                /
+                /
+                /
+            "
+            .unindent()
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
+            ]
+        );
+
+        view.undo(&Undo, cx);
+        assert_eq!(
+            view.text(cx),
+            "
+                a
+                b
+                c
+            "
+            .unindent()
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
+            ]
+        );
+
+        // Ensure inserting the last character of a multi-byte bracket pair
+        // doesn't surround the selections with the bracket.
+        view.handle_input("*", cx);
+        assert_eq!(
+            view.text(cx),
+            "
+                *
+                *
+                *
+            "
+            .unindent()
+        );
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            [
+                DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+                DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
+                DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
+            ]
+        );
+    });
+}
 
-//     // The comment prefix is inserted at the same column for every line in a
-//     // selection.
-//     cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+#[gpui::test]
+async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            brackets: BracketPairConfig {
+                pairs: vec![BracketPair {
+                    start: "{".to_string(),
+                    end: "}".to_string(),
+                    close: true,
+                    newline: true,
+                }],
+                ..Default::default()
+            },
+            autoclose_before: "}".to_string(),
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ));
+
+    let text = r#"
+        a
+        b
+        c
+    "#
+    .unindent();
+
+    let buffer = cx.build_model(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+    });
+    let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
+    let cx = &mut cx;
+    editor
+        .condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+        .await;
+
+    editor.update(cx, |editor, cx| {
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([
+                Point::new(0, 1)..Point::new(0, 1),
+                Point::new(1, 1)..Point::new(1, 1),
+                Point::new(2, 1)..Point::new(2, 1),
+            ])
+        });
 
-//     cx.assert_editor_state(indoc! {"
-//         fn a() {
-//             // «b();
-//             // c();
-//             ˇ»//  d();
-//         }
-//     "});
+        editor.handle_input("{", cx);
+        editor.handle_input("{", cx);
+        editor.handle_input("_", cx);
+        assert_eq!(
+            editor.text(cx),
+            "
+                a{{_}}
+                b{{_}}
+                c{{_}}
+            "
+            .unindent()
+        );
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [
+                Point::new(0, 4)..Point::new(0, 4),
+                Point::new(1, 4)..Point::new(1, 4),
+                Point::new(2, 4)..Point::new(2, 4)
+            ]
+        );
+
+        editor.backspace(&Default::default(), cx);
+        editor.backspace(&Default::default(), cx);
+        assert_eq!(
+            editor.text(cx),
+            "
+                a{}
+                b{}
+                c{}
+            "
+            .unindent()
+        );
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [
+                Point::new(0, 2)..Point::new(0, 2),
+                Point::new(1, 2)..Point::new(1, 2),
+                Point::new(2, 2)..Point::new(2, 2)
+            ]
+        );
+
+        editor.delete_to_previous_word_start(&Default::default(), cx);
+        assert_eq!(
+            editor.text(cx),
+            "
+                a
+                b
+                c
+            "
+            .unindent()
+        );
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [
+                Point::new(0, 1)..Point::new(0, 1),
+                Point::new(1, 1)..Point::new(1, 1),
+                Point::new(2, 1)..Point::new(2, 1)
+            ]
+        );
+    });
+}
 
-//     // If a selection ends at the beginning of a line, that line is not toggled.
-//     cx.set_selections_state(indoc! {"
-//         fn a() {
-//             // b();
-//             «// c();
-//         ˇ»    //  d();
-//         }
-//     "});
+// todo!(select_anchor_ranges)
+// #[gpui::test]
+// async fn test_snippets(cx: &mut gpui::TestAppContext) {
+//     init_test(cx, |_| {});
 
-//     cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+//     let (text, insertion_ranges) = marked_text_ranges(
+//         indoc! {"
+//             a.ˇ b
+//             a.ˇ b
+//             a.ˇ b
+//         "},
+//         false,
+//     );
 
-//     cx.assert_editor_state(indoc! {"
-//         fn a() {
-//             // b();
-//             «c();
-//         ˇ»    //  d();
-//         }
-//     "});
+//     let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
+//     let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
+//     let cx = &mut cx;
 
-//     // If a selection span a single line and is empty, the line is toggled.
-//     cx.set_state(indoc! {"
-//         fn a() {
-//             a();
-//             b();
-//         ˇ
-//         }
-//     "});
+//     editor.update(cx, |editor, cx| {
+//         let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
 
-//     cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+//         editor
+//             .insert_snippet(&insertion_ranges, snippet, cx)
+//             .unwrap();
 
-//     cx.assert_editor_state(indoc! {"
-//         fn a() {
-//             a();
-//             b();
-//         //•ˇ
+//         fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
+//             let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
+//             assert_eq!(editor.text(cx), expected_text);
+//             assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
 //         }
-//     "});
 
-//     // If a selection span multiple lines, empty lines are not toggled.
-//     cx.set_state(indoc! {"
-//         fn a() {
-//             «a();
+//         assert(
+//             editor,
+//             cx,
+//             indoc! {"
+//                 a.f(«one», two, «three») b
+//                 a.f(«one», two, «three») b
+//                 a.f(«one», two, «three») b
+//             "},
+//         );
 
-//             c();ˇ»
-//         }
-//     "});
+//         // Can't move earlier than the first tab stop
+//         assert!(!editor.move_to_prev_snippet_tabstop(cx));
+//         assert(
+//             editor,
+//             cx,
+//             indoc! {"
+//                 a.f(«one», two, «three») b
+//                 a.f(«one», two, «three») b
+//                 a.f(«one», two, «three») b
+//             "},
+//         );
 
-//     cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+//         assert!(editor.move_to_next_snippet_tabstop(cx));
+//         assert(
+//             editor,
+//             cx,
+//             indoc! {"
+//                 a.f(one, «two», three) b
+//                 a.f(one, «two», three) b
+//                 a.f(one, «two», three) b
+//             "},
+//         );
 
-//     cx.assert_editor_state(indoc! {"
-//         fn a() {
-//             // «a();
+//         editor.move_to_prev_snippet_tabstop(cx);
+//         assert(
+//             editor,
+//             cx,
+//             indoc! {"
+//                 a.f(«one», two, «three») b
+//                 a.f(«one», two, «three») b
+//                 a.f(«one», two, «three») b
+//             "},
+//         );
 
-//             // c();ˇ»
-//         }
-//     "});
+//         assert!(editor.move_to_next_snippet_tabstop(cx));
+//         assert(
+//             editor,
+//             cx,
+//             indoc! {"
+//                 a.f(one, «two», three) b
+//                 a.f(one, «two», three) b
+//                 a.f(one, «two», three) b
+//             "},
+//         );
+//         assert!(editor.move_to_next_snippet_tabstop(cx));
+//         assert(
+//             editor,
+//             cx,
+//             indoc! {"
+//                 a.f(one, two, three)ˇ b
+//                 a.f(one, two, three)ˇ b
+//                 a.f(one, two, three)ˇ b
+//             "},
+//         );
+
+//         // As soon as the last tab stop is reached, snippet state is gone
+//         editor.move_to_prev_snippet_tabstop(cx);
+//         assert(
+//             editor,
+//             cx,
+//             indoc! {"
+//                 a.f(one, two, three)ˇ b
+//                 a.f(one, two, three)ˇ b
+//                 a.f(one, two, three)ˇ b
+//             "},
+//         );
+//     });
 // }
 
-// #[gpui::test]
-// async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                document_formatting_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_file("/file.rs", Default::default()).await;
+
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+        .await
+        .unwrap();
+
+    cx.executor().start_waiting();
+    let fake_server = fake_servers.next().await.unwrap();
+
+    let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
+    let cx = &mut cx;
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+    assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+    let save = editor
+        .update(cx, |editor, cx| editor.save(project.clone(), cx))
+        .unwrap();
+    fake_server
+        .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            assert_eq!(params.options.tab_size, 4);
+            Ok(Some(vec![lsp::TextEdit::new(
+                lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+                ", ".to_string(),
+            )]))
+        })
+        .next()
+        .await;
+    cx.executor().start_waiting();
+    let x = save.await;
+
+    assert_eq!(
+        editor.update(cx, |editor, cx| editor.text(cx)),
+        "one, two\nthree\n"
+    );
+    assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+    assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+    // Ensure we can still save even if formatting hangs.
+    fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+        assert_eq!(
+            params.text_document.uri,
+            lsp::Url::from_file_path("/file.rs").unwrap()
+        );
+        futures::future::pending::<()>().await;
+        unreachable!()
+    });
+    let save = editor
+        .update(cx, |editor, cx| editor.save(project.clone(), cx))
+        .unwrap();
+    cx.executor().advance_clock(super::FORMAT_TIMEOUT);
+    cx.executor().start_waiting();
+    save.await;
+    assert_eq!(
+        editor.update(cx, |editor, cx| editor.text(cx)),
+        "one\ntwo\nthree\n"
+    );
+    assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+    // Set rust language override and assert overridden tabsize is sent to language server
+    update_test_language_settings(cx, |settings| {
+        settings.languages.insert(
+            "Rust".into(),
+            LanguageSettingsContent {
+                tab_size: NonZeroU32::new(8),
+                ..Default::default()
+            },
+        );
+    });
 
-//     let language = Arc::new(Language::new(
-//         LanguageConfig {
-//             line_comment: Some("// ".into()),
-//             ..Default::default()
-//         },
-//         Some(tree_sitter_rust::language()),
-//     ));
+    let save = editor
+        .update(cx, |editor, cx| editor.save(project.clone(), cx))
+        .unwrap();
+    fake_server
+        .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            assert_eq!(params.options.tab_size, 8);
+            Ok(Some(vec![]))
+        })
+        .next()
+        .await;
+    cx.executor().start_waiting();
+    save.await;
+}
 
-//     let registry = Arc::new(LanguageRegistry::test());
-//     registry.add(language.clone());
+#[gpui::test]
+async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_file("/file.rs", Default::default()).await;
+
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+        .await
+        .unwrap();
+
+    cx.executor().start_waiting();
+    let fake_server = fake_servers.next().await.unwrap();
+
+    let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
+    let cx = &mut cx;
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+    assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+    let save = editor
+        .update(cx, |editor, cx| editor.save(project.clone(), cx))
+        .unwrap();
+    fake_server
+        .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            assert_eq!(params.options.tab_size, 4);
+            Ok(Some(vec![lsp::TextEdit::new(
+                lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+                ", ".to_string(),
+            )]))
+        })
+        .next()
+        .await;
+    cx.executor().start_waiting();
+    save.await;
+    assert_eq!(
+        editor.update(cx, |editor, cx| editor.text(cx)),
+        "one, two\nthree\n"
+    );
+    assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+    assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+    // Ensure we can still save even if formatting hangs.
+    fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
+        move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            futures::future::pending::<()>().await;
+            unreachable!()
+        },
+    );
+    let save = editor
+        .update(cx, |editor, cx| editor.save(project.clone(), cx))
+        .unwrap();
+    cx.executor().advance_clock(super::FORMAT_TIMEOUT);
+    cx.executor().start_waiting();
+    save.await;
+    assert_eq!(
+        editor.update(cx, |editor, cx| editor.text(cx)),
+        "one\ntwo\nthree\n"
+    );
+    assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+    // Set rust language override and assert overridden tabsize is sent to language server
+    update_test_language_settings(cx, |settings| {
+        settings.languages.insert(
+            "Rust".into(),
+            LanguageSettingsContent {
+                tab_size: NonZeroU32::new(8),
+                ..Default::default()
+            },
+        );
+    });
 
-//     let mut cx = EditorTestContext::new(cx).await;
-//     cx.update_buffer(|buffer, cx| {
-//         buffer.set_language_registry(registry);
-//         buffer.set_language(Some(language), cx);
-//     });
+    let save = editor
+        .update(cx, |editor, cx| editor.save(project.clone(), cx))
+        .unwrap();
+    fake_server
+        .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            assert_eq!(params.options.tab_size, 8);
+            Ok(Some(vec![]))
+        })
+        .next()
+        .await;
+    cx.executor().start_waiting();
+    save.await;
+}
 
-//     let toggle_comments = &ToggleComments {
-//         advance_downwards: true,
-//     };
+#[gpui::test]
+async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
+    });
 
-//     // Single cursor on one line -> advance
-//     // Cursor moves horizontally 3 characters as well on non-blank line
-//     cx.set_state(indoc!(
-//         "fn a() {
-//              ˇdog();
-//              cat();
-//         }"
-//     ));
-//     cx.update_editor(|editor, cx| {
-//         editor.toggle_comments(toggle_comments, cx);
-//     });
-//     cx.assert_editor_state(indoc!(
-//         "fn a() {
-//              // dog();
-//              catˇ();
-//         }"
-//     ));
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            // Enable Prettier formatting for the same buffer, and ensure
+            // LSP is called instead of Prettier.
+            prettier_parser_name: Some("test_parser".to_string()),
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                document_formatting_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_file("/file.rs", Default::default()).await;
+
+    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    project.update(cx, |project, _| {
+        project.languages().add(Arc::new(language));
+    });
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+        .await
+        .unwrap();
+
+    cx.executor().start_waiting();
+    let fake_server = fake_servers.next().await.unwrap();
+
+    let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
+    let cx = &mut cx;
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+
+    let format = editor
+        .update(cx, |editor, cx| {
+            editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+        })
+        .unwrap();
+    fake_server
+        .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+            assert_eq!(
+                params.text_document.uri,
+                lsp::Url::from_file_path("/file.rs").unwrap()
+            );
+            assert_eq!(params.options.tab_size, 4);
+            Ok(Some(vec![lsp::TextEdit::new(
+                lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+                ", ".to_string(),
+            )]))
+        })
+        .next()
+        .await;
+    cx.executor().start_waiting();
+    format.await;
+    assert_eq!(
+        editor.update(cx, |editor, cx| editor.text(cx)),
+        "one, two\nthree\n"
+    );
+
+    editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+    // Ensure we don't lock if formatting hangs.
+    fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+        assert_eq!(
+            params.text_document.uri,
+            lsp::Url::from_file_path("/file.rs").unwrap()
+        );
+        futures::future::pending::<()>().await;
+        unreachable!()
+    });
+    let format = editor
+        .update(cx, |editor, cx| {
+            editor.perform_format(project, FormatTrigger::Manual, cx)
+        })
+        .unwrap();
+    cx.executor().advance_clock(super::FORMAT_TIMEOUT);
+    cx.executor().start_waiting();
+    format.await;
+    assert_eq!(
+        editor.update(cx, |editor, cx| editor.text(cx)),
+        "one\ntwo\nthree\n"
+    );
+}
 
-//     // Single selection on one line -> don't advance
-//     cx.set_state(indoc!(
-//         "fn a() {
-//              «dog()ˇ»;
-//              cat();
-//         }"
-//     ));
-//     cx.update_editor(|editor, cx| {
-//         editor.toggle_comments(toggle_comments, cx);
-//     });
-//     cx.assert_editor_state(indoc!(
-//         "fn a() {
-//              // «dog()ˇ»;
-//              cat();
-//         }"
-//     ));
+#[gpui::test]
+async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            document_formatting_provider: Some(lsp::OneOf::Left(true)),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    cx.set_state(indoc! {"
+        one.twoˇ
+    "});
+
+    // The format request takes a long time. When it completes, it inserts
+    // a newline and an indent before the `.`
+    cx.lsp
+        .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
+            let executor = cx.background_executor().clone();
+            async move {
+                executor.timer(Duration::from_millis(100)).await;
+                Ok(Some(vec![lsp::TextEdit {
+                    range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
+                    new_text: "\n    ".into(),
+                }]))
+            }
+        });
 
-//     // Multiple cursors on one line -> advance
-//     cx.set_state(indoc!(
-//         "fn a() {
-//              ˇdˇog();
-//              cat();
-//         }"
-//     ));
-//     cx.update_editor(|editor, cx| {
-//         editor.toggle_comments(toggle_comments, cx);
-//     });
-//     cx.assert_editor_state(indoc!(
-//         "fn a() {
-//              // dog();
-//              catˇ(ˇ);
-//         }"
-//     ));
+    // Submit a format request.
+    let format_1 = cx
+        .update_editor(|editor, cx| editor.format(&Format, cx))
+        .unwrap();
+    cx.executor().run_until_parked();
+
+    // Submit a second format request.
+    let format_2 = cx
+        .update_editor(|editor, cx| editor.format(&Format, cx))
+        .unwrap();
+    cx.executor().run_until_parked();
+
+    // Wait for both format requests to complete
+    cx.executor().advance_clock(Duration::from_millis(200));
+    cx.executor().start_waiting();
+    format_1.await.unwrap();
+    cx.executor().start_waiting();
+    format_2.await.unwrap();
+
+    // The formatting edits only happens once.
+    cx.assert_editor_state(indoc! {"
+        one
+            .twoˇ
+    "});
+}
 
-//     // Multiple cursors on one line, with selection -> don't advance
-//     cx.set_state(indoc!(
-//         "fn a() {
-//              ˇdˇog«()ˇ»;
-//              cat();
-//         }"
-//     ));
-//     cx.update_editor(|editor, cx| {
-//         editor.toggle_comments(toggle_comments, cx);
-//     });
-//     cx.assert_editor_state(indoc!(
-//         "fn a() {
-//              // ˇdˇog«()ˇ»;
-//              cat();
-//         }"
-//     ));
+#[gpui::test]
+async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.formatter = Some(language_settings::Formatter::Auto)
+    });
 
-//     // Single cursor on one line -> advance
-//     // Cursor moves to column 0 on blank line
-//     cx.set_state(indoc!(
-//         "fn a() {
-//              ˇdog();
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            document_formatting_provider: Some(lsp::OneOf::Left(true)),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    // Set up a buffer white some trailing whitespace and no trailing newline.
+    cx.set_state(
+        &[
+            "one ",   //
+            "twoˇ",   //
+            "three ", //
+            "four",   //
+        ]
+        .join("\n"),
+    );
+
+    // Submit a format request.
+    let format = cx
+        .update_editor(|editor, cx| editor.format(&Format, cx))
+        .unwrap();
+
+    // Record which buffer changes have been sent to the language server
+    let buffer_changes = Arc::new(Mutex::new(Vec::new()));
+    cx.lsp
+        .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
+            let buffer_changes = buffer_changes.clone();
+            move |params, _| {
+                buffer_changes.lock().extend(
+                    params
+                        .content_changes
+                        .into_iter()
+                        .map(|e| (e.range.unwrap(), e.text)),
+                );
+            }
+        });
 
-//              cat();
-//         }"
-//     ));
-//     cx.update_editor(|editor, cx| {
-//         editor.toggle_comments(toggle_comments, cx);
-//     });
-//     cx.assert_editor_state(indoc!(
-//         "fn a() {
-//              // dog();
-//         ˇ
-//              cat();
-//         }"
-//     ));
+    // Handle formatting requests to the language server.
+    cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
+        let buffer_changes = buffer_changes.clone();
+        move |_, _| {
+            // When formatting is requested, trailing whitespace has already been stripped,
+            // and the trailing newline has already been added.
+            assert_eq!(
+                &buffer_changes.lock()[1..],
+                &[
+                    (
+                        lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
+                        "".into()
+                    ),
+                    (
+                        lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
+                        "".into()
+                    ),
+                    (
+                        lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
+                        "\n".into()
+                    ),
+                ]
+            );
+
+            // Insert blank lines between each line of the buffer.
+            async move {
+                Ok(Some(vec![
+                    lsp::TextEdit {
+                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
+                        new_text: "\n".into(),
+                    },
+                    lsp::TextEdit {
+                        range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
+                        new_text: "\n".into(),
+                    },
+                ]))
+            }
+        }
+    });
 
-//     // Single cursor on one line -> advance
-//     // Cursor starts and ends at column 0
-//     cx.set_state(indoc!(
-//         "fn a() {
-//          ˇ    dog();
-//              cat();
-//         }"
-//     ));
-//     cx.update_editor(|editor, cx| {
-//         editor.toggle_comments(toggle_comments, cx);
-//     });
-//     cx.assert_editor_state(indoc!(
-//         "fn a() {
-//              // dog();
-//          ˇ    cat();
-//         }"
-//     ));
-// }
+    // After formatting the buffer, the trailing whitespace is stripped,
+    // a newline is appended, and the edits provided by the language server
+    // have been applied.
+    format.await.unwrap();
+    cx.assert_editor_state(
+        &[
+            "one",   //
+            "",      //
+            "twoˇ",  //
+            "",      //
+            "three", //
+            "four",  //
+            "",      //
+        ]
+        .join("\n"),
+    );
+
+    // Undoing the formatting undoes the trailing whitespace removal, the
+    // trailing newline, and the LSP edits.
+    cx.update_buffer(|buffer, cx| buffer.undo(cx));
+    cx.assert_editor_state(
+        &[
+            "one ",   //
+            "twoˇ",   //
+            "three ", //
+            "four",   //
+        ]
+        .join("\n"),
+    );
+}
 
+//todo!(completion)
 // #[gpui::test]
-// async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
+// async fn test_completion(cx: &mut gpui::TestAppContext) {
 //     init_test(cx, |_| {});
 
-//     let mut cx = EditorTestContext::new(cx).await;
-
-//     let html_language = Arc::new(
-//         Language::new(
-//             LanguageConfig {
-//                 name: "HTML".into(),
-//                 block_comment: Some(("<!-- ".into(), " -->".into())),
+//     let mut cx = EditorLspTestContext::new_rust(
+//         lsp::ServerCapabilities {
+//             completion_provider: Some(lsp::CompletionOptions {
+//                 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+//                 resolve_provider: Some(true),
 //                 ..Default::default()
-//             },
-//             Some(tree_sitter_html::language()),
-//         )
-//         .with_injection_query(
-//             r#"
-//             (script_element
-//                 (raw_text) @content
-//                 (#set! "language" "javascript"))
-//             "#,
-//         )
-//         .unwrap(),
-//     );
-
-//     let javascript_language = Arc::new(Language::new(
-//         LanguageConfig {
-//             name: "JavaScript".into(),
-//             line_comment: Some("// ".into()),
+//             }),
 //             ..Default::default()
 //         },
-//         Some(tree_sitter_typescript::language_tsx()),
-//     ));
-
-//     let registry = Arc::new(LanguageRegistry::test());
-//     registry.add(html_language.clone());
-//     registry.add(javascript_language.clone());
+//         cx,
+//     )
+//     .await;
 
-//     cx.update_buffer(|buffer, cx| {
-//         buffer.set_language_registry(registry);
-//         buffer.set_language(Some(html_language), cx);
+//     cx.set_state(indoc! {"
+//         oneˇ
+//         two
+//         three
+//     "});
+//     cx.simulate_keystroke(".");
+//     handle_completion_request(
+//         &mut cx,
+//         indoc! {"
+//             one.|<>
+//             two
+//             three
+//         "},
+//         vec!["first_completion", "second_completion"],
+//     )
+//     .await;
+//     cx.condition(|editor, _| editor.context_menu_visible())
+//         .await;
+//     let apply_additional_edits = cx.update_editor(|editor, cx| {
+//         editor.context_menu_next(&Default::default(), cx);
+//         editor
+//             .confirm_completion(&ConfirmCompletion::default(), cx)
+//             .unwrap()
 //     });
+//     cx.assert_editor_state(indoc! {"
+//         one.second_completionˇ
+//         two
+//         three
+//     "});
 
-//     // Toggle comments for empty selections
-//     cx.set_state(
-//         &r#"
-//             <p>A</p>ˇ
-//             <p>B</p>ˇ
-//             <p>C</p>ˇ
-//         "#
-//         .unindent(),
-//     );
-//     cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
-//     cx.assert_editor_state(
-//         &r#"
-//             <!-- <p>A</p>ˇ -->
-//             <!-- <p>B</p>ˇ -->
-//             <!-- <p>C</p>ˇ -->
-//         "#
-//         .unindent(),
-//     );
-//     cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
-//     cx.assert_editor_state(
-//         &r#"
-//             <p>A</p>ˇ
-//             <p>B</p>ˇ
-//             <p>C</p>ˇ
-//         "#
-//         .unindent(),
-//     );
-
-//     // Toggle comments for mixture of empty and non-empty selections, where
-//     // multiple selections occupy a given line.
-//     cx.set_state(
-//         &r#"
-//             <p>A«</p>
-//             <p>ˇ»B</p>ˇ
-//             <p>C«</p>
-//             <p>ˇ»D</p>ˇ
-//         "#
-//         .unindent(),
-//     );
-
-//     cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
-//     cx.assert_editor_state(
-//         &r#"
-//             <!-- <p>A«</p>
-//             <p>ˇ»B</p>ˇ -->
-//             <!-- <p>C«</p>
-//             <p>ˇ»D</p>ˇ -->
-//         "#
-//         .unindent(),
-//     );
-//     cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
-//     cx.assert_editor_state(
-//         &r#"
-//             <p>A«</p>
-//             <p>ˇ»B</p>ˇ
-//             <p>C«</p>
-//             <p>ˇ»D</p>ˇ
-//         "#
-//         .unindent(),
-//     );
-
-//     // Toggle comments when different languages are active for different
-//     // selections.
-//     cx.set_state(
-//         &r#"
-//             ˇ<script>
-//                 ˇvar x = new Y();
-//             ˇ</script>
-//         "#
-//         .unindent(),
-//     );
-//     cx.foreground().run_until_parked();
-//     cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
-//     cx.assert_editor_state(
-//         &r#"
-//             <!-- ˇ<script> -->
-//                 // ˇvar x = new Y();
-//             <!-- ˇ</script> -->
-//         "#
-//         .unindent(),
-//     );
-// }
-
-// #[gpui::test]
-// fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
+//     handle_resolve_completion_request(
+//         &mut cx,
+//         Some(vec![
+//             (
+//                 //This overlaps with the primary completion edit which is
+//                 //misbehavior from the LSP spec, test that we filter it out
+//                 indoc! {"
+//                     one.second_ˇcompletion
+//                     two
+//                     threeˇ
+//                 "},
+//                 "overlapping additional edit",
+//             ),
+//             (
+//                 indoc! {"
+//                     one.second_completion
+//                     two
+//                     threeˇ
+//                 "},
+//                 "\nadditional edit",
+//             ),
+//         ]),
+//     )
+//     .await;
+//     apply_additional_edits.await.unwrap();
+//     cx.assert_editor_state(indoc! {"
+//         one.second_completionˇ
+//         two
+//         three
+//         additional edit
+//     "});
 
-//     let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a')));
-//     let multibuffer = cx.add_model(|cx| {
-//         let mut multibuffer = MultiBuffer::new(0);
-//         multibuffer.push_excerpts(
-//             buffer.clone(),
-//             [
-//                 ExcerptRange {
-//                     context: Point::new(0, 0)..Point::new(0, 4),
-//                     primary: None,
-//                 },
-//                 ExcerptRange {
-//                     context: Point::new(1, 0)..Point::new(1, 4),
-//                     primary: None,
-//                 },
-//             ],
-//             cx,
-//         );
-//         assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
-//         multibuffer
-//     });
+//     cx.set_state(indoc! {"
+//         one.second_completion
+//         twoˇ
+//         threeˇ
+//         additional edit
+//     "});
+//     cx.simulate_keystroke(" ");
+//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+//     cx.simulate_keystroke("s");
+//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
 
-//     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| {
-//             s.select_ranges([
-//                 Point::new(0, 0)..Point::new(0, 0),
-//                 Point::new(1, 0)..Point::new(1, 0),
-//             ])
-//         });
+//     cx.assert_editor_state(indoc! {"
+//         one.second_completion
+//         two sˇ
+//         three sˇ
+//         additional edit
+//     "});
+//     handle_completion_request(
+//         &mut cx,
+//         indoc! {"
+//             one.second_completion
+//             two s
+//             three <s|>
+//             additional edit
+//         "},
+//         vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+//     )
+//     .await;
+//     cx.condition(|editor, _| editor.context_menu_visible())
+//         .await;
 
-//         view.handle_input("X", cx);
-//         assert_eq!(view.text(cx), "Xaaaa\nXbbbb");
-//         assert_eq!(
-//             view.selections.ranges(cx),
-//             [
-//                 Point::new(0, 1)..Point::new(0, 1),
-//                 Point::new(1, 1)..Point::new(1, 1),
-//             ]
-//         );
+//     cx.simulate_keystroke("i");
 
-//         // Ensure the cursor's head is respected when deleting across an excerpt boundary.
-//         view.change_selections(None, cx, |s| {
-//             s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
-//         });
-//         view.backspace(&Default::default(), cx);
-//         assert_eq!(view.text(cx), "Xa\nbbb");
-//         assert_eq!(
-//             view.selections.ranges(cx),
-//             [Point::new(1, 0)..Point::new(1, 0)]
-//         );
+//     handle_completion_request(
+//         &mut cx,
+//         indoc! {"
+//             one.second_completion
+//             two si
+//             three <si|>
+//             additional edit
+//         "},
+//         vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+//     )
+//     .await;
+//     cx.condition(|editor, _| editor.context_menu_visible())
+//         .await;
 
-//         view.change_selections(None, cx, |s| {
-//             s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
-//         });
-//         view.backspace(&Default::default(), cx);
-//         assert_eq!(view.text(cx), "X\nbb");
-//         assert_eq!(
-//             view.selections.ranges(cx),
-//             [Point::new(0, 1)..Point::new(0, 1)]
-//         );
+//     let apply_additional_edits = cx.update_editor(|editor, cx| {
+//         editor
+//             .confirm_completion(&ConfirmCompletion::default(), cx)
+//             .unwrap()
 //     });
-// }
+//     cx.assert_editor_state(indoc! {"
+//         one.second_completion
+//         two sixth_completionˇ
+//         three sixth_completionˇ
+//         additional edit
+//     "});
 
-// #[gpui::test]
-// fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
+//     handle_resolve_completion_request(&mut cx, None).await;
+//     apply_additional_edits.await.unwrap();
 
-//     let markers = vec![('[', ']').into(), ('(', ')').into()];
-//     let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
-//         indoc! {"
-//             [aaaa
-//             (bbbb]
-//             cccc)",
-//         },
-//         markers.clone(),
-//     );
-//     let excerpt_ranges = markers.into_iter().map(|marker| {
-//         let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
-//         ExcerptRange {
-//             context,
-//             primary: None,
-//         }
+//     cx.update(|cx| {
+//         cx.update_global::<SettingsStore, _, _>(|settings, cx| {
+//             settings.update_user_settings::<EditorSettings>(cx, |settings| {
+//                 settings.show_completions_on_input = Some(false);
+//             });
+//         })
 //     });
-//     let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text));
-//     let multibuffer = cx.add_model(|cx| {
-//         let mut multibuffer = MultiBuffer::new(0);
-//         multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
-//         multibuffer
+//     cx.set_state("editorˇ");
+//     cx.simulate_keystroke(".");
+//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+//     cx.simulate_keystroke("c");
+//     cx.simulate_keystroke("l");
+//     cx.simulate_keystroke("o");
+//     cx.assert_editor_state("editor.cloˇ");
+//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+//     cx.update_editor(|editor, cx| {
+//         editor.show_completions(&ShowCompletions, 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! {"
-//                 aaaa
-//                 bˇbbb
-//                 bˇbbˇb
-//                 cccc"
-//             },
-//             true,
-//         );
-//         assert_eq!(view.text(cx), expected_text);
-//         view.change_selections(None, cx, |s| s.select_ranges(selection_ranges));
-
-//         view.handle_input("X", cx);
-
-//         let (expected_text, expected_selections) = marked_text_ranges(
-//             indoc! {"
-//                 aaaa
-//                 bXˇbbXb
-//                 bXˇbbXˇb
-//                 cccc"
-//             },
-//             false,
-//         );
-//         assert_eq!(view.text(cx), expected_text);
-//         assert_eq!(view.selections.ranges(cx), expected_selections);
-
-//         view.newline(&Newline, cx);
-//         let (expected_text, expected_selections) = marked_text_ranges(
-//             indoc! {"
-//                 aaaa
-//                 bX
-//                 ˇbbX
-//                 b
-//                 bX
-//                 ˇbbX
-//                 ˇb
-//                 cccc"
-//             },
-//             false,
-//         );
-//         assert_eq!(view.text(cx), expected_text);
-//         assert_eq!(view.selections.ranges(cx), expected_selections);
+//     handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
+//     cx.condition(|editor, _| editor.context_menu_visible())
+//         .await;
+//     let apply_additional_edits = cx.update_editor(|editor, cx| {
+//         editor
+//             .confirm_completion(&ConfirmCompletion::default(), cx)
+//             .unwrap()
 //     });
+//     cx.assert_editor_state("editor.closeˇ");
+//     handle_resolve_completion_request(&mut cx, None).await;
+//     apply_additional_edits.await.unwrap();
 // }
 
-// #[gpui::test]
-// fn test_refresh_selections(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            line_comment: Some("// ".into()),
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ));
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+    // If multiple selections intersect a line, the line is only toggled once.
+    cx.set_state(indoc! {"
+        fn a() {
+            «//b();
+            ˇ»// «c();
+            //ˇ»  d();
+        }
+    "});
+
+    cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+    cx.assert_editor_state(indoc! {"
+        fn a() {
+            «b();
+            c();
+            ˇ» d();
+        }
+    "});
+
+    // The comment prefix is inserted at the same column for every line in a
+    // selection.
+    cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+    cx.assert_editor_state(indoc! {"
+        fn a() {
+            // «b();
+            // c();
+            ˇ»//  d();
+        }
+    "});
+
+    // If a selection ends at the beginning of a line, that line is not toggled.
+    cx.set_selections_state(indoc! {"
+        fn a() {
+            // b();
+            «// c();
+        ˇ»    //  d();
+        }
+    "});
+
+    cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+    cx.assert_editor_state(indoc! {"
+        fn a() {
+            // b();
+            «c();
+        ˇ»    //  d();
+        }
+    "});
+
+    // If a selection span a single line and is empty, the line is toggled.
+    cx.set_state(indoc! {"
+        fn a() {
+            a();
+            b();
+        ˇ
+        }
+    "});
+
+    cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+    cx.assert_editor_state(indoc! {"
+        fn a() {
+            a();
+            b();
+        //•ˇ
+        }
+    "});
+
+    // If a selection span multiple lines, empty lines are not toggled.
+    cx.set_state(indoc! {"
+        fn a() {
+            «a();
+
+            c();ˇ»
+        }
+    "});
+
+    cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+    cx.assert_editor_state(indoc! {"
+        fn a() {
+            // «a();
+
+            // c();ˇ»
+        }
+    "});
+}
 
-//     let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a')));
-//     let mut excerpt1_id = None;
-//     let multibuffer = cx.add_model(|cx| {
-//         let mut multibuffer = MultiBuffer::new(0);
-//         excerpt1_id = multibuffer
-//             .push_excerpts(
-//                 buffer.clone(),
-//                 [
-//                     ExcerptRange {
-//                         context: Point::new(0, 0)..Point::new(1, 4),
-//                         primary: None,
-//                     },
-//                     ExcerptRange {
-//                         context: Point::new(1, 0)..Point::new(2, 4),
-//                         primary: None,
-//                     },
-//                 ],
-//                 cx,
-//             )
-//             .into_iter()
-//             .next();
-//         assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
-//         multibuffer
-//     });
+#[gpui::test]
+async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language = Arc::new(Language::new(
+        LanguageConfig {
+            line_comment: Some("// ".into()),
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    ));
+
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(language.clone());
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(registry);
+        buffer.set_language(Some(language), cx);
+    });
 
-//     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);
+    let toggle_comments = &ToggleComments {
+        advance_downwards: true,
+    };
+
+    // Single cursor on one line -> advance
+    // Cursor moves horizontally 3 characters as well on non-blank line
+    cx.set_state(indoc!(
+        "fn a() {
+             ˇdog();
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // dog();
+             catˇ();
+        }"
+    ));
+
+    // Single selection on one line -> don't advance
+    cx.set_state(indoc!(
+        "fn a() {
+             «dog()ˇ»;
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // «dog()ˇ»;
+             cat();
+        }"
+    ));
+
+    // Multiple cursors on one line -> advance
+    cx.set_state(indoc!(
+        "fn a() {
+             ˇdˇog();
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // dog();
+             catˇ(ˇ);
+        }"
+    ));
+
+    // Multiple cursors on one line, with selection -> don't advance
+    cx.set_state(indoc!(
+        "fn a() {
+             ˇdˇog«()ˇ»;
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // ˇdˇog«()ˇ»;
+             cat();
+        }"
+    ));
+
+    // Single cursor on one line -> advance
+    // Cursor moves to column 0 on blank line
+    cx.set_state(indoc!(
+        "fn a() {
+             ˇdog();
+
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // dog();
+        ˇ
+             cat();
+        }"
+    ));
+
+    // Single cursor on one line -> advance
+    // Cursor starts and ends at column 0
+    cx.set_state(indoc!(
+        "fn a() {
+         ˇ    dog();
+             cat();
+        }"
+    ));
+    cx.update_editor(|editor, cx| {
+        editor.toggle_comments(toggle_comments, cx);
+    });
+    cx.assert_editor_state(indoc!(
+        "fn a() {
+             // dog();
+         ˇ    cat();
+        }"
+    ));
+}
 
-//     // Refreshing selections is a no-op when excerpts haven't changed.
-//     editor.update(cx, |editor, cx| {
-//         editor.change_selections(None, cx, |s| s.refresh());
-//         assert_eq!(
-//             editor.selections.ranges(cx),
-//             [
-//                 Point::new(1, 3)..Point::new(1, 3),
-//                 Point::new(2, 1)..Point::new(2, 1),
-//             ]
-//         );
-//     });
+#[gpui::test]
+async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let html_language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "HTML".into(),
+                block_comment: Some(("<!-- ".into(), " -->".into())),
+                ..Default::default()
+            },
+            Some(tree_sitter_html::language()),
+        )
+        .with_injection_query(
+            r#"
+            (script_element
+                (raw_text) @content
+                (#set! "language" "javascript"))
+            "#,
+        )
+        .unwrap(),
+    );
+
+    let javascript_language = Arc::new(Language::new(
+        LanguageConfig {
+            name: "JavaScript".into(),
+            line_comment: Some("// ".into()),
+            ..Default::default()
+        },
+        Some(tree_sitter_typescript::language_tsx()),
+    ));
+
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(html_language.clone());
+    registry.add(javascript_language.clone());
+
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(registry);
+        buffer.set_language(Some(html_language), cx);
+    });
 
-//     multibuffer.update(cx, |multibuffer, cx| {
-//         multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
-//     });
-//     editor.update(cx, |editor, cx| {
-//         // Removing an excerpt causes the first selection to become degenerate.
-//         assert_eq!(
-//             editor.selections.ranges(cx),
-//             [
-//                 Point::new(0, 0)..Point::new(0, 0),
-//                 Point::new(0, 1)..Point::new(0, 1)
-//             ]
-//         );
+    // Toggle comments for empty selections
+    cx.set_state(
+        &r#"
+            <p>A</p>ˇ
+            <p>B</p>ˇ
+            <p>C</p>ˇ
+        "#
+        .unindent(),
+    );
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+            <!-- <p>A</p>ˇ -->
+            <!-- <p>B</p>ˇ -->
+            <!-- <p>C</p>ˇ -->
+        "#
+        .unindent(),
+    );
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+            <p>A</p>ˇ
+            <p>B</p>ˇ
+            <p>C</p>ˇ
+        "#
+        .unindent(),
+    );
+
+    // Toggle comments for mixture of empty and non-empty selections, where
+    // multiple selections occupy a given line.
+    cx.set_state(
+        &r#"
+            <p>A«</p>
+            <p>ˇ»B</p>ˇ
+            <p>C«</p>
+            <p>ˇ»D</p>ˇ
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+            <!-- <p>A«</p>
+            <p>ˇ»B</p>ˇ -->
+            <!-- <p>C«</p>
+            <p>ˇ»D</p>ˇ -->
+        "#
+        .unindent(),
+    );
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+            <p>A«</p>
+            <p>ˇ»B</p>ˇ
+            <p>C«</p>
+            <p>ˇ»D</p>ˇ
+        "#
+        .unindent(),
+    );
+
+    // Toggle comments when different languages are active for different
+    // selections.
+    cx.set_state(
+        &r#"
+            ˇ<script>
+                ˇvar x = new Y();
+            ˇ</script>
+        "#
+        .unindent(),
+    );
+    cx.executor().run_until_parked();
+    cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+    cx.assert_editor_state(
+        &r#"
+            <!-- ˇ<script> -->
+                // ˇvar x = new Y();
+            <!-- ˇ</script> -->
+        "#
+        .unindent(),
+    );
+}
 
-//         // Refreshing selections will relocate the first selection to the original buffer
-//         // location.
-//         editor.change_selections(None, cx, |s| s.refresh());
-//         assert_eq!(
-//             editor.selections.ranges(cx),
-//             [
-//                 Point::new(0, 1)..Point::new(0, 1),
-//                 Point::new(0, 3)..Point::new(0, 3)
-//             ]
-//         );
-//         assert!(editor.selections.pending_anchor().is_some());
-//     });
-// }
+#[gpui::test]
+fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let buffer =
+        cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
+    let multibuffer = cx.build_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        multibuffer.push_excerpts(
+            buffer.clone(),
+            [
+                ExcerptRange {
+                    context: Point::new(0, 0)..Point::new(0, 4),
+                    primary: None,
+                },
+                ExcerptRange {
+                    context: Point::new(1, 0)..Point::new(1, 4),
+                    primary: None,
+                },
+            ],
+            cx,
+        );
+        assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
+        multibuffer
+    });
 
-// #[gpui::test]
-// fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
+    let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
+    let cx = &mut cx;
+    view.update(cx, |view, cx| {
+        assert_eq!(view.text(cx), "aaaa\nbbbb");
+        view.change_selections(None, cx, |s| {
+            s.select_ranges([
+                Point::new(0, 0)..Point::new(0, 0),
+                Point::new(1, 0)..Point::new(1, 0),
+            ])
+        });
 
-//     let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a')));
-//     let mut excerpt1_id = None;
-//     let multibuffer = cx.add_model(|cx| {
-//         let mut multibuffer = MultiBuffer::new(0);
-//         excerpt1_id = multibuffer
-//             .push_excerpts(
-//                 buffer.clone(),
-//                 [
-//                     ExcerptRange {
-//                         context: Point::new(0, 0)..Point::new(1, 4),
-//                         primary: None,
-//                     },
-//                     ExcerptRange {
-//                         context: Point::new(1, 0)..Point::new(2, 4),
-//                         primary: None,
-//                     },
-//                 ],
-//                 cx,
-//             )
-//             .into_iter()
-//             .next();
-//         assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
-//         multibuffer
-//     });
+        view.handle_input("X", cx);
+        assert_eq!(view.text(cx), "Xaaaa\nXbbbb");
+        assert_eq!(
+            view.selections.ranges(cx),
+            [
+                Point::new(0, 1)..Point::new(0, 1),
+                Point::new(1, 1)..Point::new(1, 1),
+            ]
+        );
+
+        // Ensure the cursor's head is respected when deleting across an excerpt boundary.
+        view.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
+        });
+        view.backspace(&Default::default(), cx);
+        assert_eq!(view.text(cx), "Xa\nbbb");
+        assert_eq!(
+            view.selections.ranges(cx),
+            [Point::new(1, 0)..Point::new(1, 0)]
+        );
+
+        view.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
+        });
+        view.backspace(&Default::default(), cx);
+        assert_eq!(view.text(cx), "X\nbb");
+        assert_eq!(
+            view.selections.ranges(cx),
+            [Point::new(0, 1)..Point::new(0, 1)]
+        );
+    });
+}
 
-//     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);
+#[gpui::test]
+fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let markers = vec![('[', ']').into(), ('(', ')').into()];
+    let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
+        indoc! {"
+            [aaaa
+            (bbbb]
+            cccc)",
+        },
+        markers.clone(),
+    );
+    let excerpt_ranges = markers.into_iter().map(|marker| {
+        let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
+        ExcerptRange {
+            context,
+            primary: None,
+        }
+    });
+    let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), initial_text));
+    let multibuffer = cx.build_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
+        multibuffer
+    });
 
-//     multibuffer.update(cx, |multibuffer, cx| {
-//         multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
-//     });
-//     editor.update(cx, |editor, cx| {
-//         assert_eq!(
-//             editor.selections.ranges(cx),
-//             [Point::new(0, 0)..Point::new(0, 0)]
-//         );
+    let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
+    let cx = &mut cx;
+    view.update(cx, |view, cx| {
+        let (expected_text, selection_ranges) = marked_text_ranges(
+            indoc! {"
+                aaaa
+                bˇbbb
+                bˇbbˇb
+                cccc"
+            },
+            true,
+        );
+        assert_eq!(view.text(cx), expected_text);
+        view.change_selections(None, cx, |s| s.select_ranges(selection_ranges));
+
+        view.handle_input("X", cx);
+
+        let (expected_text, expected_selections) = marked_text_ranges(
+            indoc! {"
+                aaaa
+                bXˇbbXb
+                bXˇbbXˇb
+                cccc"
+            },
+            false,
+        );
+        assert_eq!(view.text(cx), expected_text);
+        assert_eq!(view.selections.ranges(cx), expected_selections);
+
+        view.newline(&Newline, cx);
+        let (expected_text, expected_selections) = marked_text_ranges(
+            indoc! {"
+                aaaa
+                bX
+                ˇbbX
+                b
+                bX
+                ˇbbX
+                ˇb
+                cccc"
+            },
+            false,
+        );
+        assert_eq!(view.text(cx), expected_text);
+        assert_eq!(view.selections.ranges(cx), expected_selections);
+    });
+}
 
-//         // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
-//         editor.change_selections(None, cx, |s| s.refresh());
-//         assert_eq!(
-//             editor.selections.ranges(cx),
-//             [Point::new(0, 3)..Point::new(0, 3)]
-//         );
-//         assert!(editor.selections.pending_anchor().is_some());
-//     });
-// }
+#[gpui::test]
+fn test_refresh_selections(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let buffer =
+        cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
+    let mut excerpt1_id = None;
+    let multibuffer = cx.build_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        excerpt1_id = multibuffer
+            .push_excerpts(
+                buffer.clone(),
+                [
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(1, 4),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(1, 0)..Point::new(2, 4),
+                        primary: None,
+                    },
+                ],
+                cx,
+            )
+            .into_iter()
+            .next();
+        assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
+        multibuffer
+    });
 
-// #[gpui::test]
-// async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+    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 language = Arc::new(
-//         Language::new(
-//             LanguageConfig {
-//                 brackets: BracketPairConfig {
-//                     pairs: vec![
-//                         BracketPair {
-//                             start: "{".to_string(),
-//                             end: "}".to_string(),
-//                             close: true,
-//                             newline: true,
-//                         },
-//                         BracketPair {
-//                             start: "/* ".to_string(),
-//                             end: " */".to_string(),
-//                             close: true,
-//                             newline: true,
-//                         },
-//                     ],
-//                     ..Default::default()
-//                 },
-//                 ..Default::default()
-//             },
-//             Some(tree_sitter_rust::language()),
-//         )
-//         .with_indents_query("")
-//         .unwrap(),
-//     );
+    // Refreshing selections is a no-op when excerpts haven't changed.
+    editor.update(cx, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.refresh());
+        assert_eq!(
+            editor.selections.ranges(cx),
+            [
+                Point::new(1, 3)..Point::new(1, 3),
+                Point::new(2, 1)..Point::new(2, 1),
+            ]
+        );
+    });
 
-//     let text = concat!(
-//         "{   }\n",     //
-//         "  x\n",       //
-//         "  /*   */\n", //
-//         "x\n",         //
-//         "{{} }\n",     //
-//     );
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
+    });
+    editor.update(cx, |editor, cx| {
+        // Removing an excerpt causes the first selection to become degenerate.
+        assert_eq!(
+            editor.selections.ranges(cx),
+            [
+                Point::new(0, 0)..Point::new(0, 0),
+                Point::new(0, 1)..Point::new(0, 1)
+            ]
+        );
+
+        // Refreshing selections will relocate the first selection to the original buffer
+        // location.
+        editor.change_selections(None, cx, |s| s.refresh());
+        assert_eq!(
+            editor.selections.ranges(cx),
+            [
+                Point::new(0, 1)..Point::new(0, 1),
+                Point::new(0, 3)..Point::new(0, 3)
+            ]
+        );
+        assert!(editor.selections.pending_anchor().is_some());
+    });
+}
 
-//     let buffer =
-//         cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
-//     let buffer = cx.add_model(|cx| MultiBuffer::singleton(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;
+#[gpui::test]
+fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let buffer =
+        cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
+    let mut excerpt1_id = None;
+    let multibuffer = cx.build_model(|cx| {
+        let mut multibuffer = MultiBuffer::new(0);
+        excerpt1_id = multibuffer
+            .push_excerpts(
+                buffer.clone(),
+                [
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(1, 4),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(1, 0)..Point::new(2, 4),
+                        primary: None,
+                    },
+                ],
+                cx,
+            )
+            .into_iter()
+            .next();
+        assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
+        multibuffer
+    });
 
-//     view.update(cx, |view, cx| {
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([
-//                 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
-//                 DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
-//                 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
-//             ])
-//         });
-//         view.newline(&Newline, cx);
+    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
+    });
 
-//         assert_eq!(
-//             view.buffer().read(cx).read(cx).text(),
-//             concat!(
-//                 "{ \n",    // Suppress rustfmt
-//                 "\n",      //
-//                 "}\n",     //
-//                 "  x\n",   //
-//                 "  /* \n", //
-//                 "  \n",    //
-//                 "  */\n",  //
-//                 "x\n",     //
-//                 "{{} \n",  //
-//                 "}\n",     //
-//             )
-//         );
-//     });
-// }
+    multibuffer.update(cx, |multibuffer, cx| {
+        multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
+    });
+    editor.update(cx, |editor, cx| {
+        assert_eq!(
+            editor.selections.ranges(cx),
+            [Point::new(0, 0)..Point::new(0, 0)]
+        );
+
+        // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
+        editor.change_selections(None, cx, |s| s.refresh());
+        assert_eq!(
+            editor.selections.ranges(cx),
+            [Point::new(0, 3)..Point::new(0, 3)]
+        );
+        assert!(editor.selections.pending_anchor().is_some());
+    });
+}
+
+#[gpui::test]
+async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "{".to_string(),
+                            end: "}".to_string(),
+                            close: true,
+                            newline: true,
+                        },
+                        BracketPair {
+                            start: "/* ".to_string(),
+                            end: " */".to_string(),
+                            close: true,
+                            newline: true,
+                        },
+                    ],
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query("")
+        .unwrap(),
+    );
+
+    let text = concat!(
+        "{   }\n",     //
+        "  x\n",       //
+        "  /*   */\n", //
+        "x\n",         //
+        "{{} }\n",     //
+    );
+
+    let buffer = cx.build_model(|cx| {
+        Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+    });
+    let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+    let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
+    let cx = &mut cx;
+    view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+        .await;
+
+    view.update(cx, |view, cx| {
+        view.change_selections(None, cx, |s| {
+            s.select_display_ranges([
+                DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
+                DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
+                DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
+            ])
+        });
+        view.newline(&Newline, cx);
+
+        assert_eq!(
+            view.buffer().read(cx).read(cx).text(),
+            concat!(
+                "{ \n",    // Suppress rustfmt
+                "\n",      //
+                "}\n",     //
+                "  x\n",   //
+                "  /* \n", //
+                "  \n",    //
+                "  */\n",  //
+                "x\n",     //
+                "{{} \n",  //
+                "}\n",     //
+            )
+        );
+    });
+}
 
+//todo!(finish editor tests)
 // #[gpui::test]
 // 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)
-//         })
-//         .root(cx);
+//     let editor = cx.add_window(|cx| {
+//         let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
+//         build_editor(buffer.clone(), cx)
+//     });
 
 //     editor.update(cx, |editor, cx| {
 //         struct Type1;

crates/editor2/src/element.rs 🔗

@@ -1,5 +1,8 @@
 use crate::{
-    display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, ToDisplayPoint},
+    display_map::{
+        BlockContext, BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, ToDisplayPoint,
+        TransformBlock,
+    },
     editor_settings::ShowScrollbar,
     git::{diff_hunk_to_display, DisplayDiffHunk},
     hover_popover::hover_at,
@@ -15,17 +18,19 @@ use crate::{
 use anyhow::Result;
 use collections::{BTreeMap, HashMap};
 use gpui::{
-    black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
-    BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchContext, DispatchPhase,
-    Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla,
-    InputHandler, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton,
-    MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size,
-    Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout,
+    point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow,
+    Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId,
+    ElementInputHandler, Entity, EntityId, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent,
+    MouseUpEvent, ParentComponent, Pixels, ScrollWheelEvent, Size, Style, Styled, TextRun,
+    TextStyle, View, ViewContext, WindowContext,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
 use multi_buffer::Anchor;
-use project::project_settings::{GitGutterSetting, ProjectSettings};
+use project::{
+    project_settings::{GitGutterSetting, ProjectSettings},
+    ProjectPath,
+};
 use settings::Settings;
 use smallvec::SmallVec;
 use std::{
@@ -39,6 +44,7 @@ use std::{
 };
 use sum_tree::Bias;
 use theme::{ActiveTheme, PlayerColor};
+use ui::{h_stack, IconButton};
 use util::ResultExt;
 use workspace::item::Item;
 
@@ -105,12 +111,16 @@ impl SelectionLayout {
 }
 
 pub struct EditorElement {
+    editor_id: EntityId,
     style: EditorStyle,
 }
 
 impl EditorElement {
-    pub fn new(style: EditorStyle) -> Self {
-        Self { style }
+    pub fn new(editor: &View<Editor>, style: EditorStyle) -> Self {
+        Self {
+            editor_id: editor.entity_id(),
+            style,
+        }
     }
 
     fn mouse_down(
@@ -616,7 +626,7 @@ impl EditorElement {
         let line_end_overshoot = 0.15 * layout.position_map.line_height;
         let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces;
 
-        cx.with_content_mask(ContentMask { bounds }, |cx| {
+        cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
             // todo!("cursor region")
             // cx.scene().push_cursor_region(CursorRegion {
             //     bounds,
@@ -1171,30 +1181,31 @@ impl EditorElement {
         }
     }
 
-    // fn paint_blocks(
-    //     &mut self,
-    //     bounds: Bounds<Pixels>,
-    //     visible_bounds: Bounds<Pixels>,
-    //     layout: &mut LayoutState,
-    //     editor: &mut Editor,
-    //     cx: &mut ViewContext<Editor>,
-    // ) {
-    //     let scroll_position = layout.position_map.snapshot.scroll_position();
-    //     let scroll_left = scroll_position.x * layout.position_map.em_width;
-    //     let scroll_top = scroll_position.y * layout.position_map.line_height;
-
-    //     for block in &mut layout.blocks {
-    //         let mut origin = bounds.origin
-    //             + point(
-    //                 0.,
-    //                 block.row as f32 * layout.position_map.line_height - scroll_top,
-    //             );
-    //         if !matches!(block.style, BlockStyle::Sticky) {
-    //             origin += point(-scroll_left, 0.);
-    //         }
-    //         block.element.paint(origin, visible_bounds, editor, cx);
-    //     }
-    // }
+    fn paint_blocks(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        layout: &mut LayoutState,
+        editor: &mut Editor,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let scroll_position = layout.position_map.snapshot.scroll_position();
+        let scroll_left = scroll_position.x * layout.position_map.em_width;
+        let scroll_top = scroll_position.y * layout.position_map.line_height;
+
+        for block in &mut layout.blocks {
+            let mut origin = bounds.origin
+                + point(
+                    Pixels::ZERO,
+                    block.row as f32 * layout.position_map.line_height - scroll_top,
+                );
+            if !matches!(block.style, BlockStyle::Sticky) {
+                origin += point(-scroll_left, Pixels::ZERO);
+            }
+            block
+                .element
+                .draw(origin, block.available_space, editor, cx);
+        }
+    }
 
     fn column_pixels(&self, column: usize, cx: &ViewContext<Editor>) -> Pixels {
         let style = &self.style;
@@ -1441,6 +1452,7 @@ impl EditorElement {
 
         let snapshot = editor.snapshot(cx);
         let style = self.style.clone();
+
         let font_id = cx.text_system().font_id(&style.text.font()).unwrap();
         let font_size = style.text.font_size.to_pixels(cx.rem_size());
         let line_height = style.text.line_height_in_pixels(cx.rem_size());
@@ -1683,21 +1695,24 @@ impl EditorElement {
             ShowScrollbar::Never => false,
         };
 
-        let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Hsla)> = fold_ranges
-            .into_iter()
-            .map(|(id, fold)| {
-                todo!("folds!")
-                // let color = self
-                //     .style
-                //     .folds
-                //     .ellipses
-                //     .background
-                //     .style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize))
-                //     .color;
-
-                // (id, fold, color)
-            })
-            .collect();
+        let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Hsla)> = Vec::new();
+        // todo!()
+
+        // fold_ranges
+        // .into_iter()
+        // .map(|(id, fold)| {
+        //     // todo!("folds!")
+        //     // let color = self
+        //     //     .style
+        //     //     .folds
+        //     //     .ellipses
+        //     //     .background
+        //     //     .style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize))
+        //     //     .color;
+
+        //     // (id, fold, color)
+        // })
+        // .collect();
 
         let head_for_relative = newest_selection_head.unwrap_or_else(|| {
             let newest = editor.selections.newest::<Point>(cx);
@@ -1738,22 +1753,22 @@ impl EditorElement {
             .unwrap()
             .width;
         let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width;
-        // todo!("blocks")
-        // let (scroll_width, blocks) = self.layout_blocks(
-        //     start_row..end_row,
-        //     &snapshot,
-        //     size.x,
-        //     scroll_width,
-        //     gutter_padding,
-        //     gutter_width,
-        //     em_width,
-        //     gutter_width + gutter_margin,
-        //     line_height,
-        //     &style,
-        //     &line_layouts,
-        //     editor,
-        //     cx,
-        // );
+
+        let (scroll_width, blocks) = self.layout_blocks(
+            start_row..end_row,
+            &snapshot,
+            bounds.size.width,
+            scroll_width,
+            gutter_padding,
+            gutter_width,
+            em_width,
+            gutter_width + gutter_margin,
+            line_height,
+            &style,
+            &line_layouts,
+            editor,
+            cx,
+        );
 
         let scroll_max = point(
             f32::from((scroll_width - text_size.width) / em_width).max(0.0),
@@ -1934,7 +1949,7 @@ impl EditorElement {
             fold_ranges,
             line_number_layouts,
             display_hunks,
-            // blocks,
+            blocks,
             selections,
             context_menu,
             code_actions_indicator,
@@ -1945,226 +1960,177 @@ impl EditorElement {
         }
     }
 
-    // #[allow(clippy::too_many_arguments)]
-    // fn layout_blocks(
-    //     &mut self,
-    //     rows: Range<u32>,
-    //     snapshot: &EditorSnapshot,
-    //     editor_width: f32,
-    //     scroll_width: f32,
-    //     gutter_padding: f32,
-    //     gutter_width: f32,
-    //     em_width: f32,
-    //     text_x: f32,
-    //     line_height: f32,
-    //     style: &EditorStyle,
-    //     line_layouts: &[LineWithInvisibles],
-    //     editor: &mut Editor,
-    //     cx: &mut ViewContext<Editor>,
-    // ) -> (f32, Vec<BlockLayout>) {
-    //     let mut block_id = 0;
-    //     let scroll_x = snapshot.scroll_anchor.offset.x;
-    //     let (fixed_blocks, non_fixed_blocks) = snapshot
-    //         .blocks_in_range(rows.clone())
-    //         .partition::<Vec<_>, _>(|(_, block)| match block {
-    //             TransformBlock::ExcerptHeader { .. } => false,
-    //             TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
-    //         });
-    //     let mut render_block = |block: &TransformBlock, width: f32, block_id: usize| {
-    //         let mut element = match block {
-    //             TransformBlock::Custom(block) => {
-    //                 let align_to = block
-    //                     .position()
-    //                     .to_point(&snapshot.buffer_snapshot)
-    //                     .to_display_point(snapshot);
-    //                 let anchor_x = text_x
-    //                     + if rows.contains(&align_to.row()) {
-    //                         line_layouts[(align_to.row() - rows.start) as usize]
-    //                             .line
-    //                             .x_for_index(align_to.column() as usize)
-    //                     } else {
-    //                         layout_line(align_to.row(), snapshot, style, cx.text_layout_cache())
-    //                             .x_for_index(align_to.column() as usize)
-    //                     };
-
-    //                 block.render(&mut BlockContext {
-    //                     view_context: cx,
-    //                     anchor_x,
-    //                     gutter_padding,
-    //                     line_height,
-    //                     scroll_x,
-    //                     gutter_width,
-    //                     em_width,
-    //                     block_id,
-    //                 })
-    //             }
-    //             TransformBlock::ExcerptHeader {
-    //                 id,
-    //                 buffer,
-    //                 range,
-    //                 starts_new_buffer,
-    //                 ..
-    //             } => {
-    //                 let tooltip_style = theme::current(cx).tooltip.clone();
-    //                 let include_root = editor
-    //                     .project
-    //                     .as_ref()
-    //                     .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
-    //                     .unwrap_or_default();
-    //                 let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
-    //                     let jump_path = ProjectPath {
-    //                         worktree_id: file.worktree_id(cx),
-    //                         path: file.path.clone(),
-    //                     };
-    //                     let jump_anchor = range
-    //                         .primary
-    //                         .as_ref()
-    //                         .map_or(range.context.start, |primary| primary.start);
-    //                     let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
-
-    //                     enum JumpIcon {}
-    //                     MouseEventHandler::new::<JumpIcon, _>((*id).into(), cx, |state, _| {
-    //                         let style = style.jump_icon.style_for(state);
-    //                         Svg::new("icons/arrow_up_right.svg")
-    //                             .with_color(style.color)
-    //                             .constrained()
-    //                             .with_width(style.icon_width)
-    //                             .aligned()
-    //                             .contained()
-    //                             .with_style(style.container)
-    //                             .constrained()
-    //                             .with_width(style.button_width)
-    //                             .with_height(style.button_width)
-    //                     })
-    //                     .with_cursor_style(CursorStyle::PointingHand)
-    //                     .on_click(MouseButton::Left, move |_, editor, cx| {
-    //                         if let Some(workspace) = editor
-    //                             .workspace
-    //                             .as_ref()
-    //                             .and_then(|(workspace, _)| workspace.upgrade(cx))
-    //                         {
-    //                             workspace.update(cx, |workspace, cx| {
-    //                                 Editor::jump(
-    //                                     workspace,
-    //                                     jump_path.clone(),
-    //                                     jump_position,
-    //                                     jump_anchor,
-    //                                     cx,
-    //                                 );
-    //                             });
-    //                         }
-    //                     })
-    //                     .with_tooltip::<JumpIcon>(
-    //                         (*id).into(),
-    //                         "Jump to Buffer".to_string(),
-    //                         Some(Box::new(crate::OpenExcerpts)),
-    //                         tooltip_style.clone(),
-    //                         cx,
-    //                     )
-    //                     .aligned()
-    //                     .flex_float()
-    //                 });
-
-    //                 if *starts_new_buffer {
-    //                     let editor_font_size = style.text.font_size;
-    //                     let style = &style.diagnostic_path_header;
-    //                     let font_size = (style.text_scale_factor * editor_font_size).round();
-
-    //                     let path = buffer.resolve_file_path(cx, include_root);
-    //                     let mut filename = None;
-    //                     let mut parent_path = None;
-    //                     // Can't use .and_then() because `.file_name()` and `.parent()` return references :(
-    //                     if let Some(path) = path {
-    //                         filename = path.file_name().map(|f| f.to_string_lossy.to_string());
-    //                         parent_path =
-    //                             path.parent().map(|p| p.to_string_lossy.to_string() + "/");
-    //                     }
+    #[allow(clippy::too_many_arguments)]
+    fn layout_blocks(
+        &mut self,
+        rows: Range<u32>,
+        snapshot: &EditorSnapshot,
+        editor_width: Pixels,
+        scroll_width: Pixels,
+        gutter_padding: Pixels,
+        gutter_width: Pixels,
+        em_width: Pixels,
+        text_x: Pixels,
+        line_height: Pixels,
+        style: &EditorStyle,
+        line_layouts: &[LineWithInvisibles],
+        editor: &mut Editor,
+        cx: &mut ViewContext<Editor>,
+    ) -> (Pixels, Vec<BlockLayout>) {
+        let mut block_id = 0;
+        let scroll_x = snapshot.scroll_anchor.offset.x;
+        let (fixed_blocks, non_fixed_blocks) = snapshot
+            .blocks_in_range(rows.clone())
+            .partition::<Vec<_>, _>(|(_, block)| match block {
+                TransformBlock::ExcerptHeader { .. } => false,
+                TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
+            });
+        let mut render_block = |block: &TransformBlock,
+                                available_space: Size<AvailableSpace>,
+                                block_id: usize,
+                                editor: &mut Editor,
+                                cx: &mut ViewContext<Editor>| {
+            let mut element = match block {
+                TransformBlock::Custom(block) => {
+                    let align_to = block
+                        .position()
+                        .to_point(&snapshot.buffer_snapshot)
+                        .to_display_point(snapshot);
+                    let anchor_x = text_x
+                        + if rows.contains(&align_to.row()) {
+                            line_layouts[(align_to.row() - rows.start) as usize]
+                                .line
+                                .x_for_index(align_to.column() as usize)
+                        } else {
+                            layout_line(align_to.row(), snapshot, style, cx)
+                                .unwrap()
+                                .x_for_index(align_to.column() as usize)
+                        };
+
+                    block.render(&mut BlockContext {
+                        view_context: cx,
+                        anchor_x,
+                        gutter_padding,
+                        line_height,
+                        gutter_width,
+                        em_width,
+                        block_id,
+                        editor_style: &self.style,
+                    })
+                }
+                TransformBlock::ExcerptHeader {
+                    id,
+                    buffer,
+                    range,
+                    starts_new_buffer,
+                    ..
+                } => {
+                    let include_root = editor
+                        .project
+                        .as_ref()
+                        .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
+                        .unwrap_or_default();
+                    let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
+                        let jump_path = ProjectPath {
+                            worktree_id: file.worktree_id(cx),
+                            path: file.path.clone(),
+                        };
+                        let jump_anchor = range
+                            .primary
+                            .as_ref()
+                            .map_or(range.context.start, |primary| primary.start);
+                        let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
+
+                        // todo!("avoid ElementId collision risk here")
+                        let icon_button_id: usize = id.clone().into();
+                        IconButton::new(icon_button_id, ui::Icon::ArrowUpRight)
+                            .on_click(move |editor: &mut Editor, cx| {
+                                editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
+                            })
+                            .tooltip("Jump to Buffer") // todo!(pass an action as well to show key binding)
+                    });
 
-    //                     Flex::row()
-    //                         .with_child(
-    //                             Label::new(
-    //                                 filename.unwrap_or_else(|| "untitled".to_string()),
-    //                                 style.filename.text.clone().with_font_size(font_size),
-    //                             )
-    //                             .contained()
-    //                             .with_style(style.filename.container)
-    //                             .aligned(),
-    //                         )
-    //                         .with_children(parent_path.map(|path| {
-    //                             Label::new(path, style.path.text.clone().with_font_size(font_size))
-    //                                 .contained()
-    //                                 .with_style(style.path.container)
-    //                                 .aligned()
-    //                         }))
-    //                         .with_children(jump_icon)
-    //                         .contained()
-    //                         .with_style(style.container)
-    //                         .with_padding_left(gutter_padding)
-    //                         .with_padding_right(gutter_padding)
-    //                         .expanded()
-    //                         .into_any_named("path header block")
-    //                 } else {
-    //                     let text_style = style.text.clone();
-    //                     Flex::row()
-    //                         .with_child(Label::new("⋯", text_style))
-    //                         .with_children(jump_icon)
-    //                         .contained()
-    //                         .with_padding_left(gutter_padding)
-    //                         .with_padding_right(gutter_padding)
-    //                         .expanded()
-    //                         .into_any_named("collapsed context")
-    //                 }
-    //             }
-    //         };
-
-    //         element.layout(
-    //             SizeConstraint {
-    //                 min: gpui::Point::<Pixels>::zero(),
-    //                 max: point(width, block.height() as f32 * line_height),
-    //             },
-    //             editor,
-    //             cx,
-    //         );
-    //         element
-    //     };
-
-    //     let mut fixed_block_max_width = 0f32;
-    //     let mut blocks = Vec::new();
-    //     for (row, block) in fixed_blocks {
-    //         let element = render_block(block, f32::INFINITY, block_id);
-    //         block_id += 1;
-    //         fixed_block_max_width = fixed_block_max_width.max(element.size().x + em_width);
-    //         blocks.push(BlockLayout {
-    //             row,
-    //             element,
-    //             style: BlockStyle::Fixed,
-    //         });
-    //     }
-    //     for (row, block) in non_fixed_blocks {
-    //         let style = match block {
-    //             TransformBlock::Custom(block) => block.style(),
-    //             TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky,
-    //         };
-    //         let width = match style {
-    //             BlockStyle::Sticky => editor_width,
-    //             BlockStyle::Flex => editor_width
-    //                 .max(fixed_block_max_width)
-    //                 .max(gutter_width + scroll_width),
-    //             BlockStyle::Fixed => unreachable!(),
-    //         };
-    //         let element = render_block(block, width, block_id);
-    //         block_id += 1;
-    //         blocks.push(BlockLayout {
-    //             row,
-    //             element,
-    //             style,
-    //         });
-    //     }
-    //     (
-    //         scroll_width.max(fixed_block_max_width - gutter_width),
-    //         blocks,
-    //     )
-    // }
+                    let element = if *starts_new_buffer {
+                        let path = buffer.resolve_file_path(cx, include_root);
+                        let mut filename = None;
+                        let mut parent_path = None;
+                        // Can't use .and_then() because `.file_name()` and `.parent()` return references :(
+                        if let Some(path) = path {
+                            filename = path.file_name().map(|f| f.to_string_lossy().to_string());
+                            parent_path =
+                                path.parent().map(|p| p.to_string_lossy().to_string() + "/");
+                        }
+
+                        h_stack()
+                            .size_full()
+                            .bg(gpui::red())
+                            .child(filename.unwrap_or_else(|| "untitled".to_string()))
+                            .children(parent_path)
+                            .children(jump_icon) // .p_x(gutter_padding)
+                    } else {
+                        let text_style = style.text.clone();
+                        h_stack()
+                            .size_full()
+                            .bg(gpui::red())
+                            .child("⋯")
+                            .children(jump_icon) // .p_x(gutter_padding)
+                    };
+                    element.render()
+                }
+            };
+
+            let size = element.measure(available_space, editor, cx);
+            (element, size)
+        };
+
+        let mut fixed_block_max_width = Pixels::ZERO;
+        let mut blocks = Vec::new();
+        for (row, block) in fixed_blocks {
+            let available_space = size(
+                AvailableSpace::MinContent,
+                AvailableSpace::Definite(block.height() as f32 * line_height),
+            );
+            let (element, element_size) =
+                render_block(block, available_space, block_id, editor, cx);
+            block_id += 1;
+            fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
+            blocks.push(BlockLayout {
+                row,
+                element,
+                available_space,
+                style: BlockStyle::Fixed,
+            });
+        }
+        for (row, block) in non_fixed_blocks {
+            let style = match block {
+                TransformBlock::Custom(block) => block.style(),
+                TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky,
+            };
+            let width = match style {
+                BlockStyle::Sticky => editor_width,
+                BlockStyle::Flex => editor_width
+                    .max(fixed_block_max_width)
+                    .max(gutter_width + scroll_width),
+                BlockStyle::Fixed => unreachable!(),
+            };
+            let available_space = size(
+                AvailableSpace::Definite(width),
+                AvailableSpace::Definite(block.height() as f32 * line_height),
+            );
+            let (element, _) = render_block(block, available_space, block_id, editor, cx);
+            block_id += 1;
+            blocks.push(BlockLayout {
+                row,
+                element,
+                available_space,
+                style,
+            });
+        }
+        (
+            scroll_width.max(fixed_block_max_width - gutter_width),
+            blocks,
+        )
+    }
 
     fn paint_mouse_listeners(
         &mut self,
@@ -2292,11 +2258,7 @@ impl LineWithInvisibles {
 
                 if !line_chunk.is_empty() && !line_exceeded_max_len {
                     let text_style = if let Some(style) = highlighted_chunk.style {
-                        text_style
-                            .clone()
-                            .highlight(style)
-                            .map(Cow::Owned)
-                            .unwrap_or_else(|_| Cow::Borrowed(text_style))
+                        Cow::Owned(text_style.clone().highlight(style))
                     } else {
                         Cow::Borrowed(text_style)
                     };
@@ -2442,8 +2404,8 @@ enum Invisible {
 impl Element<Editor> for EditorElement {
     type ElementState = ();
 
-    fn id(&self) -> Option<gpui::ElementId> {
-        None
+    fn element_id(&self) -> Option<gpui::ElementId> {
+        Some(self.editor_id.into())
     }
 
     fn initialize(
@@ -2453,15 +2415,6 @@ impl Element<Editor> for EditorElement {
         cx: &mut gpui::ViewContext<Editor>,
     ) -> Self::ElementState {
         editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this.
-
-        let dispatch_context = editor.dispatch_context(cx);
-        cx.with_element_id(cx.view().entity_id(), |global_id, cx| {
-            cx.with_key_dispatch_context(dispatch_context, |cx| {
-                cx.with_key_listeners(build_key_listeners(global_id), |cx| {
-                    cx.with_focus(editor.focus_handle.clone(), |_| {})
-                });
-            })
-        });
     }
 
     fn layout(
@@ -2498,25 +2451,45 @@ impl Element<Editor> for EditorElement {
             size: layout.text_size,
         };
 
-        // We call with_z_index to establish a new stacking context.
-        cx.with_z_index(0, |cx| {
-            cx.with_content_mask(ContentMask { bounds }, |cx| {
-                self.paint_mouse_listeners(
-                    bounds,
-                    gutter_bounds,
-                    text_bounds,
-                    &layout.position_map,
-                    cx,
-                );
-                self.paint_background(gutter_bounds, text_bounds, &layout, cx);
-                if layout.gutter_size.width > Pixels::ZERO {
-                    self.paint_gutter(gutter_bounds, &mut layout, editor, cx);
-                }
-                self.paint_text(text_bounds, &mut layout, editor, cx);
-                let input_handler = ElementInputHandler::new(bounds, cx);
-                cx.handle_input(&editor.focus_handle, input_handler);
-            });
-        });
+        let dispatch_context = editor.dispatch_context(cx);
+        cx.with_key_dispatch(
+            dispatch_context,
+            Some(editor.focus_handle.clone()),
+            |_, cx| {
+                register_actions(cx);
+
+                // We call with_z_index to establish a new stacking context.
+                cx.with_z_index(0, |cx| {
+                    cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
+                        self.paint_mouse_listeners(
+                            bounds,
+                            gutter_bounds,
+                            text_bounds,
+                            &layout.position_map,
+                            cx,
+                        );
+                        self.paint_background(gutter_bounds, text_bounds, &layout, cx);
+                        if layout.gutter_size.width > Pixels::ZERO {
+                            self.paint_gutter(gutter_bounds, &mut layout, editor, cx);
+                        }
+                        self.paint_text(text_bounds, &mut layout, editor, cx);
+
+                        if !layout.blocks.is_empty() {
+                            self.paint_blocks(bounds, &mut layout, editor, cx);
+                        }
+
+                        let input_handler = ElementInputHandler::new(bounds, cx);
+                        cx.handle_input(&editor.focus_handle, input_handler);
+                    });
+                });
+            },
+        )
+    }
+}
+
+impl Component<Editor> for EditorElement {
+    fn render(self) -> AnyElement<Editor> {
+        AnyElement::new(self)
     }
 }
 
@@ -3133,7 +3106,7 @@ pub struct LayoutState {
     highlighted_rows: Option<Range<u32>>,
     line_number_layouts: Vec<Option<gpui::Line>>,
     display_hunks: Vec<DisplayDiffHunk>,
-    // blocks: Vec<BlockLayout>,
+    blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
     fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Hsla)>,
     selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
@@ -3236,6 +3209,7 @@ impl PositionMap {
 struct BlockLayout {
     row: u32,
     element: AnyElement<Editor>,
+    available_space: Size<AvailableSpace>,
     style: BlockStyle,
 }
 
@@ -3995,212 +3969,187 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
 //     }
 // }
 
-fn build_key_listeners(
-    global_element_id: GlobalElementId,
-) -> impl IntoIterator<Item = (TypeId, KeyListener<Editor>)> {
-    [
-        build_action_listener(Editor::move_left),
-        build_action_listener(Editor::move_right),
-        build_action_listener(Editor::move_down),
-        build_action_listener(Editor::move_up),
-        // build_action_listener(Editor::new_file), todo!()
-        // build_action_listener(Editor::new_file_in_direction), todo!()
-        build_action_listener(Editor::cancel),
-        build_action_listener(Editor::newline),
-        build_action_listener(Editor::newline_above),
-        build_action_listener(Editor::newline_below),
-        build_action_listener(Editor::backspace),
-        build_action_listener(Editor::delete),
-        build_action_listener(Editor::tab),
-        build_action_listener(Editor::tab_prev),
-        build_action_listener(Editor::indent),
-        build_action_listener(Editor::outdent),
-        build_action_listener(Editor::delete_line),
-        build_action_listener(Editor::join_lines),
-        build_action_listener(Editor::sort_lines_case_sensitive),
-        build_action_listener(Editor::sort_lines_case_insensitive),
-        build_action_listener(Editor::reverse_lines),
-        build_action_listener(Editor::shuffle_lines),
-        build_action_listener(Editor::convert_to_upper_case),
-        build_action_listener(Editor::convert_to_lower_case),
-        build_action_listener(Editor::convert_to_title_case),
-        build_action_listener(Editor::convert_to_snake_case),
-        build_action_listener(Editor::convert_to_kebab_case),
-        build_action_listener(Editor::convert_to_upper_camel_case),
-        build_action_listener(Editor::convert_to_lower_camel_case),
-        build_action_listener(Editor::delete_to_previous_word_start),
-        build_action_listener(Editor::delete_to_previous_subword_start),
-        build_action_listener(Editor::delete_to_next_word_end),
-        build_action_listener(Editor::delete_to_next_subword_end),
-        build_action_listener(Editor::delete_to_beginning_of_line),
-        build_action_listener(Editor::delete_to_end_of_line),
-        build_action_listener(Editor::cut_to_end_of_line),
-        build_action_listener(Editor::duplicate_line),
-        build_action_listener(Editor::move_line_up),
-        build_action_listener(Editor::move_line_down),
-        build_action_listener(Editor::transpose),
-        build_action_listener(Editor::cut),
-        build_action_listener(Editor::copy),
-        build_action_listener(Editor::paste),
-        build_action_listener(Editor::undo),
-        build_action_listener(Editor::redo),
-        build_action_listener(Editor::move_page_up),
-        build_action_listener(Editor::move_page_down),
-        build_action_listener(Editor::next_screen),
-        build_action_listener(Editor::scroll_cursor_top),
-        build_action_listener(Editor::scroll_cursor_center),
-        build_action_listener(Editor::scroll_cursor_bottom),
-        build_action_listener(|editor, _: &LineDown, cx| {
-            editor.scroll_screen(&ScrollAmount::Line(1.), cx)
-        }),
-        build_action_listener(|editor, _: &LineUp, cx| {
-            editor.scroll_screen(&ScrollAmount::Line(-1.), cx)
-        }),
-        build_action_listener(|editor, _: &HalfPageDown, cx| {
-            editor.scroll_screen(&ScrollAmount::Page(0.5), cx)
-        }),
-        build_action_listener(|editor, _: &HalfPageUp, cx| {
-            editor.scroll_screen(&ScrollAmount::Page(-0.5), cx)
-        }),
-        build_action_listener(|editor, _: &PageDown, cx| {
-            editor.scroll_screen(&ScrollAmount::Page(1.), cx)
-        }),
-        build_action_listener(|editor, _: &PageUp, cx| {
-            editor.scroll_screen(&ScrollAmount::Page(-1.), cx)
-        }),
-        build_action_listener(Editor::move_to_previous_word_start),
-        build_action_listener(Editor::move_to_previous_subword_start),
-        build_action_listener(Editor::move_to_next_word_end),
-        build_action_listener(Editor::move_to_next_subword_end),
-        build_action_listener(Editor::move_to_beginning_of_line),
-        build_action_listener(Editor::move_to_end_of_line),
-        build_action_listener(Editor::move_to_start_of_paragraph),
-        build_action_listener(Editor::move_to_end_of_paragraph),
-        build_action_listener(Editor::move_to_beginning),
-        build_action_listener(Editor::move_to_end),
-        build_action_listener(Editor::select_up),
-        build_action_listener(Editor::select_down),
-        build_action_listener(Editor::select_left),
-        build_action_listener(Editor::select_right),
-        build_action_listener(Editor::select_to_previous_word_start),
-        build_action_listener(Editor::select_to_previous_subword_start),
-        build_action_listener(Editor::select_to_next_word_end),
-        build_action_listener(Editor::select_to_next_subword_end),
-        build_action_listener(Editor::select_to_beginning_of_line),
-        build_action_listener(Editor::select_to_end_of_line),
-        build_action_listener(Editor::select_to_start_of_paragraph),
-        build_action_listener(Editor::select_to_end_of_paragraph),
-        build_action_listener(Editor::select_to_beginning),
-        build_action_listener(Editor::select_to_end),
-        build_action_listener(Editor::select_all),
-        build_action_listener(|editor, action, cx| {
-            editor.select_all_matches(action, cx).log_err();
-        }),
-        build_action_listener(Editor::select_line),
-        build_action_listener(Editor::split_selection_into_lines),
-        build_action_listener(Editor::add_selection_above),
-        build_action_listener(Editor::add_selection_below),
-        build_action_listener(|editor, action, cx| {
-            editor.select_next(action, cx).log_err();
-        }),
-        build_action_listener(|editor, action, cx| {
-            editor.select_previous(action, cx).log_err();
-        }),
-        build_action_listener(Editor::toggle_comments),
-        build_action_listener(Editor::select_larger_syntax_node),
-        build_action_listener(Editor::select_smaller_syntax_node),
-        build_action_listener(Editor::move_to_enclosing_bracket),
-        build_action_listener(Editor::undo_selection),
-        build_action_listener(Editor::redo_selection),
-        build_action_listener(Editor::go_to_diagnostic),
-        build_action_listener(Editor::go_to_prev_diagnostic),
-        build_action_listener(Editor::go_to_hunk),
-        build_action_listener(Editor::go_to_prev_hunk),
-        build_action_listener(Editor::go_to_definition),
-        build_action_listener(Editor::go_to_definition_split),
-        build_action_listener(Editor::go_to_type_definition),
-        build_action_listener(Editor::go_to_type_definition_split),
-        build_action_listener(Editor::fold),
-        build_action_listener(Editor::fold_at),
-        build_action_listener(Editor::unfold_lines),
-        build_action_listener(Editor::unfold_at),
-        build_action_listener(Editor::fold_selected_ranges),
-        build_action_listener(Editor::show_completions),
-        build_action_listener(Editor::toggle_code_actions),
-        // build_action_listener(Editor::open_excerpts), todo!()
-        build_action_listener(Editor::toggle_soft_wrap),
-        build_action_listener(Editor::toggle_inlay_hints),
-        build_action_listener(Editor::reveal_in_finder),
-        build_action_listener(Editor::copy_path),
-        build_action_listener(Editor::copy_relative_path),
-        build_action_listener(Editor::copy_highlight_json),
-        build_action_listener(|editor, action, cx| {
-            editor
-                .format(action, cx)
-                .map(|task| task.detach_and_log_err(cx));
-        }),
-        build_action_listener(Editor::restart_language_server),
-        build_action_listener(Editor::show_character_palette),
-        // build_action_listener(Editor::confirm_completion), todo!()
-        build_action_listener(|editor, action, cx| {
-            editor
-                .confirm_code_action(action, cx)
-                .map(|task| task.detach_and_log_err(cx));
-        }),
-        // build_action_listener(Editor::rename), todo!()
-        // build_action_listener(Editor::confirm_rename), todo!()
-        // build_action_listener(Editor::find_all_references), todo!()
-        build_action_listener(Editor::next_copilot_suggestion),
-        build_action_listener(Editor::previous_copilot_suggestion),
-        build_action_listener(Editor::copilot_suggest),
-        build_action_listener(Editor::context_menu_first),
-        build_action_listener(Editor::context_menu_prev),
-        build_action_listener(Editor::context_menu_next),
-        build_action_listener(Editor::context_menu_last),
-        build_key_listener(
-            move |editor, key_down: &KeyDownEvent, dispatch_context, phase, cx| {
-                if phase == DispatchPhase::Bubble {
-                    if let KeyMatch::Some(action) = cx.match_keystroke(
-                        &global_element_id,
-                        &key_down.keystroke,
-                        dispatch_context,
-                    ) {
-                        return Some(action);
-                    }
-                }
-
-                None
-            },
-        ),
-    ]
-}
-
-fn build_key_listener<T: 'static>(
-    listener: impl Fn(
-            &mut Editor,
-            &T,
-            &[&DispatchContext],
-            DispatchPhase,
-            &mut ViewContext<Editor>,
-        ) -> Option<Box<dyn Action>>
-        + 'static,
-) -> (TypeId, KeyListener<Editor>) {
-    (
-        TypeId::of::<T>(),
-        Box::new(move |editor, event, dispatch_context, phase, cx| {
-            let key_event = event.downcast_ref::<T>()?;
-            listener(editor, key_event, dispatch_context, phase, cx)
-        }),
-    )
+fn register_actions(cx: &mut ViewContext<Editor>) {
+    register_action(cx, Editor::move_left);
+    register_action(cx, Editor::move_right);
+    register_action(cx, Editor::move_down);
+    register_action(cx, Editor::move_up);
+    // on_action(cx, Editor::new_file); todo!()
+    // on_action(cx, Editor::new_file_in_direction); todo!()
+    register_action(cx, Editor::cancel);
+    register_action(cx, Editor::newline);
+    register_action(cx, Editor::newline_above);
+    register_action(cx, Editor::newline_below);
+    register_action(cx, Editor::backspace);
+    register_action(cx, Editor::delete);
+    register_action(cx, Editor::tab);
+    register_action(cx, Editor::tab_prev);
+    register_action(cx, Editor::indent);
+    register_action(cx, Editor::outdent);
+    register_action(cx, Editor::delete_line);
+    register_action(cx, Editor::join_lines);
+    register_action(cx, Editor::sort_lines_case_sensitive);
+    register_action(cx, Editor::sort_lines_case_insensitive);
+    register_action(cx, Editor::reverse_lines);
+    register_action(cx, Editor::shuffle_lines);
+    register_action(cx, Editor::convert_to_upper_case);
+    register_action(cx, Editor::convert_to_lower_case);
+    register_action(cx, Editor::convert_to_title_case);
+    register_action(cx, Editor::convert_to_snake_case);
+    register_action(cx, Editor::convert_to_kebab_case);
+    register_action(cx, Editor::convert_to_upper_camel_case);
+    register_action(cx, Editor::convert_to_lower_camel_case);
+    register_action(cx, Editor::delete_to_previous_word_start);
+    register_action(cx, Editor::delete_to_previous_subword_start);
+    register_action(cx, Editor::delete_to_next_word_end);
+    register_action(cx, Editor::delete_to_next_subword_end);
+    register_action(cx, Editor::delete_to_beginning_of_line);
+    register_action(cx, Editor::delete_to_end_of_line);
+    register_action(cx, Editor::cut_to_end_of_line);
+    register_action(cx, Editor::duplicate_line);
+    register_action(cx, Editor::move_line_up);
+    register_action(cx, Editor::move_line_down);
+    register_action(cx, Editor::transpose);
+    register_action(cx, Editor::cut);
+    register_action(cx, Editor::copy);
+    register_action(cx, Editor::paste);
+    register_action(cx, Editor::undo);
+    register_action(cx, Editor::redo);
+    register_action(cx, Editor::move_page_up);
+    register_action(cx, Editor::move_page_down);
+    register_action(cx, Editor::next_screen);
+    register_action(cx, Editor::scroll_cursor_top);
+    register_action(cx, Editor::scroll_cursor_center);
+    register_action(cx, Editor::scroll_cursor_bottom);
+    register_action(cx, |editor, _: &LineDown, cx| {
+        editor.scroll_screen(&ScrollAmount::Line(1.), cx)
+    });
+    register_action(cx, |editor, _: &LineUp, cx| {
+        editor.scroll_screen(&ScrollAmount::Line(-1.), cx)
+    });
+    register_action(cx, |editor, _: &HalfPageDown, cx| {
+        editor.scroll_screen(&ScrollAmount::Page(0.5), cx)
+    });
+    register_action(cx, |editor, _: &HalfPageUp, cx| {
+        editor.scroll_screen(&ScrollAmount::Page(-0.5), cx)
+    });
+    register_action(cx, |editor, _: &PageDown, cx| {
+        editor.scroll_screen(&ScrollAmount::Page(1.), cx)
+    });
+    register_action(cx, |editor, _: &PageUp, cx| {
+        editor.scroll_screen(&ScrollAmount::Page(-1.), cx)
+    });
+    register_action(cx, Editor::move_to_previous_word_start);
+    register_action(cx, Editor::move_to_previous_subword_start);
+    register_action(cx, Editor::move_to_next_word_end);
+    register_action(cx, Editor::move_to_next_subword_end);
+    register_action(cx, Editor::move_to_beginning_of_line);
+    register_action(cx, Editor::move_to_end_of_line);
+    register_action(cx, Editor::move_to_start_of_paragraph);
+    register_action(cx, Editor::move_to_end_of_paragraph);
+    register_action(cx, Editor::move_to_beginning);
+    register_action(cx, Editor::move_to_end);
+    register_action(cx, Editor::select_up);
+    register_action(cx, Editor::select_down);
+    register_action(cx, Editor::select_left);
+    register_action(cx, Editor::select_right);
+    register_action(cx, Editor::select_to_previous_word_start);
+    register_action(cx, Editor::select_to_previous_subword_start);
+    register_action(cx, Editor::select_to_next_word_end);
+    register_action(cx, Editor::select_to_next_subword_end);
+    register_action(cx, Editor::select_to_beginning_of_line);
+    register_action(cx, Editor::select_to_end_of_line);
+    register_action(cx, Editor::select_to_start_of_paragraph);
+    register_action(cx, Editor::select_to_end_of_paragraph);
+    register_action(cx, Editor::select_to_beginning);
+    register_action(cx, Editor::select_to_end);
+    register_action(cx, Editor::select_all);
+    register_action(cx, |editor, action, cx| {
+        editor.select_all_matches(action, cx).log_err();
+    });
+    register_action(cx, Editor::select_line);
+    register_action(cx, Editor::split_selection_into_lines);
+    register_action(cx, Editor::add_selection_above);
+    register_action(cx, Editor::add_selection_below);
+    register_action(cx, |editor, action, cx| {
+        editor.select_next(action, cx).log_err();
+    });
+    register_action(cx, |editor, action, cx| {
+        editor.select_previous(action, cx).log_err();
+    });
+    register_action(cx, Editor::toggle_comments);
+    register_action(cx, Editor::select_larger_syntax_node);
+    register_action(cx, Editor::select_smaller_syntax_node);
+    register_action(cx, Editor::move_to_enclosing_bracket);
+    register_action(cx, Editor::undo_selection);
+    register_action(cx, Editor::redo_selection);
+    register_action(cx, Editor::go_to_diagnostic);
+    register_action(cx, Editor::go_to_prev_diagnostic);
+    register_action(cx, Editor::go_to_hunk);
+    register_action(cx, Editor::go_to_prev_hunk);
+    register_action(cx, Editor::go_to_definition);
+    register_action(cx, Editor::go_to_definition_split);
+    register_action(cx, Editor::go_to_type_definition);
+    register_action(cx, Editor::go_to_type_definition_split);
+    register_action(cx, Editor::fold);
+    register_action(cx, Editor::fold_at);
+    register_action(cx, Editor::unfold_lines);
+    register_action(cx, Editor::unfold_at);
+    register_action(cx, Editor::fold_selected_ranges);
+    register_action(cx, Editor::show_completions);
+    register_action(cx, Editor::toggle_code_actions);
+    // on_action(cx, Editor::open_excerpts); todo!()
+    register_action(cx, Editor::toggle_soft_wrap);
+    register_action(cx, Editor::toggle_inlay_hints);
+    register_action(cx, Editor::reveal_in_finder);
+    register_action(cx, Editor::copy_path);
+    register_action(cx, Editor::copy_relative_path);
+    register_action(cx, Editor::copy_highlight_json);
+    register_action(cx, |editor, action, cx| {
+        editor
+            .format(action, cx)
+            .map(|task| task.detach_and_log_err(cx));
+    });
+    register_action(cx, Editor::restart_language_server);
+    register_action(cx, Editor::show_character_palette);
+    // on_action(cx, Editor::confirm_completion); todo!()
+    register_action(cx, |editor, action, cx| {
+        editor
+            .confirm_code_action(action, cx)
+            .map(|task| task.detach_and_log_err(cx));
+    });
+    register_action(cx, |editor, action, cx| {
+        editor
+            .rename(action, cx)
+            .map(|task| task.detach_and_log_err(cx));
+    });
+    register_action(cx, |editor, action, cx| {
+        editor
+            .confirm_rename(action, cx)
+            .map(|task| task.detach_and_log_err(cx));
+    });
+    register_action(cx, |editor, action, cx| {
+        editor
+            .find_all_references(action, cx)
+            .map(|task| task.detach_and_log_err(cx));
+    });
+    register_action(cx, Editor::next_copilot_suggestion);
+    register_action(cx, Editor::previous_copilot_suggestion);
+    register_action(cx, Editor::copilot_suggest);
+    register_action(cx, Editor::context_menu_first);
+    register_action(cx, Editor::context_menu_prev);
+    register_action(cx, Editor::context_menu_next);
+    register_action(cx, Editor::context_menu_last);
 }
 
-fn build_action_listener<T: Action>(
+fn register_action<T: Action>(
+    cx: &mut ViewContext<Editor>,
     listener: impl Fn(&mut Editor, &T, &mut ViewContext<Editor>) + 'static,
-) -> (TypeId, KeyListener<Editor>) {
-    build_key_listener(move |editor, action: &T, dispatch_context, phase, cx| {
+) {
+    cx.on_action(TypeId::of::<T>(), move |editor, action, phase, cx| {
+        let action = action.downcast_ref().unwrap();
         if phase == DispatchPhase::Bubble {
             listener(editor, action, cx);
         }
-        None
     })
 }

crates/editor2/src/inlay_hint_cache.rs 🔗

@@ -1220,8 +1220,6 @@ pub mod tests {
 
     use super::*;
 
-    // todo!()
-    #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
     #[gpui::test]
     async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
         let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
@@ -1345,8 +1343,6 @@ pub mod tests {
         });
     }
 
-    // todo!()
-    #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
     #[gpui::test]
     async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
@@ -1458,8 +1454,6 @@ pub mod tests {
         });
     }
 
-    // todo!()
-    #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
     #[gpui::test]
     async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
@@ -1668,8 +1662,6 @@ pub mod tests {
         });
     }
 
-    // todo!()
-    #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
     #[gpui::test]
     async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
         let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
@@ -1998,8 +1990,6 @@ pub mod tests {
         });
     }
 
-    // todo!()
-    #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
     #[gpui::test]
     async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
@@ -2126,8 +2116,6 @@ pub mod tests {
         });
     }
 
-    // todo!()
-    #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
     #[gpui::test(iterations = 10)]
     async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
@@ -2411,8 +2399,6 @@ pub mod tests {
         });
     }
 
-    // todo!()
-    #[ignore = "fails due to text.rs `measurement has not been performed` error"]
     #[gpui::test(iterations = 10)]
     async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
@@ -2455,14 +2441,9 @@ pub 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 worktree_id = workspace
-            .update(cx, |workspace, cx| {
-                workspace.project().read_with(cx, |project, cx| {
-                    project.worktrees().next().unwrap().read(cx).id()
-                })
-            })
-            .unwrap();
+        let worktree_id = project.update(cx, |project, cx| {
+            project.worktrees().next().unwrap().read(cx).id()
+        });
 
         let buffer_1 = project
             .update(cx, |project, cx| {
@@ -2620,6 +2601,10 @@ pub mod tests {
                 "main hint #1".to_string(),
                 "main hint #2".to_string(),
                 "main hint #3".to_string(),
+                // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther
+                // (or renders less?) note that tests below pass
+                "main hint #4".to_string(),
+                "main hint #5".to_string(),
             ];
             assert_eq!(
                 expected_hints,
@@ -2755,8 +2740,6 @@ all hints should be invalidated and requeried for all of its visible excerpts"
         });
     }
 
-    // todo!()
-    #[ignore = "fails due to text.rs `measurement has not been performed` error"]
     #[gpui::test]
     async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
@@ -2799,14 +2782,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 worktree_id = workspace
-            .update(cx, |workspace, cx| {
-                workspace.project().read_with(cx, |project, cx| {
-                    project.worktrees().next().unwrap().read(cx).id()
-                })
-            })
-            .unwrap();
+        let worktree_id = project.update(cx, |project, cx| {
+            project.worktrees().next().unwrap().read(cx).id()
+        });
 
         let buffer_1 = project
             .update(cx, |project, cx| {
@@ -2985,8 +2963,6 @@ all hints should be invalidated and requeried for all of its visible excerpts"
         });
     }
 
-    // todo!()
-    #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
     #[gpui::test]
     async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
@@ -3078,8 +3054,6 @@ all hints should be invalidated and requeried for all of its visible excerpts"
         });
     }
 
-    // todo!()
-    #[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
     #[gpui::test]
     async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {

crates/editor2/src/items.rs 🔗

@@ -9,7 +9,7 @@ use collections::HashSet;
 use futures::future::try_join_all;
 use gpui::{
     div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter,
-    FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View,
+    FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View,
     ViewContext, VisualContext, WeakView,
 };
 use language::{
@@ -30,6 +30,7 @@ use std::{
 };
 use text::Selection;
 use theme::{ActiveTheme, Theme};
+use ui::{Label, TextColor};
 use util::{paths::PathExt, ResultExt, TryFutureExt};
 use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
 use workspace::{
@@ -595,16 +596,19 @@ impl Item for Editor {
                 .flex_row()
                 .items_center()
                 .gap_2()
-                .child(self.title(cx).to_string())
+                .child(Label::new(self.title(cx).to_string()))
                 .children(detail.and_then(|detail| {
                     let path = path_for_buffer(&self.buffer, detail, false, cx)?;
                     let description = path.to_string_lossy();
 
                     Some(
-                        div()
-                            .text_color(theme.colors().text_muted)
-                            .text_xs()
-                            .child(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)),
+                        div().child(
+                            Label::new(util::truncate_and_trailoff(
+                                &description,
+                                MAX_TAB_TITLE_LEN,
+                            ))
+                            .color(TextColor::Muted),
+                        ),
                     )
                 })),
         )

crates/editor2/src/scroll/scroll_amount.rs 🔗

@@ -11,19 +11,18 @@ pub enum ScrollAmount {
 
 impl ScrollAmount {
     pub fn lines(&self, editor: &mut Editor) -> f32 {
-        todo!()
-        // match self {
-        //     Self::Line(count) => *count,
-        //     Self::Page(count) => editor
-        //         .visible_line_count()
-        //         .map(|mut l| {
-        //             // for full pages subtract one to leave an anchor line
-        //             if count.abs() == 1.0 {
-        //                 l -= 1.0
-        //             }
-        //             (l * count).trunc()
-        //         })
-        //         .unwrap_or(0.),
-        // }
+        match self {
+            Self::Line(count) => *count,
+            Self::Page(count) => editor
+                .visible_line_count()
+                .map(|mut l| {
+                    // for full pages subtract one to leave an anchor line
+                    if count.abs() == 1.0 {
+                        l -= 1.0
+                    }
+                    (l * count).trunc()
+                })
+                .unwrap_or(0.),
+        }
     }
 }

crates/editor2/src/selections_collection.rs 🔗

@@ -315,11 +315,14 @@ impl SelectionsCollection {
 
         let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
 
+        dbg!("****START COL****");
         let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
         if start_col < line_len || (is_empty && positions.start == layed_out_line.width) {
             let start = DisplayPoint::new(row, start_col);
+            dbg!("****END COL****");
             let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
             let end = DisplayPoint::new(row, end_col);
+            dbg!(start_col, end_col);
 
             Some(Selection {
                 id: post_inc(&mut self.next_selection_id),

crates/editor2/src/test.rs 🔗

@@ -1,81 +1,74 @@
 pub mod editor_lsp_test_context;
 pub mod editor_test_context;
 
-// todo!()
-// use crate::{
-//     display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
-//     DisplayPoint, Editor, EditorMode, MultiBuffer,
-// };
+use crate::{
+    display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
+    DisplayPoint, Editor, EditorMode, MultiBuffer,
+};
 
-// use gpui::{Model, ViewContext};
+use gpui::{Context, Model, Pixels, ViewContext};
 
-// use project::Project;
-// use util::test::{marked_text_offsets, marked_text_ranges};
+use project::Project;
+use util::test::{marked_text_offsets, marked_text_ranges};
 
-// #[cfg(test)]
-// #[ctor::ctor]
-// fn init_logger() {
-//     if std::env::var("RUST_LOG").is_ok() {
-//         env_logger::init();
-//     }
-// }
+#[cfg(test)]
+#[ctor::ctor]
+fn init_logger() {
+    if std::env::var("RUST_LOG").is_ok() {
+        env_logger::init();
+    }
+}
 
-// // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
-// pub fn marked_display_snapshot(
-//     text: &str,
-//     cx: &mut gpui::AppContext,
-// ) -> (DisplaySnapshot, Vec<DisplayPoint>) {
-//     let (unmarked_text, markers) = marked_text_offsets(text);
+// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
+pub fn marked_display_snapshot(
+    text: &str,
+    cx: &mut gpui::AppContext,
+) -> (DisplaySnapshot, Vec<DisplayPoint>) {
+    let (unmarked_text, markers) = marked_text_offsets(text);
 
-//     let family_id = cx
-//         .font_cache()
-//         .load_family(&["Helvetica"], &Default::default())
-//         .unwrap();
-//     let font_id = cx
-//         .font_cache()
-//         .select_font(family_id, &Default::default())
-//         .unwrap();
-//     let font_size = 14.0;
+    let font = cx.text_style().font();
+    let font_size: Pixels = 14.into();
 
-//     let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
-//     let display_map =
-//         cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
-//     let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
-//     let markers = markers
-//         .into_iter()
-//         .map(|offset| offset.to_display_point(&snapshot))
-//         .collect();
+    let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
+    let display_map = cx.build_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx));
+    let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+    let markers = markers
+        .into_iter()
+        .map(|offset| offset.to_display_point(&snapshot))
+        .collect();
 
-//     (snapshot, markers)
-// }
+    (snapshot, markers)
+}
 
-// pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
-//     let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
-//     assert_eq!(editor.text(cx), unmarked_text);
-//     editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
-// }
+pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
+    let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
+    assert_eq!(editor.text(cx), unmarked_text);
+    editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
+}
 
-// pub fn assert_text_with_selections(
-//     editor: &mut Editor,
-//     marked_text: &str,
-//     cx: &mut ViewContext<Editor>,
-// ) {
-//     let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
-//     assert_eq!(editor.text(cx), unmarked_text);
-//     assert_eq!(editor.selections.ranges(cx), text_ranges);
-// }
+pub fn assert_text_with_selections(
+    editor: &mut Editor,
+    marked_text: &str,
+    cx: &mut ViewContext<Editor>,
+) {
+    let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
+    assert_eq!(editor.text(cx), unmarked_text);
+    assert_eq!(editor.selections.ranges(cx), text_ranges);
+}
 
-// // RA thinks this is dead code even though it is used in a whole lot of tests
-// #[allow(dead_code)]
-// #[cfg(any(test, feature = "test-support"))]
-// pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
-//     Editor::new(EditorMode::Full, buffer, None, None, cx)
-// }
+// RA thinks this is dead code even though it is used in a whole lot of tests
+#[allow(dead_code)]
+#[cfg(any(test, feature = "test-support"))]
+pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
+    // todo!()
+    Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx)
+}
 
-// pub(crate) fn build_editor_with_project(
-//     project: Model<Project>,
-//     buffer: Model<MultiBuffer>,
-//     cx: &mut ViewContext<Editor>,
-// ) -> Editor {
-//     Editor::new(EditorMode::Full, buffer, Some(project), None, cx)
-// }
+pub(crate) fn build_editor_with_project(
+    project: Model<Project>,
+    buffer: Model<MultiBuffer>,
+    cx: &mut ViewContext<Editor>,
+) -> Editor {
+    // todo!()
+    Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx)
+}

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

@@ -1,297 +1,298 @@
-// use std::{
-//     borrow::Cow,
-//     ops::{Deref, DerefMut, Range},
-//     sync::Arc,
-// };
-
-// use anyhow::Result;
-
-// use crate::{Editor, ToPoint};
-// use collections::HashSet;
-// use futures::Future;
-// use gpui::{json, View, ViewContext};
-// use indoc::indoc;
-// use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
-// use lsp::{notification, request};
-// use multi_buffer::ToPointUtf16;
-// use project::Project;
-// use smol::stream::StreamExt;
-// use workspace::{AppState, Workspace, WorkspaceHandle};
-
-// use super::editor_test_context::EditorTestContext;
-
-// pub struct EditorLspTestContext<'a> {
-//     pub cx: EditorTestContext<'a>,
-//     pub lsp: lsp::FakeLanguageServer,
-//     pub workspace: View<Workspace>,
-//     pub buffer_lsp_url: lsp::Url,
-// }
-
-// impl<'a> EditorLspTestContext<'a> {
-//     pub async fn new(
-//         mut language: Language,
-//         capabilities: lsp::ServerCapabilities,
-//         cx: &'a mut gpui::TestAppContext,
-//     ) -> EditorLspTestContext<'a> {
-//         use json::json;
-
-//         let app_state = cx.update(AppState::test);
-
-//         cx.update(|cx| {
-//             language::init(cx);
-//             crate::init(cx);
-//             workspace::init(app_state.clone(), cx);
-//             Project::init_settings(cx);
-//         });
-
-//         let file_name = format!(
-//             "file.{}",
-//             language
-//                 .path_suffixes()
-//                 .first()
-//                 .expect("language must have a path suffix for EditorLspTestContext")
-//         );
-
-//         let mut fake_servers = language
-//             .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-//                 capabilities,
-//                 ..Default::default()
-//             }))
-//             .await;
-
-//         let project = Project::test(app_state.fs.clone(), [], cx).await;
-//         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
-
-//         app_state
-//             .fs
-//             .as_fake()
-//             .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
-//             .await;
-
-//         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)
-//             })
-//             .await
-//             .unwrap();
-//         cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-//             .await;
-
-//         let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
-//         let item = workspace
-//             .update(cx, |workspace, cx| {
-//                 workspace.open_path(file, None, true, cx)
-//             })
-//             .await
-//             .expect("Could not open test file");
-
-//         let editor = cx.update(|cx| {
-//             item.act_as::<Editor>(cx)
-//                 .expect("Opened test file wasn't an editor")
-//         });
-//         editor.update(cx, |_, cx| cx.focus_self());
-
-//         let lsp = fake_servers.next().await.unwrap();
-
-//         Self {
-//             cx: EditorTestContext {
-//                 cx,
-//                 window: window.into(),
-//                 editor,
-//             },
-//             lsp,
-//             workspace,
-//             buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(),
-//         }
-//     }
-
-//     pub async fn new_rust(
-//         capabilities: lsp::ServerCapabilities,
-//         cx: &'a mut gpui::TestAppContext,
-//     ) -> EditorLspTestContext<'a> {
-//         let language = Language::new(
-//             LanguageConfig {
-//                 name: "Rust".into(),
-//                 path_suffixes: vec!["rs".to_string()],
-//                 ..Default::default()
-//             },
-//             Some(tree_sitter_rust::language()),
-//         )
-//         .with_queries(LanguageQueries {
-//             indents: Some(Cow::from(indoc! {r#"
-//                 [
-//                     ((where_clause) _ @end)
-//                     (field_expression)
-//                     (call_expression)
-//                     (assignment_expression)
-//                     (let_declaration)
-//                     (let_chain)
-//                     (await_expression)
-//                 ] @indent
-
-//                 (_ "[" "]" @end) @indent
-//                 (_ "<" ">" @end) @indent
-//                 (_ "{" "}" @end) @indent
-//                 (_ "(" ")" @end) @indent"#})),
-//             brackets: Some(Cow::from(indoc! {r#"
-//                 ("(" @open ")" @close)
-//                 ("[" @open "]" @close)
-//                 ("{" @open "}" @close)
-//                 ("<" @open ">" @close)
-//                 ("\"" @open "\"" @close)
-//                 (closure_parameters "|" @open "|" @close)"#})),
-//             ..Default::default()
-//         })
-//         .expect("Could not parse queries");
-
-//         Self::new(language, capabilities, cx).await
-//     }
-
-//     pub async fn new_typescript(
-//         capabilities: lsp::ServerCapabilities,
-//         cx: &'a mut gpui::TestAppContext,
-//     ) -> EditorLspTestContext<'a> {
-//         let mut word_characters: HashSet<char> = Default::default();
-//         word_characters.insert('$');
-//         word_characters.insert('#');
-//         let language = Language::new(
-//             LanguageConfig {
-//                 name: "Typescript".into(),
-//                 path_suffixes: vec!["ts".to_string()],
-//                 brackets: language::BracketPairConfig {
-//                     pairs: vec![language::BracketPair {
-//                         start: "{".to_string(),
-//                         end: "}".to_string(),
-//                         close: true,
-//                         newline: true,
-//                     }],
-//                     disabled_scopes_by_bracket_ix: Default::default(),
-//                 },
-//                 word_characters,
-//                 ..Default::default()
-//             },
-//             Some(tree_sitter_typescript::language_typescript()),
-//         )
-//         .with_queries(LanguageQueries {
-//             brackets: Some(Cow::from(indoc! {r#"
-//                 ("(" @open ")" @close)
-//                 ("[" @open "]" @close)
-//                 ("{" @open "}" @close)
-//                 ("<" @open ">" @close)
-//                 ("\"" @open "\"" @close)"#})),
-//             indents: Some(Cow::from(indoc! {r#"
-//                 [
-//                     (call_expression)
-//                     (assignment_expression)
-//                     (member_expression)
-//                     (lexical_declaration)
-//                     (variable_declaration)
-//                     (assignment_expression)
-//                     (if_statement)
-//                     (for_statement)
-//                 ] @indent
-
-//                 (_ "[" "]" @end) @indent
-//                 (_ "<" ">" @end) @indent
-//                 (_ "{" "}" @end) @indent
-//                 (_ "(" ")" @end) @indent
-//                 "#})),
-//             ..Default::default()
-//         })
-//         .expect("Could not parse queries");
-
-//         Self::new(language, capabilities, cx).await
-//     }
-
-//     // Constructs lsp range using a marked string with '[', ']' range delimiters
-//     pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
-//         let ranges = self.ranges(marked_text);
-//         self.to_lsp_range(ranges[0].clone())
-//     }
-
-//     pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
-//         let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
-//         let start_point = range.start.to_point(&snapshot.buffer_snapshot);
-//         let end_point = range.end.to_point(&snapshot.buffer_snapshot);
-
-//         self.editor(|editor, cx| {
-//             let buffer = editor.buffer().read(cx);
-//             let start = point_to_lsp(
-//                 buffer
-//                     .point_to_buffer_offset(start_point, cx)
-//                     .unwrap()
-//                     .1
-//                     .to_point_utf16(&buffer.read(cx)),
-//             );
-//             let end = point_to_lsp(
-//                 buffer
-//                     .point_to_buffer_offset(end_point, cx)
-//                     .unwrap()
-//                     .1
-//                     .to_point_utf16(&buffer.read(cx)),
-//             );
-
-//             lsp::Range { start, end }
-//         })
-//     }
-
-//     pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
-//         let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
-//         let point = offset.to_point(&snapshot.buffer_snapshot);
-
-//         self.editor(|editor, cx| {
-//             let buffer = editor.buffer().read(cx);
-//             point_to_lsp(
-//                 buffer
-//                     .point_to_buffer_offset(point, cx)
-//                     .unwrap()
-//                     .1
-//                     .to_point_utf16(&buffer.read(cx)),
-//             )
-//         })
-//     }
-
-//     pub fn update_workspace<F, T>(&mut self, update: F) -> T
-//     where
-//         F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
-//     {
-//         self.workspace.update(self.cx.cx, update)
-//     }
-
-//     pub fn handle_request<T, F, Fut>(
-//         &self,
-//         mut handler: F,
-//     ) -> futures::channel::mpsc::UnboundedReceiver<()>
-//     where
-//         T: 'static + request::Request,
-//         T::Params: 'static + Send,
-//         F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
-//         Fut: 'static + Send + Future<Output = Result<T::Result>>,
-//     {
-//         let url = self.buffer_lsp_url.clone();
-//         self.lsp.handle_request::<T, _, _>(move |params, cx| {
-//             let url = url.clone();
-//             handler(url, params, cx)
-//         })
-//     }
-
-//     pub fn notify<T: notification::Notification>(&self, params: T::Params) {
-//         self.lsp.notify::<T>(params);
-//     }
-// }
-
-// impl<'a> Deref for EditorLspTestContext<'a> {
-//     type Target = EditorTestContext<'a>;
-
-//     fn deref(&self) -> &Self::Target {
-//         &self.cx
-//     }
-// }
-
-// impl<'a> DerefMut for EditorLspTestContext<'a> {
-//     fn deref_mut(&mut self) -> &mut Self::Target {
-//         &mut self.cx
-//     }
-// }
+use std::{
+    borrow::Cow,
+    ops::{Deref, DerefMut, Range},
+    sync::Arc,
+};
+
+use anyhow::Result;
+use serde_json::json;
+
+use crate::{Editor, ToPoint};
+use collections::HashSet;
+use futures::Future;
+use gpui::{View, ViewContext, VisualTestContext};
+use indoc::indoc;
+use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
+use lsp::{notification, request};
+use multi_buffer::ToPointUtf16;
+use project::Project;
+use smol::stream::StreamExt;
+use workspace::{AppState, Workspace, WorkspaceHandle};
+
+use super::editor_test_context::{AssertionContextManager, EditorTestContext};
+
+pub struct EditorLspTestContext<'a> {
+    pub cx: EditorTestContext<'a>,
+    pub lsp: lsp::FakeLanguageServer,
+    pub workspace: View<Workspace>,
+    pub buffer_lsp_url: lsp::Url,
+}
+
+impl<'a> EditorLspTestContext<'a> {
+    pub async fn new(
+        mut language: Language,
+        capabilities: lsp::ServerCapabilities,
+        cx: &'a mut gpui::TestAppContext,
+    ) -> EditorLspTestContext<'a> {
+        let app_state = cx.update(AppState::test);
+
+        cx.update(|cx| {
+            language::init(cx);
+            crate::init(cx);
+            workspace::init(app_state.clone(), cx);
+            Project::init_settings(cx);
+        });
+
+        let file_name = format!(
+            "file.{}",
+            language
+                .path_suffixes()
+                .first()
+                .expect("language must have a path suffix for EditorLspTestContext")
+        );
+
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities,
+                ..Default::default()
+            }))
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
+            .await;
+
+        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+
+        let workspace = window.root_view(cx).unwrap();
+
+        let mut cx = VisualTestContext::from_window(*window.deref(), cx);
+        project
+            .update(&mut cx, |project, cx| {
+                project.find_or_create_local_worktree("/root", true, cx)
+            })
+            .await
+            .unwrap();
+        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
+            .await;
+        let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
+        let item = workspace
+            .update(&mut cx, |workspace, cx| {
+                workspace.open_path(file, None, true, cx)
+            })
+            .await
+            .expect("Could not open test file");
+        let editor = cx.update(|cx| {
+            item.act_as::<Editor>(cx)
+                .expect("Opened test file wasn't an editor")
+        });
+        editor.update(&mut cx, |editor, cx| editor.focus(cx));
+
+        let lsp = fake_servers.next().await.unwrap();
+        Self {
+            cx: EditorTestContext {
+                cx,
+                window: window.into(),
+                editor,
+                assertion_cx: AssertionContextManager::new(),
+            },
+            lsp,
+            workspace,
+            buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(),
+        }
+    }
+
+    pub async fn new_rust(
+        capabilities: lsp::ServerCapabilities,
+        cx: &'a mut gpui::TestAppContext,
+    ) -> EditorLspTestContext<'a> {
+        let language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_queries(LanguageQueries {
+            indents: Some(Cow::from(indoc! {r#"
+                [
+                    ((where_clause) _ @end)
+                    (field_expression)
+                    (call_expression)
+                    (assignment_expression)
+                    (let_declaration)
+                    (let_chain)
+                    (await_expression)
+                ] @indent
+
+                (_ "[" "]" @end) @indent
+                (_ "<" ">" @end) @indent
+                (_ "{" "}" @end) @indent
+                (_ "(" ")" @end) @indent"#})),
+            brackets: Some(Cow::from(indoc! {r#"
+                ("(" @open ")" @close)
+                ("[" @open "]" @close)
+                ("{" @open "}" @close)
+                ("<" @open ">" @close)
+                ("\"" @open "\"" @close)
+                (closure_parameters "|" @open "|" @close)"#})),
+            ..Default::default()
+        })
+        .expect("Could not parse queries");
+
+        Self::new(language, capabilities, cx).await
+    }
+
+    pub async fn new_typescript(
+        capabilities: lsp::ServerCapabilities,
+        cx: &'a mut gpui::TestAppContext,
+    ) -> EditorLspTestContext<'a> {
+        let mut word_characters: HashSet<char> = Default::default();
+        word_characters.insert('$');
+        word_characters.insert('#');
+        let language = Language::new(
+            LanguageConfig {
+                name: "Typescript".into(),
+                path_suffixes: vec!["ts".to_string()],
+                brackets: language::BracketPairConfig {
+                    pairs: vec![language::BracketPair {
+                        start: "{".to_string(),
+                        end: "}".to_string(),
+                        close: true,
+                        newline: true,
+                    }],
+                    disabled_scopes_by_bracket_ix: Default::default(),
+                },
+                word_characters,
+                ..Default::default()
+            },
+            Some(tree_sitter_typescript::language_typescript()),
+        )
+        .with_queries(LanguageQueries {
+            brackets: Some(Cow::from(indoc! {r#"
+                ("(" @open ")" @close)
+                ("[" @open "]" @close)
+                ("{" @open "}" @close)
+                ("<" @open ">" @close)
+                ("\"" @open "\"" @close)"#})),
+            indents: Some(Cow::from(indoc! {r#"
+                [
+                    (call_expression)
+                    (assignment_expression)
+                    (member_expression)
+                    (lexical_declaration)
+                    (variable_declaration)
+                    (assignment_expression)
+                    (if_statement)
+                    (for_statement)
+                ] @indent
+
+                (_ "[" "]" @end) @indent
+                (_ "<" ">" @end) @indent
+                (_ "{" "}" @end) @indent
+                (_ "(" ")" @end) @indent
+                "#})),
+            ..Default::default()
+        })
+        .expect("Could not parse queries");
+
+        Self::new(language, capabilities, cx).await
+    }
+
+    // Constructs lsp range using a marked string with '[', ']' range delimiters
+    pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
+        let ranges = self.ranges(marked_text);
+        self.to_lsp_range(ranges[0].clone())
+    }
+
+    pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
+        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+        let start_point = range.start.to_point(&snapshot.buffer_snapshot);
+        let end_point = range.end.to_point(&snapshot.buffer_snapshot);
+
+        self.editor(|editor, cx| {
+            let buffer = editor.buffer().read(cx);
+            let start = point_to_lsp(
+                buffer
+                    .point_to_buffer_offset(start_point, cx)
+                    .unwrap()
+                    .1
+                    .to_point_utf16(&buffer.read(cx)),
+            );
+            let end = point_to_lsp(
+                buffer
+                    .point_to_buffer_offset(end_point, cx)
+                    .unwrap()
+                    .1
+                    .to_point_utf16(&buffer.read(cx)),
+            );
+
+            lsp::Range { start, end }
+        })
+    }
+
+    pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
+        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+        let point = offset.to_point(&snapshot.buffer_snapshot);
+
+        self.editor(|editor, cx| {
+            let buffer = editor.buffer().read(cx);
+            point_to_lsp(
+                buffer
+                    .point_to_buffer_offset(point, cx)
+                    .unwrap()
+                    .1
+                    .to_point_utf16(&buffer.read(cx)),
+            )
+        })
+    }
+
+    pub fn update_workspace<F, T>(&mut self, update: F) -> T
+    where
+        F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
+    {
+        self.workspace.update(&mut self.cx.cx, update)
+    }
+
+    pub fn handle_request<T, F, Fut>(
+        &self,
+        mut handler: F,
+    ) -> futures::channel::mpsc::UnboundedReceiver<()>
+    where
+        T: 'static + request::Request,
+        T::Params: 'static + Send,
+        F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
+        Fut: 'static + Send + Future<Output = Result<T::Result>>,
+    {
+        let url = self.buffer_lsp_url.clone();
+        self.lsp.handle_request::<T, _, _>(move |params, cx| {
+            let url = url.clone();
+            handler(url, params, cx)
+        })
+    }
+
+    pub fn notify<T: notification::Notification>(&self, params: T::Params) {
+        self.lsp.notify::<T>(params);
+    }
+}
+
+impl<'a> Deref for EditorLspTestContext<'a> {
+    type Target = EditorTestContext<'a>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.cx
+    }
+}
+
+impl<'a> DerefMut for EditorLspTestContext<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.cx
+    }
+}

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

@@ -1,331 +1,400 @@
 use crate::{
     display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
 };
+use collections::BTreeMap;
 use futures::Future;
 use gpui::{
     AnyWindowHandle, AppContext, ForegroundExecutor, Keystroke, ModelContext, View, ViewContext,
+    VisualTestContext, WindowHandle,
 };
 use indoc::indoc;
+use itertools::Itertools;
 use language::{Buffer, BufferSnapshot};
+use parking_lot::RwLock;
 use project::{FakeFs, Project};
 use std::{
     any::TypeId,
     ops::{Deref, DerefMut, Range},
+    sync::{
+        atomic::{AtomicUsize, Ordering},
+        Arc,
+    },
 };
 use util::{
     assert_set_eq,
     test::{generate_marked_text, marked_text_ranges},
 };
 
-// use super::build_editor_with_project;
-
-// pub struct EditorTestContext<'a> {
-//     pub cx: &'a mut gpui::TestAppContext,
-//     pub window: AnyWindowHandle,
-//     pub editor: View<Editor>,
-// }
-
-// impl<'a> EditorTestContext<'a> {
-//     pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
-//         let fs = FakeFs::new(cx.background());
-//         // fs.insert_file("/file", "".to_owned()).await;
-//         fs.insert_tree(
-//             "/root",
-//             gpui::serde_json::json!({
-//                 "file": "",
-//             }),
-//         )
-//         .await;
-//         let project = Project::test(fs, ["/root".as_ref()], cx).await;
-//         let buffer = project
-//             .update(cx, |project, cx| {
-//                 project.open_local_buffer("/root/file", cx)
-//             })
-//             .await
-//             .unwrap();
-//         let window = cx.add_window(|cx| {
-//             cx.focus_self();
-//             build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx)
-//         });
-//         let editor = window.root(cx);
-//         Self {
-//             cx,
-//             window: window.into(),
-//             editor,
-//         }
-//     }
-
-//     pub fn condition(
-//         &self,
-//         predicate: impl FnMut(&Editor, &AppContext) -> bool,
-//     ) -> impl Future<Output = ()> {
-//         self.editor.condition(self.cx, predicate)
-//     }
-
-//     pub fn editor<F, T>(&self, read: F) -> T
-//     where
-//         F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
-//     {
-//         self.editor.update(self.cx, read)
-//     }
-
-//     pub fn update_editor<F, T>(&mut self, update: F) -> T
-//     where
-//         F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
-//     {
-//         self.editor.update(self.cx, update)
-//     }
-
-//     pub fn multibuffer<F, T>(&self, read: F) -> T
-//     where
-//         F: FnOnce(&MultiBuffer, &AppContext) -> T,
-//     {
-//         self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
-//     }
-
-//     pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
-//     where
-//         F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
-//     {
-//         self.update_editor(|editor, cx| editor.buffer().update(cx, update))
-//     }
-
-//     pub fn buffer_text(&self) -> String {
-//         self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
-//     }
-
-//     pub fn buffer<F, T>(&self, read: F) -> T
-//     where
-//         F: FnOnce(&Buffer, &AppContext) -> T,
-//     {
-//         self.multibuffer(|multibuffer, cx| {
-//             let buffer = multibuffer.as_singleton().unwrap().read(cx);
-//             read(buffer, cx)
-//         })
-//     }
-
-//     pub fn update_buffer<F, T>(&mut self, update: F) -> T
-//     where
-//         F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
-//     {
-//         self.update_multibuffer(|multibuffer, cx| {
-//             let buffer = multibuffer.as_singleton().unwrap();
-//             buffer.update(cx, update)
-//         })
-//     }
-
-//     pub fn buffer_snapshot(&self) -> BufferSnapshot {
-//         self.buffer(|buffer, _| buffer.snapshot())
-//     }
-
-// pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
-//     let keystroke_under_test_handle =
-//         self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
-//     let keystroke = Keystroke::parse(keystroke_text).unwrap();
-
-//     self.cx.dispatch_keystroke(self.window, keystroke, false);
-
-//     keystroke_under_test_handle
-// }
-
-// pub fn simulate_keystrokes<const COUNT: usize>(
-//     &mut self,
-//     keystroke_texts: [&str; COUNT],
-// ) -> ContextHandle {
-//     let keystrokes_under_test_handle =
-//         self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
-//     for keystroke_text in keystroke_texts.into_iter() {
-//         self.simulate_keystroke(keystroke_text);
-//     }
-//     // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
-//     // before returning.
-//     // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
-//     // quickly races with async actions.
-//     if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() {
-//         executor.run_until_parked();
-//     } else {
-//         unreachable!();
-//     }
-
-//     keystrokes_under_test_handle
-// }
-
-// pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
-//     let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
-//     assert_eq!(self.buffer_text(), unmarked_text);
-//     ranges
-// }
-
-// pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
-//     let ranges = self.ranges(marked_text);
-//     let snapshot = self
-//         .editor
-//         .update(self.cx, |editor, cx| editor.snapshot(cx));
-//     ranges[0].start.to_display_point(&snapshot)
-// }
-
-// // Returns anchors for the current buffer using `«` and `»`
-// pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
-//     let ranges = self.ranges(marked_text);
-//     let snapshot = self.buffer_snapshot();
-//     snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
-// }
-
-// pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
-//     let diff_base = diff_base.map(String::from);
-//     self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
-// }
-
-// /// Change the editor's text and selections using a string containing
-// /// embedded range markers that represent the ranges and directions of
-// /// each selection.
-// ///
-// /// Returns a context handle so that assertion failures can print what
-// /// editor state was needed to cause the failure.
-// ///
-// /// See the `util::test::marked_text_ranges` function for more information.
-// pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
-//     let state_context = self.add_assertion_context(format!(
-//         "Initial Editor State: \"{}\"",
-//         marked_text.escape_debug().to_string()
-//     ));
-//     let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
-//     self.editor.update(self.cx, |editor, cx| {
-//         editor.set_text(unmarked_text, cx);
-//         editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-//             s.select_ranges(selection_ranges)
-//         })
-//     });
-//     state_context
-// }
-
-// /// Only change the editor's selections
-// pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
-//     let state_context = self.add_assertion_context(format!(
-//         "Initial Editor State: \"{}\"",
-//         marked_text.escape_debug().to_string()
-//     ));
-//     let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
-//     self.editor.update(self.cx, |editor, cx| {
-//         assert_eq!(editor.text(cx), unmarked_text);
-//         editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-//             s.select_ranges(selection_ranges)
-//         })
-//     });
-//     state_context
-// }
-
-// /// Make an assertion about the editor's text and the ranges and directions
-// /// of its selections using a string containing embedded range markers.
-// ///
-// /// See the `util::test::marked_text_ranges` function for more information.
-// #[track_caller]
-// pub fn assert_editor_state(&mut self, marked_text: &str) {
-//     let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
-//     let buffer_text = self.buffer_text();
-
-//     if buffer_text != unmarked_text {
-//         panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
-//     }
-
-//     self.assert_selections(expected_selections, marked_text.to_string())
-// }
-
-// pub fn editor_state(&mut self) -> String {
-//     generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
-// }
-
-// #[track_caller]
-// pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
-//     let expected_ranges = self.ranges(marked_text);
-//     let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
-//         let snapshot = editor.snapshot(cx);
-//         editor
-//             .background_highlights
-//             .get(&TypeId::of::<Tag>())
-//             .map(|h| h.1.clone())
-//             .unwrap_or_default()
-//             .into_iter()
-//             .map(|range| range.to_offset(&snapshot.buffer_snapshot))
-//             .collect()
-//     });
-//     assert_set_eq!(actual_ranges, expected_ranges);
-// }
-
-// #[track_caller]
-// pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
-//     let expected_ranges = self.ranges(marked_text);
-//     let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
-//     let actual_ranges: Vec<Range<usize>> = snapshot
-//         .text_highlight_ranges::<Tag>()
-//         .map(|ranges| ranges.as_ref().clone().1)
-//         .unwrap_or_default()
-//         .into_iter()
-//         .map(|range| range.to_offset(&snapshot.buffer_snapshot))
-//         .collect();
-//     assert_set_eq!(actual_ranges, expected_ranges);
-// }
-
-// #[track_caller]
-// pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
-//     let expected_marked_text =
-//         generate_marked_text(&self.buffer_text(), &expected_selections, true);
-//     self.assert_selections(expected_selections, expected_marked_text)
-// }
-
-// fn editor_selections(&self) -> Vec<Range<usize>> {
-//     self.editor
-//         .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
-//         .into_iter()
-//         .map(|s| {
-//             if s.reversed {
-//                 s.end..s.start
-//             } else {
-//                 s.start..s.end
-//             }
-//         })
-//         .collect::<Vec<_>>()
-// }
-
-// #[track_caller]
-// fn assert_selections(
-//     &mut self,
-//     expected_selections: Vec<Range<usize>>,
-//     expected_marked_text: String,
-// ) {
-//     let actual_selections = self.editor_selections();
-//     let actual_marked_text =
-//         generate_marked_text(&self.buffer_text(), &actual_selections, true);
-//     if expected_selections != actual_selections {
-//         panic!(
-//             indoc! {"
-
-//                 {}Editor has unexpected selections.
-
-//                 Expected selections:
-//                 {}
-
-//                 Actual selections:
-//                 {}
-//             "},
-//             self.assertion_context(),
-//             expected_marked_text,
-//             actual_marked_text,
-//         );
-//     }
-// }
-// }
-//
-// impl<'a> Deref for EditorTestContext<'a> {
-//     type Target = gpui::TestAppContext;
-
-//     fn deref(&self) -> &Self::Target {
-//         self.cx
-//     }
-// }
-
-// impl<'a> DerefMut for EditorTestContext<'a> {
-//     fn deref_mut(&mut self) -> &mut Self::Target {
-//         &mut self.cx
-//     }
-// }
+use super::build_editor_with_project;
+
+pub struct EditorTestContext<'a> {
+    pub cx: gpui::VisualTestContext<'a>,
+    pub window: AnyWindowHandle,
+    pub editor: View<Editor>,
+    pub assertion_cx: AssertionContextManager,
+}
+
+impl<'a> EditorTestContext<'a> {
+    pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
+        let fs = FakeFs::new(cx.executor());
+        // fs.insert_file("/file", "".to_owned()).await;
+        fs.insert_tree(
+            "/root",
+            gpui::serde_json::json!({
+                "file": "",
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/root".as_ref()], cx).await;
+        let buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/root/file", cx)
+            })
+            .await
+            .unwrap();
+        let editor = cx.add_window(|cx| {
+            let editor =
+                build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx);
+            editor.focus(cx);
+            editor
+        });
+        let editor_view = editor.root_view(cx).unwrap();
+        Self {
+            cx: VisualTestContext::from_window(*editor.deref(), cx),
+            window: editor.into(),
+            editor: editor_view,
+            assertion_cx: AssertionContextManager::new(),
+        }
+    }
+
+    pub fn condition(
+        &self,
+        predicate: impl FnMut(&Editor, &AppContext) -> bool,
+    ) -> impl Future<Output = ()> {
+        self.editor.condition::<crate::Event>(&self.cx, predicate)
+    }
+
+    #[track_caller]
+    pub fn editor<F, T>(&mut self, read: F) -> T
+    where
+        F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
+    {
+        self.editor
+            .update(&mut self.cx, |this, cx| read(&this, &cx))
+    }
+
+    #[track_caller]
+    pub fn update_editor<F, T>(&mut self, update: F) -> T
+    where
+        F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
+    {
+        self.editor.update(&mut self.cx, update)
+    }
+
+    pub fn multibuffer<F, T>(&mut self, read: F) -> T
+    where
+        F: FnOnce(&MultiBuffer, &AppContext) -> T,
+    {
+        self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
+    }
+
+    pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
+    where
+        F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
+    {
+        self.update_editor(|editor, cx| editor.buffer().update(cx, update))
+    }
+
+    pub fn buffer_text(&mut self) -> String {
+        self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
+    }
+
+    pub fn buffer<F, T>(&mut self, read: F) -> T
+    where
+        F: FnOnce(&Buffer, &AppContext) -> T,
+    {
+        self.multibuffer(|multibuffer, cx| {
+            let buffer = multibuffer.as_singleton().unwrap().read(cx);
+            read(buffer, cx)
+        })
+    }
+
+    pub fn update_buffer<F, T>(&mut self, update: F) -> T
+    where
+        F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
+    {
+        self.update_multibuffer(|multibuffer, cx| {
+            let buffer = multibuffer.as_singleton().unwrap();
+            buffer.update(cx, update)
+        })
+    }
+
+    pub fn buffer_snapshot(&mut self) -> BufferSnapshot {
+        self.buffer(|buffer, _| buffer.snapshot())
+    }
+
+    pub fn add_assertion_context(&self, context: String) -> ContextHandle {
+        self.assertion_cx.add_context(context)
+    }
+
+    pub fn assertion_context(&self) -> String {
+        self.assertion_cx.context()
+    }
+
+    pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
+        let keystroke_under_test_handle =
+            self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
+        let keystroke = Keystroke::parse(keystroke_text).unwrap();
+
+        self.cx.dispatch_keystroke(self.window, keystroke, false);
+
+        keystroke_under_test_handle
+    }
+
+    pub fn simulate_keystrokes<const COUNT: usize>(
+        &mut self,
+        keystroke_texts: [&str; COUNT],
+    ) -> ContextHandle {
+        let keystrokes_under_test_handle =
+            self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
+        for keystroke_text in keystroke_texts.into_iter() {
+            self.simulate_keystroke(keystroke_text);
+        }
+        // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
+        // before returning.
+        // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
+        // quickly races with async actions.
+        self.cx.background_executor.run_until_parked();
+
+        keystrokes_under_test_handle
+    }
+
+    pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
+        let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
+        assert_eq!(self.buffer_text(), unmarked_text);
+        ranges
+    }
+
+    pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
+        let ranges = self.ranges(marked_text);
+        let snapshot = self
+            .editor
+            .update(&mut self.cx, |editor, cx| editor.snapshot(cx));
+        ranges[0].start.to_display_point(&snapshot)
+    }
+
+    // Returns anchors for the current buffer using `«` and `»`
+    pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
+        let ranges = self.ranges(marked_text);
+        let snapshot = self.buffer_snapshot();
+        snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
+    }
+
+    pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
+        let diff_base = diff_base.map(String::from);
+        self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
+    }
+
+    /// Change the editor's text and selections using a string containing
+    /// embedded range markers that represent the ranges and directions of
+    /// each selection.
+    ///
+    /// Returns a context handle so that assertion failures can print what
+    /// editor state was needed to cause the failure.
+    ///
+    /// See the `util::test::marked_text_ranges` function for more information.
+    pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
+        let state_context = self.add_assertion_context(format!(
+            "Initial Editor State: \"{}\"",
+            marked_text.escape_debug().to_string()
+        ));
+        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
+        self.editor.update(&mut self.cx, |editor, cx| {
+            editor.set_text(unmarked_text, cx);
+            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select_ranges(selection_ranges)
+            })
+        });
+        state_context
+    }
+
+    /// Only change the editor's selections
+    pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
+        let state_context = self.add_assertion_context(format!(
+            "Initial Editor State: \"{}\"",
+            marked_text.escape_debug().to_string()
+        ));
+        let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
+        self.editor.update(&mut self.cx, |editor, cx| {
+            assert_eq!(editor.text(cx), unmarked_text);
+            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select_ranges(selection_ranges)
+            })
+        });
+        state_context
+    }
+
+    /// Make an assertion about the editor's text and the ranges and directions
+    /// of its selections using a string containing embedded range markers.
+    ///
+    /// See the `util::test::marked_text_ranges` function for more information.
+    #[track_caller]
+    pub fn assert_editor_state(&mut self, marked_text: &str) {
+        let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
+        let buffer_text = self.buffer_text();
+
+        if buffer_text != unmarked_text {
+            panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
+        }
+
+        self.assert_selections(expected_selections, marked_text.to_string())
+    }
+
+    pub fn editor_state(&mut self) -> String {
+        generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
+    }
+
+    #[track_caller]
+    pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
+        let expected_ranges = self.ranges(marked_text);
+        let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            editor
+                .background_highlights
+                .get(&TypeId::of::<Tag>())
+                .map(|h| h.1.clone())
+                .unwrap_or_default()
+                .into_iter()
+                .map(|range| range.to_offset(&snapshot.buffer_snapshot))
+                .collect()
+        });
+        assert_set_eq!(actual_ranges, expected_ranges);
+    }
+
+    #[track_caller]
+    pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
+        let expected_ranges = self.ranges(marked_text);
+        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+        let actual_ranges: Vec<Range<usize>> = snapshot
+            .text_highlight_ranges::<Tag>()
+            .map(|ranges| ranges.as_ref().clone().1)
+            .unwrap_or_default()
+            .into_iter()
+            .map(|range| range.to_offset(&snapshot.buffer_snapshot))
+            .collect();
+        assert_set_eq!(actual_ranges, expected_ranges);
+    }
+
+    #[track_caller]
+    pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
+        let expected_marked_text =
+            generate_marked_text(&self.buffer_text(), &expected_selections, true);
+        self.assert_selections(expected_selections, expected_marked_text)
+    }
+
+    #[track_caller]
+    fn editor_selections(&mut self) -> Vec<Range<usize>> {
+        self.editor
+            .update(&mut self.cx, |editor, cx| {
+                editor.selections.all::<usize>(cx)
+            })
+            .into_iter()
+            .map(|s| {
+                if s.reversed {
+                    s.end..s.start
+                } else {
+                    s.start..s.end
+                }
+            })
+            .collect::<Vec<_>>()
+    }
+
+    #[track_caller]
+    fn assert_selections(
+        &mut self,
+        expected_selections: Vec<Range<usize>>,
+        expected_marked_text: String,
+    ) {
+        let actual_selections = self.editor_selections();
+        let actual_marked_text =
+            generate_marked_text(&self.buffer_text(), &actual_selections, true);
+        if expected_selections != actual_selections {
+            panic!(
+                indoc! {"
+
+                {}Editor has unexpected selections.
+
+                Expected selections:
+                {}
+
+                Actual selections:
+                {}
+            "},
+                self.assertion_context(),
+                expected_marked_text,
+                actual_marked_text,
+            );
+        }
+    }
+}
+
+impl<'a> Deref for EditorTestContext<'a> {
+    type Target = gpui::TestAppContext;
+
+    fn deref(&self) -> &Self::Target {
+        &self.cx
+    }
+}
+
+impl<'a> DerefMut for EditorTestContext<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.cx
+    }
+}
+
+/// Tracks string context to be printed when assertions fail.
+/// Often this is done by storing a context string in the manager and returning the handle.
+#[derive(Clone)]
+pub struct AssertionContextManager {
+    id: Arc<AtomicUsize>,
+    contexts: Arc<RwLock<BTreeMap<usize, String>>>,
+}
+
+impl AssertionContextManager {
+    pub fn new() -> Self {
+        Self {
+            id: Arc::new(AtomicUsize::new(0)),
+            contexts: Arc::new(RwLock::new(BTreeMap::new())),
+        }
+    }
+
+    pub fn add_context(&self, context: String) -> ContextHandle {
+        let id = self.id.fetch_add(1, Ordering::Relaxed);
+        let mut contexts = self.contexts.write();
+        contexts.insert(id, context);
+        ContextHandle {
+            id,
+            manager: self.clone(),
+        }
+    }
+
+    pub fn context(&self) -> String {
+        let contexts = self.contexts.read();
+        format!("\n{}\n", contexts.values().join("\n"))
+    }
+}
+
+/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
+/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
+/// the state that was set initially for the failure can be printed in the error message
+pub struct ContextHandle {
+    id: usize,
+    manager: AssertionContextManager,
+}
+
+impl Drop for ContextHandle {
+    fn drop(&mut self) {
+        let mut contexts = self.manager.contexts.write();
+        contexts.remove(&self.id);
+    }
+}

crates/file_finder2/Cargo.toml 🔗

@@ -0,0 +1,37 @@
+[package]
+name = "file_finder2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/file_finder.rs"
+doctest = false
+
+[dependencies]
+editor = { package = "editor2", path = "../editor2" }
+collections = { path = "../collections" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+menu = { package = "menu2", path = "../menu2" }
+picker = { package = "picker2", path = "../picker2" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+text = { package = "text2", path = "../text2" }
+util = { path = "../util" }
+theme = { package = "theme2", path = "../theme2" }
+ui = { package = "ui2", path = "../ui2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+postage.workspace = true
+serde.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+language = { package = "language2", path = "../language2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
+
+serde_json.workspace = true
+ctor.workspace = true
+env_logger.workspace = true

crates/file_finder2/src/file_finder.rs 🔗

@@ -0,0 +1,1973 @@
+use collections::HashMap;
+use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
+use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
+use gpui::{
+    actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model,
+    ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
+    WindowContext,
+};
+use picker::{Picker, PickerDelegate};
+use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
+use std::{
+    path::{Path, PathBuf},
+    sync::{
+        atomic::{self, AtomicBool},
+        Arc,
+    },
+};
+use text::Point;
+use theme::ActiveTheme;
+use ui::{v_stack, HighlightedLabel, StyledExt};
+use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
+use workspace::{Modal, ModalEvent, Workspace};
+
+actions!(Toggle);
+
+pub struct FileFinder {
+    picker: View<Picker<FileFinderDelegate>>,
+}
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(FileFinder::register).detach();
+}
+
+impl FileFinder {
+    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+        workspace.register_action(|workspace, _: &Toggle, cx| {
+            let Some(file_finder) = workspace.current_modal::<Self>(cx) else {
+                Self::open(workspace, cx);
+                return;
+            };
+
+            file_finder.update(cx, |file_finder, cx| {
+                file_finder
+                    .picker
+                    .update(cx, |picker, cx| picker.cycle_selection(cx))
+            });
+        });
+    }
+
+    fn open(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+        let project = workspace.project().read(cx);
+
+        let currently_opened_path = workspace
+            .active_item(cx)
+            .and_then(|item| item.project_path(cx))
+            .map(|project_path| {
+                let abs_path = project
+                    .worktree_for_id(project_path.worktree_id, cx)
+                    .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
+                FoundPath::new(project_path, abs_path)
+            });
+
+        // if exists, bubble the currently opened path to the top
+        let history_items = currently_opened_path
+            .clone()
+            .into_iter()
+            .chain(
+                workspace
+                    .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
+                    .into_iter()
+                    .filter(|(history_path, _)| {
+                        Some(history_path)
+                            != currently_opened_path
+                                .as_ref()
+                                .map(|found_path| &found_path.project)
+                    })
+                    .filter(|(_, history_abs_path)| {
+                        history_abs_path.as_ref()
+                            != currently_opened_path
+                                .as_ref()
+                                .and_then(|found_path| found_path.absolute.as_ref())
+                    })
+                    .filter(|(_, history_abs_path)| match history_abs_path {
+                        Some(abs_path) => history_file_exists(abs_path),
+                        None => true,
+                    })
+                    .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
+            )
+            .collect();
+
+        let project = workspace.project().clone();
+        let weak_workspace = cx.view().downgrade();
+        workspace.toggle_modal(cx, |cx| {
+            let delegate = FileFinderDelegate::new(
+                cx.view().downgrade(),
+                weak_workspace,
+                project,
+                currently_opened_path,
+                history_items,
+                cx,
+            );
+
+            FileFinder::new(delegate, cx)
+        });
+    }
+
+    fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
+        Self {
+            picker: cx.build_view(|cx| Picker::new(delegate, cx)),
+        }
+    }
+}
+
+impl EventEmitter<ModalEvent> for FileFinder {}
+impl Modal for FileFinder {
+    fn focus(&self, cx: &mut WindowContext) {
+        self.picker.update(cx, |picker, cx| picker.focus(cx))
+    }
+}
+impl Render for FileFinder {
+    type Element = Div<Self>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        v_stack().w_96().child(self.picker.clone())
+    }
+}
+
+pub struct FileFinderDelegate {
+    file_finder: WeakView<FileFinder>,
+    workspace: WeakView<Workspace>,
+    project: Model<Project>,
+    search_count: usize,
+    latest_search_id: usize,
+    latest_search_did_cancel: bool,
+    latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
+    currently_opened_path: Option<FoundPath>,
+    matches: Matches,
+    selected_index: Option<usize>,
+    cancel_flag: Arc<AtomicBool>,
+    history_items: Vec<FoundPath>,
+}
+
+#[derive(Debug, Default)]
+struct Matches {
+    history: Vec<(FoundPath, Option<PathMatch>)>,
+    search: Vec<PathMatch>,
+}
+
+#[derive(Debug)]
+enum Match<'a> {
+    History(&'a FoundPath, Option<&'a PathMatch>),
+    Search(&'a PathMatch),
+}
+
+impl Matches {
+    fn len(&self) -> usize {
+        self.history.len() + self.search.len()
+    }
+
+    fn get(&self, index: usize) -> Option<Match<'_>> {
+        if index < self.history.len() {
+            self.history
+                .get(index)
+                .map(|(path, path_match)| Match::History(path, path_match.as_ref()))
+        } else {
+            self.search
+                .get(index - self.history.len())
+                .map(Match::Search)
+        }
+    }
+
+    fn push_new_matches(
+        &mut self,
+        history_items: &Vec<FoundPath>,
+        query: &PathLikeWithPosition<FileSearchQuery>,
+        mut new_search_matches: Vec<PathMatch>,
+        extend_old_matches: bool,
+    ) {
+        let matching_history_paths = matching_history_item_paths(history_items, query);
+        new_search_matches
+            .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
+        let history_items_to_show = history_items
+            .iter()
+            .filter_map(|history_item| {
+                Some((
+                    history_item.clone(),
+                    Some(
+                        matching_history_paths
+                            .get(&history_item.project.path)?
+                            .clone(),
+                    ),
+                ))
+            })
+            .collect::<Vec<_>>();
+        self.history = history_items_to_show;
+        if extend_old_matches {
+            self.search
+                .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
+            util::extend_sorted(
+                &mut self.search,
+                new_search_matches.into_iter(),
+                100,
+                |a, b| b.cmp(a),
+            )
+        } else {
+            self.search = new_search_matches;
+        }
+    }
+}
+
+fn matching_history_item_paths(
+    history_items: &Vec<FoundPath>,
+    query: &PathLikeWithPosition<FileSearchQuery>,
+) -> HashMap<Arc<Path>, PathMatch> {
+    let history_items_by_worktrees = history_items
+        .iter()
+        .filter_map(|found_path| {
+            let candidate = PathMatchCandidate {
+                path: &found_path.project.path,
+                // Only match history items names, otherwise their paths may match too many queries, producing false positives.
+                // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
+                // it would be shown first always, despite the latter being a better match.
+                char_bag: CharBag::from_iter(
+                    found_path
+                        .project
+                        .path
+                        .file_name()?
+                        .to_string_lossy()
+                        .to_lowercase()
+                        .chars(),
+                ),
+            };
+            Some((found_path.project.worktree_id, candidate))
+        })
+        .fold(
+            HashMap::default(),
+            |mut candidates, (worktree_id, new_candidate)| {
+                candidates
+                    .entry(worktree_id)
+                    .or_insert_with(Vec::new)
+                    .push(new_candidate);
+                candidates
+            },
+        );
+    let mut matching_history_paths = HashMap::default();
+    for (worktree, candidates) in history_items_by_worktrees {
+        let max_results = candidates.len() + 1;
+        matching_history_paths.extend(
+            fuzzy::match_fixed_path_set(
+                candidates,
+                worktree.to_usize(),
+                query.path_like.path_query(),
+                false,
+                max_results,
+            )
+            .into_iter()
+            .map(|path_match| (Arc::clone(&path_match.path), path_match)),
+        );
+    }
+    matching_history_paths
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct FoundPath {
+    project: ProjectPath,
+    absolute: Option<PathBuf>,
+}
+
+impl FoundPath {
+    fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
+        Self { project, absolute }
+    }
+}
+
+const MAX_RECENT_SELECTIONS: usize = 20;
+
+#[cfg(not(test))]
+fn history_file_exists(abs_path: &PathBuf) -> bool {
+    abs_path.exists()
+}
+
+#[cfg(test)]
+fn history_file_exists(abs_path: &PathBuf) -> bool {
+    !abs_path.ends_with("nonexistent.rs")
+}
+
+pub enum Event {
+    Selected(ProjectPath),
+    Dismissed,
+}
+
+#[derive(Debug, Clone)]
+struct FileSearchQuery {
+    raw_query: String,
+    file_query_end: Option<usize>,
+}
+
+impl FileSearchQuery {
+    fn path_query(&self) -> &str {
+        match self.file_query_end {
+            Some(file_path_end) => &self.raw_query[..file_path_end],
+            None => &self.raw_query,
+        }
+    }
+}
+
+impl FileFinderDelegate {
+    fn new(
+        file_finder: WeakView<FileFinder>,
+        workspace: WeakView<Workspace>,
+        project: Model<Project>,
+        currently_opened_path: Option<FoundPath>,
+        history_items: Vec<FoundPath>,
+        cx: &mut ViewContext<FileFinder>,
+    ) -> Self {
+        cx.observe(&project, |file_finder, _, cx| {
+            //todo!() We should probably not re-render on every project anything
+            file_finder
+                .picker
+                .update(cx, |picker, cx| picker.refresh(cx))
+        })
+        .detach();
+
+        Self {
+            file_finder,
+            workspace,
+            project,
+            search_count: 0,
+            latest_search_id: 0,
+            latest_search_did_cancel: false,
+            latest_search_query: None,
+            currently_opened_path,
+            matches: Matches::default(),
+            selected_index: None,
+            cancel_flag: Arc::new(AtomicBool::new(false)),
+            history_items,
+        }
+    }
+
+    fn spawn_search(
+        &mut self,
+        query: PathLikeWithPosition<FileSearchQuery>,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Task<()> {
+        let relative_to = self
+            .currently_opened_path
+            .as_ref()
+            .map(|found_path| Arc::clone(&found_path.project.path));
+        let worktrees = self
+            .project
+            .read(cx)
+            .visible_worktrees(cx)
+            .collect::<Vec<_>>();
+        let include_root_name = worktrees.len() > 1;
+        let candidate_sets = worktrees
+            .into_iter()
+            .map(|worktree| {
+                let worktree = worktree.read(cx);
+                PathMatchCandidateSet {
+                    snapshot: worktree.snapshot(),
+                    include_ignored: worktree
+                        .root_entry()
+                        .map_or(false, |entry| entry.is_ignored),
+                    include_root_name,
+                }
+            })
+            .collect::<Vec<_>>();
+
+        let search_id = util::post_inc(&mut self.search_count);
+        self.cancel_flag.store(true, atomic::Ordering::Relaxed);
+        self.cancel_flag = Arc::new(AtomicBool::new(false));
+        let cancel_flag = self.cancel_flag.clone();
+        cx.spawn(|picker, mut cx| async move {
+            let matches = fuzzy::match_path_sets(
+                candidate_sets.as_slice(),
+                query.path_like.path_query(),
+                relative_to,
+                false,
+                100,
+                &cancel_flag,
+                cx.background_executor().clone(),
+            )
+            .await;
+            let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
+            picker
+                .update(&mut cx, |picker, cx| {
+                    picker
+                        .delegate
+                        .set_search_matches(search_id, did_cancel, query, matches, cx)
+                })
+                .log_err();
+        })
+    }
+
+    fn set_search_matches(
+        &mut self,
+        search_id: usize,
+        did_cancel: bool,
+        query: PathLikeWithPosition<FileSearchQuery>,
+        matches: Vec<PathMatch>,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) {
+        if search_id >= self.latest_search_id {
+            self.latest_search_id = search_id;
+            let extend_old_matches = self.latest_search_did_cancel
+                && Some(query.path_like.path_query())
+                    == self
+                        .latest_search_query
+                        .as_ref()
+                        .map(|query| query.path_like.path_query());
+            self.matches
+                .push_new_matches(&self.history_items, &query, matches, extend_old_matches);
+            self.latest_search_query = Some(query);
+            self.latest_search_did_cancel = did_cancel;
+            cx.notify();
+        }
+    }
+
+    fn labels_for_match(
+        &self,
+        path_match: Match,
+        cx: &AppContext,
+        ix: usize,
+    ) -> (String, Vec<usize>, String, Vec<usize>) {
+        let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
+            Match::History(found_path, found_path_match) => {
+                let worktree_id = found_path.project.worktree_id;
+                let project_relative_path = &found_path.project.path;
+                let has_worktree = self
+                    .project
+                    .read(cx)
+                    .worktree_for_id(worktree_id, cx)
+                    .is_some();
+
+                if !has_worktree {
+                    if let Some(absolute_path) = &found_path.absolute {
+                        return (
+                            absolute_path
+                                .file_name()
+                                .map_or_else(
+                                    || project_relative_path.to_string_lossy(),
+                                    |file_name| file_name.to_string_lossy(),
+                                )
+                                .to_string(),
+                            Vec::new(),
+                            absolute_path.to_string_lossy().to_string(),
+                            Vec::new(),
+                        );
+                    }
+                }
+
+                let mut path = Arc::clone(project_relative_path);
+                if project_relative_path.as_ref() == Path::new("") {
+                    if let Some(absolute_path) = &found_path.absolute {
+                        path = Arc::from(absolute_path.as_path());
+                    }
+                }
+
+                let mut path_match = PathMatch {
+                    score: ix as f64,
+                    positions: Vec::new(),
+                    worktree_id: worktree_id.to_usize(),
+                    path,
+                    path_prefix: "".into(),
+                    distance_to_relative_ancestor: usize::MAX,
+                };
+                if let Some(found_path_match) = found_path_match {
+                    path_match
+                        .positions
+                        .extend(found_path_match.positions.iter())
+                }
+
+                self.labels_for_path_match(&path_match)
+            }
+            Match::Search(path_match) => self.labels_for_path_match(path_match),
+        };
+
+        if file_name_positions.is_empty() {
+            if let Some(user_home_path) = std::env::var("HOME").ok() {
+                let user_home_path = user_home_path.trim();
+                if !user_home_path.is_empty() {
+                    if (&full_path).starts_with(user_home_path) {
+                        return (
+                            file_name,
+                            file_name_positions,
+                            full_path.replace(user_home_path, "~"),
+                            full_path_positions,
+                        );
+                    }
+                }
+            }
+        }
+
+        (
+            file_name,
+            file_name_positions,
+            full_path,
+            full_path_positions,
+        )
+    }
+
+    fn labels_for_path_match(
+        &self,
+        path_match: &PathMatch,
+    ) -> (String, Vec<usize>, String, Vec<usize>) {
+        let path = &path_match.path;
+        let path_string = path.to_string_lossy();
+        let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
+        let path_positions = path_match.positions.clone();
+
+        let file_name = path.file_name().map_or_else(
+            || path_match.path_prefix.to_string(),
+            |file_name| file_name.to_string_lossy().to_string(),
+        );
+        let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
+            - file_name.chars().count();
+        let file_name_positions = path_positions
+            .iter()
+            .filter_map(|pos| {
+                if pos >= &file_name_start {
+                    Some(pos - file_name_start)
+                } else {
+                    None
+                }
+            })
+            .collect();
+
+        (file_name, file_name_positions, full_path, path_positions)
+    }
+}
+
+impl PickerDelegate for FileFinderDelegate {
+    type ListItem = Div<Picker<Self>>;
+
+    fn placeholder_text(&self) -> Arc<str> {
+        "Search project files...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index.unwrap_or(0)
+    }
+
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
+        self.selected_index = Some(ix);
+        cx.notify();
+    }
+
+    fn update_matches(
+        &mut self,
+        raw_query: String,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Task<()> {
+        if raw_query.is_empty() {
+            let project = self.project.read(cx);
+            self.latest_search_id = post_inc(&mut self.search_count);
+            self.matches = Matches {
+                history: self
+                    .history_items
+                    .iter()
+                    .filter(|history_item| {
+                        project
+                            .worktree_for_id(history_item.project.worktree_id, cx)
+                            .is_some()
+                            || (project.is_local() && history_item.absolute.is_some())
+                    })
+                    .cloned()
+                    .map(|p| (p, None))
+                    .collect(),
+                search: Vec::new(),
+            };
+            cx.notify();
+            Task::ready(())
+        } else {
+            let raw_query = &raw_query;
+            let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
+                Ok::<_, std::convert::Infallible>(FileSearchQuery {
+                    raw_query: raw_query.to_owned(),
+                    file_query_end: if path_like_str == raw_query {
+                        None
+                    } else {
+                        Some(path_like_str.len())
+                    },
+                })
+            })
+            .expect("infallible");
+            self.spawn_search(query, cx)
+        }
+    }
+
+    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
+        if let Some(m) = self.matches.get(self.selected_index()) {
+            if let Some(workspace) = self.workspace.upgrade() {
+                let open_task = workspace.update(cx, move |workspace, cx| {
+                    let split_or_open = |workspace: &mut Workspace, project_path, cx| {
+                        if secondary {
+                            workspace.split_path(project_path, cx)
+                        } else {
+                            workspace.open_path(project_path, None, true, cx)
+                        }
+                    };
+                    match m {
+                        Match::History(history_match, _) => {
+                            let worktree_id = history_match.project.worktree_id;
+                            if workspace
+                                .project()
+                                .read(cx)
+                                .worktree_for_id(worktree_id, cx)
+                                .is_some()
+                            {
+                                split_or_open(
+                                    workspace,
+                                    ProjectPath {
+                                        worktree_id,
+                                        path: Arc::clone(&history_match.project.path),
+                                    },
+                                    cx,
+                                )
+                            } else {
+                                match history_match.absolute.as_ref() {
+                                    Some(abs_path) => {
+                                        if secondary {
+                                            workspace.split_abs_path(
+                                                abs_path.to_path_buf(),
+                                                false,
+                                                cx,
+                                            )
+                                        } else {
+                                            workspace.open_abs_path(
+                                                abs_path.to_path_buf(),
+                                                false,
+                                                cx,
+                                            )
+                                        }
+                                    }
+                                    None => split_or_open(
+                                        workspace,
+                                        ProjectPath {
+                                            worktree_id,
+                                            path: Arc::clone(&history_match.project.path),
+                                        },
+                                        cx,
+                                    ),
+                                }
+                            }
+                        }
+                        Match::Search(m) => split_or_open(
+                            workspace,
+                            ProjectPath {
+                                worktree_id: WorktreeId::from_usize(m.worktree_id),
+                                path: m.path.clone(),
+                            },
+                            cx,
+                        ),
+                    }
+                });
+
+                let row = self
+                    .latest_search_query
+                    .as_ref()
+                    .and_then(|query| query.row)
+                    .map(|row| row.saturating_sub(1));
+                let col = self
+                    .latest_search_query
+                    .as_ref()
+                    .and_then(|query| query.column)
+                    .unwrap_or(0)
+                    .saturating_sub(1);
+                let finder = self.file_finder.clone();
+
+                cx.spawn(|_, mut cx| async move {
+                    let item = open_task.await.log_err()?;
+                    if let Some(row) = row {
+                        if let Some(active_editor) = item.downcast::<Editor>() {
+                            active_editor
+                                .downgrade()
+                                .update(&mut cx, |editor, cx| {
+                                    let snapshot = editor.snapshot(cx).display_snapshot;
+                                    let point = snapshot
+                                        .buffer_snapshot
+                                        .clip_point(Point::new(row, col), Bias::Left);
+                                    editor.change_selections(Some(Autoscroll::center()), cx, |s| {
+                                        s.select_ranges([point..point])
+                                    });
+                                })
+                                .log_err();
+                        }
+                    }
+                    finder
+                        .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+                        .ok()?;
+
+                    Some(())
+                })
+                .detach();
+            }
+        }
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
+        self.file_finder
+            .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+            .log_err();
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Self::ListItem {
+        let path_match = self
+            .matches
+            .get(ix)
+            .expect("Invalid matches state: no element for index {ix}");
+        let theme = cx.theme();
+        let colors = theme.colors();
+
+        let (file_name, file_name_positions, full_path, full_path_positions) =
+            self.labels_for_match(path_match, cx, ix);
+
+        div()
+            .px_1()
+            .text_color(colors.text)
+            .text_ui()
+            .bg(colors.ghost_element_background)
+            .rounded_md()
+            .when(selected, |this| this.bg(colors.ghost_element_selected))
+            .hover(|this| this.bg(colors.ghost_element_hover))
+            .child(
+                v_stack()
+                    .child(HighlightedLabel::new(file_name, file_name_positions))
+                    .child(HighlightedLabel::new(full_path, full_path_positions)),
+            )
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
+
+//     use super::*;
+//     use editor::Editor;
+//     use gpui::{Entity, TestAppContext, VisualTestContext};
+//     use menu::{Confirm, SelectNext};
+//     use serde_json::json;
+//     use workspace::{AppState, Workspace};
+
+//     #[ctor::ctor]
+//     fn init_logger() {
+//         if std::env::var("RUST_LOG").is_ok() {
+//             env_logger::init();
+//         }
+//     }
+
+//     #[gpui::test]
+//     async fn test_matching_paths(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/root",
+//                 json!({
+//                     "a": {
+//                         "banana": "",
+//                         "bandana": "",
+//                     }
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+
+//         let (picker, workspace, mut cx) = build_find_picker(project, cx);
+//         let cx = &mut cx;
+
+//         picker
+//             .update(cx, |picker, cx| {
+//                 picker.delegate.update_matches("bna".to_string(), cx)
+//             })
+//             .await;
+
+//         picker.update(cx, |picker, _| {
+//             assert_eq!(picker.delegate.matches.len(), 2);
+//         });
+
+//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+//         cx.dispatch_action(SelectNext);
+//         cx.dispatch_action(Confirm);
+//         active_pane
+//             .condition(cx, |pane, _| pane.active_item().is_some())
+//             .await;
+//         cx.read(|cx| {
+//             let active_item = active_pane.read(cx).active_item().unwrap();
+//             assert_eq!(
+//                 active_item
+//                     .to_any()
+//                     .downcast::<Editor>()
+//                     .unwrap()
+//                     .read(cx)
+//                     .title(cx),
+//                 "bandana"
+//             );
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+
+//         let first_file_name = "first.rs";
+//         let first_file_contents = "// First Rust file";
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/src",
+//                 json!({
+//                     "test": {
+//                         first_file_name: first_file_contents,
+//                         "second.rs": "// Second Rust file",
+//                     }
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+
+//         let (picker, workspace, mut cx) = build_find_picker(project, cx);
+//         let cx = &mut cx;
+
+//         let file_query = &first_file_name[..3];
+//         let file_row = 1;
+//         let file_column = 3;
+//         assert!(file_column <= first_file_contents.len());
+//         let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
+//         picker
+//             .update(cx, |finder, cx| {
+//                 finder
+//                     .delegate
+//                     .update_matches(query_inside_file.to_string(), cx)
+//             })
+//             .await;
+//         picker.update(cx, |finder, _| {
+//             let finder = &finder.delegate;
+//             assert_eq!(finder.matches.len(), 1);
+//             let latest_search_query = finder
+//                 .latest_search_query
+//                 .as_ref()
+//                 .expect("Finder should have a query after the update_matches call");
+//             assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
+//             assert_eq!(
+//                 latest_search_query.path_like.file_query_end,
+//                 Some(file_query.len())
+//             );
+//             assert_eq!(latest_search_query.row, Some(file_row));
+//             assert_eq!(latest_search_query.column, Some(file_column as u32));
+//         });
+
+//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+//         cx.dispatch_action(SelectNext);
+//         cx.dispatch_action(Confirm);
+//         active_pane
+//             .condition(cx, |pane, _| pane.active_item().is_some())
+//             .await;
+//         let editor = cx.update(|cx| {
+//             let active_item = active_pane.read(cx).active_item().unwrap();
+//             active_item.downcast::<Editor>().unwrap()
+//         });
+//         cx.executor().advance_clock(Duration::from_secs(2));
+
+//         editor.update(cx, |editor, cx| {
+//             let all_selections = editor.selections.all_adjusted(cx);
+//             assert_eq!(
+//                 all_selections.len(),
+//                 1,
+//                 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
+//             );
+//             let caret_selection = all_selections.into_iter().next().unwrap();
+//             assert_eq!(caret_selection.start, caret_selection.end,
+//                 "Caret selection should have its start and end at the same position");
+//             assert_eq!(file_row, caret_selection.start.row + 1,
+//                 "Query inside file should get caret with the same focus row");
+//             assert_eq!(file_column, caret_selection.start.column as usize + 1,
+//                 "Query inside file should get caret with the same focus column");
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+
+//         let first_file_name = "first.rs";
+//         let first_file_contents = "// First Rust file";
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/src",
+//                 json!({
+//                     "test": {
+//                         first_file_name: first_file_contents,
+//                         "second.rs": "// Second Rust file",
+//                     }
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+
+//         let (picker, workspace, mut cx) = build_find_picker(project, cx);
+//         let cx = &mut cx;
+
+//         let file_query = &first_file_name[..3];
+//         let file_row = 200;
+//         let file_column = 300;
+//         assert!(file_column > first_file_contents.len());
+//         let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
+//         picker
+//             .update(cx, |picker, cx| {
+//                 picker
+//                     .delegate
+//                     .update_matches(query_outside_file.to_string(), cx)
+//             })
+//             .await;
+//         picker.update(cx, |finder, _| {
+//             let delegate = &finder.delegate;
+//             assert_eq!(delegate.matches.len(), 1);
+//             let latest_search_query = delegate
+//                 .latest_search_query
+//                 .as_ref()
+//                 .expect("Finder should have a query after the update_matches call");
+//             assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
+//             assert_eq!(
+//                 latest_search_query.path_like.file_query_end,
+//                 Some(file_query.len())
+//             );
+//             assert_eq!(latest_search_query.row, Some(file_row));
+//             assert_eq!(latest_search_query.column, Some(file_column as u32));
+//         });
+
+//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+//         cx.dispatch_action(SelectNext);
+//         cx.dispatch_action(Confirm);
+//         active_pane
+//             .condition(cx, |pane, _| pane.active_item().is_some())
+//             .await;
+//         let editor = cx.update(|cx| {
+//             let active_item = active_pane.read(cx).active_item().unwrap();
+//             active_item.downcast::<Editor>().unwrap()
+//         });
+//         cx.executor().advance_clock(Duration::from_secs(2));
+
+//         editor.update(cx, |editor, cx| {
+//             let all_selections = editor.selections.all_adjusted(cx);
+//             assert_eq!(
+//                 all_selections.len(),
+//                 1,
+//                 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
+//             );
+//             let caret_selection = all_selections.into_iter().next().unwrap();
+//             assert_eq!(caret_selection.start, caret_selection.end,
+//                 "Caret selection should have its start and end at the same position");
+//             assert_eq!(0, caret_selection.start.row,
+//                 "Excessive rows (as in query outside file borders) should get trimmed to last file row");
+//             assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
+//                 "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_matching_cancellation(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/dir",
+//                 json!({
+//                     "hello": "",
+//                     "goodbye": "",
+//                     "halogen-light": "",
+//                     "happiness": "",
+//                     "height": "",
+//                     "hi": "",
+//                     "hiccup": "",
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
+
+//         let (picker, _, mut cx) = build_find_picker(project, cx);
+//         let cx = &mut cx;
+
+//         let query = test_path_like("hi");
+//         picker
+//             .update(cx, |picker, cx| {
+//                 picker.delegate.spawn_search(query.clone(), cx)
+//             })
+//             .await;
+
+//         picker.update(cx, |picker, _cx| {
+//             assert_eq!(picker.delegate.matches.len(), 5)
+//         });
+
+//         picker.update(cx, |picker, cx| {
+//             let delegate = &mut picker.delegate;
+//             assert!(
+//                 delegate.matches.history.is_empty(),
+//                 "Search matches expected"
+//             );
+//             let matches = delegate.matches.search.clone();
+
+//             // Simulate a search being cancelled after the time limit,
+//             // returning only a subset of the matches that would have been found.
+//             drop(delegate.spawn_search(query.clone(), cx));
+//             delegate.set_search_matches(
+//                 delegate.latest_search_id,
+//                 true, // did-cancel
+//                 query.clone(),
+//                 vec![matches[1].clone(), matches[3].clone()],
+//                 cx,
+//             );
+
+//             // Simulate another cancellation.
+//             drop(delegate.spawn_search(query.clone(), cx));
+//             delegate.set_search_matches(
+//                 delegate.latest_search_id,
+//                 true, // did-cancel
+//                 query.clone(),
+//                 vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
+//                 cx,
+//             );
+
+//             assert!(
+//                 delegate.matches.history.is_empty(),
+//                 "Search matches expected"
+//             );
+//             assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]);
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_ignored_files(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/ancestor",
+//                 json!({
+//                     ".gitignore": "ignored-root",
+//                     "ignored-root": {
+//                         "happiness": "",
+//                         "height": "",
+//                         "hi": "",
+//                         "hiccup": "",
+//                     },
+//                     "tracked-root": {
+//                         ".gitignore": "height",
+//                         "happiness": "",
+//                         "height": "",
+//                         "hi": "",
+//                         "hiccup": "",
+//                     },
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(
+//             app_state.fs.clone(),
+//             [
+//                 "/ancestor/tracked-root".as_ref(),
+//                 "/ancestor/ignored-root".as_ref(),
+//             ],
+//             cx,
+//         )
+//         .await;
+
+//         let (picker, _, mut cx) = build_find_picker(project, cx);
+//         let cx = &mut cx;
+
+//         picker
+//             .update(cx, |picker, cx| {
+//                 picker.delegate.spawn_search(test_path_like("hi"), cx)
+//             })
+//             .await;
+//         picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7));
+//     }
+
+// #[gpui::test]
+// async fn test_single_file_worktrees(cx: &mut TestAppContext) {
+//     let app_state = init_test(cx);
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
+//         .await;
+
+//     let project = Project::test(
+//         app_state.fs.clone(),
+//         ["/root/the-parent-dir/the-file".as_ref()],
+//         cx,
+//     )
+//     .await;
+
+//     let (picker, _, mut cx) = build_find_picker(project, cx);
+//     let cx = &mut cx;
+
+//     // Even though there is only one worktree, that worktree's filename
+//     // is included in the matching, because the worktree is a single file.
+//     picker
+//         .update(cx, |picker, cx| {
+//             picker.delegate.spawn_search(test_path_like("thf"), cx)
+//         })
+//         .await;
+//     cx.read(|cx| {
+//         let picker = picker.read(cx);
+//         let delegate = &picker.delegate;
+//         assert!(
+//             delegate.matches.history.is_empty(),
+//             "Search matches expected"
+//         );
+//         let matches = delegate.matches.search.clone();
+//         assert_eq!(matches.len(), 1);
+
+//         let (file_name, file_name_positions, full_path, full_path_positions) =
+//             delegate.labels_for_path_match(&matches[0]);
+//         assert_eq!(file_name, "the-file");
+//         assert_eq!(file_name_positions, &[0, 1, 4]);
+//         assert_eq!(full_path, "the-file");
+//         assert_eq!(full_path_positions, &[0, 1, 4]);
+//     });
+
+//     // Since the worktree root is a file, searching for its name followed by a slash does
+//     // not match anything.
+//     picker
+//         .update(cx, |f, cx| {
+//             f.delegate.spawn_search(test_path_like("thf/"), cx)
+//         })
+//         .await;
+//     picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
+// }
+
+// #[gpui::test]
+// async fn test_path_distance_ordering(cx: &mut TestAppContext) {
+//     let app_state = init_test(cx);
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/root",
+//             json!({
+//                 "dir1": { "a.txt": "" },
+//                 "dir2": {
+//                     "a.txt": "",
+//                     "b.txt": ""
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+//     let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//     let cx = &mut cx;
+
+//     let worktree_id = cx.read(|cx| {
+//         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+//         assert_eq!(worktrees.len(), 1);
+//         WorktreeId::from_usize(worktrees[0].id())
+//     });
+
+//     // When workspace has an active item, sort items which are closer to that item
+//     // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
+//     // so that one should be sorted earlier
+//     let b_path = Some(dummy_found_path(ProjectPath {
+//         worktree_id,
+//         path: Arc::from(Path::new("/root/dir2/b.txt")),
+//     }));
+//     cx.dispatch_action(Toggle);
+
+//     let finder = cx
+//         .add_window(|cx| {
+//             Picker::new(
+//                 FileFinderDelegate::new(
+//                     workspace.downgrade(),
+//                     workspace.read(cx).project().clone(),
+//                     b_path,
+//                     Vec::new(),
+//                     cx,
+//                 ),
+//                 cx,
+//             )
+//         })
+//         .root(cx);
+
+//     finder
+//         .update(cx, |f, cx| {
+//             f.delegate.spawn_search(test_path_like("a.txt"), cx)
+//         })
+//         .await;
+
+//     finder.read_with(cx, |f, _| {
+//         let delegate = &f.delegate;
+//         assert!(
+//             delegate.matches.history.is_empty(),
+//             "Search matches expected"
+//         );
+//         let matches = delegate.matches.search.clone();
+//         assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
+//         assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
+//     let app_state = init_test(cx);
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/root",
+//             json!({
+//                 "dir1": {},
+//                 "dir2": {
+//                     "dir3": {}
+//                 }
+//             }),
+//         )
+//         .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))
+//         .root(cx);
+//     let finder = cx
+//         .add_window(|cx| {
+//             Picker::new(
+//                 FileFinderDelegate::new(
+//                     workspace.downgrade(),
+//                     workspace.read(cx).project().clone(),
+//                     None,
+//                     Vec::new(),
+//                     cx,
+//                 ),
+//                 cx,
+//             )
+//         })
+//         .root(cx);
+//     finder
+//         .update(cx, |f, cx| {
+//             f.delegate.spawn_search(test_path_like("dir"), cx)
+//         })
+//         .await;
+//     cx.read(|cx| {
+//         let finder = finder.read(cx);
+//         assert_eq!(finder.delegate.matches.len(), 0);
+//     });
+// }
+
+//     #[gpui::test]
+//     async fn test_query_history(cx: &mut gpui::TestAppContext) {
+//         let app_state = init_test(cx);
+
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/src",
+//                 json!({
+//                     "test": {
+//                         "first.rs": "// First Rust file",
+//                         "second.rs": "// Second Rust file",
+//                         "third.rs": "// Third Rust file",
+//                     }
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+//         let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//         let cx = &mut cx;
+//         let worktree_id = cx.read(|cx| {
+//             let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+//             assert_eq!(worktrees.len(), 1);
+//             WorktreeId::from_usize(worktrees[0].id())
+//         });
+
+//         // Open and close panels, getting their history items afterwards.
+//         // Ensure history items get populated with opened items, and items are kept in a certain order.
+//         // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
+//         //
+//         // TODO: without closing, the opened items do not propagate their history changes for some reason
+//         // it does work in real app though, only tests do not propagate.
+
+//         let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+//         assert!(
+//             initial_history.is_empty(),
+//             "Should have no history before opening any files"
+//         );
+
+//         let history_after_first =
+//             open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+//         assert_eq!(
+//             history_after_first,
+//             vec![FoundPath::new(
+//                 ProjectPath {
+//                     worktree_id,
+//                     path: Arc::from(Path::new("test/first.rs")),
+//                 },
+//                 Some(PathBuf::from("/src/test/first.rs"))
+//             )],
+//             "Should show 1st opened item in the history when opening the 2nd item"
+//         );
+
+//         let history_after_second =
+//             open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+//         assert_eq!(
+//             history_after_second,
+//             vec![
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/second.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/second.rs"))
+//                 ),
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/first.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/first.rs"))
+//                 ),
+//             ],
+//             "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
+// 2nd item should be the first in the history, as the last opened."
+//         );
+
+//         let history_after_third =
+//             open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+//         assert_eq!(
+//             history_after_third,
+//             vec![
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/third.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/third.rs"))
+//                 ),
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/second.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/second.rs"))
+//                 ),
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/first.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/first.rs"))
+//                 ),
+//             ],
+//             "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
+// 3rd item should be the first in the history, as the last opened."
+//         );
+
+//         let history_after_second_again =
+//             open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+//         assert_eq!(
+//             history_after_second_again,
+//             vec![
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/second.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/second.rs"))
+//                 ),
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/third.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/third.rs"))
+//                 ),
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/first.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/first.rs"))
+//                 ),
+//             ],
+//             "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
+// 2nd item, as the last opened, 3rd item should go next as it was opened right before."
+//         );
+//     }
+
+// #[gpui::test]
+// async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
+//     let app_state = init_test(cx);
+
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/src",
+//             json!({
+//                 "test": {
+//                     "first.rs": "// First Rust file",
+//                     "second.rs": "// Second Rust file",
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/external-src",
+//             json!({
+//                 "test": {
+//                     "third.rs": "// Third Rust file",
+//                     "fourth.rs": "// Fourth Rust file",
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+//     cx.update(|cx| {
+//         project.update(cx, |project, cx| {
+//             project.find_or_create_local_worktree("/external-src", false, cx)
+//         })
+//     })
+//     .detach();
+//     cx.background_executor.run_until_parked();
+
+//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//     let cx = &mut cx;
+//     let worktree_id = cx.read(|cx| {
+//         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+//         assert_eq!(worktrees.len(), 1,);
+
+//         WorktreeId::from_usize(worktrees[0].id())
+//     });
+//     workspace
+//         .update(cx, |workspace, cx| {
+//             workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
+//         })
+//         .detach();
+//     cx.background_executor.run_until_parked();
+//     let external_worktree_id = cx.read(|cx| {
+//         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+//         assert_eq!(
+//             worktrees.len(),
+//             2,
+//             "External file should get opened in a new worktree"
+//         );
+
+//         WorktreeId::from_usize(
+//             worktrees
+//                 .into_iter()
+//                 .find(|worktree| worktree.entity_id() != worktree_id.to_usize())
+//                 .expect("New worktree should have a different id")
+//                 .id(),
+//         )
+//     });
+//     close_active_item(&workspace, cx).await;
+
+//     let initial_history_items =
+//         open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+//     assert_eq!(
+//         initial_history_items,
+//         vec![FoundPath::new(
+//             ProjectPath {
+//                 worktree_id: external_worktree_id,
+//                 path: Arc::from(Path::new("")),
+//             },
+//             Some(PathBuf::from("/external-src/test/third.rs"))
+//         )],
+//         "Should show external file with its full path in the history after it was open"
+//     );
+
+//     let updated_history_items =
+//         open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+//     assert_eq!(
+//         updated_history_items,
+//         vec![
+//             FoundPath::new(
+//                 ProjectPath {
+//                     worktree_id,
+//                     path: Arc::from(Path::new("test/second.rs")),
+//                 },
+//                 Some(PathBuf::from("/src/test/second.rs"))
+//             ),
+//             FoundPath::new(
+//                 ProjectPath {
+//                     worktree_id: external_worktree_id,
+//                     path: Arc::from(Path::new("")),
+//                 },
+//                 Some(PathBuf::from("/external-src/test/third.rs"))
+//             ),
+//         ],
+//         "Should keep external file with history updates",
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
+//     let app_state = init_test(cx);
+
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/src",
+//             json!({
+//                 "test": {
+//                     "first.rs": "// First Rust file",
+//                     "second.rs": "// Second Rust file",
+//                     "third.rs": "// Third Rust file",
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//     let cx = &mut cx;
+
+//     // generate some history to select from
+//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+//     cx.executor().run_until_parked();
+//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+//     open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+//     let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+
+//     for expected_selected_index in 0..current_history.len() {
+//         cx.dispatch_action(Toggle);
+//         let selected_index = workspace.update(cx, |workspace, cx| {
+//             workspace
+//                 .current_modal::<FileFinder>(cx)
+//                 .unwrap()
+//                 .read(cx)
+//                 .picker
+//                 .read(cx)
+//                 .delegate
+//                 .selected_index()
+//         });
+//         assert_eq!(
+//             selected_index, expected_selected_index,
+//             "Should select the next item in the history"
+//         );
+//     }
+
+//     cx.dispatch_action(Toggle);
+//     let selected_index = workspace.update(cx, |workspace, cx| {
+//         workspace
+//             .current_modal::<FileFinder>(cx)
+//             .unwrap()
+//             .read(cx)
+//             .picker
+//             .read(cx)
+//             .delegate
+//             .selected_index()
+//     });
+//     assert_eq!(
+//         selected_index, 0,
+//         "Should wrap around the history and start all over"
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
+//     let app_state = init_test(cx);
+
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/src",
+//             json!({
+//                 "test": {
+//                     "first.rs": "// First Rust file",
+//                     "second.rs": "// Second Rust file",
+//                     "third.rs": "// Third Rust file",
+//                     "fourth.rs": "// Fourth Rust file",
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//     let cx = &mut cx;
+//     let worktree_id = cx.read(|cx| {
+//         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+//         assert_eq!(worktrees.len(), 1,);
+
+//         WorktreeId::from_usize(worktrees[0].entity_id())
+//     });
+
+//     // generate some history to select from
+//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+//     open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+
+//     cx.dispatch_action(Toggle);
+//     let first_query = "f";
+//     let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+//     finder
+//         .update(cx, |finder, cx| {
+//             finder.delegate.update_matches(first_query.to_string(), cx)
+//         })
+//         .await;
+//     finder.read_with(cx, |finder, _| {
+//         let delegate = &finder.delegate;
+//         assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
+//         let history_match = delegate.matches.history.first().unwrap();
+//         assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
+//         assert_eq!(history_match.0, FoundPath::new(
+//             ProjectPath {
+//                 worktree_id,
+//                 path: Arc::from(Path::new("test/first.rs")),
+//             },
+//             Some(PathBuf::from("/src/test/first.rs"))
+//         ));
+//         assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
+//         assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
+//     });
+
+//     let second_query = "fsdasdsa";
+//     let finder = workspace.update(cx, |workspace, cx| {
+//         workspace
+//             .current_modal::<FileFinder>(cx)
+//             .unwrap()
+//             .read(cx)
+//             .picker
+//     });
+//     finder
+//         .update(cx, |finder, cx| {
+//             finder.delegate.update_matches(second_query.to_string(), cx)
+//         })
+//         .await;
+//     finder.update(cx, |finder, _| {
+//         let delegate = &finder.delegate;
+//         assert!(
+//             delegate.matches.history.is_empty(),
+//             "No history entries should match {second_query}"
+//         );
+//         assert!(
+//             delegate.matches.search.is_empty(),
+//             "No search entries should match {second_query}"
+//         );
+//     });
+
+//     let first_query_again = first_query;
+
+//     let finder = workspace.update(cx, |workspace, cx| {
+//         workspace
+//             .current_modal::<FileFinder>(cx)
+//             .unwrap()
+//             .read(cx)
+//             .picker
+//     });
+//     finder
+//         .update(cx, |finder, cx| {
+//             finder
+//                 .delegate
+//                 .update_matches(first_query_again.to_string(), cx)
+//         })
+//         .await;
+//     finder.read_with(cx, |finder, _| {
+//         let delegate = &finder.delegate;
+//         assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
+//         let history_match = delegate.matches.history.first().unwrap();
+//         assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
+//         assert_eq!(history_match.0, FoundPath::new(
+//             ProjectPath {
+//                 worktree_id,
+//                 path: Arc::from(Path::new("test/first.rs")),
+//             },
+//             Some(PathBuf::from("/src/test/first.rs"))
+//         ));
+//         assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
+//         assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
+//     let app_state = init_test(cx);
+
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/src",
+//             json!({
+//                 "collab_ui": {
+//                     "first.rs": "// First Rust file",
+//                     "second.rs": "// Second Rust file",
+//                     "third.rs": "// Third Rust file",
+//                     "collab_ui.rs": "// Fourth Rust file",
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//     let cx = &mut cx;
+//     // generate some history to select from
+//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+//     open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+
+//     cx.dispatch_action(Toggle);
+//     let query = "collab_ui";
+//     let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+//     finder
+//         .update(cx, |finder, cx| {
+//             finder.delegate.update_matches(query.to_string(), cx)
+//         })
+//         .await;
+//     finder.read_with(cx, |finder, _| {
+//         let delegate = &finder.delegate;
+//         assert!(
+//             delegate.matches.history.is_empty(),
+//             "History items should not math query {query}, they should be matched by name only"
+//         );
+
+//         let search_entries = delegate
+//             .matches
+//             .search
+//             .iter()
+//             .map(|path_match| path_match.path.to_path_buf())
+//             .collect::<Vec<_>>();
+//         assert_eq!(
+//             search_entries,
+//             vec![
+//                 PathBuf::from("collab_ui/collab_ui.rs"),
+//                 PathBuf::from("collab_ui/third.rs"),
+//                 PathBuf::from("collab_ui/first.rs"),
+//                 PathBuf::from("collab_ui/second.rs"),
+//             ],
+//             "Despite all search results having the same directory name, the most matching one should be on top"
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
+//     let app_state = init_test(cx);
+
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/src",
+//             json!({
+//                 "test": {
+//                     "first.rs": "// First Rust file",
+//                     "nonexistent.rs": "// Second Rust file",
+//                     "third.rs": "// Third Rust file",
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//     let cx = &mut cx;
+//     // generate some history to select from
+//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+//     open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
+//     open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+
+//     cx.dispatch_action(Toggle);
+//     let query = "rs";
+//     let finder = cx.read(|cx| workspace.read(cx).current_modal::<FileFinder>().unwrap());
+//     finder
+//         .update(cx, |finder, cx| {
+//             finder.picker.update(cx, |picker, cx| {
+//                 picker.delegate.update_matches(query.to_string(), cx)
+//             })
+//         })
+//         .await;
+//     finder.update(cx, |finder, _| {
+//         let history_entries = finder.delegate
+//             .matches
+//             .history
+//             .iter()
+//             .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf())
+//             .collect::<Vec<_>>();
+//         assert_eq!(
+//             history_entries,
+//             vec![
+//                 PathBuf::from("test/first.rs"),
+//                 PathBuf::from("test/third.rs"),
+//             ],
+//             "Should have all opened files in the history, except the ones that do not exist on disk"
+//         );
+//     });
+// }
+
+//     async fn open_close_queried_buffer(
+//         input: &str,
+//         expected_matches: usize,
+//         expected_editor_title: &str,
+//         workspace: &View<Workspace>,
+//         cx: &mut gpui::VisualTestContext<'_>,
+//     ) -> Vec<FoundPath> {
+//         cx.dispatch_action(Toggle);
+//         let picker = workspace.update(cx, |workspace, cx| {
+//             workspace
+//                 .current_modal::<FileFinder>(cx)
+//                 .unwrap()
+//                 .read(cx)
+//                 .picker
+//                 .clone()
+//         });
+//         picker
+//             .update(cx, |finder, cx| {
+//                 finder.delegate.update_matches(input.to_string(), cx)
+//             })
+//             .await;
+//         let history_items = picker.update(cx, |finder, _| {
+//             assert_eq!(
+//                 finder.delegate.matches.len(),
+//                 expected_matches,
+//                 "Unexpected number of matches found for query {input}"
+//             );
+//             finder.delegate.history_items.clone()
+//         });
+
+//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+//         cx.dispatch_action(SelectNext);
+//         cx.dispatch_action(Confirm);
+//         cx.background_executor.run_until_parked();
+//         active_pane
+//             .condition(cx, |pane, _| pane.active_item().is_some())
+//             .await;
+//         cx.read(|cx| {
+//             let active_item = active_pane.read(cx).active_item().unwrap();
+//             let active_editor_title = active_item
+//                 .to_any()
+//                 .downcast::<Editor>()
+//                 .unwrap()
+//                 .read(cx)
+//                 .title(cx);
+//             assert_eq!(
+//                 expected_editor_title, active_editor_title,
+//                 "Unexpected editor title for query {input}"
+//             );
+//         });
+
+//         close_active_item(workspace, cx).await;
+
+//         history_items
+//     }
+
+//     async fn close_active_item(workspace: &View<Workspace>, cx: &mut VisualTestContext<'_>) {
+//         let mut original_items = HashMap::new();
+//         cx.read(|cx| {
+//             for pane in workspace.read(cx).panes() {
+//                 let pane_id = pane.entity_id();
+//                 let pane = pane.read(cx);
+//                 let insertion_result = original_items.insert(pane_id, pane.items().count());
+//                 assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
+//             }
+//         });
+
+//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+//         active_pane
+//             .update(cx, |pane, cx| {
+//                 pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx)
+//                     .unwrap()
+//             })
+//             .await
+//             .unwrap();
+//         cx.background_executor.run_until_parked();
+//         cx.read(|cx| {
+//             for pane in workspace.read(cx).panes() {
+//                 let pane_id = pane.entity_id();
+//                 let pane = pane.read(cx);
+//                 match original_items.remove(&pane_id) {
+//                     Some(original_items) => {
+//                         assert_eq!(
+//                             pane.items().count(),
+//                             original_items.saturating_sub(1),
+//                             "Pane id {pane_id} should have item closed"
+//                         );
+//                     }
+//                     None => panic!("Pane id {pane_id} not found in original items"),
+//                 }
+//             }
+//         });
+//         assert!(
+//             original_items.len() <= 1,
+//             "At most one panel should got closed"
+//         );
+//     }
+
+//     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+//         cx.update(|cx| {
+//             let state = AppState::test(cx);
+//             theme::init(cx);
+//             language::init(cx);
+//             super::init(cx);
+//             editor::init(cx);
+//             workspace::init_settings(cx);
+//             Project::init_settings(cx);
+//             state
+//         })
+//     }
+
+//     fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
+//         PathLikeWithPosition::parse_str(test_str, |path_like_str| {
+//             Ok::<_, std::convert::Infallible>(FileSearchQuery {
+//                 raw_query: test_str.to_owned(),
+//                 file_query_end: if path_like_str == test_str {
+//                     None
+//                 } else {
+//                     Some(path_like_str.len())
+//                 },
+//             })
+//         })
+//         .unwrap()
+//     }
+
+//     fn dummy_found_path(project_path: ProjectPath) -> FoundPath {
+//         FoundPath {
+//             project: project_path,
+//             absolute: None,
+//         }
+//     }
+
+//     fn build_find_picker(
+//         project: Model<Project>,
+//         cx: &mut TestAppContext,
+//     ) -> (
+//         View<Picker<FileFinderDelegate>>,
+//         View<Workspace>,
+//         VisualTestContext,
+//     ) {
+//         let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//         cx.dispatch_action(Toggle);
+//         let picker = workspace.update(&mut cx, |workspace, cx| {
+//             workspace
+//                 .current_modal::<FileFinder>(cx)
+//                 .unwrap()
+//                 .read(cx)
+//                 .picker
+//                 .clone()
+//         });
+//         (picker, workspace, cx)
+//     }
+// }

crates/go_to_line2/src/go_to_line.rs 🔗

@@ -1,12 +1,11 @@
 use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString,
-    StatefulInteractivity, StatelessInteractive, Styled, Subscription, View, ViewContext,
-    VisualContext, WindowContext,
+    actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString,
+    Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
 };
 use text::{Bias, Point};
 use theme::ActiveTheme;
-use ui::{h_stack, modal, v_stack, Label, LabelColor};
+use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
 use workspace::{Modal, ModalEvent, Workspace};
 
@@ -146,11 +145,12 @@ impl GoToLine {
 }
 
 impl Render for GoToLine {
-    type Element = Div<Self, StatefulInteractivity<Self>>;
+    type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        modal(cx)
-            .id("go to line")
+        div()
+            .elevation_2(cx)
+            .key_context("GoToLine")
             .on_action(Self::cancel)
             .on_action(Self::confirm)
             .w_96()
@@ -176,7 +176,7 @@ impl Render for GoToLine {
                             .justify_between()
                             .px_2()
                             .py_1()
-                            .child(Label::new(self.current_text.clone()).color(LabelColor::Muted)),
+                            .child(Label::new(self.current_text.clone()).color(TextColor::Muted)),
                     ),
             )
     }

crates/gpui/src/app.rs 🔗

@@ -2112,6 +2112,10 @@ impl AppContext {
         AsyncAppContext(self.weak_self.as_ref().unwrap().upgrade().unwrap())
     }
 
+    pub fn open_url(&self, url: &str) {
+        self.platform.open_url(url)
+    }
+
     pub fn write_to_clipboard(&self, item: ClipboardItem) {
         self.platform.write_to_clipboard(item);
     }

crates/gpui2/docs/contexts.md 🔗

@@ -0,0 +1,41 @@
+# Contexts
+
+GPUI makes extensive use of *context parameters*, typically named `cx` and positioned at the end of the parameter list, unless they're before a final function parameter. A context reference provides access to application state and services.
+
+There are multiple kinds of contexts, and contexts implement the `Deref` trait so that a function taking `&mut AppContext` could be passed a `&mut WindowContext` or `&mut ViewContext` instead.
+
+```
+     AppContext
+     /        \
+ModelContext  WindowContext
+              /
+        ViewContext
+```
+
+- The `AppContext` forms the root of the hierarchy
+- `ModelContext` and `WindowContext` both dereference to `AppContext`
+- `ViewContext` dereferences to `WindowContext`
+
+## `AppContext`
+
+Provides access to the global application state. All other kinds of contexts ultimately deref to an `AppContext`. You can update a `Model<T>` by passing an `AppContext`, but you can't update a view. For that you need a `WindowContext`...
+
+## `WindowContext`
+
+Provides access to the state of an application window, and also derefs to an `AppContext`, so you can pass a window context reference to any method taking an app context. Obtain this context by calling `WindowHandle::update`.
+
+## `ModelContext<T>`
+
+Available when you create or update a `Model<T>`. It derefs to an `AppContext`, but also contains methods specific to the particular model, such as the ability to notify change observers or emit events.
+
+## `ViewContext<V>`
+
+Available when you create or update a `View<V>`. It derefs to a `WindowContext`, but also contains methods specific to the particular view, such as the ability to notify change observers or emit events.
+
+## `AsyncAppContext` and `AsyncWindowContext`
+
+Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with `Model`s or `View`s with an async context, the calls become fallible, because the context may outlive the window or even the app itself.
+
+## `TestAppContext` and `TestVisualContext`
+
+These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests.

crates/gpui2/docs/key_dispatch.md 🔗

@@ -0,0 +1,101 @@
+# Key Dispatch
+
+GPUI is designed for keyboard-first interactivity.
+
+To expose functionality to the mouse, you render a button with a click handler.
+
+To expose functionality to the keyboard, you bind an *action* in a *key context*.
+
+Actions are similar to framework-level events like `MouseDown`, `KeyDown`, etc, but you can define them yourself:
+
+```rust
+mod menu {
+    #[gpui::action]
+    struct MoveUp;
+
+    #[gpui::action]
+    struct MoveDown;
+}
+```
+
+Actions are frequently unit structs, for which we have a macro. The above could also be written:
+
+```rust
+mod menu {
+    actions!(MoveUp, MoveDown);
+}
+```
+
+Actions can also be more complex types:
+
+```rust
+mod menu {
+    #[gpui::action]
+    struct Move {
+        direction: Direction,
+        select: bool,
+    }
+}
+```
+
+To bind actions, chain `on_action` on to your element:
+
+```rust
+impl Render for Menu {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component {
+        div()
+            .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext<Menu>| {
+                // ...
+            })
+            .on_action(|this, move: &MoveDown, cx| {
+                // ...
+            })
+            .children(todo!())
+    }
+}
+```
+
+In order to bind keys to actions, you need to declare a *key context* for part of the element tree by calling `key_context`.
+
+```rust
+impl Render for Menu {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component {
+        div()
+            .key_context("menu")
+            .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext<Menu>| {
+                // ...
+            })
+            .on_action(|this, move: &MoveDown, cx| {
+                // ...
+            })
+            .children(todo!())
+    }
+}
+```
+
+Now you can target your context in the keymap. Note how actions are identified in the keymap by their fully-qualified type name.
+
+```json
+{
+  "context": "menu",
+  "bindings": {
+    "up": "menu::MoveUp",
+    "down": "menu::MoveDown"
+  }
+}
+```
+
+If you had opted for the more complex type definition, you'd provide the serialized representation of the action alongside the name:
+
+```json
+{
+  "context": "menu",
+  "bindings": {
+    "up": ["menu::Move", {direction: "up", select: false}]
+    "down": ["menu::Move", {direction: "down", select: false}]
+    "shift-up": ["menu::Move", {direction: "up", select: true}]
+    "shift-down": ["menu::Move", {direction: "down", select: true}]
+  }
+}
+
+```

crates/gpui2/src/action.rs 🔗

@@ -1,6 +1,6 @@
 use crate::SharedString;
 use anyhow::{anyhow, Context, Result};
-use collections::{HashMap, HashSet};
+use collections::HashMap;
 use lazy_static::lazy_static;
 use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
 use serde::Deserialize;
@@ -68,8 +68,12 @@ where
     A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
 {
     fn qualified_name() -> SharedString {
+        let name = type_name::<A>();
+        let mut separator_matches = name.rmatch_indices("::");
+        separator_matches.next().unwrap();
+        let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
         // todo!() remove the 2 replacement when migration is done
-        type_name::<A>().replace("2::", "::").into()
+        name[name_start_ix..].replace("2::", "::").into()
     }
 
     fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
@@ -176,8 +180,7 @@ macro_rules! actions {
     () => {};
 
     ( $name:ident ) => {
-        #[gpui::register_action]
-        #[derive(::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq, $crate::serde::Deserialize)]
+        #[gpui::action]
         pub struct $name;
     };
 
@@ -186,401 +189,3 @@ macro_rules! actions {
         actions!($($rest)*);
     };
 }
-
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-pub struct DispatchContext {
-    set: HashSet<SharedString>,
-    map: HashMap<SharedString, SharedString>,
-}
-
-impl<'a> TryFrom<&'a str> for DispatchContext {
-    type Error = anyhow::Error;
-
-    fn try_from(value: &'a str) -> Result<Self> {
-        Self::parse(value)
-    }
-}
-
-impl DispatchContext {
-    pub fn parse(source: &str) -> Result<Self> {
-        let mut context = Self::default();
-        let source = skip_whitespace(source);
-        Self::parse_expr(&source, &mut context)?;
-        Ok(context)
-    }
-
-    fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
-        if source.is_empty() {
-            return Ok(());
-        }
-
-        let key = source
-            .chars()
-            .take_while(|c| is_identifier_char(*c))
-            .collect::<String>();
-        source = skip_whitespace(&source[key.len()..]);
-        if let Some(suffix) = source.strip_prefix('=') {
-            source = skip_whitespace(suffix);
-            let value = source
-                .chars()
-                .take_while(|c| is_identifier_char(*c))
-                .collect::<String>();
-            source = skip_whitespace(&source[value.len()..]);
-            context.set(key, value);
-        } else {
-            context.insert(key);
-        }
-
-        Self::parse_expr(source, context)
-    }
-
-    pub fn is_empty(&self) -> bool {
-        self.set.is_empty() && self.map.is_empty()
-    }
-
-    pub fn clear(&mut self) {
-        self.set.clear();
-        self.map.clear();
-    }
-
-    pub fn extend(&mut self, other: &Self) {
-        for v in &other.set {
-            self.set.insert(v.clone());
-        }
-        for (k, v) in &other.map {
-            self.map.insert(k.clone(), v.clone());
-        }
-    }
-
-    pub fn insert<I: Into<SharedString>>(&mut self, identifier: I) {
-        self.set.insert(identifier.into());
-    }
-
-    pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
-        self.map.insert(key.into(), value.into());
-    }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, Hash)]
-pub enum DispatchContextPredicate {
-    Identifier(SharedString),
-    Equal(SharedString, SharedString),
-    NotEqual(SharedString, SharedString),
-    Child(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
-    Not(Box<DispatchContextPredicate>),
-    And(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
-    Or(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
-}
-
-impl DispatchContextPredicate {
-    pub fn parse(source: &str) -> Result<Self> {
-        let source = skip_whitespace(source);
-        let (predicate, rest) = Self::parse_expr(source, 0)?;
-        if let Some(next) = rest.chars().next() {
-            Err(anyhow!("unexpected character {next:?}"))
-        } else {
-            Ok(predicate)
-        }
-    }
-
-    pub fn eval(&self, contexts: &[&DispatchContext]) -> bool {
-        let Some(context) = contexts.last() else {
-            return false;
-        };
-        match self {
-            Self::Identifier(name) => context.set.contains(name),
-            Self::Equal(left, right) => context
-                .map
-                .get(left)
-                .map(|value| value == right)
-                .unwrap_or(false),
-            Self::NotEqual(left, right) => context
-                .map
-                .get(left)
-                .map(|value| value != right)
-                .unwrap_or(true),
-            Self::Not(pred) => !pred.eval(contexts),
-            Self::Child(parent, child) => {
-                parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts)
-            }
-            Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
-            Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
-        }
-    }
-
-    fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
-        type Op = fn(
-            DispatchContextPredicate,
-            DispatchContextPredicate,
-        ) -> Result<DispatchContextPredicate>;
-
-        let (mut predicate, rest) = Self::parse_primary(source)?;
-        source = rest;
-
-        'parse: loop {
-            for (operator, precedence, constructor) in [
-                (">", PRECEDENCE_CHILD, Self::new_child as Op),
-                ("&&", PRECEDENCE_AND, Self::new_and as Op),
-                ("||", PRECEDENCE_OR, Self::new_or as Op),
-                ("==", PRECEDENCE_EQ, Self::new_eq as Op),
-                ("!=", PRECEDENCE_EQ, Self::new_neq as Op),
-            ] {
-                if source.starts_with(operator) && precedence >= min_precedence {
-                    source = skip_whitespace(&source[operator.len()..]);
-                    let (right, rest) = Self::parse_expr(source, precedence + 1)?;
-                    predicate = constructor(predicate, right)?;
-                    source = rest;
-                    continue 'parse;
-                }
-            }
-            break;
-        }
-
-        Ok((predicate, source))
-    }
-
-    fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
-        let next = source
-            .chars()
-            .next()
-            .ok_or_else(|| anyhow!("unexpected eof"))?;
-        match next {
-            '(' => {
-                source = skip_whitespace(&source[1..]);
-                let (predicate, rest) = Self::parse_expr(source, 0)?;
-                if rest.starts_with(')') {
-                    source = skip_whitespace(&rest[1..]);
-                    Ok((predicate, source))
-                } else {
-                    Err(anyhow!("expected a ')'"))
-                }
-            }
-            '!' => {
-                let source = skip_whitespace(&source[1..]);
-                let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
-                Ok((DispatchContextPredicate::Not(Box::new(predicate)), source))
-            }
-            _ if is_identifier_char(next) => {
-                let len = source
-                    .find(|c: char| !is_identifier_char(c))
-                    .unwrap_or(source.len());
-                let (identifier, rest) = source.split_at(len);
-                source = skip_whitespace(rest);
-                Ok((
-                    DispatchContextPredicate::Identifier(identifier.to_string().into()),
-                    source,
-                ))
-            }
-            _ => Err(anyhow!("unexpected character {next:?}")),
-        }
-    }
-
-    fn new_or(self, other: Self) -> Result<Self> {
-        Ok(Self::Or(Box::new(self), Box::new(other)))
-    }
-
-    fn new_and(self, other: Self) -> Result<Self> {
-        Ok(Self::And(Box::new(self), Box::new(other)))
-    }
-
-    fn new_child(self, other: Self) -> Result<Self> {
-        Ok(Self::Child(Box::new(self), Box::new(other)))
-    }
-
-    fn new_eq(self, other: Self) -> Result<Self> {
-        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
-            Ok(Self::Equal(left, right))
-        } else {
-            Err(anyhow!("operands must be identifiers"))
-        }
-    }
-
-    fn new_neq(self, other: Self) -> Result<Self> {
-        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
-            Ok(Self::NotEqual(left, right))
-        } else {
-            Err(anyhow!("operands must be identifiers"))
-        }
-    }
-}
-
-const PRECEDENCE_CHILD: u32 = 1;
-const PRECEDENCE_OR: u32 = 2;
-const PRECEDENCE_AND: u32 = 3;
-const PRECEDENCE_EQ: u32 = 4;
-const PRECEDENCE_NOT: u32 = 5;
-
-fn is_identifier_char(c: char) -> bool {
-    c.is_alphanumeric() || c == '_' || c == '-'
-}
-
-fn skip_whitespace(source: &str) -> &str {
-    let len = source
-        .find(|c: char| !c.is_whitespace())
-        .unwrap_or(source.len());
-    &source[len..]
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate as gpui;
-    use DispatchContextPredicate::*;
-
-    #[test]
-    fn test_actions_definition() {
-        {
-            actions!(A, B, C, D, E, F, G);
-        }
-
-        {
-            actions!(
-                A,
-                B,
-                C,
-                D,
-                E,
-                F,
-                G, // Don't wrap, test the trailing comma
-            );
-        }
-    }
-
-    #[test]
-    fn test_parse_context() {
-        let mut expected = DispatchContext::default();
-        expected.set("foo", "bar");
-        expected.insert("baz");
-        assert_eq!(DispatchContext::parse("baz foo=bar").unwrap(), expected);
-        assert_eq!(DispatchContext::parse("foo = bar baz").unwrap(), expected);
-        assert_eq!(
-            DispatchContext::parse("  baz foo   =   bar baz").unwrap(),
-            expected
-        );
-        assert_eq!(DispatchContext::parse(" foo = bar baz").unwrap(), expected);
-    }
-
-    #[test]
-    fn test_parse_identifiers() {
-        // Identifiers
-        assert_eq!(
-            DispatchContextPredicate::parse("abc12").unwrap(),
-            Identifier("abc12".into())
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("_1a").unwrap(),
-            Identifier("_1a".into())
-        );
-    }
-
-    #[test]
-    fn test_parse_negations() {
-        assert_eq!(
-            DispatchContextPredicate::parse("!abc").unwrap(),
-            Not(Box::new(Identifier("abc".into())))
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse(" ! ! abc").unwrap(),
-            Not(Box::new(Not(Box::new(Identifier("abc".into())))))
-        );
-    }
-
-    #[test]
-    fn test_parse_equality_operators() {
-        assert_eq!(
-            DispatchContextPredicate::parse("a == b").unwrap(),
-            Equal("a".into(), "b".into())
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("c!=d").unwrap(),
-            NotEqual("c".into(), "d".into())
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("c == !d")
-                .unwrap_err()
-                .to_string(),
-            "operands must be identifiers"
-        );
-    }
-
-    #[test]
-    fn test_parse_boolean_operators() {
-        assert_eq!(
-            DispatchContextPredicate::parse("a || b").unwrap(),
-            Or(
-                Box::new(Identifier("a".into())),
-                Box::new(Identifier("b".into()))
-            )
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("a || !b && c").unwrap(),
-            Or(
-                Box::new(Identifier("a".into())),
-                Box::new(And(
-                    Box::new(Not(Box::new(Identifier("b".into())))),
-                    Box::new(Identifier("c".into()))
-                ))
-            )
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("a && b || c&&d").unwrap(),
-            Or(
-                Box::new(And(
-                    Box::new(Identifier("a".into())),
-                    Box::new(Identifier("b".into()))
-                )),
-                Box::new(And(
-                    Box::new(Identifier("c".into())),
-                    Box::new(Identifier("d".into()))
-                ))
-            )
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("a == b && c || d == e && f").unwrap(),
-            Or(
-                Box::new(And(
-                    Box::new(Equal("a".into(), "b".into())),
-                    Box::new(Identifier("c".into()))
-                )),
-                Box::new(And(
-                    Box::new(Equal("d".into(), "e".into())),
-                    Box::new(Identifier("f".into()))
-                ))
-            )
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse("a && b && c && d").unwrap(),
-            And(
-                Box::new(And(
-                    Box::new(And(
-                        Box::new(Identifier("a".into())),
-                        Box::new(Identifier("b".into()))
-                    )),
-                    Box::new(Identifier("c".into())),
-                )),
-                Box::new(Identifier("d".into()))
-            ),
-        );
-    }
-
-    #[test]
-    fn test_parse_parenthesized_expressions() {
-        assert_eq!(
-            DispatchContextPredicate::parse("a && (b == c || d != e)").unwrap(),
-            And(
-                Box::new(Identifier("a".into())),
-                Box::new(Or(
-                    Box::new(Equal("b".into(), "c".into())),
-                    Box::new(NotEqual("d".into(), "e".into())),
-                )),
-            ),
-        );
-        assert_eq!(
-            DispatchContextPredicate::parse(" ( a || b ) ").unwrap(),
-            Or(
-                Box::new(Identifier("a".into())),
-                Box::new(Identifier("b".into())),
-            )
-        );
-    }
-}

crates/gpui2/src/app.rs 🔗

@@ -234,10 +234,10 @@ impl AppContext {
             app_version: platform.app_version().ok(),
         };
 
-        Rc::new_cyclic(|this| AppCell {
+        let app = Rc::new_cyclic(|this| AppCell {
             app: RefCell::new(AppContext {
                 this: this.clone(),
-                platform,
+                platform: platform.clone(),
                 app_metadata,
                 text_system,
                 flushing_effects: false,
@@ -269,12 +269,21 @@ impl AppContext {
                 layout_id_buffer: Default::default(),
                 propagate_event: true,
             }),
-        })
+        });
+
+        platform.on_quit(Box::new({
+            let cx = app.clone();
+            move || {
+                cx.borrow_mut().shutdown();
+            }
+        }));
+
+        app
     }
 
     /// Quit the application gracefully. Handlers registered with `ModelContext::on_app_quit`
     /// will be given 100ms to complete before exiting.
-    pub fn quit(&mut self) {
+    pub fn shutdown(&mut self) {
         let mut futures = Vec::new();
 
         for observer in self.quit_observers.remove(&()) {
@@ -292,8 +301,10 @@ impl AppContext {
         {
             log::error!("timed out waiting on app_will_quit");
         }
+    }
 
-        self.globals_by_type.clear();
+    pub fn quit(&mut self) {
+        self.platform.quit();
     }
 
     pub fn app_metadata(&self) -> AppMetadata {
@@ -431,6 +442,18 @@ impl AppContext {
         self.platform.activate(ignoring_other_apps);
     }
 
+    pub fn hide(&self) {
+        self.platform.hide();
+    }
+
+    pub fn hide_other_apps(&self) {
+        self.platform.hide_other_apps();
+    }
+
+    pub fn unhide_other_apps(&self) {
+        self.platform.unhide_other_apps();
+    }
+
     /// Returns the list of currently active displays.
     pub fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
         self.platform.displays()
@@ -641,14 +664,19 @@ impl AppContext {
                 // The window might change focus multiple times in an effect cycle.
                 // We only honor effects for the most recently focused handle.
                 if cx.window.focus == focused {
+                    // if someone calls focus multiple times in one frame with the same handle
+                    // the first apply_focus_changed_effect will have taken the last blur already
+                    // and run the rest of this, so we can return.
+                    let Some(last_blur) = cx.window.last_blur.take() else {
+                        return;
+                    };
+
                     let focused = focused
                         .map(|id| FocusHandle::for_id(id, &cx.window.focus_handles).unwrap());
-                    let blurred = cx
-                        .window
-                        .last_blur
-                        .take()
-                        .unwrap()
-                        .and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles));
+
+                    let blurred =
+                        last_blur.and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles));
+
                     let focus_changed = focused.is_some() || blurred.is_some();
                     let event = FocusEvent { focused, blurred };
 
@@ -1007,6 +1035,29 @@ impl Context for AppContext {
         let entity = self.entities.read(handle);
         read(entity, self)
     }
+
+    fn read_window<T, R>(
+        &self,
+        window: &WindowHandle<T>,
+        read: impl FnOnce(View<T>, &AppContext) -> R,
+    ) -> Result<R>
+    where
+        T: 'static,
+    {
+        let window = self
+            .windows
+            .get(window.id)
+            .ok_or_else(|| anyhow!("window not found"))?
+            .as_ref()
+            .unwrap();
+
+        let root_view = window.root_view.clone().unwrap();
+        let view = root_view
+            .downcast::<T>()
+            .map_err(|_| anyhow!("root view's type has changed"))?;
+
+        Ok(read(view, self))
+    }
 }
 
 /// These effects are processed at the end of each application update cycle.
@@ -1063,7 +1114,7 @@ impl<G: 'static> DerefMut for GlobalLease<G> {
 
 /// Contains state associated with an active drag operation, started by dragging an element
 /// within the window or by dragging into the app from the underlying platform.
-pub(crate) struct AnyDrag {
+pub struct AnyDrag {
     pub view: AnyView,
     pub cursor_offset: Point<Pixels>,
 }

crates/gpui2/src/app/async_context.rs 🔗

@@ -66,6 +66,19 @@ impl Context for AsyncAppContext {
         let mut lock = app.borrow_mut();
         lock.update_window(window, f)
     }
+
+    fn read_window<T, R>(
+        &self,
+        window: &WindowHandle<T>,
+        read: impl FnOnce(View<T>, &AppContext) -> R,
+    ) -> Result<R>
+    where
+        T: 'static,
+    {
+        let app = self.app.upgrade().context("app was released")?;
+        let lock = app.borrow();
+        lock.read_window(window, read)
+    }
 }
 
 impl AsyncAppContext {
@@ -250,6 +263,17 @@ impl Context for AsyncWindowContext {
     {
         self.app.read_model(handle, read)
     }
+
+    fn read_window<T, R>(
+        &self,
+        window: &WindowHandle<T>,
+        read: impl FnOnce(View<T>, &AppContext) -> R,
+    ) -> Result<R>
+    where
+        T: 'static,
+    {
+        self.app.read_window(window, read)
+    }
 }
 
 impl VisualContext for AsyncWindowContext {

crates/gpui2/src/app/entity_map.rs 🔗

@@ -26,7 +26,7 @@ impl EntityId {
 
 impl Display for EntityId {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self)
+        write!(f, "{}", self.as_u64())
     }
 }
 

crates/gpui2/src/app/model_context.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId,
-    EventEmitter, Model, Subscription, Task, WeakModel, WindowContext,
+    EventEmitter, Model, Subscription, Task, View, WeakModel, WindowContext, WindowHandle,
 };
 use anyhow::Result;
 use derive_more::{Deref, DerefMut};
@@ -239,6 +239,17 @@ impl<'a, T> Context for ModelContext<'a, T> {
     {
         self.app.read_model(handle, read)
     }
+
+    fn read_window<U, R>(
+        &self,
+        window: &WindowHandle<U>,
+        read: impl FnOnce(View<U>, &AppContext) -> R,
+    ) -> Result<R>
+    where
+        U: 'static,
+    {
+        self.app.read_window(window, read)
+    }
 }
 
 impl<T> Borrow<AppContext> for ModelContext<'_, T> {

crates/gpui2/src/app/test_context.rs 🔗

@@ -1,12 +1,12 @@
 use crate::{
-    AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context,
-    EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext,
-    Render, Result, Task, TestDispatcher, TestPlatform, ViewContext, VisualContext, WindowContext,
-    WindowHandle, WindowOptions,
+    div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
+    BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
+    Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View,
+    ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
 };
 use anyhow::{anyhow, bail};
 use futures::{Stream, StreamExt};
-use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
+use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
 
 #[derive(Clone)]
 pub struct TestAppContext {
@@ -14,6 +14,7 @@ pub struct TestAppContext {
     pub background_executor: BackgroundExecutor,
     pub foreground_executor: ForegroundExecutor,
     pub dispatcher: TestDispatcher,
+    pub test_platform: Rc<TestPlatform>,
 }
 
 impl Context for TestAppContext {
@@ -58,6 +59,18 @@ impl Context for TestAppContext {
         let app = self.app.borrow();
         app.read_model(handle, read)
     }
+
+    fn read_window<T, R>(
+        &self,
+        window: &WindowHandle<T>,
+        read: impl FnOnce(View<T>, &AppContext) -> R,
+    ) -> Result<R>
+    where
+        T: 'static,
+    {
+        let app = self.app.borrow();
+        app.read_window(window, read)
+    }
 }
 
 impl TestAppContext {
@@ -65,17 +78,16 @@ impl TestAppContext {
         let arc_dispatcher = Arc::new(dispatcher.clone());
         let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
         let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
-        let platform = Rc::new(TestPlatform::new(
-            background_executor.clone(),
-            foreground_executor.clone(),
-        ));
+        let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
         let asset_source = Arc::new(());
         let http_client = util::http::FakeHttpClient::with_404_response();
+
         Self {
-            app: AppContext::new(platform, asset_source, http_client),
+            app: AppContext::new(platform.clone(), asset_source, http_client),
             background_executor,
             foreground_executor,
             dispatcher: dispatcher.clone(),
+            test_platform: platform,
         }
     }
 
@@ -84,7 +96,7 @@ impl TestAppContext {
     }
 
     pub fn quit(&self) {
-        self.app.borrow_mut().quit();
+        self.app.borrow_mut().shutdown();
     }
 
     pub fn refresh(&mut self) -> Result<()> {
@@ -93,8 +105,8 @@ impl TestAppContext {
         Ok(())
     }
 
-    pub fn executor(&self) -> &BackgroundExecutor {
-        &self.background_executor
+    pub fn executor(&self) -> BackgroundExecutor {
+        self.background_executor.clone()
     }
 
     pub fn foreground_executor(&self) -> &ForegroundExecutor {
@@ -120,6 +132,41 @@ impl TestAppContext {
         cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window))
     }
 
+    pub fn add_empty_window(&mut self) -> AnyWindowHandle {
+        let mut cx = self.app.borrow_mut();
+        cx.open_window(WindowOptions::default(), |cx| {
+            cx.build_view(|_| EmptyView {})
+        })
+        .any_handle
+    }
+
+    pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, VisualTestContext)
+    where
+        F: FnOnce(&mut ViewContext<V>) -> V,
+        V: Render,
+    {
+        let mut cx = self.app.borrow_mut();
+        let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
+        drop(cx);
+        let view = window.root_view(self).unwrap();
+        (view, VisualTestContext::from_window(*window.deref(), self))
+    }
+
+    pub fn simulate_new_path_selection(
+        &self,
+        select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
+    ) {
+        self.test_platform.simulate_new_path_selection(select_path);
+    }
+
+    pub fn simulate_prompt_answer(&self, button_ix: usize) {
+        self.test_platform.simulate_prompt_answer(button_ix);
+    }
+
+    pub fn has_pending_prompt(&self) -> bool {
+        self.test_platform.has_pending_prompt()
+    }
+
     pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
     where
         Fut: Future<Output = R> + 'static,
@@ -146,6 +193,11 @@ impl TestAppContext {
         Some(read(lock.try_global()?, &lock))
     }
 
+    pub fn set_global<G: 'static>(&mut self, global: G) {
+        let mut lock = self.app.borrow_mut();
+        lock.set_global(global);
+    }
+
     pub fn update_global<G: 'static, R>(
         &mut self,
         update: impl FnOnce(&mut G, &mut AppContext) -> R,
@@ -162,6 +214,15 @@ impl TestAppContext {
         }
     }
 
+    pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
+    where
+        A: Action,
+    {
+        window
+            .update(self, |_, cx| cx.dispatch_action(action.boxed_clone()))
+            .unwrap()
+    }
+
     pub fn dispatch_keystroke(
         &mut self,
         window: AnyWindowHandle,
@@ -259,3 +320,198 @@ impl<T: Send> Model<T> {
             .expect("model was dropped")
     }
 }
+
+impl<V> View<V> {
+    pub fn condition<Evt>(
+        &self,
+        cx: &TestAppContext,
+        mut predicate: impl FnMut(&V, &AppContext) -> bool,
+    ) -> impl Future<Output = ()>
+    where
+        Evt: 'static,
+        V: EventEmitter<Evt>,
+    {
+        use postage::prelude::{Sink as _, Stream as _};
+
+        let (tx, mut rx) = postage::mpsc::channel(1024);
+        let timeout_duration = Duration::from_millis(100); //todo!() cx.condition_duration();
+
+        let mut cx = cx.app.borrow_mut();
+        let subscriptions = (
+            cx.observe(self, {
+                let mut tx = tx.clone();
+                move |_, _| {
+                    tx.blocking_send(()).ok();
+                }
+            }),
+            cx.subscribe(self, {
+                let mut tx = tx.clone();
+                move |_, _: &Evt, _| {
+                    tx.blocking_send(()).ok();
+                }
+            }),
+        );
+
+        let cx = cx.this.upgrade().unwrap();
+        let handle = self.downgrade();
+
+        async move {
+            crate::util::timeout(timeout_duration, async move {
+                loop {
+                    {
+                        let cx = cx.borrow();
+                        let cx = &*cx;
+                        if predicate(
+                            handle
+                                .upgrade()
+                                .expect("view dropped with pending condition")
+                                .read(cx),
+                            cx,
+                        ) {
+                            break;
+                        }
+                    }
+
+                    // todo!(start_waiting)
+                    // cx.borrow().foreground_executor().start_waiting();
+                    rx.recv()
+                        .await
+                        .expect("view dropped with pending condition");
+                    // cx.borrow().foreground_executor().finish_waiting();
+                }
+            })
+            .await
+            .expect("condition timed out");
+            drop(subscriptions);
+        }
+    }
+}
+
+use derive_more::{Deref, DerefMut};
+#[derive(Deref, DerefMut)]
+pub struct VisualTestContext<'a> {
+    #[deref]
+    #[deref_mut]
+    cx: &'a mut TestAppContext,
+    window: AnyWindowHandle,
+}
+
+impl<'a> VisualTestContext<'a> {
+    pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self {
+        Self { cx, window }
+    }
+
+    pub fn dispatch_action<A>(&mut self, action: A)
+    where
+        A: Action,
+    {
+        self.cx.dispatch_action(self.window, action)
+    }
+}
+
+impl<'a> Context for VisualTestContext<'a> {
+    type Result<T> = <TestAppContext as Context>::Result<T>;
+
+    fn build_model<T: 'static>(
+        &mut self,
+        build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
+    ) -> Self::Result<Model<T>> {
+        self.cx.build_model(build_model)
+    }
+
+    fn update_model<T, R>(
+        &mut self,
+        handle: &Model<T>,
+        update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
+    ) -> Self::Result<R>
+    where
+        T: 'static,
+    {
+        self.cx.update_model(handle, update)
+    }
+
+    fn read_model<T, R>(
+        &self,
+        handle: &Model<T>,
+        read: impl FnOnce(&T, &AppContext) -> R,
+    ) -> Self::Result<R>
+    where
+        T: 'static,
+    {
+        self.cx.read_model(handle, read)
+    }
+
+    fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
+    where
+        F: FnOnce(AnyView, &mut WindowContext<'_>) -> T,
+    {
+        self.cx.update_window(window, f)
+    }
+
+    fn read_window<T, R>(
+        &self,
+        window: &WindowHandle<T>,
+        read: impl FnOnce(View<T>, &AppContext) -> R,
+    ) -> Result<R>
+    where
+        T: 'static,
+    {
+        self.cx.read_window(window, read)
+    }
+}
+
+impl<'a> VisualContext for VisualTestContext<'a> {
+    fn build_view<V>(
+        &mut self,
+        build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
+    ) -> Self::Result<View<V>>
+    where
+        V: 'static + Render,
+    {
+        self.window
+            .update(self.cx, |_, cx| cx.build_view(build_view))
+            .unwrap()
+    }
+
+    fn update_view<V: 'static, R>(
+        &mut self,
+        view: &View<V>,
+        update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
+    ) -> Self::Result<R> {
+        self.window
+            .update(self.cx, |_, cx| cx.update_view(view, update))
+            .unwrap()
+    }
+
+    fn replace_root_view<V>(
+        &mut self,
+        build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
+    ) -> Self::Result<View<V>>
+    where
+        V: Render,
+    {
+        self.window
+            .update(self.cx, |_, cx| cx.replace_root_view(build_view))
+            .unwrap()
+    }
+}
+
+impl AnyWindowHandle {
+    pub fn build_view<V: Render + 'static>(
+        &self,
+        cx: &mut TestAppContext,
+        build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
+    ) -> View<V> {
+        self.update(cx, |_, cx| cx.build_view(build_view)).unwrap()
+    }
+}
+
+pub struct EmptyView {}
+
+impl Render for EmptyView {
+    type Element = Div<Self>;
+
+    fn render(&mut self, _cx: &mut crate::ViewContext<Self>) -> Self::Element {
+        div()
+    }
+}

crates/gpui2/src/color.rs 🔗

@@ -167,7 +167,7 @@ impl TryFrom<&'_ str> for Rgba {
     }
 }
 
-#[derive(Default, Copy, Clone, Debug, PartialEq)]
+#[derive(Default, Copy, Clone, Debug)]
 #[repr(C)]
 pub struct Hsla {
     pub h: f32,
@@ -176,10 +176,63 @@ pub struct Hsla {
     pub a: f32,
 }
 
+impl PartialEq for Hsla {
+    fn eq(&self, other: &Self) -> bool {
+        self.h
+            .total_cmp(&other.h)
+            .then(self.s.total_cmp(&other.s))
+            .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
+            .is_eq()
+    }
+}
+
+impl PartialOrd for Hsla {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        // SAFETY: The total ordering relies on this always being Some()
+        Some(
+            self.h
+                .total_cmp(&other.h)
+                .then(self.s.total_cmp(&other.s))
+                .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))),
+        )
+    }
+}
+
+impl Ord for Hsla {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        // SAFETY: The partial comparison is a total comparison
+        unsafe { self.partial_cmp(other).unwrap_unchecked() }
+    }
+}
+
 impl Hsla {
     pub fn to_rgb(self) -> Rgba {
         self.into()
     }
+
+    pub fn red() -> Self {
+        red()
+    }
+
+    pub fn green() -> Self {
+        green()
+    }
+
+    pub fn blue() -> Self {
+        blue()
+    }
+
+    pub fn black() -> Self {
+        black()
+    }
+
+    pub fn white() -> Self {
+        white()
+    }
+
+    pub fn transparent_black() -> Self {
+        transparent_black()
+    }
 }
 
 impl Eq for Hsla {}
@@ -238,6 +291,24 @@ pub fn blue() -> Hsla {
     }
 }
 
+pub fn green() -> Hsla {
+    Hsla {
+        h: 0.33,
+        s: 1.,
+        l: 0.5,
+        a: 1.,
+    }
+}
+
+pub fn yellow() -> Hsla {
+    Hsla {
+        h: 0.16,
+        s: 1.,
+        l: 0.5,
+        a: 1.,
+    }
+}
+
 impl Hsla {
     /// Returns true if the HSLA color is fully transparent, false otherwise.
     pub fn is_transparent(&self) -> bool {

crates/gpui2/src/element.rs 🔗

@@ -8,7 +8,7 @@ use std::{any::Any, mem};
 pub trait Element<V: 'static> {
     type ElementState: 'static;
 
-    fn id(&self) -> Option<ElementId>;
+    fn element_id(&self) -> Option<ElementId>;
 
     /// Called to initialize this element for the current frame. If this
     /// element had state in a previous frame, it will be passed in for the 3rd argument.
@@ -38,7 +38,7 @@ pub trait Element<V: 'static> {
 #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
 pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
 
-pub trait ParentElement<V: 'static> {
+pub trait ParentComponent<V: 'static> {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
 
     fn child(mut self, child: impl Component<V>) -> Self
@@ -120,7 +120,7 @@ where
     E::ElementState: 'static,
 {
     fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
-        let frame_state = if let Some(id) = self.element.id() {
+        let frame_state = if let Some(id) = self.element.element_id() {
             cx.with_element_state(id, |element_state, cx| {
                 let element_state = self.element.initialize(view_state, element_state, cx);
                 ((), element_state)
@@ -142,7 +142,7 @@ where
                 frame_state: initial_frame_state,
             } => {
                 frame_state = initial_frame_state;
-                if let Some(id) = self.element.id() {
+                if let Some(id) = self.element.element_id() {
                     layout_id = cx.with_element_state(id, |element_state, cx| {
                         let mut element_state = element_state.unwrap();
                         let layout_id = self.element.layout(state, &mut element_state, cx);
@@ -181,7 +181,7 @@ where
                 ..
             } => {
                 let bounds = cx.layout_bounds(layout_id);
-                if let Some(id) = self.element.id() {
+                if let Some(id) = self.element.element_id() {
                     cx.with_element_state(id, |element_state, cx| {
                         let mut element_state = element_state.unwrap();
                         self.element
@@ -255,7 +255,7 @@ where
         // Ignore the element offset when drawing this element, as the origin is already specified
         // in absolute terms.
         origin -= cx.element_offset();
-        cx.with_element_offset(Some(origin), |cx| self.paint(view_state, cx))
+        cx.with_element_offset(origin, |cx| self.paint(view_state, cx))
     }
 }
 
@@ -351,7 +351,7 @@ where
 {
     type ElementState = AnyElement<V>;
 
-    fn id(&self) -> Option<ElementId> {
+    fn element_id(&self) -> Option<ElementId> {
         None
     }
 

crates/gpui2/src/elements/div.rs 🔗

@@ -1,209 +1,606 @@
 use crate::{
-    point, AnyElement, BorrowWindow, Bounds, Component, Element, ElementFocus, ElementId,
-    ElementInteractivity, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable,
-    GlobalElementId, GroupBounds, InteractiveElementState, LayoutId, Overflow, ParentElement,
-    Pixels, Point, SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive,
-    StatelessInteractivity, Style, StyleRefinement, Styled, ViewContext, Visibility,
+    point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext,
+    BorrowWindow, Bounds, ClickEvent, Component, DispatchPhase, Element, ElementId, FocusEvent,
+    FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent,
+    MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, Point, Render, ScrollWheelEvent,
+    SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility,
 };
+use collections::HashMap;
+use parking_lot::Mutex;
 use refineable::Refineable;
 use smallvec::SmallVec;
+use std::{
+    any::{Any, TypeId},
+    fmt::Debug,
+    marker::PhantomData,
+    mem,
+    sync::Arc,
+    time::Duration,
+};
+use taffy::style::Overflow;
+use util::ResultExt;
 
-pub struct Div<
-    V: 'static,
-    I: ElementInteractivity<V> = StatelessInteractivity<V>,
-    F: ElementFocus<V> = FocusDisabled,
-> {
-    interactivity: I,
-    focus: F,
-    children: SmallVec<[AnyElement<V>; 2]>,
-    group: Option<SharedString>,
-    base_style: StyleRefinement,
+const DRAG_THRESHOLD: f64 = 2.;
+const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
+const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
+
+pub struct GroupStyle {
+    pub group: SharedString,
+    pub style: StyleRefinement,
 }
 
-pub fn div<V: 'static>() -> Div<V, StatelessInteractivity<V>, FocusDisabled> {
-    Div {
-        interactivity: StatelessInteractivity::default(),
-        focus: FocusDisabled,
-        children: SmallVec::new(),
-        group: None,
-        base_style: StyleRefinement::default(),
+pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
+    fn interactivity(&mut self) -> &mut Interactivity<V>;
+
+    fn group(mut self, group: impl Into<SharedString>) -> Self {
+        self.interactivity().group = Some(group.into());
+        self
     }
-}
 
-impl<V, F> Div<V, StatelessInteractivity<V>, F>
-where
-    V: 'static,
-    F: ElementFocus<V>,
-{
-    pub fn id(self, id: impl Into<ElementId>) -> Div<V, StatefulInteractivity<V>, F> {
-        Div {
-            interactivity: StatefulInteractivity::new(id.into(), self.interactivity),
-            focus: self.focus,
-            children: self.children,
-            group: self.group,
-            base_style: self.base_style,
+    fn id(mut self, id: impl Into<ElementId>) -> Stateful<V, Self> {
+        self.interactivity().element_id = Some(id.into());
+
+        Stateful {
+            element: self,
+            view_type: PhantomData,
         }
     }
-}
 
-impl<V, I, F> Div<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
-{
-    pub fn group(mut self, group: impl Into<SharedString>) -> Self {
-        self.group = Some(group.into());
+    fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable<V, Self> {
+        self.interactivity().focusable = true;
+        self.interactivity().tracked_focus_handle = Some(focus_handle.clone());
+        Focusable {
+            element: self,
+            view_type: PhantomData,
+        }
+    }
+
+    fn key_context<C, E>(mut self, key_context: C) -> Self
+    where
+        C: TryInto<KeyContext, Error = E>,
+        E: Debug,
+    {
+        if let Some(key_context) = key_context.try_into().log_err() {
+            self.interactivity().key_context = key_context;
+        }
         self
     }
 
-    pub fn z_index(mut self, z_index: u32) -> Self {
-        self.base_style.z_index = Some(z_index);
+    fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self {
+        self.interactivity().hover_style = f(StyleRefinement::default());
         self
     }
 
-    pub fn overflow_hidden(mut self) -> Self {
-        self.base_style.overflow.x = Some(Overflow::Hidden);
-        self.base_style.overflow.y = Some(Overflow::Hidden);
+    fn group_hover(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self {
+        self.interactivity().group_hover_style = Some(GroupStyle {
+            group: group_name.into(),
+            style: f(StyleRefinement::default()),
+        });
         self
     }
 
-    pub fn overflow_hidden_x(mut self) -> Self {
-        self.base_style.overflow.x = Some(Overflow::Hidden);
+    fn on_mouse_down(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_down_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble
+                    && event.button == button
+                    && bounds.contains_point(&event.position)
+                {
+                    handler(view, event, cx)
+                }
+            },
+        ));
         self
     }
 
-    pub fn overflow_hidden_y(mut self) -> Self {
-        self.base_style.overflow.y = Some(Overflow::Hidden);
+    fn on_any_mouse_down(
+        mut self,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_down_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx)
+                }
+            },
+        ));
         self
     }
 
-    fn with_element_id<R>(
-        &mut self,
-        cx: &mut ViewContext<V>,
-        f: impl FnOnce(&mut Self, Option<GlobalElementId>, &mut ViewContext<V>) -> R,
-    ) -> R {
-        if let Some(id) = self.id() {
-            cx.with_element_id(id, |global_id, cx| f(self, Some(global_id), cx))
-        } else {
-            f(self, None, cx)
-        }
+    fn on_mouse_up(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_up_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble
+                    && event.button == button
+                    && bounds.contains_point(&event.position)
+                {
+                    handler(view, event, cx)
+                }
+            },
+        ));
+        self
     }
 
-    pub fn compute_style(
-        &self,
-        bounds: Bounds<Pixels>,
-        element_state: &DivState,
-        cx: &mut ViewContext<V>,
-    ) -> Style {
-        let mut computed_style = Style::default();
-        computed_style.refine(&self.base_style);
-        self.focus.refine_style(&mut computed_style, cx);
-        self.interactivity.refine_style(
-            &mut computed_style,
-            bounds,
-            &element_state.interactive,
-            cx,
-        );
-        computed_style
+    fn on_any_mouse_up(
+        mut self,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_up_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx)
+                }
+            },
+        ));
+        self
     }
-}
 
-impl<V: 'static> Div<V, StatefulInteractivity<V>, FocusDisabled> {
-    pub fn focusable(self) -> Div<V, StatefulInteractivity<V>, FocusEnabled<V>> {
-        Div {
-            interactivity: self.interactivity,
-            focus: FocusEnabled::new(),
-            children: self.children,
-            group: self.group,
-            base_style: self.base_style,
-        }
+    fn on_mouse_down_out(
+        mut self,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_down_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) {
+                    handler(view, event, cx)
+                }
+            },
+        ));
+        self
     }
 
-    pub fn track_focus(
-        self,
-        handle: &FocusHandle,
-    ) -> Div<V, StatefulInteractivity<V>, FocusEnabled<V>> {
-        Div {
-            interactivity: self.interactivity,
-            focus: FocusEnabled::tracked(handle),
-            children: self.children,
-            group: self.group,
-            base_style: self.base_style,
-        }
+    fn on_mouse_up_out(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_up_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture
+                    && event.button == button
+                    && !bounds.contains_point(&event.position)
+                {
+                    handler(view, event, cx);
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_mouse_move(
+        mut self,
+        handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_move_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx);
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_scroll_wheel(
+        mut self,
+        handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().scroll_wheel_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx);
+                }
+            },
+        ));
+        self
+    }
+
+    /// Capture the given action, fires during the capture phase
+    fn capture_action<A: Action>(
+        mut self,
+        listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().action_listeners.push((
+            TypeId::of::<A>(),
+            Box::new(move |view, action, phase, cx| {
+                let action = action.downcast_ref().unwrap();
+                if phase == DispatchPhase::Capture {
+                    listener(view, action, cx)
+                }
+            }),
+        ));
+        self
+    }
+
+    /// Add a listener for the given action, fires during the bubble event phase
+    fn on_action<A: Action>(
+        mut self,
+        listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().action_listeners.push((
+            TypeId::of::<A>(),
+            Box::new(move |view, action, phase, cx| {
+                let action = action.downcast_ref().unwrap();
+                if phase == DispatchPhase::Bubble {
+                    listener(view, action, cx)
+                }
+            }),
+        ));
+        self
+    }
+
+    fn on_key_down(
+        mut self,
+        listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity()
+            .key_down_listeners
+            .push(Box::new(move |view, event, phase, cx| {
+                listener(view, event, phase, cx)
+            }));
+        self
+    }
+
+    fn on_key_up(
+        mut self,
+        listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity()
+            .key_up_listeners
+            .push(Box::new(move |view, event, phase, cx| {
+                listener(view, event, phase, cx)
+            }));
+        self
     }
 
-    pub fn overflow_scroll(mut self) -> Self {
-        self.base_style.overflow.x = Some(Overflow::Scroll);
-        self.base_style.overflow.y = Some(Overflow::Scroll);
+    fn drag_over<S: 'static>(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self {
+        self.interactivity()
+            .drag_over_styles
+            .push((TypeId::of::<S>(), f(StyleRefinement::default())));
         self
     }
 
-    pub fn overflow_x_scroll(mut self) -> Self {
-        self.base_style.overflow.x = Some(Overflow::Scroll);
+    fn group_drag_over<S: 'static>(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self {
+        self.interactivity().group_drag_over_styles.push((
+            TypeId::of::<S>(),
+            GroupStyle {
+                group: group_name.into(),
+                style: f(StyleRefinement::default()),
+            },
+        ));
         self
     }
 
-    pub fn overflow_y_scroll(mut self) -> Self {
-        self.base_style.overflow.y = Some(Overflow::Scroll);
+    fn on_drop<W: 'static>(
+        mut self,
+        listener: impl Fn(&mut V, View<W>, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().drop_listeners.push((
+            TypeId::of::<W>(),
+            Box::new(move |view, dragged_view, cx| {
+                listener(view, dragged_view.downcast().unwrap(), cx);
+            }),
+        ));
         self
     }
 }
 
-impl<V: 'static> Div<V, StatelessInteractivity<V>, FocusDisabled> {
-    pub fn track_focus(
-        self,
-        handle: &FocusHandle,
-    ) -> Div<V, StatefulInteractivity<V>, FocusEnabled<V>> {
-        Div {
-            interactivity: self.interactivity.into_stateful(handle),
-            focus: handle.clone().into(),
-            children: self.children,
-            group: self.group,
-            base_style: self.base_style,
+pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveComponent<V> {
+    fn focusable(mut self) -> Focusable<V, Self> {
+        self.interactivity().focusable = true;
+        Focusable {
+            element: self,
+            view_type: PhantomData,
         }
     }
+
+    fn overflow_scroll(mut self) -> Self {
+        self.interactivity().base_style.overflow.x = Some(Overflow::Scroll);
+        self.interactivity().base_style.overflow.y = Some(Overflow::Scroll);
+        self
+    }
+
+    fn overflow_x_scroll(mut self) -> Self {
+        self.interactivity().base_style.overflow.x = Some(Overflow::Scroll);
+        self
+    }
+
+    fn overflow_y_scroll(mut self) -> Self {
+        self.interactivity().base_style.overflow.y = Some(Overflow::Scroll);
+        self
+    }
+
+    fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().active_style = f(StyleRefinement::default());
+        self
+    }
+
+    fn group_active(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().group_active_style = Some(GroupStyle {
+            group: group_name.into(),
+            style: f(StyleRefinement::default()),
+        });
+        self
+    }
+
+    fn on_click(
+        mut self,
+        listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity()
+            .click_listeners
+            .push(Box::new(move |view, event, cx| listener(view, event, cx)));
+        self
+    }
+
+    fn on_drag<W>(
+        mut self,
+        listener: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+        W: 'static + Render,
+    {
+        debug_assert!(
+            self.interactivity().drag_listener.is_none(),
+            "calling on_drag more than once on the same element is not supported"
+        );
+        self.interactivity().drag_listener =
+            Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag {
+                view: listener(view_state, cx).into(),
+                cursor_offset,
+            }));
+        self
+    }
+
+    fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext<V>)) -> Self
+    where
+        Self: Sized,
+    {
+        debug_assert!(
+            self.interactivity().hover_listener.is_none(),
+            "calling on_hover more than once on the same element is not supported"
+        );
+        self.interactivity().hover_listener = Some(Box::new(listener));
+        self
+    }
+
+    fn tooltip<W>(
+        mut self,
+        build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+        W: 'static + Render,
+    {
+        debug_assert!(
+            self.interactivity().tooltip_builder.is_none(),
+            "calling tooltip more than once on the same element is not supported"
+        );
+        self.interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| {
+            build_tooltip(view_state, cx).into()
+        }));
+
+        self
+    }
 }
 
-impl<V, I> Focusable<V> for Div<V, I, FocusEnabled<V>>
-where
-    V: 'static,
-    I: ElementInteractivity<V>,
-{
-    fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
-        &mut self.focus.focus_listeners
+pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
+    fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_style = f(StyleRefinement::default());
+        self
+    }
+
+    fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_in_style = f(StyleRefinement::default());
+        self
+    }
+
+    fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().in_focus_style = f(StyleRefinement::default());
+        self
+    }
+
+    fn on_focus(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_listeners.push(Box::new(
+            move |view, focus_handle, event, cx| {
+                if event.focused.as_ref() == Some(focus_handle) {
+                    listener(view, event, cx)
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_blur(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_listeners.push(Box::new(
+            move |view, focus_handle, event, cx| {
+                if event.blurred.as_ref() == Some(focus_handle) {
+                    listener(view, event, cx)
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_focus_in(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_listeners.push(Box::new(
+            move |view, focus_handle, event, cx| {
+                let descendant_blurred = event
+                    .blurred
+                    .as_ref()
+                    .map_or(false, |blurred| focus_handle.contains(blurred, cx));
+                let descendant_focused = event
+                    .focused
+                    .as_ref()
+                    .map_or(false, |focused| focus_handle.contains(focused, cx));
+
+                if !descendant_blurred && descendant_focused {
+                    listener(view, event, cx)
+                }
+            },
+        ));
+        self
     }
 
-    fn set_focus_style(&mut self, style: StyleRefinement) {
-        self.focus.focus_style = style;
+    fn on_focus_out(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_listeners.push(Box::new(
+            move |view, focus_handle, event, cx| {
+                let descendant_blurred = event
+                    .blurred
+                    .as_ref()
+                    .map_or(false, |blurred| focus_handle.contains(blurred, cx));
+                let descendant_focused = event
+                    .focused
+                    .as_ref()
+                    .map_or(false, |focused| focus_handle.contains(focused, cx));
+                if descendant_blurred && !descendant_focused {
+                    listener(view, event, cx)
+                }
+            },
+        ));
+        self
     }
+}
+
+pub type FocusListeners<V> = SmallVec<[FocusListener<V>; 2]>;
+
+pub type FocusListener<V> =
+    Box<dyn Fn(&mut V, &FocusHandle, &FocusEvent, &mut ViewContext<V>) + 'static>;
+
+pub type MouseDownListener<V> = Box<
+    dyn Fn(&mut V, &MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
+>;
+pub type MouseUpListener<V> = Box<
+    dyn Fn(&mut V, &MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
+>;
+
+pub type MouseMoveListener<V> = Box<
+    dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
+>;
+
+pub type ScrollWheelListener<V> = Box<
+    dyn Fn(&mut V, &ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
+        + 'static,
+>;
+
+pub type ClickListener<V> = Box<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static>;
+
+pub type DragListener<V> =
+    Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + 'static>;
+
+type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
 
-    fn set_focus_in_style(&mut self, style: StyleRefinement) {
-        self.focus.focus_in_style = style;
+pub type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
+
+pub type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
+
+pub type KeyDownListener<V> =
+    Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
+
+pub type KeyUpListener<V> =
+    Box<dyn Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
+
+pub type ActionListener<V> =
+    Box<dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) + 'static>;
+
+pub fn div<V: 'static>() -> Div<V> {
+    Div {
+        interactivity: Interactivity::default(),
+        children: SmallVec::default(),
     }
+}
 
-    fn set_in_focus_style(&mut self, style: StyleRefinement) {
-        self.focus.in_focus_style = style;
+pub struct Div<V> {
+    interactivity: Interactivity<V>,
+    children: SmallVec<[AnyElement<V>; 2]>,
+}
+
+impl<V> Styled for Div<V> {
+    fn style(&mut self) -> &mut StyleRefinement {
+        &mut self.interactivity.base_style
     }
 }
 
-#[derive(Default)]
-pub struct DivState {
-    interactive: InteractiveElementState,
-    focus_handle: Option<FocusHandle>,
-    child_layout_ids: SmallVec<[LayoutId; 4]>,
+impl<V: 'static> InteractiveComponent<V> for Div<V> {
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        &mut self.interactivity
+    }
 }
 
-impl<V, I, F> Element<V> for Div<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
-{
+impl<V: 'static> ParentComponent<V> for Div<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        &mut self.children
+    }
+}
+
+impl<V: 'static> Element<V> for Div<V> {
     type ElementState = DivState;
 
-    fn id(&self) -> Option<ElementId> {
-        self.interactivity
-            .as_stateful()
-            .map(|identified| identified.id.clone())
+    fn element_id(&self) -> Option<ElementId> {
+        self.interactivity.element_id.clone()
     }
 
     fn initialize(
@@ -212,17 +609,18 @@ where
         element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> Self::ElementState {
-        let mut element_state = element_state.unwrap_or_default();
-        self.interactivity.initialize(cx, |cx| {
-            self.focus
-                .initialize(element_state.focus_handle.take(), cx, |focus_handle, cx| {
-                    element_state.focus_handle = focus_handle;
-                    for child in &mut self.children {
-                        child.initialize(view_state, cx);
-                    }
-                })
-        });
-        element_state
+        let interactive_state = self
+            .interactivity
+            .initialize(element_state.map(|s| s.interactive_state), cx);
+
+        for child in &mut self.children {
+            child.initialize(view_state, cx);
+        }
+
+        DivState {
+            interactive_state,
+            child_layout_ids: SmallVec::new(),
+        }
     }
 
     fn layout(
@@ -230,19 +628,21 @@ where
         view_state: &mut V,
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
-    ) -> LayoutId {
-        let style = self.compute_style(Bounds::default(), element_state, cx);
-        style.apply_text_style(cx, |cx| {
-            self.with_element_id(cx, |this, _global_id, cx| {
-                let layout_ids = this
-                    .children
-                    .iter_mut()
-                    .map(|child| child.layout(view_state, cx))
-                    .collect::<SmallVec<_>>();
-                element_state.child_layout_ids = layout_ids.clone();
-                cx.request_layout(&style, layout_ids)
-            })
-        })
+    ) -> crate::LayoutId {
+        let mut interactivity = mem::take(&mut self.interactivity);
+        let layout_id =
+            interactivity.layout(&mut element_state.interactive_state, cx, |style, cx| {
+                cx.with_text_style(style.text_style().cloned(), |cx| {
+                    element_state.child_layout_ids = self
+                        .children
+                        .iter_mut()
+                        .map(|child| child.layout(view_state, cx))
+                        .collect::<SmallVec<_>>();
+                    cx.request_layout(&style, element_state.child_layout_ids.iter().copied())
+                })
+            });
+        self.interactivity = interactivity;
+        layout_id
     }
 
     fn paint(
@@ -252,117 +652,801 @@ where
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) {
-        self.with_element_id(cx, |this, _global_id, cx| {
-            let style = this.compute_style(bounds, element_state, cx);
-            if style.visibility == Visibility::Hidden {
-                return;
+        let mut child_min = point(Pixels::MAX, Pixels::MAX);
+        let mut child_max = Point::default();
+        let content_size = if element_state.child_layout_ids.is_empty() {
+            bounds.size
+        } else {
+            for child_layout_id in &element_state.child_layout_ids {
+                let child_bounds = cx.layout_bounds(*child_layout_id);
+                child_min = child_min.min(&child_bounds.origin);
+                child_max = child_max.max(&child_bounds.lower_right());
             }
+            (child_max - child_min).into()
+        };
 
-            if let Some(mouse_cursor) = style.mouse_cursor {
-                let hovered = bounds.contains_point(&cx.mouse_position());
-                if hovered {
-                    cx.set_cursor_style(mouse_cursor);
+        let mut interactivity = mem::take(&mut self.interactivity);
+        interactivity.paint(
+            bounds,
+            content_size,
+            &mut element_state.interactive_state,
+            cx,
+            |style, scroll_offset, cx| {
+                if style.visibility == Visibility::Hidden {
+                    return;
                 }
-            }
 
-            if let Some(group) = this.group.clone() {
-                GroupBounds::push(group, bounds, cx);
+                let z_index = style.z_index.unwrap_or(0);
+
+                cx.with_z_index(z_index, |cx| {
+                    cx.with_z_index(0, |cx| {
+                        style.paint(bounds, cx);
+                    });
+                    cx.with_z_index(1, |cx| {
+                        cx.with_text_style(style.text_style().cloned(), |cx| {
+                            cx.with_content_mask(style.overflow_mask(bounds), |cx| {
+                                cx.with_element_offset(scroll_offset, |cx| {
+                                    for child in &mut self.children {
+                                        child.paint(view_state, cx);
+                                    }
+                                })
+                            })
+                        })
+                    })
+                })
+            },
+        );
+        self.interactivity = interactivity;
+    }
+}
+
+impl<V: 'static> Component<V> for Div<V> {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
+}
+
+pub struct DivState {
+    child_layout_ids: SmallVec<[LayoutId; 4]>,
+    interactive_state: InteractiveElementState,
+}
+
+pub struct Interactivity<V> {
+    pub element_id: Option<ElementId>,
+    pub key_context: KeyContext,
+    pub focusable: bool,
+    pub tracked_focus_handle: Option<FocusHandle>,
+    pub focus_listeners: FocusListeners<V>,
+    pub group: Option<SharedString>,
+    pub base_style: StyleRefinement,
+    pub focus_style: StyleRefinement,
+    pub focus_in_style: StyleRefinement,
+    pub in_focus_style: StyleRefinement,
+    pub hover_style: StyleRefinement,
+    pub group_hover_style: Option<GroupStyle>,
+    pub active_style: StyleRefinement,
+    pub group_active_style: Option<GroupStyle>,
+    pub drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
+    pub group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>,
+    pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
+    pub mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>,
+    pub mouse_move_listeners: SmallVec<[MouseMoveListener<V>; 2]>,
+    pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener<V>; 2]>,
+    pub key_down_listeners: SmallVec<[KeyDownListener<V>; 2]>,
+    pub key_up_listeners: SmallVec<[KeyUpListener<V>; 2]>,
+    pub action_listeners: SmallVec<[(TypeId, ActionListener<V>); 8]>,
+    pub drop_listeners: SmallVec<[(TypeId, Box<DropListener<V>>); 2]>,
+    pub click_listeners: SmallVec<[ClickListener<V>; 2]>,
+    pub drag_listener: Option<DragListener<V>>,
+    pub hover_listener: Option<HoverListener<V>>,
+    pub tooltip_builder: Option<TooltipBuilder<V>>,
+}
+
+impl<V> Interactivity<V>
+where
+    V: 'static,
+{
+    pub fn initialize(
+        &mut self,
+        element_state: Option<InteractiveElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> InteractiveElementState {
+        let mut element_state = element_state.unwrap_or_default();
+
+        // Ensure we store a focus handle in our element state if we're focusable.
+        // If there's an explicit focus handle we're tracking, use that. Otherwise
+        // create a new handle and store it in the element state, which lives for as
+        // as frames contain an element with this id.
+        if self.focusable {
+            element_state.focus_handle.get_or_insert_with(|| {
+                self.tracked_focus_handle
+                    .clone()
+                    .unwrap_or_else(|| cx.focus_handle())
+            });
+        }
+
+        element_state
+    }
+
+    pub fn layout(
+        &mut self,
+        element_state: &mut InteractiveElementState,
+        cx: &mut ViewContext<V>,
+        f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
+    ) -> LayoutId {
+        let style = self.compute_style(None, element_state, cx);
+        f(style, cx)
+    }
+
+    pub fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        content_size: Size<Pixels>,
+        element_state: &mut InteractiveElementState,
+        cx: &mut ViewContext<V>,
+        f: impl FnOnce(Style, Point<Pixels>, &mut ViewContext<V>),
+    ) {
+        let style = self.compute_style(Some(bounds), element_state, cx);
+
+        if let Some(mouse_cursor) = style.mouse_cursor {
+            let hovered = bounds.contains_point(&cx.mouse_position());
+            if hovered {
+                cx.set_cursor_style(mouse_cursor);
             }
+        }
 
-            let z_index = style.z_index.unwrap_or(0);
+        for listener in self.mouse_down_listeners.drain(..) {
+            cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| {
+                listener(state, event, &bounds, phase, cx);
+            })
+        }
 
-            let mut child_min = point(Pixels::MAX, Pixels::MAX);
-            let mut child_max = Point::default();
+        for listener in self.mouse_up_listeners.drain(..) {
+            cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| {
+                listener(state, event, &bounds, phase, cx);
+            })
+        }
 
-            let content_size = if element_state.child_layout_ids.is_empty() {
-                bounds.size
-            } else {
-                for child_layout_id in &element_state.child_layout_ids {
-                    let child_bounds = cx.layout_bounds(*child_layout_id);
-                    child_min = child_min.min(&child_bounds.origin);
-                    child_max = child_max.max(&child_bounds.lower_right());
-                }
-                (child_max - child_min).into()
-            };
-
-            cx.with_z_index(z_index, |cx| {
-                cx.with_z_index(0, |cx| {
-                    style.paint(bounds, cx);
-                    this.focus.paint(bounds, cx);
-                    this.interactivity.paint(
-                        bounds,
-                        content_size,
-                        style.overflow,
-                        &mut element_state.interactive,
-                        cx,
-                    );
+        for listener in self.mouse_move_listeners.drain(..) {
+            cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| {
+                listener(state, event, &bounds, phase, cx);
+            })
+        }
+
+        for listener in self.scroll_wheel_listeners.drain(..) {
+            cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| {
+                listener(state, event, &bounds, phase, cx);
+            })
+        }
+
+        let hover_group_bounds = self
+            .group_hover_style
+            .as_ref()
+            .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
+
+        if let Some(group_bounds) = hover_group_bounds {
+            let hovered = group_bounds.contains_point(&cx.mouse_position());
+            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    if group_bounds.contains_point(&event.position) != hovered {
+                        cx.notify();
+                    }
+                }
+            });
+        }
+
+        if self.hover_style.is_some()
+            || (cx.active_drag.is_some() && !self.drag_over_styles.is_empty())
+        {
+            let hovered = bounds.contains_point(&cx.mouse_position());
+            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    if bounds.contains_point(&event.position) != hovered {
+                        cx.notify();
+                    }
+                }
+            });
+        }
+
+        if cx.active_drag.is_some() {
+            let drop_listeners = mem::take(&mut self.drop_listeners);
+            cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    if let Some(drag_state_type) =
+                        cx.active_drag.as_ref().map(|drag| drag.view.entity_type())
+                    {
+                        for (drop_state_type, listener) in &drop_listeners {
+                            if *drop_state_type == drag_state_type {
+                                let drag = cx
+                                    .active_drag
+                                    .take()
+                                    .expect("checked for type drag state type above");
+                                listener(view, drag.view.clone(), cx);
+                                cx.notify();
+                                cx.stop_propagation();
+                            }
+                        }
+                    }
+                }
+            });
+        }
+
+        let click_listeners = mem::take(&mut self.click_listeners);
+        let drag_listener = mem::take(&mut self.drag_listener);
+
+        if !click_listeners.is_empty() || drag_listener.is_some() {
+            let pending_mouse_down = element_state.pending_mouse_down.clone();
+            let mouse_down = pending_mouse_down.lock().clone();
+            if let Some(mouse_down) = mouse_down {
+                if let Some(drag_listener) = drag_listener {
+                    let active_state = element_state.clicked_state.clone();
+
+                    cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+                        if cx.active_drag.is_some() {
+                            if phase == DispatchPhase::Capture {
+                                cx.notify();
+                            }
+                        } else if phase == DispatchPhase::Bubble
+                            && bounds.contains_point(&event.position)
+                            && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
+                        {
+                            *active_state.lock() = ElementClickedState::default();
+                            let cursor_offset = event.position - bounds.origin;
+                            let drag = drag_listener(view_state, cursor_offset, cx);
+                            cx.active_drag = Some(drag);
+                            cx.notify();
+                            cx.stop_propagation();
+                        }
+                    });
+                }
+
+                cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| {
+                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                        let mouse_click = ClickEvent {
+                            down: mouse_down.clone(),
+                            up: event.clone(),
+                        };
+                        for listener in &click_listeners {
+                            listener(view_state, &mouse_click, cx);
+                        }
+                    }
+                    *pending_mouse_down.lock() = None;
+                    cx.notify();
                 });
-                cx.with_z_index(1, |cx| {
-                    style.apply_text_style(cx, |cx| {
-                        style.apply_overflow(bounds, cx, |cx| {
-                            let scroll_offset = element_state.interactive.scroll_offset();
-                            cx.with_element_offset(scroll_offset, |cx| {
-                                for child in &mut this.children {
-                                    child.paint(view_state, cx);
-                                }
-                            });
-                        })
-                    })
+            } else {
+                cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| {
+                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                        *pending_mouse_down.lock() = Some(event.clone());
+                        cx.notify();
+                    }
                 });
+            }
+        }
+
+        if let Some(hover_listener) = self.hover_listener.take() {
+            let was_hovered = element_state.hover_state.clone();
+            let has_mouse_down = element_state.pending_mouse_down.clone();
+
+            cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+                if phase != DispatchPhase::Bubble {
+                    return;
+                }
+                let is_hovered =
+                    bounds.contains_point(&event.position) && has_mouse_down.lock().is_none();
+                let mut was_hovered = was_hovered.lock();
+
+                if is_hovered != was_hovered.clone() {
+                    *was_hovered = is_hovered;
+                    drop(was_hovered);
+
+                    hover_listener(view_state, is_hovered, cx);
+                }
             });
+        }
+
+        if let Some(tooltip_builder) = self.tooltip_builder.take() {
+            let active_tooltip = element_state.active_tooltip.clone();
+            let pending_mouse_down = element_state.pending_mouse_down.clone();
+
+            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+                if phase != DispatchPhase::Bubble {
+                    return;
+                }
+
+                let is_hovered =
+                    bounds.contains_point(&event.position) && pending_mouse_down.lock().is_none();
+                if !is_hovered {
+                    active_tooltip.lock().take();
+                    return;
+                }
+
+                if active_tooltip.lock().is_none() {
+                    let task = cx.spawn({
+                        let active_tooltip = active_tooltip.clone();
+                        let tooltip_builder = tooltip_builder.clone();
+
+                        move |view, mut cx| async move {
+                            cx.background_executor().timer(TOOLTIP_DELAY).await;
+                            view.update(&mut cx, move |view_state, cx| {
+                                active_tooltip.lock().replace(ActiveTooltip {
+                                    waiting: None,
+                                    tooltip: Some(AnyTooltip {
+                                        view: tooltip_builder(view_state, cx),
+                                        cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
+                                    }),
+                                });
+                                cx.notify();
+                            })
+                            .ok();
+                        }
+                    });
+                    active_tooltip.lock().replace(ActiveTooltip {
+                        waiting: Some(task),
+                        tooltip: None,
+                    });
+                }
+            });
+
+            if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
+                if active_tooltip.tooltip.is_some() {
+                    cx.active_tooltip = active_tooltip.tooltip.clone()
+                }
+            }
+        }
+
+        let active_state = element_state.clicked_state.clone();
+        if !active_state.lock().is_clicked() {
+            cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    *active_state.lock() = ElementClickedState::default();
+                    cx.notify();
+                }
+            });
+        } else {
+            let active_group_bounds = self
+                .group_active_style
+                .as_ref()
+                .and_then(|group_active| GroupBounds::get(&group_active.group, cx));
+            cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| {
+                if phase == DispatchPhase::Bubble {
+                    let group = active_group_bounds
+                        .map_or(false, |bounds| bounds.contains_point(&down.position));
+                    let element = bounds.contains_point(&down.position);
+                    if group || element {
+                        *active_state.lock() = ElementClickedState { group, element };
+                        cx.notify();
+                    }
+                }
+            });
+        }
+
+        let overflow = style.overflow;
+        if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
+            let scroll_offset = element_state
+                .scroll_offset
+                .get_or_insert_with(Arc::default)
+                .clone();
+            let line_height = cx.line_height();
+            let scroll_max = (content_size - bounds.size).max(&Size::default());
+
+            cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    let mut scroll_offset = scroll_offset.lock();
+                    let old_scroll_offset = *scroll_offset;
+                    let delta = event.delta.pixel_delta(line_height);
+
+                    if overflow.x == Overflow::Scroll {
+                        scroll_offset.x =
+                            (scroll_offset.x + delta.x).clamp(-scroll_max.width, px(0.));
+                    }
+
+                    if overflow.y == Overflow::Scroll {
+                        scroll_offset.y =
+                            (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.));
+                    }
+
+                    if *scroll_offset != old_scroll_offset {
+                        cx.notify();
+                        cx.stop_propagation();
+                    }
+                }
+            });
+        }
+
+        if let Some(group) = self.group.clone() {
+            GroupBounds::push(group, bounds, cx);
+        }
 
-            if let Some(group) = this.group.as_ref() {
-                GroupBounds::pop(group, cx);
+        let scroll_offset = element_state
+            .scroll_offset
+            .as_ref()
+            .map(|scroll_offset| *scroll_offset.lock());
+
+        cx.with_key_dispatch(
+            self.key_context.clone(),
+            element_state.focus_handle.clone(),
+            |_, cx| {
+                for listener in self.key_down_listeners.drain(..) {
+                    cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| {
+                        listener(state, event, phase, cx);
+                    })
+                }
+
+                for listener in self.key_up_listeners.drain(..) {
+                    cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| {
+                        listener(state, event, phase, cx);
+                    })
+                }
+
+                for (action_type, listener) in self.action_listeners.drain(..) {
+                    cx.on_action(action_type, listener)
+                }
+
+                if let Some(focus_handle) = element_state.focus_handle.as_ref() {
+                    for listener in self.focus_listeners.drain(..) {
+                        let focus_handle = focus_handle.clone();
+                        cx.on_focus_changed(move |view, event, cx| {
+                            listener(view, &focus_handle, event, cx)
+                        });
+                    }
+                }
+
+                f(style, scroll_offset.unwrap_or_default(), cx)
+            },
+        );
+
+        if let Some(group) = self.group.as_ref() {
+            GroupBounds::pop(group, cx);
+        }
+    }
+
+    pub fn compute_style(
+        &self,
+        bounds: Option<Bounds<Pixels>>,
+        element_state: &mut InteractiveElementState,
+        cx: &mut ViewContext<V>,
+    ) -> Style {
+        let mut style = Style::default();
+        style.refine(&self.base_style);
+
+        if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
+            if focus_handle.contains_focused(cx) {
+                style.refine(&self.focus_in_style);
             }
-        })
+
+            if focus_handle.within_focused(cx) {
+                style.refine(&self.in_focus_style);
+            }
+
+            if focus_handle.is_focused(cx) {
+                style.refine(&self.focus_style);
+            }
+        }
+
+        if let Some(bounds) = bounds {
+            let mouse_position = cx.mouse_position();
+            if let Some(group_hover) = self.group_hover_style.as_ref() {
+                if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
+                    if group_bounds.contains_point(&mouse_position) {
+                        style.refine(&group_hover.style);
+                    }
+                }
+            }
+            if bounds.contains_point(&mouse_position) {
+                style.refine(&self.hover_style);
+            }
+
+            if let Some(drag) = cx.active_drag.take() {
+                for (state_type, group_drag_style) in &self.group_drag_over_styles {
+                    if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
+                        if *state_type == drag.view.entity_type()
+                            && group_bounds.contains_point(&mouse_position)
+                        {
+                            style.refine(&group_drag_style.style);
+                        }
+                    }
+                }
+
+                for (state_type, drag_over_style) in &self.drag_over_styles {
+                    if *state_type == drag.view.entity_type()
+                        && bounds.contains_point(&mouse_position)
+                    {
+                        style.refine(drag_over_style);
+                    }
+                }
+
+                cx.active_drag = Some(drag);
+            }
+        }
+
+        let clicked_state = element_state.clicked_state.lock();
+        if clicked_state.group {
+            if let Some(group) = self.group_active_style.as_ref() {
+                style.refine(&group.style)
+            }
+        }
+
+        if clicked_state.element {
+            style.refine(&self.active_style)
+        }
+
+        style
+    }
+}
+
+impl<V: 'static> Default for Interactivity<V> {
+    fn default() -> Self {
+        Self {
+            element_id: None,
+            key_context: KeyContext::default(),
+            focusable: false,
+            tracked_focus_handle: None,
+            focus_listeners: SmallVec::default(),
+            // scroll_offset: Point::default(),
+            group: None,
+            base_style: StyleRefinement::default(),
+            focus_style: StyleRefinement::default(),
+            focus_in_style: StyleRefinement::default(),
+            in_focus_style: StyleRefinement::default(),
+            hover_style: StyleRefinement::default(),
+            group_hover_style: None,
+            active_style: StyleRefinement::default(),
+            group_active_style: None,
+            drag_over_styles: SmallVec::new(),
+            group_drag_over_styles: SmallVec::new(),
+            mouse_down_listeners: SmallVec::new(),
+            mouse_up_listeners: SmallVec::new(),
+            mouse_move_listeners: SmallVec::new(),
+            scroll_wheel_listeners: SmallVec::new(),
+            key_down_listeners: SmallVec::new(),
+            key_up_listeners: SmallVec::new(),
+            action_listeners: SmallVec::new(),
+            drop_listeners: SmallVec::new(),
+            click_listeners: SmallVec::new(),
+            drag_listener: None,
+            hover_listener: None,
+            tooltip_builder: None,
+        }
+    }
+}
+
+#[derive(Default)]
+pub struct InteractiveElementState {
+    pub focus_handle: Option<FocusHandle>,
+    pub clicked_state: Arc<Mutex<ElementClickedState>>,
+    pub hover_state: Arc<Mutex<bool>>,
+    pub pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
+    pub scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
+    pub active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
+}
+
+pub struct ActiveTooltip {
+    #[allow(unused)] // used to drop the task
+    waiting: Option<Task<()>>,
+    tooltip: Option<AnyTooltip>,
+}
+
+/// Whether or not the element or a group that contains it is clicked by the mouse.
+#[derive(Copy, Clone, Default, Eq, PartialEq)]
+pub struct ElementClickedState {
+    pub group: bool,
+    pub element: bool,
+}
+
+impl ElementClickedState {
+    fn is_clicked(&self) -> bool {
+        self.group || self.element
+    }
+}
+
+#[derive(Default)]
+pub struct GroupBounds(HashMap<SharedString, SmallVec<[Bounds<Pixels>; 1]>>);
+
+impl GroupBounds {
+    pub fn get(name: &SharedString, cx: &mut AppContext) -> Option<Bounds<Pixels>> {
+        cx.default_global::<Self>()
+            .0
+            .get(name)
+            .and_then(|bounds_stack| bounds_stack.last())
+            .cloned()
+    }
+
+    pub fn push(name: SharedString, bounds: Bounds<Pixels>, cx: &mut AppContext) {
+        cx.default_global::<Self>()
+            .0
+            .entry(name)
+            .or_default()
+            .push(bounds);
+    }
+
+    pub fn pop(name: &SharedString, cx: &mut AppContext) {
+        cx.default_global::<Self>().0.get_mut(name).unwrap().pop();
+    }
+}
+
+pub struct Focusable<V, E> {
+    element: E,
+    view_type: PhantomData<V>,
+}
+
+impl<V: 'static, E: InteractiveComponent<V>> FocusableComponent<V> for Focusable<V, E> {}
+
+impl<V, E> InteractiveComponent<V> for Focusable<V, E>
+where
+    V: 'static,
+    E: InteractiveComponent<V>,
+{
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        self.element.interactivity()
+    }
+}
+
+impl<V: 'static, E: StatefulInteractiveComponent<V, E>> StatefulInteractiveComponent<V, E>
+    for Focusable<V, E>
+{
+}
+
+impl<V, E> Styled for Focusable<V, E>
+where
+    V: 'static,
+    E: Styled,
+{
+    fn style(&mut self) -> &mut StyleRefinement {
+        self.element.style()
+    }
+}
+
+impl<V, E> Element<V> for Focusable<V, E>
+where
+    V: 'static,
+    E: Element<V>,
+{
+    type ElementState = E::ElementState;
+
+    fn element_id(&self) -> Option<ElementId> {
+        self.element.element_id()
+    }
+
+    fn initialize(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> Self::ElementState {
+        self.element.initialize(view_state, element_state, cx)
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) -> LayoutId {
+        self.element.layout(view_state, element_state, cx)
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        self.element.paint(bounds, view_state, element_state, cx);
     }
 }
 
-impl<V, I, F> Component<V> for Div<V, I, F>
+impl<V, E> Component<V> for Focusable<V, E>
 where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
+    V: 'static,
+    E: 'static + Element<V>,
 {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
     }
 }
 
-impl<V, I, F> ParentElement<V> for Div<V, I, F>
+impl<V, E> ParentComponent<V> for Focusable<V, E>
 where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
+    V: 'static,
+    E: ParentComponent<V>,
 {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
-        &mut self.children
+        self.element.children_mut()
     }
 }
 
-impl<V, I, F> Styled for Div<V, I, F>
+pub struct Stateful<V, E> {
+    element: E,
+    view_type: PhantomData<V>,
+}
+
+impl<V, E> Styled for Stateful<V, E>
 where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
+    V: 'static,
+    E: Styled,
 {
     fn style(&mut self) -> &mut StyleRefinement {
-        &mut self.base_style
+        self.element.style()
     }
 }
 
-impl<V, I, F> StatelessInteractive<V> for Div<V, I, F>
+impl<V, E> StatefulInteractiveComponent<V, E> for Stateful<V, E>
 where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
+    V: 'static,
+    E: Element<V>,
+    Self: InteractiveComponent<V>,
+{
+}
+
+impl<V, E> InteractiveComponent<V> for Stateful<V, E>
+where
+    V: 'static,
+    E: InteractiveComponent<V>,
 {
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
-        self.interactivity.as_stateless_mut()
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        self.element.interactivity()
     }
 }
 
-impl<V, F> StatefulInteractive<V> for Div<V, StatefulInteractivity<V>, F>
+impl<V: 'static, E: FocusableComponent<V>> FocusableComponent<V> for Stateful<V, E> {}
+
+impl<V, E> Element<V> for Stateful<V, E>
 where
-    F: ElementFocus<V>,
+    V: 'static,
+    E: Element<V>,
 {
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
-        &mut self.interactivity
+    type ElementState = E::ElementState;
+
+    fn element_id(&self) -> Option<ElementId> {
+        self.element.element_id()
+    }
+
+    fn initialize(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> Self::ElementState {
+        self.element.initialize(view_state, element_state, cx)
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) -> LayoutId {
+        self.element.layout(view_state, element_state, cx)
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        self.element.paint(bounds, view_state, element_state, cx)
+    }
+}
+
+impl<V, E> Component<V> for Stateful<V, E>
+where
+    V: 'static,
+    E: 'static + Element<V>,
+{
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
+}
+
+impl<V, E> ParentComponent<V> for Stateful<V, E>
+where
+    V: 'static,
+    E: ParentComponent<V>,
+{
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        self.element.children_mut()
     }
 }

crates/gpui2/src/elements/img.rs 🔗

@@ -1,35 +1,28 @@
 use crate::{
-    div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementFocus,
-    ElementId, ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable,
-    LayoutId, Pixels, SharedString, StatefulInteractive, StatefulInteractivity,
-    StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext,
+    AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent,
+    InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
+    Styled, ViewContext,
 };
 use futures::FutureExt;
 use util::ResultExt;
 
-pub struct Img<
-    V: 'static,
-    I: ElementInteractivity<V> = StatelessInteractivity<V>,
-    F: ElementFocus<V> = FocusDisabled,
-> {
-    base: Div<V, I, F>,
+pub struct Img<V: 'static> {
+    interactivity: Interactivity<V>,
     uri: Option<SharedString>,
     grayscale: bool,
 }
 
-pub fn img<V: 'static>() -> Img<V, StatelessInteractivity<V>, FocusDisabled> {
+pub fn img<V: 'static>() -> Img<V> {
     Img {
-        base: div(),
+        interactivity: Interactivity::default(),
         uri: None,
         grayscale: false,
     }
 }
 
-impl<V, I, F> Img<V, I, F>
+impl<V> Img<V>
 where
     V: 'static,
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
 {
     pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
         self.uri = Some(uri.into());
@@ -42,145 +35,90 @@ where
     }
 }
 
-impl<V, F> Img<V, StatelessInteractivity<V>, F>
-where
-    F: ElementFocus<V>,
-{
-    pub fn id(self, id: impl Into<ElementId>) -> Img<V, StatefulInteractivity<V>, F> {
-        Img {
-            base: self.base.id(id),
-            uri: self.uri,
-            grayscale: self.grayscale,
-        }
-    }
-}
-
-impl<V, I, F> Component<V> for Img<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
-{
+impl<V> Component<V> for Img<V> {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
     }
 }
 
-impl<V, I, F> Element<V> for Img<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
-{
-    type ElementState = DivState;
+impl<V> Element<V> for Img<V> {
+    type ElementState = InteractiveElementState;
 
-    fn id(&self) -> Option<crate::ElementId> {
-        self.base.id()
+    fn element_id(&self) -> Option<crate::ElementId> {
+        self.interactivity.element_id.clone()
     }
 
     fn initialize(
         &mut self,
-        view_state: &mut V,
+        _view_state: &mut V,
         element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> Self::ElementState {
-        self.base.initialize(view_state, element_state, cx)
+        self.interactivity.initialize(element_state, cx)
     }
 
     fn layout(
         &mut self,
-        view_state: &mut V,
+        _view_state: &mut V,
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) -> LayoutId {
-        self.base.layout(view_state, element_state, cx)
+        self.interactivity.layout(element_state, cx, |style, cx| {
+            cx.request_layout(&style, None)
+        })
     }
 
     fn paint(
         &mut self,
         bounds: Bounds<Pixels>,
-        view: &mut V,
+        _view_state: &mut V,
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) {
-        cx.with_z_index(0, |cx| {
-            self.base.paint(bounds, view, element_state, cx);
-        });
-
-        let style = self.base.compute_style(bounds, element_state, cx);
-        let corner_radii = style.corner_radii;
-
-        if let Some(uri) = self.uri.clone() {
-            // eprintln!(">>> image_cache.get({uri}");
-            let image_future = cx.image_cache.get(uri.clone());
-            // eprintln!("<<< image_cache.get({uri}");
-            if let Some(data) = image_future
-                .clone()
-                .now_or_never()
-                .and_then(ResultExt::log_err)
-            {
-                let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
-                cx.with_z_index(1, |cx| {
-                    cx.paint_image(bounds, corner_radii, data, self.grayscale)
-                        .log_err()
-                });
-            } else {
-                cx.spawn(|_, mut cx| async move {
-                    if image_future.await.log_err().is_some() {
-                        cx.on_next_frame(|cx| cx.notify());
+        self.interactivity.paint(
+            bounds,
+            bounds.size,
+            element_state,
+            cx,
+            |style, _scroll_offset, cx| {
+                let corner_radii = style.corner_radii;
+
+                if let Some(uri) = self.uri.clone() {
+                    // eprintln!(">>> image_cache.get({uri}");
+                    let image_future = cx.image_cache.get(uri.clone());
+                    // eprintln!("<<< image_cache.get({uri}");
+                    if let Some(data) = image_future
+                        .clone()
+                        .now_or_never()
+                        .and_then(ResultExt::log_err)
+                    {
+                        let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
+                        cx.with_z_index(1, |cx| {
+                            cx.paint_image(bounds, corner_radii, data, self.grayscale)
+                                .log_err()
+                        });
+                    } else {
+                        cx.spawn(|_, mut cx| async move {
+                            if image_future.await.log_err().is_some() {
+                                cx.on_next_frame(|cx| cx.notify());
+                            }
+                        })
+                        .detach()
                     }
-                })
-                .detach()
-            }
-        }
+                }
+            },
+        )
     }
 }
 
-impl<V, I, F> Styled for Img<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
-{
+impl<V> Styled for Img<V> {
     fn style(&mut self) -> &mut StyleRefinement {
-        self.base.style()
+        &mut self.interactivity.base_style
     }
 }
 
-impl<V, I, F> StatelessInteractive<V> for Img<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
-{
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
-        self.base.stateless_interactivity()
-    }
-}
-
-impl<V, F> StatefulInteractive<V> for Img<V, StatefulInteractivity<V>, F>
-where
-    F: ElementFocus<V>,
-{
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
-        self.base.stateful_interactivity()
-    }
-}
-
-impl<V, I> Focusable<V> for Img<V, I, FocusEnabled<V>>
-where
-    V: 'static,
-    I: ElementInteractivity<V>,
-{
-    fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
-        self.base.focus_listeners()
-    }
-
-    fn set_focus_style(&mut self, style: StyleRefinement) {
-        self.base.set_focus_style(style)
-    }
-
-    fn set_focus_in_style(&mut self, style: StyleRefinement) {
-        self.base.set_focus_in_style(style)
-    }
-
-    fn set_in_focus_style(&mut self, style: StyleRefinement) {
-        self.base.set_in_focus_style(style)
+impl<V> InteractiveComponent<V> for Img<V> {
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        &mut self.interactivity
     }
 }

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

@@ -1,157 +1,88 @@
 use crate::{
-    div, AnyElement, Bounds, Component, Div, DivState, Element, ElementFocus, ElementId,
-    ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable, LayoutId, Pixels,
-    SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive,
-    StatelessInteractivity, StyleRefinement, Styled, ViewContext,
+    AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent,
+    InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
+    Styled, ViewContext,
 };
 use util::ResultExt;
 
-pub struct Svg<
-    V: 'static,
-    I: ElementInteractivity<V> = StatelessInteractivity<V>,
-    F: ElementFocus<V> = FocusDisabled,
-> {
-    base: Div<V, I, F>,
+pub struct Svg<V: 'static> {
+    interactivity: Interactivity<V>,
     path: Option<SharedString>,
 }
 
-pub fn svg<V: 'static>() -> Svg<V, StatelessInteractivity<V>, FocusDisabled> {
+pub fn svg<V: 'static>() -> Svg<V> {
     Svg {
-        base: div(),
+        interactivity: Interactivity::default(),
         path: None,
     }
 }
 
-impl<V, I, F> Svg<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
-{
+impl<V> Svg<V> {
     pub fn path(mut self, path: impl Into<SharedString>) -> Self {
         self.path = Some(path.into());
         self
     }
 }
 
-impl<V, F> Svg<V, StatelessInteractivity<V>, F>
-where
-    F: ElementFocus<V>,
-{
-    pub fn id(self, id: impl Into<ElementId>) -> Svg<V, StatefulInteractivity<V>, F> {
-        Svg {
-            base: self.base.id(id),
-            path: self.path,
-        }
-    }
-}
-
-impl<V, I, F> Component<V> for Svg<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
-{
+impl<V> Component<V> for Svg<V> {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
     }
 }
 
-impl<V, I, F> Element<V> for Svg<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
-{
-    type ElementState = DivState;
+impl<V> Element<V> for Svg<V> {
+    type ElementState = InteractiveElementState;
 
-    fn id(&self) -> Option<crate::ElementId> {
-        self.base.id()
+    fn element_id(&self) -> Option<ElementId> {
+        self.interactivity.element_id.clone()
     }
 
     fn initialize(
         &mut self,
-        view_state: &mut V,
+        _view_state: &mut V,
         element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> Self::ElementState {
-        self.base.initialize(view_state, element_state, cx)
+        self.interactivity.initialize(element_state, cx)
     }
 
     fn layout(
         &mut self,
-        view_state: &mut V,
+        _view_state: &mut V,
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) -> LayoutId {
-        self.base.layout(view_state, element_state, cx)
+        self.interactivity.layout(element_state, cx, |style, cx| {
+            cx.request_layout(&style, None)
+        })
     }
 
     fn paint(
         &mut self,
         bounds: Bounds<Pixels>,
-        view: &mut V,
+        _view_state: &mut V,
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) where
         Self: Sized,
     {
-        self.base.paint(bounds, view, element_state, cx);
-        let color = self
-            .base
-            .compute_style(bounds, element_state, cx)
-            .text
-            .color;
-        if let Some((path, color)) = self.path.as_ref().zip(color) {
-            cx.paint_svg(bounds, path.clone(), color).log_err();
-        }
+        self.interactivity
+            .paint(bounds, bounds.size, element_state, cx, |style, _, cx| {
+                if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
+                    cx.paint_svg(bounds, path.clone(), color).log_err();
+                }
+            })
     }
 }
 
-impl<V, I, F> Styled for Svg<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
-{
+impl<V> Styled for Svg<V> {
     fn style(&mut self) -> &mut StyleRefinement {
-        self.base.style()
-    }
-}
-
-impl<V, I, F> StatelessInteractive<V> for Svg<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
-{
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
-        self.base.stateless_interactivity()
+        &mut self.interactivity.base_style
     }
 }
 
-impl<V, F> StatefulInteractive<V> for Svg<V, StatefulInteractivity<V>, F>
-where
-    V: 'static,
-    F: ElementFocus<V>,
-{
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
-        self.base.stateful_interactivity()
-    }
-}
-
-impl<V: 'static, I> Focusable<V> for Svg<V, I, FocusEnabled<V>>
-where
-    I: ElementInteractivity<V>,
-{
-    fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
-        self.base.focus_listeners()
-    }
-
-    fn set_focus_style(&mut self, style: StyleRefinement) {
-        self.base.set_focus_style(style)
-    }
-
-    fn set_focus_in_style(&mut self, style: StyleRefinement) {
-        self.base.set_focus_in_style(style)
-    }
-
-    fn set_in_focus_style(&mut self, style: StyleRefinement) {
-        self.base.set_in_focus_style(style)
+impl<V> InteractiveComponent<V> for Svg<V> {
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        &mut self.interactivity
     }
 }

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

@@ -1,6 +1,6 @@
 use crate::{
     AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
-    Size, ViewContext,
+    Size, TextRun, ViewContext,
 };
 use parking_lot::Mutex;
 use smallvec::SmallVec;
@@ -11,6 +11,7 @@ impl<V: 'static> Component<V> for SharedString {
     fn render(self) -> AnyElement<V> {
         Text {
             text: self,
+            runs: None,
             state_type: PhantomData,
         }
         .render()
@@ -21,6 +22,7 @@ impl<V: 'static> Component<V> for &'static str {
     fn render(self) -> AnyElement<V> {
         Text {
             text: self.into(),
+            runs: None,
             state_type: PhantomData,
         }
         .render()
@@ -33,6 +35,7 @@ impl<V: 'static> Component<V> for String {
     fn render(self) -> AnyElement<V> {
         Text {
             text: self.into(),
+            runs: None,
             state_type: PhantomData,
         }
         .render()
@@ -41,9 +44,25 @@ impl<V: 'static> Component<V> for String {
 
 pub struct Text<V> {
     text: SharedString,
+    runs: Option<Vec<TextRun>>,
     state_type: PhantomData<V>,
 }
 
+impl<V: 'static> Text<V> {
+    /// styled renders text that has different runs of different styles.
+    /// callers are responsible for setting the correct style for each run.
+    ////
+    /// For uniform text you can usually just pass a string as a child, and
+    /// cx.text_style() will be used automatically.
+    pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
+        Text {
+            text,
+            runs: Some(runs),
+            state_type: Default::default(),
+        }
+    }
+}
+
 impl<V: 'static> Component<V> for Text<V> {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
@@ -53,7 +72,7 @@ impl<V: 'static> Component<V> for Text<V> {
 impl<V: 'static> Element<V> for Text<V> {
     type ElementState = Arc<Mutex<Option<TextElementState>>>;
 
-    fn id(&self) -> Option<crate::ElementId> {
+    fn element_id(&self) -> Option<crate::ElementId> {
         None
     }
 
@@ -81,6 +100,13 @@ impl<V: 'static> Element<V> for Text<V> {
         let text = self.text.clone();
 
         let rem_size = cx.rem_size();
+
+        let runs = if let Some(runs) = self.runs.take() {
+            runs
+        } else {
+            vec![text_style.to_run(text.len())]
+        };
+
         let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
             let element_state = element_state.clone();
             move |known_dimensions, _| {
@@ -88,11 +114,15 @@ impl<V: 'static> Element<V> for Text<V> {
                     .layout_text(
                         &text,
                         font_size,
-                        &[text_style.to_run(text.len())],
+                        &runs[..],
                         known_dimensions.width, // Wrap if we know the width.
                     )
                     .log_err()
                 else {
+                    element_state.lock().replace(TextElementState {
+                        lines: Default::default(),
+                        line_height,
+                    });
                     return Size::default();
                 };
 
@@ -131,7 +161,8 @@ impl<V: 'static> Element<V> for Text<V> {
         let element_state = element_state.lock();
         let element_state = element_state
             .as_ref()
-            .expect("measurement has not been performed");
+            .ok_or_else(|| anyhow::anyhow!("measurement has not been performed on {}", &self.text))
+            .unwrap();
 
         let line_height = element_state.line_height;
         let mut line_origin = bounds.origin;

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

@@ -1,24 +1,23 @@
 use crate::{
     point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element,
-    ElementId, ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size,
-    StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity,
-    StyleRefinement, Styled, ViewContext,
+    ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
+    Point, Size, StyleRefinement, Styled, ViewContext,
 };
 use parking_lot::Mutex;
 use smallvec::SmallVec;
-use std::{cmp, ops::Range, sync::Arc};
+use std::{cmp, mem, ops::Range, sync::Arc};
 use taffy::style::Overflow;
 
 /// uniform_list provides lazy rendering for a set of items that are of uniform height.
 /// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
 /// uniform_list will only render the visibile subset of items.
-pub fn uniform_list<Id, V, C>(
-    id: Id,
+pub fn uniform_list<I, V, C>(
+    id: I,
     item_count: usize,
-    f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> SmallVec<[C; 64]>,
+    f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<C>,
 ) -> UniformList<V>
 where
-    Id: Into<ElementId>,
+    I: Into<ElementId>,
     V: 'static,
     C: Component<V>,
 {
@@ -37,7 +36,10 @@ where
                 .map(|component| component.render())
                 .collect()
         }),
-        interactivity: StatefulInteractivity::new(id, StatelessInteractivity::default()),
+        interactivity: Interactivity {
+            element_id: Some(id.into()),
+            ..Default::default()
+        },
         scroll_handle: None,
     }
 }
@@ -54,7 +56,7 @@ pub struct UniformList<V: 'static> {
             &'a mut ViewContext<V>,
         ) -> SmallVec<[AnyElement<V>; 64]>,
     >,
-    interactivity: StatefulInteractivity<V>,
+    interactivity: Interactivity<V>,
     scroll_handle: Option<UniformListScrollHandle>,
 }
 
@@ -103,7 +105,7 @@ pub struct UniformListState {
 impl<V: 'static> Element<V> for UniformList<V> {
     type ElementState = UniformListState;
 
-    fn id(&self) -> Option<crate::ElementId> {
+    fn element_id(&self) -> Option<crate::ElementId> {
         Some(self.id.clone())
     }
 
@@ -113,13 +115,18 @@ impl<V: 'static> Element<V> for UniformList<V> {
         element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> Self::ElementState {
-        element_state.unwrap_or_else(|| {
+        if let Some(mut element_state) = element_state {
+            element_state.interactive = self
+                .interactivity
+                .initialize(Some(element_state.interactive), cx);
+            element_state
+        } else {
             let item_size = self.measure_item(view_state, None, cx);
             UniformListState {
-                interactive: InteractiveElementState::default(),
+                interactive: self.interactivity.initialize(None, cx),
                 item_size,
             }
-        })
+        }
     }
 
     fn layout(
@@ -132,35 +139,44 @@ impl<V: 'static> Element<V> for UniformList<V> {
         let item_size = element_state.item_size;
         let rem_size = cx.rem_size();
 
-        cx.request_measured_layout(
-            self.computed_style(),
-            rem_size,
-            move |known_dimensions: Size<Option<Pixels>>, available_space: Size<AvailableSpace>| {
-                let desired_height = item_size.height * max_items;
-                let width = known_dimensions
-                    .width
-                    .unwrap_or(match available_space.width {
-                        AvailableSpace::Definite(x) => x,
-                        AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width,
-                    });
-                let height = match available_space.height {
-                    AvailableSpace::Definite(x) => desired_height.min(x),
-                    AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height,
-                };
-                size(width, height)
-            },
-        )
+        self.interactivity
+            .layout(&mut element_state.interactive, cx, |style, cx| {
+                cx.request_measured_layout(
+                    style,
+                    rem_size,
+                    move |known_dimensions: Size<Option<Pixels>>,
+                          available_space: Size<AvailableSpace>| {
+                        let desired_height = item_size.height * max_items;
+                        let width = known_dimensions
+                            .width
+                            .unwrap_or(match available_space.width {
+                                AvailableSpace::Definite(x) => x,
+                                AvailableSpace::MinContent | AvailableSpace::MaxContent => {
+                                    item_size.width
+                                }
+                            });
+                        let height = match available_space.height {
+                            AvailableSpace::Definite(x) => desired_height.min(x),
+                            AvailableSpace::MinContent | AvailableSpace::MaxContent => {
+                                desired_height
+                            }
+                        };
+                        size(width, height)
+                    },
+                )
+            })
     }
 
     fn paint(
         &mut self,
-        bounds: crate::Bounds<crate::Pixels>,
+        bounds: Bounds<crate::Pixels>,
         view_state: &mut V,
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) {
-        let style = self.computed_style();
-
+        let style =
+            self.interactivity
+                .compute_style(Some(bounds), &mut element_state.interactive, cx);
         let border = style.border_widths.to_pixels(cx.rem_size());
         let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
 
@@ -170,74 +186,79 @@ impl<V: 'static> Element<V> for UniformList<V> {
                 - point(border.right + padding.right, border.bottom + padding.bottom),
         );
 
-        cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
-            style.paint(bounds, cx);
+        let item_size = element_state.item_size;
+        let content_size = Size {
+            width: padded_bounds.size.width,
+            height: item_size.height * self.item_count,
+        };
 
-            let content_size;
-            if self.item_count > 0 {
-                let item_height = self
-                    .measure_item(view_state, Some(padded_bounds.size.width), cx)
-                    .height;
-                if let Some(scroll_handle) = self.scroll_handle.clone() {
-                    scroll_handle.0.lock().replace(ScrollHandleState {
-                        item_height,
-                        list_height: padded_bounds.size.height,
-                        scroll_offset: element_state.interactive.track_scroll_offset(),
-                    });
-                }
-                let visible_item_count = if item_height > px(0.) {
-                    (padded_bounds.size.height / item_height).ceil() as usize + 1
-                } else {
-                    0
-                };
-                let scroll_offset = element_state
-                    .interactive
-                    .scroll_offset()
-                    .map_or((0.0).into(), |offset| offset.y);
-                let first_visible_element_ix = (-scroll_offset / item_height).floor() as usize;
-                let visible_range = first_visible_element_ix
-                    ..cmp::min(
-                        first_visible_element_ix + visible_item_count,
-                        self.item_count,
-                    );
+        let mut interactivity = mem::take(&mut self.interactivity);
+        let shared_scroll_offset = element_state
+            .interactive
+            .scroll_offset
+            .get_or_insert_with(Arc::default)
+            .clone();
 
-                let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
+        interactivity.paint(
+            bounds,
+            content_size,
+            &mut element_state.interactive,
+            cx,
+            |style, scroll_offset, cx| {
+                let border = style.border_widths.to_pixels(cx.rem_size());
+                let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
 
-                content_size = Size {
-                    width: padded_bounds.size.width,
-                    height: item_height * self.item_count,
-                };
+                let padded_bounds = Bounds::from_corners(
+                    bounds.origin + point(border.left + padding.left, border.top + padding.top),
+                    bounds.lower_right()
+                        - point(border.right + padding.right, border.bottom + padding.bottom),
+                );
 
-                cx.with_z_index(1, |cx| {
-                    for (item, ix) in items.iter_mut().zip(visible_range) {
-                        let item_origin =
-                            padded_bounds.origin + point(px(0.), item_height * ix + scroll_offset);
-                        let available_space = size(
-                            AvailableSpace::Definite(padded_bounds.size.width),
-                            AvailableSpace::Definite(item_height),
-                        );
-                        item.draw(item_origin, available_space, view_state, cx);
-                    }
-                });
-            } else {
-                content_size = Size {
-                    width: bounds.size.width,
-                    height: px(0.),
-                };
-            }
+                cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
+                    style.paint(bounds, cx);
 
-            let overflow = point(style.overflow.x, Overflow::Scroll);
+                    if self.item_count > 0 {
+                        let item_height = self
+                            .measure_item(view_state, Some(padded_bounds.size.width), cx)
+                            .height;
+                        if let Some(scroll_handle) = self.scroll_handle.clone() {
+                            scroll_handle.0.lock().replace(ScrollHandleState {
+                                item_height,
+                                list_height: padded_bounds.size.height,
+                                scroll_offset: shared_scroll_offset,
+                            });
+                        }
+                        let visible_item_count = if item_height > px(0.) {
+                            (padded_bounds.size.height / item_height).ceil() as usize + 1
+                        } else {
+                            0
+                        };
 
-            cx.with_z_index(0, |cx| {
-                self.interactivity.paint(
-                    bounds,
-                    content_size,
-                    overflow,
-                    &mut element_state.interactive,
-                    cx,
-                );
-            });
-        })
+                        let first_visible_element_ix =
+                            (-scroll_offset.y / item_height).floor() as usize;
+                        let visible_range = first_visible_element_ix
+                            ..cmp::min(
+                                first_visible_element_ix + visible_item_count,
+                                self.item_count,
+                            );
+
+                        let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
+                        cx.with_z_index(1, |cx| {
+                            for (item, ix) in items.iter_mut().zip(visible_range) {
+                                let item_origin = padded_bounds.origin
+                                    + point(px(0.), item_height * ix + scroll_offset.y);
+                                let available_space = size(
+                                    AvailableSpace::Definite(padded_bounds.size.width),
+                                    AvailableSpace::Definite(item_height),
+                                );
+                                item.draw(item_origin, available_space, view_state, cx);
+                            }
+                        });
+                    }
+                })
+            },
+        );
+        self.interactivity = interactivity;
     }
 }
 
@@ -275,14 +296,8 @@ impl<V> UniformList<V> {
     }
 }
 
-impl<V: 'static> StatelessInteractive<V> for UniformList<V> {
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
-        self.interactivity.as_stateless_mut()
-    }
-}
-
-impl<V: 'static> StatefulInteractive<V> for UniformList<V> {
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
+impl<V> InteractiveComponent<V> for UniformList<V> {
+    fn interactivity(&mut self) -> &mut crate::Interactivity<V> {
         &mut self.interactivity
     }
 }

crates/gpui2/src/focusable.rs 🔗

@@ -1,252 +0,0 @@
-use crate::{
-    Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, MouseDownEvent, Pixels, Style,
-    StyleRefinement, ViewContext, WindowContext,
-};
-use refineable::Refineable;
-use smallvec::SmallVec;
-
-pub type FocusListeners<V> = SmallVec<[FocusListener<V>; 2]>;
-
-pub type FocusListener<V> =
-    Box<dyn Fn(&mut V, &FocusHandle, &FocusEvent, &mut ViewContext<V>) + 'static>;
-
-pub trait Focusable<V: 'static>: Element<V> {
-    fn focus_listeners(&mut self) -> &mut FocusListeners<V>;
-    fn set_focus_style(&mut self, style: StyleRefinement);
-    fn set_focus_in_style(&mut self, style: StyleRefinement);
-    fn set_in_focus_style(&mut self, style: StyleRefinement);
-
-    fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.set_focus_style(f(StyleRefinement::default()));
-        self
-    }
-
-    fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.set_focus_in_style(f(StyleRefinement::default()));
-        self
-    }
-
-    fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.set_in_focus_style(f(StyleRefinement::default()));
-        self
-    }
-
-    fn on_focus(
-        mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.focus_listeners()
-            .push(Box::new(move |view, focus_handle, event, cx| {
-                if event.focused.as_ref() == Some(focus_handle) {
-                    listener(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_blur(
-        mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.focus_listeners()
-            .push(Box::new(move |view, focus_handle, event, cx| {
-                if event.blurred.as_ref() == Some(focus_handle) {
-                    listener(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_focus_in(
-        mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.focus_listeners()
-            .push(Box::new(move |view, focus_handle, event, cx| {
-                let descendant_blurred = event
-                    .blurred
-                    .as_ref()
-                    .map_or(false, |blurred| focus_handle.contains(blurred, cx));
-                let descendant_focused = event
-                    .focused
-                    .as_ref()
-                    .map_or(false, |focused| focus_handle.contains(focused, cx));
-
-                if !descendant_blurred && descendant_focused {
-                    listener(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_focus_out(
-        mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.focus_listeners()
-            .push(Box::new(move |view, focus_handle, event, cx| {
-                let descendant_blurred = event
-                    .blurred
-                    .as_ref()
-                    .map_or(false, |blurred| focus_handle.contains(blurred, cx));
-                let descendant_focused = event
-                    .focused
-                    .as_ref()
-                    .map_or(false, |focused| focus_handle.contains(focused, cx));
-                if descendant_blurred && !descendant_focused {
-                    listener(view, event, cx)
-                }
-            }));
-        self
-    }
-}
-
-pub trait ElementFocus<V: 'static>: 'static {
-    fn as_focusable(&self) -> Option<&FocusEnabled<V>>;
-    fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled<V>>;
-
-    fn initialize<R>(
-        &mut self,
-        focus_handle: Option<FocusHandle>,
-        cx: &mut ViewContext<V>,
-        f: impl FnOnce(Option<FocusHandle>, &mut ViewContext<V>) -> R,
-    ) -> R {
-        if let Some(focusable) = self.as_focusable_mut() {
-            let focus_handle = focusable
-                .focus_handle
-                .get_or_insert_with(|| focus_handle.unwrap_or_else(|| cx.focus_handle()))
-                .clone();
-            for listener in focusable.focus_listeners.drain(..) {
-                let focus_handle = focus_handle.clone();
-                cx.on_focus_changed(move |view, event, cx| {
-                    listener(view, &focus_handle, event, cx)
-                });
-            }
-            cx.with_focus(focus_handle.clone(), |cx| f(Some(focus_handle), cx))
-        } else {
-            f(None, cx)
-        }
-    }
-
-    fn refine_style(&self, style: &mut Style, cx: &WindowContext) {
-        if let Some(focusable) = self.as_focusable() {
-            let focus_handle = focusable
-                .focus_handle
-                .as_ref()
-                .expect("must call initialize before refine_style");
-            if focus_handle.contains_focused(cx) {
-                style.refine(&focusable.focus_in_style);
-            }
-
-            if focus_handle.within_focused(cx) {
-                style.refine(&focusable.in_focus_style);
-            }
-
-            if focus_handle.is_focused(cx) {
-                style.refine(&focusable.focus_style);
-            }
-        }
-    }
-
-    fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
-        if let Some(focusable) = self.as_focusable() {
-            let focus_handle = focusable
-                .focus_handle
-                .clone()
-                .expect("must call initialize before paint");
-            cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                    if !cx.default_prevented() {
-                        cx.focus(&focus_handle);
-                        cx.prevent_default();
-                    }
-                }
-            })
-        }
-    }
-}
-
-pub struct FocusEnabled<V> {
-    pub focus_handle: Option<FocusHandle>,
-    pub focus_listeners: FocusListeners<V>,
-    pub focus_style: StyleRefinement,
-    pub focus_in_style: StyleRefinement,
-    pub in_focus_style: StyleRefinement,
-}
-
-impl<V> FocusEnabled<V> {
-    pub fn new() -> Self {
-        Self {
-            focus_handle: None,
-            focus_listeners: FocusListeners::default(),
-            focus_style: StyleRefinement::default(),
-            focus_in_style: StyleRefinement::default(),
-            in_focus_style: StyleRefinement::default(),
-        }
-    }
-
-    pub fn tracked(handle: &FocusHandle) -> Self {
-        Self {
-            focus_handle: Some(handle.clone()),
-            focus_listeners: FocusListeners::default(),
-            focus_style: StyleRefinement::default(),
-            focus_in_style: StyleRefinement::default(),
-            in_focus_style: StyleRefinement::default(),
-        }
-    }
-}
-
-impl<V: 'static> ElementFocus<V> for FocusEnabled<V> {
-    fn as_focusable(&self) -> Option<&FocusEnabled<V>> {
-        Some(self)
-    }
-
-    fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled<V>> {
-        Some(self)
-    }
-}
-
-impl<V> From<FocusHandle> for FocusEnabled<V> {
-    fn from(value: FocusHandle) -> Self {
-        Self {
-            focus_handle: Some(value),
-            focus_listeners: FocusListeners::default(),
-            focus_style: StyleRefinement::default(),
-            focus_in_style: StyleRefinement::default(),
-            in_focus_style: StyleRefinement::default(),
-        }
-    }
-}
-
-pub struct FocusDisabled;
-
-impl<V: 'static> ElementFocus<V> for FocusDisabled {
-    fn as_focusable(&self) -> Option<&FocusEnabled<V>> {
-        None
-    }
-
-    fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled<V>> {
-        None
-    }
-}

crates/gpui2/src/gpui2.rs 🔗

@@ -6,13 +6,14 @@ mod color;
 mod element;
 mod elements;
 mod executor;
-mod focusable;
 mod geometry;
 mod image_cache;
 mod input;
 mod interactive;
+mod key_dispatch;
 mod keymap;
 mod platform;
+pub mod prelude;
 mod scene;
 mod style;
 mod styled;
@@ -41,12 +42,12 @@ pub use ctor::ctor;
 pub use element::*;
 pub use elements::*;
 pub use executor::*;
-pub use focusable::*;
 pub use geometry::*;
 pub use gpui2_macros::*;
 pub use image_cache::*;
 pub use input::*;
 pub use interactive::*;
+pub use key_dispatch::*;
 pub use keymap::*;
 pub use platform::*;
 use private::Sealed;
@@ -104,6 +105,14 @@ pub trait Context {
     fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
     where
         F: FnOnce(AnyView, &mut WindowContext<'_>) -> T;
+
+    fn read_window<T, R>(
+        &self,
+        window: &WindowHandle<T>,
+        read: impl FnOnce(View<T>, &AppContext) -> R,
+    ) -> Result<R>
+    where
+        T: 'static;
 }
 
 pub trait VisualContext: Context {
@@ -147,7 +156,7 @@ pub enum GlobalKey {
 }
 
 pub trait BorrowAppContext {
-    fn with_text_style<F, R>(&mut self, style: TextStyleRefinement, f: F) -> R
+    fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
     where
         F: FnOnce(&mut Self) -> R;
 
@@ -158,14 +167,18 @@ impl<C> BorrowAppContext for C
 where
     C: BorrowMut<AppContext>,
 {
-    fn with_text_style<F, R>(&mut self, style: TextStyleRefinement, f: F) -> R
+    fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
     where
         F: FnOnce(&mut Self) -> R,
     {
-        self.borrow_mut().push_text_style(style);
-        let result = f(self);
-        self.borrow_mut().pop_text_style();
-        result
+        if let Some(style) = style {
+            self.borrow_mut().push_text_style(style);
+            let result = f(self);
+            self.borrow_mut().pop_text_style();
+            result
+        } else {
+            f(self)
+        }
     }
 
     fn set_global<G: 'static>(&mut self, global: G) {

crates/gpui2/src/input.rs 🔗

@@ -45,7 +45,7 @@ impl<V: 'static> ElementInputHandler<V> {
     /// containing view.
     pub fn new(element_bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) -> Self {
         ElementInputHandler {
-            view: cx.view(),
+            view: cx.view().clone(),
             element_bounds,
             cx: cx.to_async(),
         }

crates/gpui2/src/interactive.rs 🔗

@@ -1,947 +1,9 @@
 use crate::{
-    div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, BorrowWindow, Bounds,
-    Component, DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch,
-    Keystroke, Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style,
-    StyleRefinement, Task, View, ViewContext,
+    div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render,
+    ViewContext,
 };
-use collections::HashMap;
-use derive_more::{Deref, DerefMut};
-use parking_lot::Mutex;
-use refineable::Refineable;
 use smallvec::SmallVec;
-use std::{
-    any::{Any, TypeId},
-    fmt::Debug,
-    marker::PhantomData,
-    mem,
-    ops::Deref,
-    path::PathBuf,
-    sync::Arc,
-    time::Duration,
-};
-
-const DRAG_THRESHOLD: f64 = 2.;
-const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
-const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
-
-pub trait StatelessInteractive<V: 'static>: Element<V> {
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V>;
-
-    fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().hover_style = f(StyleRefinement::default());
-        self
-    }
-
-    fn group_hover(
-        mut self,
-        group_name: impl Into<SharedString>,
-        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().group_hover_style = Some(GroupStyle {
-            group: group_name.into(),
-            style: f(StyleRefinement::default()),
-        });
-        self
-    }
-
-    fn on_mouse_down(
-        mut self,
-        button: MouseButton,
-        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .mouse_down_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble
-                    && event.button == button
-                    && bounds.contains_point(&event.position)
-                {
-                    handler(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_mouse_up(
-        mut self,
-        button: MouseButton,
-        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .mouse_up_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble
-                    && event.button == button
-                    && bounds.contains_point(&event.position)
-                {
-                    handler(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_mouse_down_out(
-        mut self,
-        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .mouse_down_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) {
-                    handler(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_mouse_up_out(
-        mut self,
-        button: MouseButton,
-        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .mouse_up_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Capture
-                    && event.button == button
-                    && !bounds.contains_point(&event.position)
-                {
-                    handler(view, event, cx);
-                }
-            }));
-        self
-    }
-
-    fn on_mouse_move(
-        mut self,
-        handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .mouse_move_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                    handler(view, event, cx);
-                }
-            }));
-        self
-    }
-
-    fn on_scroll_wheel(
-        mut self,
-        handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .scroll_wheel_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                    handler(view, event, cx);
-                }
-            }));
-        self
-    }
-
-    fn context<C>(mut self, context: C) -> Self
-    where
-        Self: Sized,
-        C: TryInto<DispatchContext>,
-        C::Error: Debug,
-    {
-        self.stateless_interactivity().dispatch_context =
-            context.try_into().expect("invalid dispatch context");
-        self
-    }
-
-    /// Capture the given action, fires during the capture phase
-    fn capture_action<A: 'static>(
-        mut self,
-        listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().key_listeners.push((
-            TypeId::of::<A>(),
-            Box::new(move |view, action, _dipatch_context, phase, cx| {
-                let action = action.downcast_ref().unwrap();
-                if phase == DispatchPhase::Capture {
-                    listener(view, action, cx)
-                }
-                None
-            }),
-        ));
-        self
-    }
-
-    /// Add a listener for the given action, fires during the bubble event phase
-    fn on_action<A: 'static>(
-        mut self,
-        listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().key_listeners.push((
-            TypeId::of::<A>(),
-            Box::new(move |view, action, _dispatch_context, phase, cx| {
-                let action = action.downcast_ref().unwrap();
-                if phase == DispatchPhase::Bubble {
-                    listener(view, action, cx)
-                }
-
-                None
-            }),
-        ));
-        self
-    }
-
-    fn on_key_down(
-        mut self,
-        listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().key_listeners.push((
-            TypeId::of::<KeyDownEvent>(),
-            Box::new(move |view, event, _, phase, cx| {
-                let event = event.downcast_ref().unwrap();
-                listener(view, event, phase, cx);
-                None
-            }),
-        ));
-        self
-    }
-
-    fn on_key_up(
-        mut self,
-        listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().key_listeners.push((
-            TypeId::of::<KeyUpEvent>(),
-            Box::new(move |view, event, _, phase, cx| {
-                let event = event.downcast_ref().unwrap();
-                listener(view, event, phase, cx);
-                None
-            }),
-        ));
-        self
-    }
-
-    fn drag_over<S: 'static>(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .drag_over_styles
-            .push((TypeId::of::<S>(), f(StyleRefinement::default())));
-        self
-    }
-
-    fn group_drag_over<S: 'static>(
-        mut self,
-        group_name: impl Into<SharedString>,
-        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().group_drag_over_styles.push((
-            TypeId::of::<S>(),
-            GroupStyle {
-                group: group_name.into(),
-                style: f(StyleRefinement::default()),
-            },
-        ));
-        self
-    }
-
-    fn on_drop<W: 'static>(
-        mut self,
-        listener: impl Fn(&mut V, View<W>, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().drop_listeners.push((
-            TypeId::of::<W>(),
-            Box::new(move |view, dragged_view, cx| {
-                listener(view, dragged_view.downcast().unwrap(), cx);
-            }),
-        ));
-        self
-    }
-}
-
-pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V>;
-
-    fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateful_interactivity().active_style = f(StyleRefinement::default());
-        self
-    }
-
-    fn group_active(
-        mut self,
-        group_name: impl Into<SharedString>,
-        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateful_interactivity().group_active_style = Some(GroupStyle {
-            group: group_name.into(),
-            style: f(StyleRefinement::default()),
-        });
-        self
-    }
-
-    fn on_click(
-        mut self,
-        listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateful_interactivity()
-            .click_listeners
-            .push(Box::new(move |view, event, cx| listener(view, event, cx)));
-        self
-    }
-
-    fn on_drag<W>(
-        mut self,
-        listener: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-        W: 'static + Render,
-    {
-        debug_assert!(
-            self.stateful_interactivity().drag_listener.is_none(),
-            "calling on_drag more than once on the same element is not supported"
-        );
-        self.stateful_interactivity().drag_listener =
-            Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag {
-                view: listener(view_state, cx).into(),
-                cursor_offset,
-            }));
-        self
-    }
-
-    fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext<V>)) -> Self
-    where
-        Self: Sized,
-    {
-        debug_assert!(
-            self.stateful_interactivity().hover_listener.is_none(),
-            "calling on_hover more than once on the same element is not supported"
-        );
-        self.stateful_interactivity().hover_listener = Some(Box::new(listener));
-        self
-    }
-
-    fn tooltip<W>(
-        mut self,
-        build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-        W: 'static + Render,
-    {
-        debug_assert!(
-            self.stateful_interactivity().tooltip_builder.is_none(),
-            "calling tooltip more than once on the same element is not supported"
-        );
-        self.stateful_interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| {
-            build_tooltip(view_state, cx).into()
-        }));
-
-        self
-    }
-}
-
-pub trait ElementInteractivity<V: 'static>: 'static {
-    fn as_stateless(&self) -> &StatelessInteractivity<V>;
-    fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity<V>;
-    fn as_stateful(&self) -> Option<&StatefulInteractivity<V>>;
-    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity<V>>;
-
-    fn initialize<R>(
-        &mut self,
-        cx: &mut ViewContext<V>,
-        f: impl FnOnce(&mut ViewContext<V>) -> R,
-    ) -> R {
-        if let Some(stateful) = self.as_stateful_mut() {
-            cx.with_element_id(stateful.id.clone(), |global_id, cx| {
-                // In addition to any key down/up listeners registered directly on the element,
-                // we also add a key listener to match actions from the keymap.
-                stateful.key_listeners.push((
-                    TypeId::of::<KeyDownEvent>(),
-                    Box::new(move |_, key_down, context, phase, cx| {
-                        if phase == DispatchPhase::Bubble {
-                            let key_down = key_down.downcast_ref::<KeyDownEvent>().unwrap();
-                            if let KeyMatch::Some(action) =
-                                cx.match_keystroke(&global_id, &key_down.keystroke, context)
-                            {
-                                return Some(action);
-                            }
-                        }
-
-                        None
-                    }),
-                ));
-
-                cx.with_key_dispatch_context(stateful.dispatch_context.clone(), |cx| {
-                    cx.with_key_listeners(mem::take(&mut stateful.key_listeners), f)
-                })
-            })
-        } else {
-            let stateless = self.as_stateless_mut();
-            cx.with_key_dispatch_context(stateless.dispatch_context.clone(), |cx| {
-                cx.with_key_listeners(mem::take(&mut stateless.key_listeners), f)
-            })
-        }
-    }
-
-    fn refine_style(
-        &self,
-        style: &mut Style,
-        bounds: Bounds<Pixels>,
-        element_state: &InteractiveElementState,
-        cx: &mut ViewContext<V>,
-    ) {
-        let mouse_position = cx.mouse_position();
-        let stateless = self.as_stateless();
-        if let Some(group_hover) = stateless.group_hover_style.as_ref() {
-            if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
-                if group_bounds.contains_point(&mouse_position) {
-                    style.refine(&group_hover.style);
-                }
-            }
-        }
-        if bounds.contains_point(&mouse_position) {
-            style.refine(&stateless.hover_style);
-        }
-
-        if let Some(drag) = cx.active_drag.take() {
-            for (state_type, group_drag_style) in &self.as_stateless().group_drag_over_styles {
-                if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
-                    if *state_type == drag.view.entity_type()
-                        && group_bounds.contains_point(&mouse_position)
-                    {
-                        style.refine(&group_drag_style.style);
-                    }
-                }
-            }
-
-            for (state_type, drag_over_style) in &self.as_stateless().drag_over_styles {
-                if *state_type == drag.view.entity_type() && bounds.contains_point(&mouse_position)
-                {
-                    style.refine(drag_over_style);
-                }
-            }
-
-            cx.active_drag = Some(drag);
-        }
-
-        if let Some(stateful) = self.as_stateful() {
-            let active_state = element_state.active_state.lock();
-            if active_state.group {
-                if let Some(group_style) = stateful.group_active_style.as_ref() {
-                    style.refine(&group_style.style);
-                }
-            }
-            if active_state.element {
-                style.refine(&stateful.active_style);
-            }
-        }
-    }
-
-    fn paint(
-        &mut self,
-        bounds: Bounds<Pixels>,
-        content_size: Size<Pixels>,
-        overflow: Point<Overflow>,
-        element_state: &mut InteractiveElementState,
-        cx: &mut ViewContext<V>,
-    ) {
-        let stateless = self.as_stateless_mut();
-        for listener in stateless.mouse_down_listeners.drain(..) {
-            cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| {
-                listener(state, event, &bounds, phase, cx);
-            })
-        }
-
-        for listener in stateless.mouse_up_listeners.drain(..) {
-            cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| {
-                listener(state, event, &bounds, phase, cx);
-            })
-        }
-
-        for listener in stateless.mouse_move_listeners.drain(..) {
-            cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| {
-                listener(state, event, &bounds, phase, cx);
-            })
-        }
-
-        for listener in stateless.scroll_wheel_listeners.drain(..) {
-            cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| {
-                listener(state, event, &bounds, phase, cx);
-            })
-        }
-
-        let hover_group_bounds = stateless
-            .group_hover_style
-            .as_ref()
-            .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
-
-        if let Some(group_bounds) = hover_group_bounds {
-            let hovered = group_bounds.contains_point(&cx.mouse_position());
-            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
-                if phase == DispatchPhase::Capture {
-                    if group_bounds.contains_point(&event.position) != hovered {
-                        cx.notify();
-                    }
-                }
-            });
-        }
-
-        if stateless.hover_style.is_some()
-            || (cx.active_drag.is_some() && !stateless.drag_over_styles.is_empty())
-        {
-            let hovered = bounds.contains_point(&cx.mouse_position());
-            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
-                if phase == DispatchPhase::Capture {
-                    if bounds.contains_point(&event.position) != hovered {
-                        cx.notify();
-                    }
-                }
-            });
-        }
-
-        if cx.active_drag.is_some() {
-            let drop_listeners = mem::take(&mut stateless.drop_listeners);
-            cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                    if let Some(drag_state_type) =
-                        cx.active_drag.as_ref().map(|drag| drag.view.entity_type())
-                    {
-                        for (drop_state_type, listener) in &drop_listeners {
-                            if *drop_state_type == drag_state_type {
-                                let drag = cx
-                                    .active_drag
-                                    .take()
-                                    .expect("checked for type drag state type above");
-                                listener(view, drag.view.clone(), cx);
-                                cx.notify();
-                                cx.stop_propagation();
-                            }
-                        }
-                    }
-                }
-            });
-        }
-
-        if let Some(stateful) = self.as_stateful_mut() {
-            let click_listeners = mem::take(&mut stateful.click_listeners);
-            let drag_listener = mem::take(&mut stateful.drag_listener);
-
-            if !click_listeners.is_empty() || drag_listener.is_some() {
-                let pending_mouse_down = element_state.pending_mouse_down.clone();
-                let mouse_down = pending_mouse_down.lock().clone();
-                if let Some(mouse_down) = mouse_down {
-                    if let Some(drag_listener) = drag_listener {
-                        let active_state = element_state.active_state.clone();
-
-                        cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
-                            if cx.active_drag.is_some() {
-                                if phase == DispatchPhase::Capture {
-                                    cx.notify();
-                                }
-                            } else if phase == DispatchPhase::Bubble
-                                && bounds.contains_point(&event.position)
-                                && (event.position - mouse_down.position).magnitude()
-                                    > DRAG_THRESHOLD
-                            {
-                                *active_state.lock() = ActiveState::default();
-                                let cursor_offset = event.position - bounds.origin;
-                                let drag = drag_listener(view_state, cursor_offset, cx);
-                                cx.active_drag = Some(drag);
-                                cx.notify();
-                                cx.stop_propagation();
-                            }
-                        });
-                    }
-
-                    cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| {
-                        if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position)
-                        {
-                            let mouse_click = ClickEvent {
-                                down: mouse_down.clone(),
-                                up: event.clone(),
-                            };
-                            for listener in &click_listeners {
-                                listener(view_state, &mouse_click, cx);
-                            }
-                        }
-                        *pending_mouse_down.lock() = None;
-                    });
-                } else {
-                    cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| {
-                        if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position)
-                        {
-                            *pending_mouse_down.lock() = Some(event.clone());
-                        }
-                    });
-                }
-            }
-
-            if let Some(hover_listener) = stateful.hover_listener.take() {
-                let was_hovered = element_state.hover_state.clone();
-                let has_mouse_down = element_state.pending_mouse_down.clone();
-
-                cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
-                    if phase != DispatchPhase::Bubble {
-                        return;
-                    }
-                    let is_hovered =
-                        bounds.contains_point(&event.position) && has_mouse_down.lock().is_none();
-                    let mut was_hovered = was_hovered.lock();
-
-                    if is_hovered != was_hovered.clone() {
-                        *was_hovered = is_hovered;
-                        drop(was_hovered);
-
-                        hover_listener(view_state, is_hovered, cx);
-                    }
-                });
-            }
-
-            if let Some(tooltip_builder) = stateful.tooltip_builder.take() {
-                let active_tooltip = element_state.active_tooltip.clone();
-                let pending_mouse_down = element_state.pending_mouse_down.clone();
-
-                cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
-                    if phase != DispatchPhase::Bubble {
-                        return;
-                    }
-
-                    let is_hovered = bounds.contains_point(&event.position)
-                        && pending_mouse_down.lock().is_none();
-                    if !is_hovered {
-                        active_tooltip.lock().take();
-                        return;
-                    }
-
-                    if active_tooltip.lock().is_none() {
-                        let task = cx.spawn({
-                            let active_tooltip = active_tooltip.clone();
-                            let tooltip_builder = tooltip_builder.clone();
-
-                            move |view, mut cx| async move {
-                                cx.background_executor().timer(TOOLTIP_DELAY).await;
-                                view.update(&mut cx, move |view_state, cx| {
-                                    active_tooltip.lock().replace(ActiveTooltip {
-                                        waiting: None,
-                                        tooltip: Some(AnyTooltip {
-                                            view: tooltip_builder(view_state, cx),
-                                            cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
-                                        }),
-                                    });
-                                    cx.notify();
-                                })
-                                .ok();
-                            }
-                        });
-                        active_tooltip.lock().replace(ActiveTooltip {
-                            waiting: Some(task),
-                            tooltip: None,
-                        });
-                    }
-                });
-
-                if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
-                    if active_tooltip.tooltip.is_some() {
-                        cx.active_tooltip = active_tooltip.tooltip.clone()
-                    }
-                }
-            }
-
-            let active_state = element_state.active_state.clone();
-            if active_state.lock().is_none() {
-                let active_group_bounds = stateful
-                    .group_active_style
-                    .as_ref()
-                    .and_then(|group_active| GroupBounds::get(&group_active.group, cx));
-                cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| {
-                    if phase == DispatchPhase::Bubble {
-                        let group = active_group_bounds
-                            .map_or(false, |bounds| bounds.contains_point(&down.position));
-                        let element = bounds.contains_point(&down.position);
-                        if group || element {
-                            *active_state.lock() = ActiveState { group, element };
-                            cx.notify();
-                        }
-                    }
-                });
-            } else {
-                cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
-                    if phase == DispatchPhase::Capture {
-                        *active_state.lock() = ActiveState::default();
-                        cx.notify();
-                    }
-                });
-            }
-
-            if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
-                let scroll_offset = element_state
-                    .scroll_offset
-                    .get_or_insert_with(Arc::default)
-                    .clone();
-                let line_height = cx.line_height();
-                let scroll_max = (content_size - bounds.size).max(&Size::default());
-
-                cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
-                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                        let mut scroll_offset = scroll_offset.lock();
-                        let old_scroll_offset = *scroll_offset;
-                        let delta = event.delta.pixel_delta(line_height);
-
-                        if overflow.x == Overflow::Scroll {
-                            scroll_offset.x =
-                                (scroll_offset.x + delta.x).clamp(-scroll_max.width, px(0.));
-                        }
-
-                        if overflow.y == Overflow::Scroll {
-                            scroll_offset.y =
-                                (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.));
-                        }
-
-                        if *scroll_offset != old_scroll_offset {
-                            cx.notify();
-                            cx.stop_propagation();
-                        }
-                    }
-                });
-            }
-        }
-    }
-}
-
-#[derive(Deref, DerefMut)]
-pub struct StatefulInteractivity<V> {
-    pub id: ElementId,
-    #[deref]
-    #[deref_mut]
-    stateless: StatelessInteractivity<V>,
-    click_listeners: SmallVec<[ClickListener<V>; 2]>,
-    active_style: StyleRefinement,
-    group_active_style: Option<GroupStyle>,
-    drag_listener: Option<DragListener<V>>,
-    hover_listener: Option<HoverListener<V>>,
-    tooltip_builder: Option<TooltipBuilder<V>>,
-}
-
-impl<V: 'static> StatefulInteractivity<V> {
-    pub fn new(id: ElementId, stateless: StatelessInteractivity<V>) -> Self {
-        Self {
-            id,
-            stateless,
-            click_listeners: SmallVec::new(),
-            active_style: StyleRefinement::default(),
-            group_active_style: None,
-            drag_listener: None,
-            hover_listener: None,
-            tooltip_builder: None,
-        }
-    }
-}
-
-impl<V: 'static> ElementInteractivity<V> for StatefulInteractivity<V> {
-    fn as_stateful(&self) -> Option<&StatefulInteractivity<V>> {
-        Some(self)
-    }
-
-    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity<V>> {
-        Some(self)
-    }
-
-    fn as_stateless(&self) -> &StatelessInteractivity<V> {
-        &self.stateless
-    }
-
-    fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity<V> {
-        &mut self.stateless
-    }
-}
-
-type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
-
-pub struct StatelessInteractivity<V> {
-    pub dispatch_context: DispatchContext,
-    pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
-    pub mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>,
-    pub mouse_move_listeners: SmallVec<[MouseMoveListener<V>; 2]>,
-    pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener<V>; 2]>,
-    pub key_listeners: SmallVec<[(TypeId, KeyListener<V>); 32]>,
-    pub hover_style: StyleRefinement,
-    pub group_hover_style: Option<GroupStyle>,
-    drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
-    group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>,
-    drop_listeners: SmallVec<[(TypeId, Box<DropListener<V>>); 2]>,
-}
-
-impl<V> StatelessInteractivity<V> {
-    pub fn into_stateful(self, id: impl Into<ElementId>) -> StatefulInteractivity<V> {
-        StatefulInteractivity {
-            id: id.into(),
-            stateless: self,
-            click_listeners: SmallVec::new(),
-            drag_listener: None,
-            hover_listener: None,
-            tooltip_builder: None,
-            active_style: StyleRefinement::default(),
-            group_active_style: None,
-        }
-    }
-}
-
-pub struct GroupStyle {
-    pub group: SharedString,
-    pub style: StyleRefinement,
-}
-
-#[derive(Default)]
-pub struct GroupBounds(HashMap<SharedString, SmallVec<[Bounds<Pixels>; 1]>>);
-
-impl GroupBounds {
-    pub fn get(name: &SharedString, cx: &mut AppContext) -> Option<Bounds<Pixels>> {
-        cx.default_global::<Self>()
-            .0
-            .get(name)
-            .and_then(|bounds_stack| bounds_stack.last())
-            .cloned()
-    }
-
-    pub fn push(name: SharedString, bounds: Bounds<Pixels>, cx: &mut AppContext) {
-        cx.default_global::<Self>()
-            .0
-            .entry(name)
-            .or_default()
-            .push(bounds);
-    }
-
-    pub fn pop(name: &SharedString, cx: &mut AppContext) {
-        cx.default_global::<Self>().0.get_mut(name).unwrap().pop();
-    }
-}
-
-#[derive(Copy, Clone, Default, Eq, PartialEq)]
-struct ActiveState {
-    pub group: bool,
-    pub element: bool,
-}
-
-impl ActiveState {
-    pub fn is_none(&self) -> bool {
-        !self.group && !self.element
-    }
-}
-
-#[derive(Default)]
-pub struct InteractiveElementState {
-    active_state: Arc<Mutex<ActiveState>>,
-    hover_state: Arc<Mutex<bool>>,
-    pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
-    scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
-    active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
-}
-
-struct ActiveTooltip {
-    #[allow(unused)] // used to drop the task
-    waiting: Option<Task<()>>,
-    tooltip: Option<AnyTooltip>,
-}
-
-impl InteractiveElementState {
-    pub fn scroll_offset(&self) -> Option<Point<Pixels>> {
-        self.scroll_offset
-            .as_ref()
-            .map(|offset| offset.lock().clone())
-    }
-
-    pub fn track_scroll_offset(&mut self) -> Arc<Mutex<Point<Pixels>>> {
-        self.scroll_offset
-            .get_or_insert_with(|| Arc::new(Mutex::new(Default::default())))
-            .clone()
-    }
-}
-
-impl<V> Default for StatelessInteractivity<V> {
-    fn default() -> Self {
-        Self {
-            dispatch_context: DispatchContext::default(),
-            mouse_down_listeners: SmallVec::new(),
-            mouse_up_listeners: SmallVec::new(),
-            mouse_move_listeners: SmallVec::new(),
-            scroll_wheel_listeners: SmallVec::new(),
-            key_listeners: SmallVec::new(),
-            hover_style: StyleRefinement::default(),
-            group_hover_style: None,
-            drag_over_styles: SmallVec::new(),
-            group_drag_over_styles: SmallVec::new(),
-            drop_listeners: SmallVec::new(),
-        }
-    }
-}
-
-impl<V: 'static> ElementInteractivity<V> for StatelessInteractivity<V> {
-    fn as_stateful(&self) -> Option<&StatefulInteractivity<V>> {
-        None
-    }
-
-    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity<V>> {
-        None
-    }
-
-    fn as_stateless(&self) -> &StatelessInteractivity<V> {
-        self
-    }
-
-    fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity<V> {
-        self
-    }
-}
+use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf};
 
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct KeyDownEvent {
@@ -1024,10 +86,6 @@ where
     }
 }
 
-// impl<S, R, V, E> Render for Drag<S, R, V, E> {
-//     // fn render(&mut self, cx: ViewContext<Self>) ->
-// }
-
 #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
 pub enum MouseButton {
     Left,
@@ -1225,47 +283,11 @@ pub struct FocusEvent {
     pub focused: Option<FocusHandle>,
 }
 
-pub type MouseDownListener<V> = Box<
-    dyn Fn(&mut V, &MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
->;
-pub type MouseUpListener<V> = Box<
-    dyn Fn(&mut V, &MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
->;
-
-pub type MouseMoveListener<V> = Box<
-    dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
->;
-
-pub type ScrollWheelListener<V> = Box<
-    dyn Fn(&mut V, &ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
-        + 'static,
->;
-
-pub type ClickListener<V> = Box<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static>;
-
-pub(crate) type DragListener<V> =
-    Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + 'static>;
-
-pub(crate) type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
-
-pub(crate) type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
-
-pub type KeyListener<V> = Box<
-    dyn Fn(
-            &mut V,
-            &dyn Any,
-            &[&DispatchContext],
-            DispatchPhase,
-            &mut ViewContext<V>,
-        ) -> Option<Box<dyn Action>>
-        + 'static,
->;
-
 #[cfg(test)]
 mod test {
     use crate::{
-        self as gpui, div, Div, FocusHandle, KeyBinding, Keystroke, ParentElement, Render,
-        StatefulInteractivity, StatelessInteractive, TestAppContext, VisualContext,
+        self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding,
+        Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext,
     };
 
     struct TestView {

crates/gpui2/src/key_dispatch.rs 🔗

@@ -0,0 +1,204 @@
+use crate::{
+    build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch,
+    Keymap, Keystroke, KeystrokeMatcher, WindowContext,
+};
+use collections::HashMap;
+use parking_lot::Mutex;
+use smallvec::SmallVec;
+use std::{
+    any::{Any, TypeId},
+    rc::Rc,
+    sync::Arc,
+};
+use util::ResultExt;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
+pub struct DispatchNodeId(usize);
+
+pub(crate) struct DispatchTree {
+    node_stack: Vec<DispatchNodeId>,
+    context_stack: Vec<KeyContext>,
+    nodes: Vec<DispatchNode>,
+    focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
+    keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
+    keymap: Arc<Mutex<Keymap>>,
+}
+
+#[derive(Default)]
+pub(crate) struct DispatchNode {
+    pub key_listeners: SmallVec<[KeyListener; 2]>,
+    pub action_listeners: SmallVec<[DispatchActionListener; 16]>,
+    pub context: KeyContext,
+    parent: Option<DispatchNodeId>,
+}
+
+type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
+
+#[derive(Clone)]
+pub(crate) struct DispatchActionListener {
+    pub(crate) action_type: TypeId,
+    pub(crate) listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
+}
+
+impl DispatchTree {
+    pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
+        Self {
+            node_stack: Vec::new(),
+            context_stack: Vec::new(),
+            nodes: Vec::new(),
+            focusable_node_ids: HashMap::default(),
+            keystroke_matchers: HashMap::default(),
+            keymap,
+        }
+    }
+
+    pub fn clear(&mut self) {
+        self.node_stack.clear();
+        self.nodes.clear();
+        self.context_stack.clear();
+        self.focusable_node_ids.clear();
+        self.keystroke_matchers.clear();
+    }
+
+    pub fn push_node(&mut self, context: KeyContext, old_dispatcher: &mut Self) {
+        let parent = self.node_stack.last().copied();
+        let node_id = DispatchNodeId(self.nodes.len());
+        self.nodes.push(DispatchNode {
+            parent,
+            ..Default::default()
+        });
+        self.node_stack.push(node_id);
+        if !context.is_empty() {
+            self.active_node().context = context.clone();
+            self.context_stack.push(context);
+            if let Some((context_stack, matcher)) = old_dispatcher
+                .keystroke_matchers
+                .remove_entry(self.context_stack.as_slice())
+            {
+                self.keystroke_matchers.insert(context_stack, matcher);
+            }
+        }
+    }
+
+    pub fn pop_node(&mut self) {
+        let node_id = self.node_stack.pop().unwrap();
+        if !self.nodes[node_id.0].context.is_empty() {
+            self.context_stack.pop();
+        }
+    }
+
+    pub fn on_key_event(&mut self, listener: KeyListener) {
+        self.active_node().key_listeners.push(listener);
+    }
+
+    pub fn on_action(
+        &mut self,
+        action_type: TypeId,
+        listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
+    ) {
+        self.active_node()
+            .action_listeners
+            .push(DispatchActionListener {
+                action_type,
+                listener,
+            });
+    }
+
+    pub fn make_focusable(&mut self, focus_id: FocusId) {
+        self.focusable_node_ids
+            .insert(focus_id, self.active_node_id());
+    }
+
+    pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool {
+        if parent == child {
+            return true;
+        }
+
+        if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) {
+            let mut current_node_id = self.focusable_node_ids.get(&child).copied();
+            while let Some(node_id) = current_node_id {
+                if node_id == *parent_node_id {
+                    return true;
+                }
+                current_node_id = self.nodes[node_id.0].parent;
+            }
+        }
+        false
+    }
+
+    pub fn available_actions(&self, target: FocusId) -> Vec<Box<dyn Action>> {
+        let mut actions = Vec::new();
+        if let Some(node) = self.focusable_node_ids.get(&target) {
+            for node_id in self.dispatch_path(*node) {
+                let node = &self.nodes[node_id.0];
+                for DispatchActionListener { action_type, .. } in &node.action_listeners {
+                    actions.extend(build_action_from_type(action_type).log_err());
+                }
+            }
+        }
+        actions
+    }
+
+    pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
+        self.keymap
+            .lock()
+            .bindings_for_action(action.type_id())
+            .filter(|candidate| candidate.action.partial_eq(action))
+            .cloned()
+            .collect()
+    }
+
+    pub fn dispatch_key(
+        &mut self,
+        keystroke: &Keystroke,
+        context: &[KeyContext],
+    ) -> Option<Box<dyn Action>> {
+        if !self.keystroke_matchers.contains_key(context) {
+            let keystroke_contexts = context.iter().cloned().collect();
+            self.keystroke_matchers.insert(
+                keystroke_contexts,
+                KeystrokeMatcher::new(self.keymap.clone()),
+            );
+        }
+
+        let keystroke_matcher = self.keystroke_matchers.get_mut(context).unwrap();
+        if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke(keystroke, context) {
+            // Clear all pending keystrokes when an action has been found.
+            for keystroke_matcher in self.keystroke_matchers.values_mut() {
+                keystroke_matcher.clear_pending();
+            }
+
+            Some(action)
+        } else {
+            None
+        }
+    }
+
+    pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> {
+        let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new();
+        let mut current_node_id = Some(target);
+        while let Some(node_id) = current_node_id {
+            dispatch_path.push(node_id);
+            current_node_id = self.nodes[node_id.0].parent;
+        }
+        dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node.
+        dispatch_path
+    }
+
+    pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode {
+        &self.nodes[node_id.0]
+    }
+
+    fn active_node(&mut self) -> &mut DispatchNode {
+        let active_node_id = self.active_node_id();
+        &mut self.nodes[active_node_id.0]
+    }
+
+    pub fn focusable_node_id(&self, target: FocusId) -> Option<DispatchNodeId> {
+        self.focusable_node_ids.get(&target).copied()
+    }
+
+    fn active_node_id(&self) -> DispatchNodeId {
+        *self.node_stack.last().unwrap()
+    }
+}

crates/gpui2/src/keymap/binding.rs 🔗

@@ -1,11 +1,21 @@
-use crate::{Action, DispatchContext, DispatchContextPredicate, KeyMatch, Keystroke};
+use crate::{Action, KeyBindingContextPredicate, KeyContext, KeyMatch, Keystroke};
 use anyhow::Result;
 use smallvec::SmallVec;
 
 pub struct KeyBinding {
-    action: Box<dyn Action>,
-    pub(super) keystrokes: SmallVec<[Keystroke; 2]>,
-    pub(super) context_predicate: Option<DispatchContextPredicate>,
+    pub(crate) action: Box<dyn Action>,
+    pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
+    pub(crate) context_predicate: Option<KeyBindingContextPredicate>,
+}
+
+impl Clone for KeyBinding {
+    fn clone(&self) -> Self {
+        KeyBinding {
+            action: self.action.boxed_clone(),
+            keystrokes: self.keystrokes.clone(),
+            context_predicate: self.context_predicate.clone(),
+        }
+    }
 }
 
 impl KeyBinding {
@@ -15,7 +25,7 @@ impl KeyBinding {
 
     pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
         let context = if let Some(context) = context {
-            Some(DispatchContextPredicate::parse(context)?)
+            Some(KeyBindingContextPredicate::parse(context)?)
         } else {
             None
         };
@@ -32,7 +42,7 @@ impl KeyBinding {
         })
     }
 
-    pub fn matches_context(&self, contexts: &[&DispatchContext]) -> bool {
+    pub fn matches_context(&self, contexts: &[KeyContext]) -> bool {
         self.context_predicate
             .as_ref()
             .map(|predicate| predicate.eval(contexts))
@@ -42,7 +52,7 @@ impl KeyBinding {
     pub fn match_keystrokes(
         &self,
         pending_keystrokes: &[Keystroke],
-        contexts: &[&DispatchContext],
+        contexts: &[KeyContext],
     ) -> KeyMatch {
         if self.keystrokes.as_ref().starts_with(&pending_keystrokes)
             && self.matches_context(contexts)
@@ -61,7 +71,7 @@ impl KeyBinding {
     pub fn keystrokes_for_action(
         &self,
         action: &dyn Action,
-        contexts: &[&DispatchContext],
+        contexts: &[KeyContext],
     ) -> Option<SmallVec<[Keystroke; 2]>> {
         if self.action.partial_eq(action) && self.matches_context(contexts) {
             Some(self.keystrokes.clone())

crates/gpui2/src/keymap/context.rs 🔗

@@ -0,0 +1,449 @@
+use crate::SharedString;
+use anyhow::{anyhow, Result};
+use smallvec::SmallVec;
+use std::fmt;
+
+#[derive(Clone, Default, Eq, PartialEq, Hash)]
+pub struct KeyContext(SmallVec<[ContextEntry; 8]>);
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+struct ContextEntry {
+    key: SharedString,
+    value: Option<SharedString>,
+}
+
+impl<'a> TryFrom<&'a str> for KeyContext {
+    type Error = anyhow::Error;
+
+    fn try_from(value: &'a str) -> Result<Self> {
+        Self::parse(value)
+    }
+}
+
+impl KeyContext {
+    pub fn parse(source: &str) -> Result<Self> {
+        let mut context = Self::default();
+        let source = skip_whitespace(source);
+        Self::parse_expr(&source, &mut context)?;
+        Ok(context)
+    }
+
+    fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
+        if source.is_empty() {
+            return Ok(());
+        }
+
+        let key = source
+            .chars()
+            .take_while(|c| is_identifier_char(*c))
+            .collect::<String>();
+        source = skip_whitespace(&source[key.len()..]);
+        if let Some(suffix) = source.strip_prefix('=') {
+            source = skip_whitespace(suffix);
+            let value = source
+                .chars()
+                .take_while(|c| is_identifier_char(*c))
+                .collect::<String>();
+            source = skip_whitespace(&source[value.len()..]);
+            context.set(key, value);
+        } else {
+            context.add(key);
+        }
+
+        Self::parse_expr(source, context)
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+
+    pub fn clear(&mut self) {
+        self.0.clear();
+    }
+
+    pub fn extend(&mut self, other: &Self) {
+        for entry in &other.0 {
+            if !self.contains(&entry.key) {
+                self.0.push(entry.clone());
+            }
+        }
+    }
+
+    pub fn add<I: Into<SharedString>>(&mut self, identifier: I) {
+        let key = identifier.into();
+
+        if !self.contains(&key) {
+            self.0.push(ContextEntry { key, value: None })
+        }
+    }
+
+    pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
+        let key = key.into();
+        if !self.contains(&key) {
+            self.0.push(ContextEntry {
+                key,
+                value: Some(value.into()),
+            })
+        }
+    }
+
+    pub fn contains(&self, key: &str) -> bool {
+        self.0.iter().any(|entry| entry.key.as_ref() == key)
+    }
+
+    pub fn get(&self, key: &str) -> Option<&SharedString> {
+        self.0
+            .iter()
+            .find(|entry| entry.key.as_ref() == key)?
+            .value
+            .as_ref()
+    }
+}
+
+impl fmt::Debug for KeyContext {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let mut entries = self.0.iter().peekable();
+        while let Some(entry) = entries.next() {
+            if let Some(ref value) = entry.value {
+                write!(f, "{}={}", entry.key, value)?;
+            } else {
+                write!(f, "{}", entry.key)?;
+            }
+            if entries.peek().is_some() {
+                write!(f, " ")?;
+            }
+        }
+        Ok(())
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub enum KeyBindingContextPredicate {
+    Identifier(SharedString),
+    Equal(SharedString, SharedString),
+    NotEqual(SharedString, SharedString),
+    Child(
+        Box<KeyBindingContextPredicate>,
+        Box<KeyBindingContextPredicate>,
+    ),
+    Not(Box<KeyBindingContextPredicate>),
+    And(
+        Box<KeyBindingContextPredicate>,
+        Box<KeyBindingContextPredicate>,
+    ),
+    Or(
+        Box<KeyBindingContextPredicate>,
+        Box<KeyBindingContextPredicate>,
+    ),
+}
+
+impl KeyBindingContextPredicate {
+    pub fn parse(source: &str) -> Result<Self> {
+        let source = skip_whitespace(source);
+        let (predicate, rest) = Self::parse_expr(source, 0)?;
+        if let Some(next) = rest.chars().next() {
+            Err(anyhow!("unexpected character {next:?}"))
+        } else {
+            Ok(predicate)
+        }
+    }
+
+    pub fn eval(&self, contexts: &[KeyContext]) -> bool {
+        let Some(context) = contexts.last() else {
+            return false;
+        };
+        match self {
+            Self::Identifier(name) => context.contains(name),
+            Self::Equal(left, right) => context
+                .get(left)
+                .map(|value| value == right)
+                .unwrap_or(false),
+            Self::NotEqual(left, right) => context
+                .get(left)
+                .map(|value| value != right)
+                .unwrap_or(true),
+            Self::Not(pred) => !pred.eval(contexts),
+            Self::Child(parent, child) => {
+                parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts)
+            }
+            Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
+            Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
+        }
+    }
+
+    fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
+        type Op = fn(
+            KeyBindingContextPredicate,
+            KeyBindingContextPredicate,
+        ) -> Result<KeyBindingContextPredicate>;
+
+        let (mut predicate, rest) = Self::parse_primary(source)?;
+        source = rest;
+
+        'parse: loop {
+            for (operator, precedence, constructor) in [
+                (">", PRECEDENCE_CHILD, Self::new_child as Op),
+                ("&&", PRECEDENCE_AND, Self::new_and as Op),
+                ("||", PRECEDENCE_OR, Self::new_or as Op),
+                ("==", PRECEDENCE_EQ, Self::new_eq as Op),
+                ("!=", PRECEDENCE_EQ, Self::new_neq as Op),
+            ] {
+                if source.starts_with(operator) && precedence >= min_precedence {
+                    source = skip_whitespace(&source[operator.len()..]);
+                    let (right, rest) = Self::parse_expr(source, precedence + 1)?;
+                    predicate = constructor(predicate, right)?;
+                    source = rest;
+                    continue 'parse;
+                }
+            }
+            break;
+        }
+
+        Ok((predicate, source))
+    }
+
+    fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
+        let next = source
+            .chars()
+            .next()
+            .ok_or_else(|| anyhow!("unexpected eof"))?;
+        match next {
+            '(' => {
+                source = skip_whitespace(&source[1..]);
+                let (predicate, rest) = Self::parse_expr(source, 0)?;
+                if rest.starts_with(')') {
+                    source = skip_whitespace(&rest[1..]);
+                    Ok((predicate, source))
+                } else {
+                    Err(anyhow!("expected a ')'"))
+                }
+            }
+            '!' => {
+                let source = skip_whitespace(&source[1..]);
+                let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
+                Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source))
+            }
+            _ if is_identifier_char(next) => {
+                let len = source
+                    .find(|c: char| !is_identifier_char(c))
+                    .unwrap_or(source.len());
+                let (identifier, rest) = source.split_at(len);
+                source = skip_whitespace(rest);
+                Ok((
+                    KeyBindingContextPredicate::Identifier(identifier.to_string().into()),
+                    source,
+                ))
+            }
+            _ => Err(anyhow!("unexpected character {next:?}")),
+        }
+    }
+
+    fn new_or(self, other: Self) -> Result<Self> {
+        Ok(Self::Or(Box::new(self), Box::new(other)))
+    }
+
+    fn new_and(self, other: Self) -> Result<Self> {
+        Ok(Self::And(Box::new(self), Box::new(other)))
+    }
+
+    fn new_child(self, other: Self) -> Result<Self> {
+        Ok(Self::Child(Box::new(self), Box::new(other)))
+    }
+
+    fn new_eq(self, other: Self) -> Result<Self> {
+        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
+            Ok(Self::Equal(left, right))
+        } else {
+            Err(anyhow!("operands must be identifiers"))
+        }
+    }
+
+    fn new_neq(self, other: Self) -> Result<Self> {
+        if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
+            Ok(Self::NotEqual(left, right))
+        } else {
+            Err(anyhow!("operands must be identifiers"))
+        }
+    }
+}
+
+const PRECEDENCE_CHILD: u32 = 1;
+const PRECEDENCE_OR: u32 = 2;
+const PRECEDENCE_AND: u32 = 3;
+const PRECEDENCE_EQ: u32 = 4;
+const PRECEDENCE_NOT: u32 = 5;
+
+fn is_identifier_char(c: char) -> bool {
+    c.is_alphanumeric() || c == '_' || c == '-'
+}
+
+fn skip_whitespace(source: &str) -> &str {
+    let len = source
+        .find(|c: char| !c.is_whitespace())
+        .unwrap_or(source.len());
+    &source[len..]
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate as gpui;
+    use KeyBindingContextPredicate::*;
+
+    #[test]
+    fn test_actions_definition() {
+        {
+            actions!(A, B, C, D, E, F, G);
+        }
+
+        {
+            actions!(
+                A,
+                B,
+                C,
+                D,
+                E,
+                F,
+                G, // Don't wrap, test the trailing comma
+            );
+        }
+    }
+
+    #[test]
+    fn test_parse_context() {
+        let mut expected = KeyContext::default();
+        expected.add("baz");
+        expected.set("foo", "bar");
+        assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected);
+        assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected);
+        assert_eq!(
+            KeyContext::parse("  baz foo   =   bar baz").unwrap(),
+            expected
+        );
+        assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected);
+    }
+
+    #[test]
+    fn test_parse_identifiers() {
+        // Identifiers
+        assert_eq!(
+            KeyBindingContextPredicate::parse("abc12").unwrap(),
+            Identifier("abc12".into())
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("_1a").unwrap(),
+            Identifier("_1a".into())
+        );
+    }
+
+    #[test]
+    fn test_parse_negations() {
+        assert_eq!(
+            KeyBindingContextPredicate::parse("!abc").unwrap(),
+            Not(Box::new(Identifier("abc".into())))
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse(" ! ! abc").unwrap(),
+            Not(Box::new(Not(Box::new(Identifier("abc".into())))))
+        );
+    }
+
+    #[test]
+    fn test_parse_equality_operators() {
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a == b").unwrap(),
+            Equal("a".into(), "b".into())
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("c!=d").unwrap(),
+            NotEqual("c".into(), "d".into())
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("c == !d")
+                .unwrap_err()
+                .to_string(),
+            "operands must be identifiers"
+        );
+    }
+
+    #[test]
+    fn test_parse_boolean_operators() {
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a || b").unwrap(),
+            Or(
+                Box::new(Identifier("a".into())),
+                Box::new(Identifier("b".into()))
+            )
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a || !b && c").unwrap(),
+            Or(
+                Box::new(Identifier("a".into())),
+                Box::new(And(
+                    Box::new(Not(Box::new(Identifier("b".into())))),
+                    Box::new(Identifier("c".into()))
+                ))
+            )
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(),
+            Or(
+                Box::new(And(
+                    Box::new(Identifier("a".into())),
+                    Box::new(Identifier("b".into()))
+                )),
+                Box::new(And(
+                    Box::new(Identifier("c".into())),
+                    Box::new(Identifier("d".into()))
+                ))
+            )
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(),
+            Or(
+                Box::new(And(
+                    Box::new(Equal("a".into(), "b".into())),
+                    Box::new(Identifier("c".into()))
+                )),
+                Box::new(And(
+                    Box::new(Equal("d".into(), "e".into())),
+                    Box::new(Identifier("f".into()))
+                ))
+            )
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a && b && c && d").unwrap(),
+            And(
+                Box::new(And(
+                    Box::new(And(
+                        Box::new(Identifier("a".into())),
+                        Box::new(Identifier("b".into()))
+                    )),
+                    Box::new(Identifier("c".into())),
+                )),
+                Box::new(Identifier("d".into()))
+            ),
+        );
+    }
+
+    #[test]
+    fn test_parse_parenthesized_expressions() {
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
+            And(
+                Box::new(Identifier("a".into())),
+                Box::new(Or(
+                    Box::new(Equal("b".into(), "c".into())),
+                    Box::new(NotEqual("d".into(), "e".into())),
+                )),
+            ),
+        );
+        assert_eq!(
+            KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(),
+            Or(
+                Box::new(Identifier("a".into())),
+                Box::new(Identifier("b".into())),
+            )
+        );
+    }
+}

crates/gpui2/src/keymap/keymap.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{DispatchContextPredicate, KeyBinding, Keystroke};
+use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke};
 use collections::HashSet;
 use smallvec::SmallVec;
 use std::{any::TypeId, collections::HashMap};
@@ -11,7 +11,7 @@ pub struct Keymap {
     bindings: Vec<KeyBinding>,
     binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
     disabled_keystrokes:
-        HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<DispatchContextPredicate>>>,
+        HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<KeyBindingContextPredicate>>>,
     version: KeymapVersion,
 }
 

crates/gpui2/src/keymap/matcher.rs 🔗

@@ -1,15 +1,15 @@
-use crate::{Action, DispatchContext, Keymap, KeymapVersion, Keystroke};
+use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke};
 use parking_lot::Mutex;
 use smallvec::SmallVec;
 use std::sync::Arc;
 
-pub struct KeyMatcher {
+pub struct KeystrokeMatcher {
     pending_keystrokes: Vec<Keystroke>,
     keymap: Arc<Mutex<Keymap>>,
     keymap_version: KeymapVersion,
 }
 
-impl KeyMatcher {
+impl KeystrokeMatcher {
     pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
         let keymap_version = keymap.lock().version();
         Self {
@@ -44,7 +44,7 @@ impl KeyMatcher {
     pub fn match_keystroke(
         &mut self,
         keystroke: &Keystroke,
-        context_stack: &[&DispatchContext],
+        context_stack: &[KeyContext],
     ) -> KeyMatch {
         let keymap = self.keymap.lock();
         // Clear pending keystrokes if the keymap has changed since the last matched keystroke.
@@ -86,7 +86,7 @@ impl KeyMatcher {
     pub fn keystrokes_for_action(
         &self,
         action: &dyn Action,
-        contexts: &[&DispatchContext],
+        contexts: &[KeyContext],
     ) -> Option<SmallVec<[Keystroke; 2]>> {
         self.keymap
             .lock()
@@ -97,6 +97,7 @@ impl KeyMatcher {
     }
 }
 
+#[derive(Debug)]
 pub enum KeyMatch {
     None,
     Pending,

crates/gpui2/src/keymap/mod.rs 🔗

@@ -1,7 +1,9 @@
 mod binding;
+mod context;
 mod keymap;
 mod matcher;
 
 pub use binding::*;
+pub use context::*;
 pub use keymap::*;
 pub use matcher::*;

crates/gpui2/src/platform.rs 🔗

@@ -184,7 +184,11 @@ pub trait PlatformTextSystem: Send + Sync {
     fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
     fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
     fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
-    fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)>;
+    fn rasterize_glyph(
+        &self,
+        params: &RenderGlyphParams,
+        raster_bounds: Bounds<DevicePixels>,
+    ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
     fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
     fn wrap_line(
         &self,

crates/gpui2/src/platform/mac/text_system.rs 🔗

@@ -116,7 +116,9 @@ impl PlatformTextSystem for MacTextSystem {
                 },
             )?;
 
-            Ok(candidates[ix])
+            let font_id = candidates[ix];
+            lock.font_selections.insert(font.clone(), font_id);
+            Ok(font_id)
         }
     }
 
@@ -145,8 +147,9 @@ impl PlatformTextSystem for MacTextSystem {
     fn rasterize_glyph(
         &self,
         glyph_id: &RenderGlyphParams,
+        raster_bounds: Bounds<DevicePixels>,
     ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
-        self.0.read().rasterize_glyph(glyph_id)
+        self.0.read().rasterize_glyph(glyph_id, raster_bounds)
     }
 
     fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
@@ -247,8 +250,11 @@ impl MacTextSystemState {
             .into())
     }
 
-    fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)> {
-        let glyph_bounds = self.raster_bounds(params)?;
+    fn rasterize_glyph(
+        &self,
+        params: &RenderGlyphParams,
+        glyph_bounds: Bounds<DevicePixels>,
+    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
         if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
             Err(anyhow!("glyph bounds are empty"))
         } else {
@@ -260,6 +266,7 @@ impl MacTextSystemState {
             if params.subpixel_variant.y > 0 {
                 bitmap_size.height += DevicePixels(1);
             }
+            let bitmap_size = bitmap_size;
 
             let mut bytes;
             let cx;

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

@@ -3,8 +3,15 @@ use crate::{
     PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
 };
 use anyhow::{anyhow, Result};
+use collections::VecDeque;
+use futures::channel::oneshot;
 use parking_lot::Mutex;
-use std::{rc::Rc, sync::Arc};
+use std::{
+    cell::RefCell,
+    path::PathBuf,
+    rc::{Rc, Weak},
+    sync::Arc,
+};
 
 pub struct TestPlatform {
     background_executor: BackgroundExecutor,
@@ -13,18 +20,60 @@ pub struct TestPlatform {
     active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
     active_display: Rc<dyn PlatformDisplay>,
     active_cursor: Mutex<CursorStyle>,
+    pub(crate) prompts: RefCell<TestPrompts>,
+    weak: Weak<Self>,
+}
+
+#[derive(Default)]
+pub(crate) struct TestPrompts {
+    multiple_choice: VecDeque<oneshot::Sender<usize>>,
+    new_path: VecDeque<(PathBuf, oneshot::Sender<Option<PathBuf>>)>,
 }
 
 impl TestPlatform {
-    pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Self {
-        TestPlatform {
+    pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc<Self> {
+        Rc::new_cyclic(|weak| TestPlatform {
             background_executor: executor,
             foreground_executor,
-
+            prompts: Default::default(),
             active_cursor: Default::default(),
             active_display: Rc::new(TestDisplay::new()),
             active_window: Default::default(),
-        }
+            weak: weak.clone(),
+        })
+    }
+
+    pub(crate) fn simulate_new_path_selection(
+        &self,
+        select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
+    ) {
+        let (path, tx) = self
+            .prompts
+            .borrow_mut()
+            .new_path
+            .pop_front()
+            .expect("no pending new path prompt");
+        tx.send(select_path(&path)).ok();
+    }
+
+    pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) {
+        let tx = self
+            .prompts
+            .borrow_mut()
+            .multiple_choice
+            .pop_front()
+            .expect("no pending multiple choice prompt");
+        tx.send(response_ix).ok();
+    }
+
+    pub(crate) fn has_pending_prompt(&self) -> bool {
+        !self.prompts.borrow().multiple_choice.is_empty()
+    }
+
+    pub(crate) fn prompt(&self) -> oneshot::Receiver<usize> {
+        let (tx, rx) = oneshot::channel();
+        self.prompts.borrow_mut().multiple_choice.push_back(tx);
+        rx
     }
 }
 
@@ -46,9 +95,7 @@ impl Platform for TestPlatform {
         unimplemented!()
     }
 
-    fn quit(&self) {
-        unimplemented!()
-    }
+    fn quit(&self) {}
 
     fn restart(&self) {
         unimplemented!()
@@ -88,7 +135,11 @@ impl Platform for TestPlatform {
         options: WindowOptions,
     ) -> Box<dyn crate::PlatformWindow> {
         *self.active_window.lock() = Some(handle);
-        Box::new(TestWindow::new(options, self.active_display.clone()))
+        Box::new(TestWindow::new(
+            options,
+            self.weak.clone(),
+            self.active_display.clone(),
+        ))
     }
 
     fn set_display_link_output_callback(
@@ -118,15 +169,20 @@ impl Platform for TestPlatform {
     fn prompt_for_paths(
         &self,
         _options: crate::PathPromptOptions,
-    ) -> futures::channel::oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
+    ) -> oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
         unimplemented!()
     }
 
     fn prompt_for_new_path(
         &self,
-        _directory: &std::path::Path,
-    ) -> futures::channel::oneshot::Receiver<Option<std::path::PathBuf>> {
-        unimplemented!()
+        directory: &std::path::Path,
+    ) -> oneshot::Receiver<Option<std::path::PathBuf>> {
+        let (tx, rx) = oneshot::channel();
+        self.prompts
+            .borrow_mut()
+            .new_path
+            .push_back((directory.to_path_buf(), tx));
+        rx
     }
 
     fn reveal_path(&self, _path: &std::path::Path) {
@@ -141,9 +197,7 @@ impl Platform for TestPlatform {
         unimplemented!()
     }
 
-    fn on_quit(&self, _callback: Box<dyn FnMut()>) {
-        unimplemented!()
-    }
+    fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
 
     fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
         unimplemented!()

crates/gpui2/src/platform/test/window.rs 🔗

@@ -1,11 +1,14 @@
-use std::{rc::Rc, sync::Arc};
-
-use parking_lot::Mutex;
-
 use crate::{
-    px, Pixels, PlatformAtlas, PlatformDisplay, PlatformWindow, Point, Scene, Size,
+    px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay,
+    PlatformInputHandler, PlatformWindow, Point, Scene, Size, TestPlatform, TileId,
     WindowAppearance, WindowBounds, WindowOptions,
 };
+use collections::HashMap;
+use parking_lot::Mutex;
+use std::{
+    rc::{Rc, Weak},
+    sync::{self, Arc},
+};
 
 #[derive(Default)]
 struct Handlers {
@@ -19,18 +22,25 @@ pub struct TestWindow {
     bounds: WindowBounds,
     current_scene: Mutex<Option<Scene>>,
     display: Rc<dyn PlatformDisplay>,
-
+    input_handler: Option<Box<dyn PlatformInputHandler>>,
     handlers: Mutex<Handlers>,
+    platform: Weak<TestPlatform>,
     sprite_atlas: Arc<dyn PlatformAtlas>,
 }
+
 impl TestWindow {
-    pub fn new(options: WindowOptions, display: Rc<dyn PlatformDisplay>) -> Self {
+    pub fn new(
+        options: WindowOptions,
+        platform: Weak<TestPlatform>,
+        display: Rc<dyn PlatformDisplay>,
+    ) -> Self {
         Self {
             bounds: options.bounds,
             current_scene: Default::default(),
             display,
-
-            sprite_atlas: Arc::new(TestAtlas),
+            platform,
+            input_handler: None,
+            sprite_atlas: Arc::new(TestAtlas::new()),
             handlers: Default::default(),
         }
     }
@@ -73,8 +83,8 @@ impl PlatformWindow for TestWindow {
         todo!()
     }
 
-    fn set_input_handler(&mut self, _input_handler: Box<dyn crate::PlatformInputHandler>) {
-        todo!()
+    fn set_input_handler(&mut self, input_handler: Box<dyn crate::PlatformInputHandler>) {
+        self.input_handler = Some(input_handler);
     }
 
     fn prompt(
@@ -83,7 +93,7 @@ impl PlatformWindow for TestWindow {
         _msg: &str,
         _answers: &[&str],
     ) -> futures::channel::oneshot::Receiver<usize> {
-        todo!()
+        self.platform.upgrade().expect("platform dropped").prompt()
     }
 
     fn activate(&self) {
@@ -154,26 +164,71 @@ impl PlatformWindow for TestWindow {
         self.current_scene.lock().replace(scene);
     }
 
-    fn sprite_atlas(&self) -> std::sync::Arc<dyn crate::PlatformAtlas> {
+    fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
         self.sprite_atlas.clone()
     }
 }
 
-pub struct TestAtlas;
+pub struct TestAtlasState {
+    next_id: u32,
+    tiles: HashMap<AtlasKey, AtlasTile>,
+}
+
+pub struct TestAtlas(Mutex<TestAtlasState>);
+
+impl TestAtlas {
+    pub fn new() -> Self {
+        TestAtlas(Mutex::new(TestAtlasState {
+            next_id: 0,
+            tiles: HashMap::default(),
+        }))
+    }
+}
 
 impl PlatformAtlas for TestAtlas {
     fn get_or_insert_with<'a>(
         &self,
-        _key: &crate::AtlasKey,
-        _build: &mut dyn FnMut() -> anyhow::Result<(
+        key: &crate::AtlasKey,
+        build: &mut dyn FnMut() -> anyhow::Result<(
             Size<crate::DevicePixels>,
             std::borrow::Cow<'a, [u8]>,
         )>,
     ) -> anyhow::Result<crate::AtlasTile> {
-        todo!()
+        let mut state = self.0.lock();
+        if let Some(tile) = state.tiles.get(key) {
+            return Ok(tile.clone());
+        }
+
+        state.next_id += 1;
+        let texture_id = state.next_id;
+        state.next_id += 1;
+        let tile_id = state.next_id;
+
+        drop(state);
+        let (size, _) = build()?;
+        let mut state = self.0.lock();
+
+        state.tiles.insert(
+            key.clone(),
+            crate::AtlasTile {
+                texture_id: AtlasTextureId {
+                    index: texture_id,
+                    kind: crate::AtlasTextureKind::Path,
+                },
+                tile_id: TileId(tile_id),
+                bounds: crate::Bounds {
+                    origin: Point::zero(),
+                    size,
+                },
+            },
+        );
+
+        Ok(state.tiles[key].clone())
     }
 
     fn clear(&self) {
-        todo!()
+        let mut state = self.0.lock();
+        state.tiles = HashMap::default();
+        state.next_id = 0;
     }
 }

crates/gpui2/src/prelude.rs 🔗

@@ -0,0 +1,4 @@
+pub use crate::{
+    BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent,
+    ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext,
+};

crates/gpui2/src/style.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
     black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
     Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
-    FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Result,
-    Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext,
+    FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
+    SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext,
 };
 use refineable::{Cascade, Refineable};
 use smallvec::SmallVec;
@@ -157,7 +157,7 @@ impl Default for TextStyle {
 }
 
 impl TextStyle {
-    pub fn highlight(mut self, style: HighlightStyle) -> Result<Self> {
+    pub fn highlight(mut self, style: HighlightStyle) -> Self {
         if let Some(weight) = style.font_weight {
             self.font_weight = weight;
         }
@@ -177,7 +177,7 @@ impl TextStyle {
             self.underline = Some(underline);
         }
 
-        Ok(self)
+        self
     }
 
     pub fn font(&self) -> Font {
@@ -220,7 +220,7 @@ pub struct HighlightStyle {
 impl Eq for HighlightStyle {}
 
 impl Style {
-    pub fn text_style(&self, _cx: &WindowContext) -> Option<&TextStyleRefinement> {
+    pub fn text_style(&self) -> Option<&TextStyleRefinement> {
         if self.text.is_some() {
             Some(&self.text)
         } else {
@@ -228,13 +228,47 @@ impl Style {
         }
     }
 
+    pub fn overflow_mask(&self, bounds: Bounds<Pixels>) -> Option<ContentMask<Pixels>> {
+        match self.overflow {
+            Point {
+                x: Overflow::Visible,
+                y: Overflow::Visible,
+            } => None,
+            _ => {
+                let current_mask = bounds;
+                let min = current_mask.origin;
+                let max = current_mask.lower_right();
+                let bounds = match (
+                    self.overflow.x == Overflow::Visible,
+                    self.overflow.y == Overflow::Visible,
+                ) {
+                    // x and y both visible
+                    (true, true) => return None,
+                    // x visible, y hidden
+                    (true, false) => Bounds::from_corners(
+                        point(min.x, bounds.origin.y),
+                        point(max.x, bounds.lower_right().y),
+                    ),
+                    // x hidden, y visible
+                    (false, true) => Bounds::from_corners(
+                        point(bounds.origin.x, min.y),
+                        point(bounds.lower_right().x, max.y),
+                    ),
+                    // both hidden
+                    (false, false) => bounds,
+                };
+                Some(ContentMask { bounds })
+            }
+        }
+    }
+
     pub fn apply_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R
     where
         C: BorrowAppContext,
         F: FnOnce(&mut C) -> R,
     {
         if self.text.is_some() {
-            cx.with_text_style(self.text.clone(), f)
+            cx.with_text_style(Some(self.text.clone()), f)
         } else {
             f(cx)
         }
@@ -274,7 +308,7 @@ impl Style {
             bounds: mask_bounds,
         };
 
-        cx.with_content_mask(mask, f)
+        cx.with_content_mask(Some(mask), f)
     }
 
     /// Paints the background of an element styled with this style.

crates/gpui2/src/styled.rs 🔗

@@ -1,26 +1,24 @@
 use crate::{
     self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
     DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position,
-    SharedString, Style, StyleRefinement, Visibility,
+    SharedString, StyleRefinement, Visibility,
 };
 use crate::{BoxShadow, TextStyleRefinement};
-use refineable::Refineable;
 use smallvec::{smallvec, SmallVec};
+use taffy::style::Overflow;
 
-pub trait Styled {
+pub trait Styled: Sized {
     fn style(&mut self) -> &mut StyleRefinement;
 
-    fn computed_style(&mut self) -> Style {
-        Style::default().refined(self.style().clone())
-    }
-
     gpui2_macros::style_helpers!();
 
+    fn z_index(mut self, z_index: u32) -> Self {
+        self.style().z_index = Some(z_index);
+        self
+    }
+
     /// Sets the size of the element to the full width and height.
-    fn full(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn full(mut self) -> Self {
         self.style().size.width = Some(relative(1.).into());
         self.style().size.height = Some(relative(1.).into());
         self
@@ -28,118 +26,98 @@ pub trait Styled {
 
     /// Sets the position of the element to `relative`.
     /// [Docs](https://tailwindcss.com/docs/position)
-    fn relative(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn relative(mut self) -> Self {
         self.style().position = Some(Position::Relative);
         self
     }
 
     /// Sets the position of the element to `absolute`.
     /// [Docs](https://tailwindcss.com/docs/position)
-    fn absolute(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn absolute(mut self) -> Self {
         self.style().position = Some(Position::Absolute);
         self
     }
 
     /// Sets the display type of the element to `block`.
     /// [Docs](https://tailwindcss.com/docs/display)
-    fn block(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn block(mut self) -> Self {
         self.style().display = Some(Display::Block);
         self
     }
 
     /// Sets the display type of the element to `flex`.
     /// [Docs](https://tailwindcss.com/docs/display)
-    fn flex(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex(mut self) -> Self {
         self.style().display = Some(Display::Flex);
         self
     }
 
     /// Sets the visibility of the element to `visible`.
     /// [Docs](https://tailwindcss.com/docs/visibility)
-    fn visible(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn visible(mut self) -> Self {
         self.style().visibility = Some(Visibility::Visible);
         self
     }
 
     /// Sets the visibility of the element to `hidden`.
     /// [Docs](https://tailwindcss.com/docs/visibility)
-    fn invisible(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn invisible(mut self) -> Self {
         self.style().visibility = Some(Visibility::Hidden);
         self
     }
 
-    fn cursor(mut self, cursor: CursorStyle) -> Self
-    where
-        Self: Sized,
-    {
+    fn overflow_hidden(mut self) -> Self {
+        self.style().overflow.x = Some(Overflow::Hidden);
+        self.style().overflow.y = Some(Overflow::Hidden);
+        self
+    }
+
+    fn overflow_hidden_x(mut self) -> Self {
+        self.style().overflow.x = Some(Overflow::Hidden);
+        self
+    }
+
+    fn overflow_hidden_y(mut self) -> Self {
+        self.style().overflow.y = Some(Overflow::Hidden);
+        self
+    }
+
+    fn cursor(mut self, cursor: CursorStyle) -> Self {
         self.style().mouse_cursor = Some(cursor);
         self
     }
 
     /// Sets the cursor style when hovering an element to `default`.
     /// [Docs](https://tailwindcss.com/docs/cursor)
-    fn cursor_default(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn cursor_default(mut self) -> Self {
         self.style().mouse_cursor = Some(CursorStyle::Arrow);
         self
     }
 
     /// Sets the cursor style when hovering an element to `pointer`.
     /// [Docs](https://tailwindcss.com/docs/cursor)
-    fn cursor_pointer(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn cursor_pointer(mut self) -> Self {
         self.style().mouse_cursor = Some(CursorStyle::PointingHand);
         self
     }
 
     /// Sets the flex direction of the element to `column`.
     /// [Docs](https://tailwindcss.com/docs/flex-direction#column)
-    fn flex_col(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_col(mut self) -> Self {
         self.style().flex_direction = Some(FlexDirection::Column);
         self
     }
 
     /// Sets the flex direction of the element to `row`.
     /// [Docs](https://tailwindcss.com/docs/flex-direction#row)
-    fn flex_row(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_row(mut self) -> Self {
         self.style().flex_direction = Some(FlexDirection::Row);
         self
     }
 
     /// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size.
     /// [Docs](https://tailwindcss.com/docs/flex#flex-1)
-    fn flex_1(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_1(mut self) -> Self {
         self.style().flex_grow = Some(1.);
         self.style().flex_shrink = Some(1.);
         self.style().flex_basis = Some(relative(0.).into());
@@ -148,10 +126,7 @@ pub trait Styled {
 
     /// Sets the element to allow a flex item to grow and shrink, taking into account its initial size.
     /// [Docs](https://tailwindcss.com/docs/flex#auto)
-    fn flex_auto(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_auto(mut self) -> Self {
         self.style().flex_grow = Some(1.);
         self.style().flex_shrink = Some(1.);
         self.style().flex_basis = Some(Length::Auto);
@@ -160,10 +135,7 @@ pub trait Styled {
 
     /// Sets the element to allow a flex item to shrink but not grow, taking into account its initial size.
     /// [Docs](https://tailwindcss.com/docs/flex#initial)
-    fn flex_initial(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_initial(mut self) -> Self {
         self.style().flex_grow = Some(0.);
         self.style().flex_shrink = Some(1.);
         self.style().flex_basis = Some(Length::Auto);
@@ -172,10 +144,7 @@ pub trait Styled {
 
     /// Sets the element to prevent a flex item from growing or shrinking.
     /// [Docs](https://tailwindcss.com/docs/flex#none)
-    fn flex_none(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_none(mut self) -> Self {
         self.style().flex_grow = Some(0.);
         self.style().flex_shrink = Some(0.);
         self
@@ -183,40 +152,28 @@ pub trait Styled {
 
     /// Sets the element to allow a flex item to grow to fill any available space.
     /// [Docs](https://tailwindcss.com/docs/flex-grow)
-    fn grow(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn grow(mut self) -> Self {
         self.style().flex_grow = Some(1.);
         self
     }
 
     /// Sets the element to align flex items to the start of the container's cross axis.
     /// [Docs](https://tailwindcss.com/docs/align-items#start)
-    fn items_start(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn items_start(mut self) -> Self {
         self.style().align_items = Some(AlignItems::FlexStart);
         self
     }
 
     /// Sets the element to align flex items to the end of the container's cross axis.
     /// [Docs](https://tailwindcss.com/docs/align-items#end)
-    fn items_end(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn items_end(mut self) -> Self {
         self.style().align_items = Some(AlignItems::FlexEnd);
         self
     }
 
     /// Sets the element to align flex items along the center of the container's cross axis.
     /// [Docs](https://tailwindcss.com/docs/align-items#center)
-    fn items_center(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn items_center(mut self) -> Self {
         self.style().align_items = Some(AlignItems::Center);
         self
     }
@@ -224,40 +181,28 @@ pub trait Styled {
     /// Sets the element to justify flex items along the container's main axis
     /// such that there is an equal amount of space between each item.
     /// [Docs](https://tailwindcss.com/docs/justify-content#space-between)
-    fn justify_between(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_between(mut self) -> Self {
         self.style().justify_content = Some(JustifyContent::SpaceBetween);
         self
     }
 
     /// Sets the element to justify flex items along the center of the container's main axis.
     /// [Docs](https://tailwindcss.com/docs/justify-content#center)
-    fn justify_center(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_center(mut self) -> Self {
         self.style().justify_content = Some(JustifyContent::Center);
         self
     }
 
     /// Sets the element to justify flex items against the start of the container's main axis.
     /// [Docs](https://tailwindcss.com/docs/justify-content#start)
-    fn justify_start(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_start(mut self) -> Self {
         self.style().justify_content = Some(JustifyContent::Start);
         self
     }
 
     /// Sets the element to justify flex items against the end of the container's main axis.
     /// [Docs](https://tailwindcss.com/docs/justify-content#end)
-    fn justify_end(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_end(mut self) -> Self {
         self.style().justify_content = Some(JustifyContent::End);
         self
     }
@@ -265,10 +210,7 @@ pub trait Styled {
     /// Sets the element to justify items along the container's main axis such
     /// that there is an equal amount of space on each side of each item.
     /// [Docs](https://tailwindcss.com/docs/justify-content#space-around)
-    fn justify_around(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_around(mut self) -> Self {
         self.style().justify_content = Some(JustifyContent::SpaceAround);
         self
     }
@@ -295,30 +237,21 @@ pub trait Styled {
 
     /// Sets the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self {
         self.style().box_shadow = Some(shadows);
         self
     }
 
     /// Clears the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow_none(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow_none(mut self) -> Self {
         self.style().box_shadow = Some(Default::default());
         self
     }
 
     /// Sets the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow_sm(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow_sm(mut self) -> Self {
         self.style().box_shadow = Some(smallvec::smallvec![BoxShadow {
             color: hsla(0., 0., 0., 0.05),
             offset: point(px(0.), px(1.)),
@@ -330,10 +263,7 @@ pub trait Styled {
 
     /// Sets the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow_md(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow_md(mut self) -> Self {
         self.style().box_shadow = Some(smallvec![
             BoxShadow {
                 color: hsla(0.5, 0., 0., 0.1),
@@ -353,10 +283,7 @@ pub trait Styled {
 
     /// Sets the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow_lg(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow_lg(mut self) -> Self {
         self.style().box_shadow = Some(smallvec![
             BoxShadow {
                 color: hsla(0., 0., 0., 0.1),
@@ -376,10 +303,7 @@ pub trait Styled {
 
     /// Sets the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow_xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow_xl(mut self) -> Self {
         self.style().box_shadow = Some(smallvec![
             BoxShadow {
                 color: hsla(0., 0., 0., 0.1),
@@ -399,10 +323,7 @@ pub trait Styled {
 
     /// Sets the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow_2xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow_2xl(mut self) -> Self {
         self.style().box_shadow = Some(smallvec![BoxShadow {
             color: hsla(0., 0., 0., 0.25),
             offset: point(px(0.), px(25.)),
@@ -417,198 +338,138 @@ pub trait Styled {
         &mut style.text
     }
 
-    fn text_color(mut self, color: impl Into<Hsla>) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_color(mut self, color: impl Into<Hsla>) -> Self {
         self.text_style().get_or_insert_with(Default::default).color = Some(color.into());
         self
     }
 
-    fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(size.into());
         self
     }
 
-    fn text_xs(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_xs(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(0.75).into());
         self
     }
 
-    fn text_sm(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_sm(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(0.875).into());
         self
     }
 
-    fn text_base(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_base(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(1.0).into());
         self
     }
 
-    fn text_lg(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_lg(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(1.125).into());
         self
     }
 
-    fn text_xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_xl(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(1.25).into());
         self
     }
 
-    fn text_2xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_2xl(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(1.5).into());
         self
     }
 
-    fn text_3xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_3xl(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(1.875).into());
         self
     }
 
-    fn text_decoration_none(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_none(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .underline = None;
         self
     }
 
-    fn text_decoration_color(mut self, color: impl Into<Hsla>) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_color(mut self, color: impl Into<Hsla>) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.color = Some(color.into());
         self
     }
 
-    fn text_decoration_solid(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_solid(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.wavy = false;
         self
     }
 
-    fn text_decoration_wavy(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_wavy(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.wavy = true;
         self
     }
 
-    fn text_decoration_0(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_0(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.thickness = px(0.);
         self
     }
 
-    fn text_decoration_1(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_1(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.thickness = px(1.);
         self
     }
 
-    fn text_decoration_2(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_2(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.thickness = px(2.);
         self
     }
 
-    fn text_decoration_4(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_4(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.thickness = px(4.);
         self
     }
 
-    fn text_decoration_8(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_8(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.thickness = px(8.);
         self
     }
 
-    fn font(mut self, family_name: impl Into<SharedString>) -> Self
-    where
-        Self: Sized,
-    {
+    fn font(mut self, family_name: impl Into<SharedString>) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_family = Some(family_name.into());
         self
     }
 
-    fn line_height(mut self, line_height: impl Into<DefiniteLength>) -> Self
-    where
-        Self: Sized,
-    {
+    fn line_height(mut self, line_height: impl Into<DefiniteLength>) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .line_height = Some(line_height.into());

crates/gpui2/src/text_system.rs 🔗

@@ -39,6 +39,7 @@ pub struct TextSystem {
     platform_text_system: Arc<dyn PlatformTextSystem>,
     font_ids_by_font: RwLock<HashMap<Font, FontId>>,
     font_metrics: RwLock<HashMap<FontId, FontMetrics>>,
+    raster_bounds: RwLock<HashMap<RenderGlyphParams, Bounds<DevicePixels>>>,
     wrapper_pool: Mutex<HashMap<FontIdWithSize, Vec<LineWrapper>>>,
     font_runs_pool: Mutex<Vec<Vec<FontRun>>>,
 }
@@ -48,10 +49,11 @@ impl TextSystem {
         TextSystem {
             line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())),
             platform_text_system,
-            font_metrics: RwLock::new(HashMap::default()),
-            font_ids_by_font: RwLock::new(HashMap::default()),
-            wrapper_pool: Mutex::new(HashMap::default()),
-            font_runs_pool: Default::default(),
+            font_metrics: RwLock::default(),
+            raster_bounds: RwLock::default(),
+            font_ids_by_font: RwLock::default(),
+            wrapper_pool: Mutex::default(),
+            font_runs_pool: Mutex::default(),
         }
     }
 
@@ -252,14 +254,24 @@ impl TextSystem {
     }
 
     pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
-        self.platform_text_system.glyph_raster_bounds(params)
+        let raster_bounds = self.raster_bounds.upgradable_read();
+        if let Some(bounds) = raster_bounds.get(params) {
+            Ok(bounds.clone())
+        } else {
+            let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
+            let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
+            raster_bounds.insert(params.clone(), bounds);
+            Ok(bounds)
+        }
     }
 
     pub fn rasterize_glyph(
         &self,
-        glyph_id: &RenderGlyphParams,
+        params: &RenderGlyphParams,
     ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
-        self.platform_text_system.rasterize_glyph(glyph_id)
+        let raster_bounds = self.raster_bounds(params)?;
+        self.platform_text_system
+            .rasterize_glyph(params, raster_bounds)
     }
 }
 
@@ -368,6 +380,7 @@ impl Display for FontStyle {
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct TextRun {
+    // number of utf8 bytes
     pub len: usize,
     pub font: Font,
     pub color: Hsla,

crates/gpui2/src/util.rs 🔗

@@ -1,16 +1,26 @@
+#[cfg(any(test, feature = "test-support"))]
+use std::time::Duration;
+
+#[cfg(any(test, feature = "test-support"))]
+use futures::Future;
+
+#[cfg(any(test, feature = "test-support"))]
+use smol::future::FutureExt;
+
 pub use util::*;
 
-// pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
-// where
-//     F: Future<Output = T>,
-// {
-//     let timer = async {
-//         smol::Timer::after(timeout).await;
-//         Err(())
-//     };
-//     let future = async move { Ok(f.await) };
-//     timer.race(future).await
-// }
+#[cfg(any(test, feature = "test-support"))]
+pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
+where
+    F: Future<Output = T>,
+{
+    let timer = async {
+        smol::Timer::after(timeout).await;
+        Err(())
+    };
+    let future = async move { Ok(f.await) };
+    timer.race(future).await
+}
 
 #[cfg(any(test, feature = "test-support"))]
 pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace);

crates/gpui2/src/view.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
-    Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, Model, Pixels,
-    Size, ViewContext, VisualContext, WeakModel, WindowContext,
+    BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId,
+    Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext,
 };
 use anyhow::{Context, Result};
 use std::{
@@ -184,10 +184,6 @@ impl AnyView {
             .compute_layout(layout_id, available_space);
         (self.paint)(self, &mut rendered_element, cx);
     }
-
-    pub(crate) fn draw_dispatch_stack(&self, cx: &mut WindowContext) {
-        (self.initialize)(self, cx);
-    }
 }
 
 impl<V: 'static> Component<V> for AnyView {
@@ -210,7 +206,7 @@ impl<V: Render> From<View<V>> for AnyView {
 impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
     type ElementState = Box<dyn Any>;
 
-    fn id(&self) -> Option<ElementId> {
+    fn element_id(&self) -> Option<ElementId> {
         Some(self.model.entity_id.into())
     }
 
@@ -285,12 +281,90 @@ where
     }
 }
 
+pub struct RenderView<C, V> {
+    view: View<V>,
+    component: Option<C>,
+}
+
+impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderView<C, ViewState>
+where
+    C: 'static + Component<ViewState>,
+    ParentViewState: 'static,
+    ViewState: 'static,
+{
+    fn render(self) -> AnyElement<ParentViewState> {
+        AnyElement::new(self)
+    }
+}
+
+impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderView<C, ViewState>
+where
+    C: 'static + Component<ViewState>,
+    ParentViewState: 'static,
+    ViewState: 'static,
+{
+    type ElementState = AnyElement<ViewState>;
+
+    fn element_id(&self) -> Option<ElementId> {
+        Some(self.view.entity_id().into())
+    }
+
+    fn initialize(
+        &mut self,
+        _: &mut ParentViewState,
+        _: Option<Self::ElementState>,
+        cx: &mut ViewContext<ParentViewState>,
+    ) -> Self::ElementState {
+        cx.with_element_id(Some(self.view.entity_id()), |cx| {
+            self.view.update(cx, |view, cx| {
+                let mut element = self.component.take().unwrap().render();
+                element.initialize(view, cx);
+                element
+            })
+        })
+    }
+
+    fn layout(
+        &mut self,
+        _: &mut ParentViewState,
+        element: &mut Self::ElementState,
+        cx: &mut ViewContext<ParentViewState>,
+    ) -> LayoutId {
+        cx.with_element_id(Some(self.view.entity_id()), |cx| {
+            self.view.update(cx, |view, cx| element.layout(view, cx))
+        })
+    }
+
+    fn paint(
+        &mut self,
+        _: Bounds<Pixels>,
+        _: &mut ParentViewState,
+        element: &mut Self::ElementState,
+        cx: &mut ViewContext<ParentViewState>,
+    ) {
+        cx.with_element_id(Some(self.view.entity_id()), |cx| {
+            self.view.update(cx, |view, cx| element.paint(view, cx))
+        })
+    }
+}
+
+pub fn render_view<C, V>(view: &View<V>, component: C) -> RenderView<C, V>
+where
+    C: 'static + Component<V>,
+    V: 'static,
+{
+    RenderView {
+        view: view.clone(),
+        component: Some(component),
+    }
+}
+
 mod any_view {
     use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
     use std::any::Any;
 
     pub(crate) fn initialize<V: Render>(view: &AnyView, cx: &mut WindowContext) -> Box<dyn Any> {
-        cx.with_element_id(view.model.entity_id, |_, cx| {
+        cx.with_element_id(Some(view.model.entity_id), |cx| {
             let view = view.clone().downcast::<V>().unwrap();
             let element = view.update(cx, |view, cx| {
                 let mut element = AnyElement::new(view.render(cx));
@@ -306,7 +380,7 @@ mod any_view {
         element: &mut Box<dyn Any>,
         cx: &mut WindowContext,
     ) -> LayoutId {
-        cx.with_element_id(view.model.entity_id, |_, cx| {
+        cx.with_element_id(Some(view.model.entity_id), |cx| {
             let view = view.clone().downcast::<V>().unwrap();
             let element = element.downcast_mut::<AnyElement<V>>().unwrap();
             view.update(cx, |view, cx| element.layout(view, cx))
@@ -318,7 +392,7 @@ mod any_view {
         element: &mut Box<dyn Any>,
         cx: &mut WindowContext,
     ) {
-        cx.with_element_id(view.model.entity_id, |_, cx| {
+        cx.with_element_id(Some(view.model.entity_id), |cx| {
             let view = view.clone().downcast::<V>().unwrap();
             let element = element.downcast_mut::<AnyElement<V>>().unwrap();
             view.update(cx, |view, cx| element.paint(view, cx))

crates/gpui2/src/window.rs 🔗

@@ -1,17 +1,17 @@
 use crate::{
-    build_action_from_type, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext,
+    key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext,
     AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
-    DevicePixels, DispatchContext, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
-    FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent,
-    IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers,
-    MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels,
-    PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite,
-    PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
-    SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription,
-    TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView,
-    WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
+    DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
+    EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData,
+    InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext,
+    Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path,
+    Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
+    PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
+    RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet,
+    Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext,
+    WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
 };
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context as _, Result};
 use collections::HashMap;
 use derive_more::{Deref, DerefMut};
 use futures::{
@@ -60,16 +60,7 @@ pub enum DispatchPhase {
 }
 
 type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
-type AnyListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
-type AnyKeyListener = Box<
-    dyn Fn(
-            &dyn Any,
-            &[&DispatchContext],
-            DispatchPhase,
-            &mut WindowContext,
-        ) -> Option<Box<dyn Action>>
-        + 'static,
->;
+type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
 type AnyFocusListener = Box<dyn Fn(&FocusEvent, &mut WindowContext) + 'static>;
 type AnyWindowFocusListener = Box<dyn FnMut(&FocusEvent, &mut WindowContext) -> bool + 'static>;
 
@@ -97,20 +88,10 @@ impl FocusId {
 
     /// Obtains whether this handle contains the given handle in the most recently rendered frame.
     pub(crate) fn contains(&self, other: Self, cx: &WindowContext) -> bool {
-        let mut ancestor = Some(other);
-        while let Some(ancestor_id) = ancestor {
-            if *self == ancestor_id {
-                return true;
-            } else {
-                ancestor = cx
-                    .window
-                    .current_frame
-                    .focus_parents_by_child
-                    .get(&ancestor_id)
-                    .copied();
-            }
-        }
-        false
+        cx.window
+            .current_frame
+            .dispatch_tree
+            .focus_contains(*self, other)
     }
 }
 
@@ -120,6 +101,12 @@ pub struct FocusHandle {
     handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
 }
 
+impl std::fmt::Debug for FocusHandle {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_fmt(format_args!("FocusHandle({:?})", self.id))
+    }
+}
+
 impl FocusHandle {
     pub(crate) fn new(handles: &Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>) -> Self {
         let id = handles.write().insert(AtomicUsize::new(1));
@@ -227,20 +214,31 @@ pub struct Window {
     pub(crate) focus: Option<FocusId>,
 }
 
-#[derive(Default)]
+// #[derive(Default)]
 pub(crate) struct Frame {
     element_states: HashMap<GlobalElementId, AnyBox>,
-    key_matchers: HashMap<GlobalElementId, KeyMatcher>,
-    mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyListener)>>,
+    mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
+    pub(crate) dispatch_tree: DispatchTree,
     pub(crate) focus_listeners: Vec<AnyFocusListener>,
-    pub(crate) key_dispatch_stack: Vec<KeyDispatchStackFrame>,
-    freeze_key_dispatch_stack: bool,
-    focus_parents_by_child: HashMap<FocusId, FocusId>,
     pub(crate) scene_builder: SceneBuilder,
     z_index_stack: StackingOrder,
     content_mask_stack: Vec<ContentMask<Pixels>>,
     element_offset_stack: Vec<Point<Pixels>>,
-    focus_stack: Vec<FocusId>,
+}
+
+impl Frame {
+    pub fn new(dispatch_tree: DispatchTree) -> Self {
+        Frame {
+            element_states: HashMap::default(),
+            mouse_listeners: HashMap::default(),
+            dispatch_tree,
+            focus_listeners: Vec::new(),
+            scene_builder: SceneBuilder::default(),
+            z_index_stack: StackingOrder::default(),
+            content_mask_stack: Vec::new(),
+            element_offset_stack: Vec::new(),
+        }
+    }
 }
 
 impl Window {
@@ -309,8 +307,8 @@ impl Window {
             layout_engine: TaffyLayoutEngine::new(),
             root_view: None,
             element_id_stack: GlobalElementId::default(),
-            previous_frame: Frame::default(),
-            current_frame: Frame::default(),
+            previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
+            current_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
             focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
             focus_listeners: SubscriberSet::new(),
             default_prevented: true,
@@ -328,18 +326,6 @@ impl Window {
     }
 }
 
-/// When constructing the element tree, we maintain a stack of key dispatch frames until we
-/// find the focused element. We interleave key listeners with dispatch contexts so we can use the
-/// contexts when matching key events against the keymap. A key listener can be either an action
-/// handler or a [KeyDown] / [KeyUp] event listener.
-pub(crate) enum KeyDispatchStackFrame {
-    Listener {
-        event_type: TypeId,
-        listener: AnyKeyListener,
-    },
-    Context(DispatchContext),
-}
-
 /// Indicates which region of the window is visible. Content falling outside of this mask will not be
 /// rendered. Currently, only rectangular content masks are supported, but we give the mask its own type
 /// to leave room to support more complex shapes in the future.
@@ -407,21 +393,16 @@ impl<'a> WindowContext<'a> {
 
     /// Move focus to the element associated with the given `FocusHandle`.
     pub fn focus(&mut self, handle: &FocusHandle) {
-        if self.window.focus == Some(handle.id) {
-            return;
-        }
+        let focus_id = handle.id;
 
         if self.window.last_blur.is_none() {
             self.window.last_blur = Some(self.window.focus);
         }
 
-        self.window.focus = Some(handle.id);
-
-        // self.window.current_frame.key_dispatch_stack.clear()
-        // self.window.root_view.initialize()
+        self.window.focus = Some(focus_id);
         self.app.push_effect(Effect::FocusChanged {
             window_handle: self.window.handle,
-            focused: Some(handle.id),
+            focused: Some(focus_id),
         });
         self.notify();
     }
@@ -441,11 +422,19 @@ impl<'a> WindowContext<'a> {
     }
 
     pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
-        self.defer(|cx| {
-            cx.app.propagate_event = true;
-            let stack = cx.dispatch_stack();
-            cx.dispatch_action_internal(action, &stack[..])
-        })
+        if let Some(focus_handle) = self.focused() {
+            self.defer(move |cx| {
+                if let Some(node_id) = cx
+                    .window
+                    .current_frame
+                    .dispatch_tree
+                    .focusable_node_id(focus_handle.id)
+                {
+                    cx.propagate_event = true;
+                    cx.dispatch_action_on_node(node_id, action);
+                }
+            })
+        }
     }
 
     /// Schedules the given function to be run at the end of the current effect cycle, allowing entities
@@ -731,6 +720,43 @@ impl<'a> WindowContext<'a> {
             ))
     }
 
+    /// Register a key event listener on the window for the current frame. The type of event
+    /// is determined by the first parameter of the given listener. When the next frame is rendered
+    /// the listener will be cleared.
+    ///
+    /// This is a fairly low-level method, so prefer using event handlers on elements unless you have
+    /// a specific need to register a global listener.
+    pub fn on_key_event<Event: 'static>(
+        &mut self,
+        handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static,
+    ) {
+        self.window
+            .current_frame
+            .dispatch_tree
+            .on_key_event(Rc::new(move |event, phase, cx| {
+                if let Some(event) = event.downcast_ref::<Event>() {
+                    handler(event, phase, cx)
+                }
+            }));
+    }
+
+    /// Register an action listener on the window for the current frame. The type of action
+    /// is determined by the first parameter of the given listener. When the next frame is rendered
+    /// the listener will be cleared.
+    ///
+    /// This is a fairly low-level method, so prefer using action handlers on elements unless you have
+    /// a specific need to register a global listener.
+    pub fn on_action(
+        &mut self,
+        action_type: TypeId,
+        handler: impl Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static,
+    ) {
+        self.window.current_frame.dispatch_tree.on_action(
+            action_type,
+            Rc::new(move |action, phase, cx| handler(action, phase, cx)),
+        );
+    }
+
     /// The position of the mouse relative to the window.
     pub fn mouse_position(&self) -> Point<Pixels> {
         self.window.mouse_position
@@ -1048,7 +1074,7 @@ impl<'a> WindowContext<'a> {
         if let Some(active_drag) = self.app.active_drag.take() {
             self.with_z_index(1, |cx| {
                 let offset = cx.mouse_position() - active_drag.cursor_offset;
-                cx.with_element_offset(Some(offset), |cx| {
+                cx.with_element_offset(offset, |cx| {
                     let available_space =
                         size(AvailableSpace::MinContent, AvailableSpace::MinContent);
                     active_drag.view.draw(available_space, cx);
@@ -1057,7 +1083,7 @@ impl<'a> WindowContext<'a> {
             });
         } else if let Some(active_tooltip) = self.app.active_tooltip.take() {
             self.with_z_index(1, |cx| {
-                cx.with_element_offset(Some(active_tooltip.cursor_offset), |cx| {
+                cx.with_element_offset(active_tooltip.cursor_offset, |cx| {
                     let available_space =
                         size(AvailableSpace::MinContent, AvailableSpace::MinContent);
                     active_tooltip.view.draw(available_space, cx);
@@ -1079,26 +1105,6 @@ impl<'a> WindowContext<'a> {
         self.window.dirty = false;
     }
 
-    pub(crate) fn dispatch_stack(&mut self) -> Vec<KeyDispatchStackFrame> {
-        let root_view = self.window.root_view.take().unwrap();
-        let window = &mut *self.window;
-        let mut spare_frame = Frame::default();
-        mem::swap(&mut spare_frame, &mut window.previous_frame);
-
-        self.start_frame();
-
-        root_view.draw_dispatch_stack(self);
-
-        let window = &mut *self.window;
-        // restore the old values of current and previous frame,
-        // putting the new frame into spare_frame.
-        mem::swap(&mut window.current_frame, &mut window.previous_frame);
-        mem::swap(&mut spare_frame, &mut window.previous_frame);
-        self.window.root_view = Some(root_view);
-
-        spare_frame.key_dispatch_stack
-    }
-
     /// Rotate the current frame and the previous frame, then clear the current frame.
     /// We repopulate all state in the current frame during each paint.
     fn start_frame(&mut self) {
@@ -1110,12 +1116,9 @@ impl<'a> WindowContext<'a> {
         mem::swap(&mut window.previous_frame, &mut window.current_frame);
         let frame = &mut window.current_frame;
         frame.element_states.clear();
-        frame.key_matchers.clear();
         frame.mouse_listeners.values_mut().for_each(Vec::clear);
         frame.focus_listeners.clear();
-        frame.key_dispatch_stack.clear();
-        frame.focus_parents_by_child.clear();
-        frame.freeze_key_dispatch_stack = false;
+        frame.dispatch_tree.clear();
     }
 
     /// Dispatch a mouse or keyboard event on the window.
@@ -1177,146 +1180,172 @@ impl<'a> WindowContext<'a> {
         };
 
         if let Some(any_mouse_event) = event.mouse_event() {
-            if let Some(mut handlers) = self
-                .window
-                .current_frame
-                .mouse_listeners
-                .remove(&any_mouse_event.type_id())
-            {
-                // Because handlers may add other handlers, we sort every time.
-                handlers.sort_by(|(a, _), (b, _)| a.cmp(b));
+            self.dispatch_mouse_event(any_mouse_event);
+        } else if let Some(any_key_event) = event.keyboard_event() {
+            self.dispatch_key_event(any_key_event);
+        }
 
-                // Capture phase, events bubble from back to front. Handlers for this phase are used for
-                // special purposes, such as detecting events outside of a given Bounds.
-                for (_, handler) in &mut handlers {
-                    handler(any_mouse_event, DispatchPhase::Capture, self);
-                    if !self.app.propagate_event {
-                        break;
-                    }
+        !self.app.propagate_event
+    }
+
+    fn dispatch_mouse_event(&mut self, event: &dyn Any) {
+        if let Some(mut handlers) = self
+            .window
+            .current_frame
+            .mouse_listeners
+            .remove(&event.type_id())
+        {
+            // Because handlers may add other handlers, we sort every time.
+            handlers.sort_by(|(a, _), (b, _)| a.cmp(b));
+
+            // Capture phase, events bubble from back to front. Handlers for this phase are used for
+            // special purposes, such as detecting events outside of a given Bounds.
+            for (_, handler) in &mut handlers {
+                handler(event, DispatchPhase::Capture, self);
+                if !self.app.propagate_event {
+                    break;
                 }
+            }
 
-                // Bubble phase, where most normal handlers do their work.
-                if self.app.propagate_event {
-                    for (_, handler) in handlers.iter_mut().rev() {
-                        handler(any_mouse_event, DispatchPhase::Bubble, self);
-                        if !self.app.propagate_event {
-                            break;
-                        }
+            // Bubble phase, where most normal handlers do their work.
+            if self.app.propagate_event {
+                for (_, handler) in handlers.iter_mut().rev() {
+                    handler(event, DispatchPhase::Bubble, self);
+                    if !self.app.propagate_event {
+                        break;
                     }
                 }
+            }
 
-                if self.app.propagate_event
-                    && any_mouse_event.downcast_ref::<MouseUpEvent>().is_some()
-                {
-                    self.active_drag = None;
-                }
+            if self.app.propagate_event && event.downcast_ref::<MouseUpEvent>().is_some() {
+                self.active_drag = None;
+            }
 
-                // Just in case any handlers added new handlers, which is weird, but possible.
-                handlers.extend(
-                    self.window
-                        .current_frame
-                        .mouse_listeners
-                        .get_mut(&any_mouse_event.type_id())
-                        .into_iter()
-                        .flat_map(|handlers| handlers.drain(..)),
-                );
+            // Just in case any handlers added new handlers, which is weird, but possible.
+            handlers.extend(
                 self.window
                     .current_frame
                     .mouse_listeners
-                    .insert(any_mouse_event.type_id(), handlers);
-            }
-        } else if let Some(any_key_event) = event.keyboard_event() {
-            let key_dispatch_stack = mem::take(&mut self.window.current_frame.key_dispatch_stack);
-            let key_event_type = any_key_event.type_id();
-            let mut context_stack = SmallVec::<[&DispatchContext; 16]>::new();
-
-            for (ix, frame) in key_dispatch_stack.iter().enumerate() {
-                match frame {
-                    KeyDispatchStackFrame::Listener {
-                        event_type,
-                        listener,
-                    } => {
-                        if key_event_type == *event_type {
-                            if let Some(action) = listener(
-                                any_key_event,
-                                &context_stack,
-                                DispatchPhase::Capture,
-                                self,
-                            ) {
-                                self.dispatch_action_internal(action, &key_dispatch_stack[..ix]);
-                            }
-                            if !self.app.propagate_event {
-                                break;
-                            }
-                        }
-                    }
-                    KeyDispatchStackFrame::Context(context) => {
-                        context_stack.push(&context);
+                    .get_mut(&event.type_id())
+                    .into_iter()
+                    .flat_map(|handlers| handlers.drain(..)),
+            );
+            self.window
+                .current_frame
+                .mouse_listeners
+                .insert(event.type_id(), handlers);
+        }
+    }
+
+    fn dispatch_key_event(&mut self, event: &dyn Any) {
+        if let Some(node_id) = self.window.focus.and_then(|focus_id| {
+            self.window
+                .current_frame
+                .dispatch_tree
+                .focusable_node_id(focus_id)
+        }) {
+            let dispatch_path = self
+                .window
+                .current_frame
+                .dispatch_tree
+                .dispatch_path(node_id);
+
+            // Capture phase
+            let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new();
+            self.propagate_event = true;
+
+            for node_id in &dispatch_path {
+                let node = self.window.current_frame.dispatch_tree.node(*node_id);
+
+                if !node.context.is_empty() {
+                    context_stack.push(node.context.clone());
+                }
+
+                for key_listener in node.key_listeners.clone() {
+                    key_listener(event, DispatchPhase::Capture, self);
+                    if !self.propagate_event {
+                        return;
                     }
                 }
             }
 
-            if self.app.propagate_event {
-                for (ix, frame) in key_dispatch_stack.iter().enumerate().rev() {
-                    match frame {
-                        KeyDispatchStackFrame::Listener {
-                            event_type,
-                            listener,
-                        } => {
-                            if key_event_type == *event_type {
-                                if let Some(action) = listener(
-                                    any_key_event,
-                                    &context_stack,
-                                    DispatchPhase::Bubble,
-                                    self,
-                                ) {
-                                    self.dispatch_action_internal(
-                                        action,
-                                        &key_dispatch_stack[..ix],
-                                    );
-                                }
-
-                                if !self.app.propagate_event {
-                                    break;
-                                }
+            // Bubble phase
+            for node_id in dispatch_path.iter().rev() {
+                // Handle low level key events
+                let node = self.window.current_frame.dispatch_tree.node(*node_id);
+                for key_listener in node.key_listeners.clone() {
+                    key_listener(event, DispatchPhase::Bubble, self);
+                    if !self.propagate_event {
+                        return;
+                    }
+                }
+
+                // Match keystrokes
+                let node = self.window.current_frame.dispatch_tree.node(*node_id);
+                if !node.context.is_empty() {
+                    if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
+                        if let Some(action) = self
+                            .window
+                            .current_frame
+                            .dispatch_tree
+                            .dispatch_key(&key_down_event.keystroke, &context_stack)
+                        {
+                            self.dispatch_action_on_node(*node_id, action);
+                            if !self.propagate_event {
+                                return;
                             }
                         }
-                        KeyDispatchStackFrame::Context(_) => {
-                            context_stack.pop();
-                        }
                     }
+
+                    context_stack.pop();
                 }
             }
-
-            drop(context_stack);
-            self.window.current_frame.key_dispatch_stack = key_dispatch_stack;
         }
-
-        !self.app.propagate_event
     }
 
-    /// Attempt to map a keystroke to an action based on the keymap.
-    pub fn match_keystroke(
-        &mut self,
-        element_id: &GlobalElementId,
-        keystroke: &Keystroke,
-        context_stack: &[&DispatchContext],
-    ) -> KeyMatch {
-        let key_match = self
+    fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box<dyn Action>) {
+        let dispatch_path = self
             .window
             .current_frame
-            .key_matchers
-            .get_mut(element_id)
-            .unwrap()
-            .match_keystroke(keystroke, context_stack);
-
-        if key_match.is_some() {
-            for matcher in self.window.current_frame.key_matchers.values_mut() {
-                matcher.clear_pending();
+            .dispatch_tree
+            .dispatch_path(node_id);
+
+        // Capture phase
+        for node_id in &dispatch_path {
+            let node = self.window.current_frame.dispatch_tree.node(*node_id);
+            for DispatchActionListener {
+                action_type,
+                listener,
+            } in node.action_listeners.clone()
+            {
+                let any_action = action.as_any();
+                if action_type == any_action.type_id() {
+                    listener(any_action, DispatchPhase::Capture, self);
+                    if !self.propagate_event {
+                        return;
+                    }
+                }
             }
         }
 
-        key_match
+        // Bubble phase
+        for node_id in dispatch_path.iter().rev() {
+            let node = self.window.current_frame.dispatch_tree.node(*node_id);
+            for DispatchActionListener {
+                action_type,
+                listener,
+            } in node.action_listeners.clone()
+            {
+                let any_action = action.as_any();
+                if action_type == any_action.type_id() {
+                    self.propagate_event = false; // Actions stop propagation by default during the bubble phase
+                    listener(any_action, DispatchPhase::Bubble, self);
+                    if !self.propagate_event {
+                        return;
+                    }
+                }
+            }
+        }
     }
 
     /// Register the given handler to be invoked whenever the global of the given type
@@ -1336,6 +1365,14 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.activate();
     }
 
+    pub fn minimize_window(&self) {
+        self.window.platform_window.minimize();
+    }
+
+    pub fn toggle_full_screen(&self) {
+        self.window.platform_window.toggle_full_screen();
+    }
+
     pub fn prompt(
         &self,
         level: PromptLevel,
@@ -1345,106 +1382,22 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.prompt(level, msg, answers)
     }
 
-    pub fn available_actions(&self) -> impl Iterator<Item = Box<dyn Action>> + '_ {
-        let key_dispatch_stack = &self.window.previous_frame.key_dispatch_stack;
-        key_dispatch_stack.iter().filter_map(|frame| {
-            match frame {
-                // todo!factor out a KeyDispatchStackFrame::Action
-                KeyDispatchStackFrame::Listener {
-                    event_type,
-                    listener: _,
-                } => {
-                    match build_action_from_type(event_type) {
-                        Ok(action) => Some(action),
-                        Err(err) => {
-                            dbg!(err);
-                            None
-                        } // we'll hit his if TypeId == KeyDown
-                    }
-                }
-                KeyDispatchStackFrame::Context(_) => None,
-            }
-        })
-    }
-
-    pub(crate) fn dispatch_action_internal(
-        &mut self,
-        action: Box<dyn Action>,
-        dispatch_stack: &[KeyDispatchStackFrame],
-    ) {
-        let action_type = action.as_any().type_id();
-
-        if let Some(mut global_listeners) = self.app.global_action_listeners.remove(&action_type) {
-            for listener in &global_listeners {
-                listener(action.as_ref(), DispatchPhase::Capture, self);
-                if !self.app.propagate_event {
-                    break;
-                }
-            }
-            global_listeners.extend(
-                self.global_action_listeners
-                    .remove(&action_type)
-                    .unwrap_or_default(),
-            );
-            self.global_action_listeners
-                .insert(action_type, global_listeners);
-        }
-
-        if self.app.propagate_event {
-            for stack_frame in dispatch_stack {
-                if let KeyDispatchStackFrame::Listener {
-                    event_type,
-                    listener,
-                } = stack_frame
-                {
-                    if action_type == *event_type {
-                        listener(action.as_any(), &[], DispatchPhase::Capture, self);
-                        if !self.app.propagate_event {
-                            break;
-                        }
-                    }
-                }
-            }
-        }
-
-        if self.app.propagate_event {
-            for stack_frame in dispatch_stack.iter().rev() {
-                if let KeyDispatchStackFrame::Listener {
-                    event_type,
-                    listener,
-                } = stack_frame
-                {
-                    if action_type == *event_type {
-                        self.app.propagate_event = false;
-                        listener(action.as_any(), &[], DispatchPhase::Bubble, self);
-                        if !self.app.propagate_event {
-                            break;
-                        }
-                    }
-                }
-            }
+    pub fn available_actions(&self) -> Vec<Box<dyn Action>> {
+        if let Some(focus_id) = self.window.focus {
+            self.window
+                .current_frame
+                .dispatch_tree
+                .available_actions(focus_id)
+        } else {
+            Vec::new()
         }
+    }
 
-        if self.app.propagate_event {
-            if let Some(mut global_listeners) =
-                self.app.global_action_listeners.remove(&action_type)
-            {
-                for listener in global_listeners.iter().rev() {
-                    self.app.propagate_event = false;
-                    listener(action.as_ref(), DispatchPhase::Bubble, self);
-                    if !self.app.propagate_event {
-                        break;
-                    }
-                }
-                global_listeners.extend(
-                    self.global_action_listeners
-                        .remove(&action_type)
-                        .unwrap_or_default(),
-                );
-                self.global_action_listeners
-                    .insert(action_type, global_listeners);
-            }
-        }
+    pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
+        self.window
+            .current_frame
+            .dispatch_tree
+            .bindings_for_action(action)
     }
 }
 
@@ -1500,6 +1453,28 @@ impl Context for WindowContext<'_> {
         let entity = self.entities.read(handle);
         read(&*entity, &*self.app)
     }
+
+    fn read_window<T, R>(
+        &self,
+        window: &WindowHandle<T>,
+        read: impl FnOnce(View<T>, &AppContext) -> R,
+    ) -> Result<R>
+    where
+        T: 'static,
+    {
+        if window.any_handle == self.window.handle {
+            let root_view = self
+                .window
+                .root_view
+                .clone()
+                .unwrap()
+                .downcast::<T>()
+                .map_err(|_| anyhow!("the type of the window's root view has changed"))?;
+            Ok(read(root_view, self))
+        } else {
+            self.app.read_window(window, read)
+        }
+    }
 }
 
 impl VisualContext for WindowContext<'_> {
@@ -1606,56 +1581,50 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
     /// used to associate state with identified elements across separate frames.
     fn with_element_id<R>(
         &mut self,
-        id: impl Into<ElementId>,
-        f: impl FnOnce(GlobalElementId, &mut Self) -> R,
+        id: Option<impl Into<ElementId>>,
+        f: impl FnOnce(&mut Self) -> R,
     ) -> R {
-        let keymap = self.app_mut().keymap.clone();
-        let window = self.window_mut();
-        window.element_id_stack.push(id.into());
-        let global_id = window.element_id_stack.clone();
-
-        if window.current_frame.key_matchers.get(&global_id).is_none() {
-            window.current_frame.key_matchers.insert(
-                global_id.clone(),
-                window
-                    .previous_frame
-                    .key_matchers
-                    .remove(&global_id)
-                    .unwrap_or_else(|| KeyMatcher::new(keymap)),
-            );
+        if let Some(id) = id.map(Into::into) {
+            let window = self.window_mut();
+            window.element_id_stack.push(id.into());
+            let result = f(self);
+            let window: &mut Window = self.borrow_mut();
+            window.element_id_stack.pop();
+            result
+        } else {
+            f(self)
         }
-
-        let result = f(global_id, self);
-        let window: &mut Window = self.borrow_mut();
-        window.element_id_stack.pop();
-        result
     }
 
     /// Invoke the given function with the given content mask after intersecting it
     /// with the current mask.
     fn with_content_mask<R>(
         &mut self,
-        mask: ContentMask<Pixels>,
+        mask: Option<ContentMask<Pixels>>,
         f: impl FnOnce(&mut Self) -> R,
     ) -> R {
-        let mask = mask.intersect(&self.content_mask());
-        self.window_mut()
-            .current_frame
-            .content_mask_stack
-            .push(mask);
-        let result = f(self);
-        self.window_mut().current_frame.content_mask_stack.pop();
-        result
+        if let Some(mask) = mask {
+            let mask = mask.intersect(&self.content_mask());
+            self.window_mut()
+                .current_frame
+                .content_mask_stack
+                .push(mask);
+            let result = f(self);
+            self.window_mut().current_frame.content_mask_stack.pop();
+            result
+        } else {
+            f(self)
+        }
     }
 
     /// Update the global element offset based on the given offset. This is used to implement
     /// scrolling and position drag handles.
     fn with_element_offset<R>(
         &mut self,
-        offset: Option<Point<Pixels>>,
+        offset: Point<Pixels>,
         f: impl FnOnce(&mut Self) -> R,
     ) -> R {
-        let Some(offset) = offset else {
+        if offset.is_zero() {
             return f(self);
         };
 
@@ -1691,7 +1660,9 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
     where
         S: 'static,
     {
-        self.with_element_id(id, |global_id, cx| {
+        self.with_element_id(Some(id), |cx| {
+            let global_id = cx.window().element_id_stack.clone();
+
             if let Some(any) = cx
                 .window_mut()
                 .current_frame
@@ -1819,9 +1790,12 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         }
     }
 
-    // todo!("change this to return a reference");
-    pub fn view(&self) -> View<V> {
-        self.view.clone()
+    pub fn entity_id(&self) -> EntityId {
+        self.view.entity_id()
+    }
+
+    pub fn view(&self) -> &View<V> {
+        self.view
     }
 
     pub fn model(&self) -> Model<V> {
@@ -1844,7 +1818,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     where
         V: 'static,
     {
-        let view = self.view();
+        let view = self.view().clone();
         self.window_cx.on_next_frame(move |cx| view.update(cx, f));
     }
 
@@ -2109,94 +2083,27 @@ impl<'a, V: 'static> ViewContext<'a, V> {
             }));
     }
 
-    pub fn with_key_listeners<R>(
-        &mut self,
-        key_listeners: impl IntoIterator<Item = (TypeId, KeyListener<V>)>,
-        f: impl FnOnce(&mut Self) -> R,
-    ) -> R {
-        let old_stack_len = self.window.current_frame.key_dispatch_stack.len();
-        if !self.window.current_frame.freeze_key_dispatch_stack {
-            for (event_type, listener) in key_listeners {
-                let handle = self.view().downgrade();
-                let listener = Box::new(
-                    move |event: &dyn Any,
-                          context_stack: &[&DispatchContext],
-                          phase: DispatchPhase,
-                          cx: &mut WindowContext<'_>| {
-                        handle
-                            .update(cx, |view, cx| {
-                                listener(view, event, context_stack, phase, cx)
-                            })
-                            .log_err()
-                            .flatten()
-                    },
-                );
-                self.window.current_frame.key_dispatch_stack.push(
-                    KeyDispatchStackFrame::Listener {
-                        event_type,
-                        listener,
-                    },
-                );
-            }
-        }
-
-        let result = f(self);
-
-        if !self.window.current_frame.freeze_key_dispatch_stack {
-            self.window
-                .current_frame
-                .key_dispatch_stack
-                .truncate(old_stack_len);
-        }
-
-        result
-    }
-
-    pub fn with_key_dispatch_context<R>(
+    pub fn with_key_dispatch<R>(
         &mut self,
-        context: DispatchContext,
-        f: impl FnOnce(&mut Self) -> R,
+        context: KeyContext,
+        focus_handle: Option<FocusHandle>,
+        f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
     ) -> R {
-        if context.is_empty() {
-            return f(self);
-        }
-
-        if !self.window.current_frame.freeze_key_dispatch_stack {
-            self.window
-                .current_frame
-                .key_dispatch_stack
-                .push(KeyDispatchStackFrame::Context(context));
-        }
-
-        let result = f(self);
-
-        if !self.window.previous_frame.freeze_key_dispatch_stack {
-            self.window.previous_frame.key_dispatch_stack.pop();
-        }
-
-        result
-    }
-
-    pub fn with_focus<R>(
-        &mut self,
-        focus_handle: FocusHandle,
-        f: impl FnOnce(&mut Self) -> R,
-    ) -> R {
-        if let Some(parent_focus_id) = self.window.current_frame.focus_stack.last().copied() {
-            self.window
+        let window = &mut self.window;
+        window
+            .current_frame
+            .dispatch_tree
+            .push_node(context.clone(), &mut window.previous_frame.dispatch_tree);
+        if let Some(focus_handle) = focus_handle.as_ref() {
+            window
                 .current_frame
-                .focus_parents_by_child
-                .insert(focus_handle.id, parent_focus_id);
+                .dispatch_tree
+                .make_focusable(focus_handle.id);
         }
-        self.window.current_frame.focus_stack.push(focus_handle.id);
+        let result = f(focus_handle, self);
 
-        if Some(focus_handle.id) == self.window.focus {
-            self.window.current_frame.freeze_key_dispatch_stack = true;
-        }
+        self.window.current_frame.dispatch_tree.pop_node();
 
-        let result = f(self);
-
-        self.window.current_frame.focus_stack.pop();
         result
     }
 
@@ -2242,7 +2149,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         &mut self,
         handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
     ) {
-        let handle = self.view();
+        let handle = self.view().clone();
         self.window_cx.on_mouse_event(move |event, phase, cx| {
             handle.update(cx, |view, cx| {
                 handler(view, event, phase, cx);
@@ -2250,6 +2157,32 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         });
     }
 
+    pub fn on_key_event<Event: 'static>(
+        &mut self,
+        handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
+    ) {
+        let handle = self.view().clone();
+        self.window_cx.on_key_event(move |event, phase, cx| {
+            handle.update(cx, |view, cx| {
+                handler(view, event, phase, cx);
+            })
+        });
+    }
+
+    pub fn on_action(
+        &mut self,
+        action_type: TypeId,
+        handler: impl Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) + 'static,
+    ) {
+        let handle = self.view().clone();
+        self.window_cx
+            .on_action(action_type, move |action, phase, cx| {
+                handle.update(cx, |view, cx| {
+                    handler(view, action, phase, cx);
+                })
+            });
+    }
+
     /// Set an input handler, such as [ElementInputHandler], which interfaces with the
     /// platform to receive textual input with proper integration with concerns such
     /// as IME interactions.

crates/gpui2_macros/src/action.rs 🔗

@@ -34,13 +34,21 @@ pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream {
     let visibility = input.vis;
 
     let output = match input.data {
-        syn::Data::Struct(ref struct_data) => {
-            let fields = &struct_data.fields;
-            quote! {
-                #attributes
-                #visibility struct #name #fields
+        syn::Data::Struct(ref struct_data) => match &struct_data.fields {
+            syn::Fields::Named(_) | syn::Fields::Unnamed(_) => {
+                let fields = &struct_data.fields;
+                quote! {
+                    #attributes
+                    #visibility struct #name #fields
+                }
             }
-        }
+            syn::Fields::Unit => {
+                quote! {
+                    #attributes
+                    #visibility struct #name;
+                }
+            }
+        },
         syn::Data::Enum(ref enum_data) => {
             let variants = &enum_data.variants;
             quote! {

crates/gpui2_macros/src/style_helpers.rs 🔗

@@ -130,7 +130,7 @@ fn generate_predefined_setter(
 
     let method = quote! {
         #[doc = #doc_string]
-        fn #method_name(mut self) -> Self where Self: std::marker::Sized {
+        fn #method_name(mut self) -> Self {
             let style = self.style();
             #(#field_assignments)*
             self
@@ -163,7 +163,7 @@ fn generate_custom_value_setter(
 
     let method = quote! {
         #[doc = #doc_string]
-        fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui::#length_type>) -> Self where Self: std::marker::Sized {
+        fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui::#length_type>) -> Self {
             let style = self.style();
             #(#field_assignments)*
             self

crates/install_cli2/Cargo.toml 🔗

@@ -14,5 +14,6 @@ test-support = []
 smol.workspace = true
 anyhow.workspace = true
 log.workspace = true
+serde.workspace = true
 gpui = { package = "gpui2", path = "../gpui2" }
 util = { path = "../util" }

crates/install_cli2/src/install_cli2.rs 🔗

@@ -1,10 +1,9 @@
 use anyhow::{anyhow, Result};
-use gpui::AsyncAppContext;
+use gpui::{actions, AsyncAppContext};
 use std::path::Path;
 use util::ResultExt;
 
-// todo!()
-// actions!(cli, [Install]);
+actions!(Install);
 
 pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
     let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??;

crates/language2/src/language2.rs 🔗

@@ -1858,7 +1858,7 @@ mod tests {
     async fn test_first_line_pattern(cx: &mut TestAppContext) {
         let mut languages = LanguageRegistry::test();
 
-        languages.set_executor(cx.executor().clone());
+        languages.set_executor(cx.executor());
         let languages = Arc::new(languages);
         languages.register(
             "/javascript",
@@ -1895,7 +1895,7 @@ mod tests {
     #[gpui::test(iterations = 10)]
     async fn test_language_loading(cx: &mut TestAppContext) {
         let mut languages = LanguageRegistry::test();
-        languages.set_executor(cx.executor().clone());
+        languages.set_executor(cx.executor());
         let languages = Arc::new(languages);
         languages.register(
             "/JSON",

crates/menu2/src/menu2.rs 🔗

@@ -1,9 +1,13 @@
 use gpui::actions;
 
-// todo!(remove this)
+// If the zed binary doesn't use anything in this crate, it will be optimized away
+// and the actions won't initialize. So we just provide an empty initialization function
+// to be called from main.
+//
+// These may provide relevant context:
 // https://github.com/rust-lang/rust/issues/47384
 // https://github.com/mmastrac/rust-ctor/issues/280
-pub fn unused() {}
+pub fn init() {}
 
 actions!(
     Cancel,

crates/node_runtime/src/node_runtime.rs 🔗

@@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use serde::Deserialize;
-use smol::{fs, io::BufReader, process::Command};
+use smol::{fs, io::BufReader, lock::Mutex, process::Command};
 use std::process::{Output, Stdio};
 use std::{
     env::consts,
@@ -45,14 +45,19 @@ pub trait NodeRuntime: Send + Sync {
 
 pub struct RealNodeRuntime {
     http: Arc<dyn HttpClient>,
+    installation_lock: Mutex<()>,
 }
 
 impl RealNodeRuntime {
     pub fn new(http: Arc<dyn HttpClient>) -> Arc<dyn NodeRuntime> {
-        Arc::new(RealNodeRuntime { http })
+        Arc::new(RealNodeRuntime {
+            http,
+            installation_lock: Mutex::new(()),
+        })
     }
 
     async fn install_if_needed(&self) -> Result<PathBuf> {
+        let _lock = self.installation_lock.lock().await;
         log::info!("Node runtime install_if_needed");
 
         let arch = match consts::ARCH {
@@ -73,6 +78,9 @@ impl RealNodeRuntime {
             .stdin(Stdio::null())
             .stdout(Stdio::null())
             .stderr(Stdio::null())
+            .args(["--cache".into(), node_dir.join("cache")])
+            .args(["--userconfig".into(), node_dir.join("blank_user_npmrc")])
+            .args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")])
             .status()
             .await;
         let valid = matches!(result, Ok(status) if status.success());
@@ -96,6 +104,11 @@ impl RealNodeRuntime {
             archive.unpack(&node_containing_dir).await?;
         }
 
+        // Note: Not in the `if !valid {}` so we can populate these for existing installations
+        _ = fs::create_dir(node_dir.join("cache")).await;
+        _ = fs::write(node_dir.join("blank_user_npmrc"), []).await;
+        _ = fs::write(node_dir.join("blank_global_npmrc"), []).await;
+
         anyhow::Ok(node_dir)
     }
 }
@@ -137,7 +150,17 @@ impl NodeRuntime for RealNodeRuntime {
 
             let mut command = Command::new(node_binary);
             command.env("PATH", env_path);
-            command.arg(npm_file).arg(subcommand).args(args);
+            command.arg(npm_file).arg(subcommand);
+            command.args(["--cache".into(), installation_path.join("cache")]);
+            command.args([
+                "--userconfig".into(),
+                installation_path.join("blank_user_npmrc"),
+            ]);
+            command.args([
+                "--globalconfig".into(),
+                installation_path.join("blank_global_npmrc"),
+            ]);
+            command.args(args);
 
             if let Some(directory) = directory {
                 command.current_dir(directory);

crates/picker2/src/picker2.rs 🔗

@@ -1,17 +1,17 @@
 use editor::Editor;
 use gpui::{
-    div, uniform_list, Component, Div, FocusEnabled, ParentElement, Render, StatefulInteractivity,
-    StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext,
-    WindowContext,
+    div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task,
+    UniformListScrollHandle, View, ViewContext, WindowContext,
 };
-use std::cmp;
-use ui::{prelude::*, v_stack, Divider};
+use std::{cmp, sync::Arc};
+use ui::{prelude::*, v_stack, Divider, Label, TextColor};
 
 pub struct Picker<D: PickerDelegate> {
     pub delegate: D,
     scroll_handle: UniformListScrollHandle,
     editor: View<Editor>,
-    pending_update_matches: Option<Task<Option<()>>>,
+    pending_update_matches: Option<Task<()>>,
+    confirm_on_update: Option<bool>,
 }
 
 pub trait PickerDelegate: Sized + 'static {
@@ -21,7 +21,7 @@ pub trait PickerDelegate: Sized + 'static {
     fn selected_index(&self) -> usize;
     fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
 
-    // fn placeholder_text(&self) -> Arc<str>;
+    fn placeholder_text(&self) -> Arc<str>;
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
 
     fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
@@ -37,21 +37,28 @@ pub trait PickerDelegate: Sized + 'static {
 
 impl<D: PickerDelegate> Picker<D> {
     pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
-        let editor = cx.build_view(|cx| Editor::single_line(cx));
+        let editor = cx.build_view(|cx| {
+            let mut editor = Editor::single_line(cx);
+            editor.set_placeholder_text(delegate.placeholder_text(), cx);
+            editor
+        });
         cx.subscribe(&editor, Self::on_input_editor_event).detach();
-        Self {
+        let mut this = Self {
             delegate,
+            editor,
             scroll_handle: UniformListScrollHandle::new(),
             pending_update_matches: None,
-            editor,
-        }
+            confirm_on_update: None,
+        };
+        this.update_matches("".to_string(), cx);
+        this
     }
 
     pub fn focus(&self, cx: &mut WindowContext) {
         self.editor.update(cx, |editor, cx| editor.focus(cx));
     }
 
-    fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
+    pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
         let count = self.delegate.match_count();
         if count > 0 {
             let index = self.delegate.selected_index();
@@ -91,16 +98,40 @@ impl<D: PickerDelegate> Picker<D> {
         }
     }
 
+    pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
+        let count = self.delegate.match_count();
+        let index = self.delegate.selected_index();
+        let new_index = if index + 1 == count { 0 } else { index + 1 };
+        self.delegate.set_selected_index(new_index, cx);
+        self.scroll_handle.scroll_to_item(new_index);
+        cx.notify();
+    }
+
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
         self.delegate.dismissed(cx);
     }
 
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
-        self.delegate.confirm(false, cx);
+        if self.pending_update_matches.is_some() {
+            self.confirm_on_update = Some(false)
+        } else {
+            self.delegate.confirm(false, cx);
+        }
     }
 
     fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
-        self.delegate.confirm(true, cx);
+        if self.pending_update_matches.is_some() {
+            self.confirm_on_update = Some(true)
+        } else {
+            self.delegate.confirm(true, cx);
+        }
+    }
+
+    fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
+        cx.stop_propagation();
+        cx.prevent_default();
+        self.delegate.set_selected_index(ix, cx);
+        self.delegate.confirm(secondary, cx);
     }
 
     fn on_input_editor_event(
@@ -115,6 +146,11 @@ impl<D: PickerDelegate> Picker<D> {
         }
     }
 
+    pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
+        let query = self.editor.read(cx).text(cx);
+        self.update_matches(query, cx);
+    }
+
     pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
         let update = self.delegate.update_matches(query, cx);
         self.matches_updated(cx);
@@ -123,7 +159,7 @@ impl<D: PickerDelegate> Picker<D> {
             this.update(&mut cx, |this, cx| {
                 this.matches_updated(cx);
             })
-            .ok()
+            .ok();
         }));
     }
 
@@ -131,18 +167,19 @@ impl<D: PickerDelegate> Picker<D> {
         let index = self.delegate.selected_index();
         self.scroll_handle.scroll_to_item(index);
         self.pending_update_matches = None;
+        if let Some(secondary) = self.confirm_on_update.take() {
+            self.delegate.confirm(secondary, cx);
+        }
         cx.notify();
     }
 }
 
 impl<D: PickerDelegate> Render for Picker<D> {
-    type Element = Div<Self, StatefulInteractivity<Self>, FocusEnabled<Self>>;
+    type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         div()
-            .context("picker")
-            .id("picker-container")
-            .focusable()
+            .key_context("picker")
             .size_full()
             .elevation_2(cx)
             .on_action(Self::select_next)
@@ -159,23 +196,51 @@ impl<D: PickerDelegate> Render for Picker<D> {
                     .child(div().px_1().py_0p5().child(self.editor.clone())),
             )
             .child(Divider::horizontal())
-            .child(
-                v_stack()
-                    .p_1()
-                    .grow()
-                    .child(
-                        uniform_list("candidates", self.delegate.match_count(), {
-                            move |this: &mut Self, visible_range, cx| {
-                                let selected_ix = this.delegate.selected_index();
-                                visible_range
-                                    .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx))
-                                    .collect()
-                            }
-                        })
-                        .track_scroll(self.scroll_handle.clone()),
-                    )
-                    .max_h_72()
-                    .overflow_hidden(),
-            )
+            .when(self.delegate.match_count() > 0, |el| {
+                el.child(
+                    v_stack()
+                        .p_1()
+                        .grow()
+                        .child(
+                            uniform_list("candidates", self.delegate.match_count(), {
+                                move |this: &mut Self, visible_range, cx| {
+                                    let selected_ix = this.delegate.selected_index();
+                                    visible_range
+                                        .map(|ix| {
+                                            div()
+                                                .on_mouse_down(
+                                                    MouseButton::Left,
+                                                    move |this: &mut Self, event, cx| {
+                                                        this.handle_click(
+                                                            ix,
+                                                            event.modifiers.command,
+                                                            cx,
+                                                        )
+                                                    },
+                                                )
+                                                .child(this.delegate.render_match(
+                                                    ix,
+                                                    ix == selected_ix,
+                                                    cx,
+                                                ))
+                                        })
+                                        .collect()
+                                }
+                            })
+                            .track_scroll(self.scroll_handle.clone()),
+                        )
+                        .max_h_72()
+                        .overflow_hidden(),
+                )
+            })
+            .when(self.delegate.match_count() == 0, |el| {
+                el.child(
+                    v_stack().p_1().grow().child(
+                        div()
+                            .px_1()
+                            .child(Label::new("No matches").color(TextColor::Muted)),
+                    ),
+                )
+            })
     }
 }

crates/prettier2/src/prettier2.rs 🔗

@@ -497,7 +497,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) {
-        let fs = FakeFs::new(cx.executor().clone());
+        let fs = FakeFs::new(cx.executor());
         fs.insert_tree(
             "/root",
             json!({
@@ -573,7 +573,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) {
-        let fs = FakeFs::new(cx.executor().clone());
+        let fs = FakeFs::new(cx.executor());
         fs.insert_tree(
             "/root",
             json!({
@@ -638,7 +638,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) {
-        let fs = FakeFs::new(cx.executor().clone());
+        let fs = FakeFs::new(cx.executor());
         fs.insert_tree(
             "/root",
             json!({
@@ -731,7 +731,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) {
-        let fs = FakeFs::new(cx.executor().clone());
+        let fs = FakeFs::new(cx.executor());
         fs.insert_tree(
             "/root",
             json!({
@@ -812,7 +812,7 @@ mod tests {
     async fn test_prettier_lookup_in_npm_workspaces_for_not_installed(
         cx: &mut gpui::TestAppContext,
     ) {
-        let fs = FakeFs::new(cx.executor().clone());
+        let fs = FakeFs::new(cx.executor());
         fs.insert_tree(
             "/root",
             json!({

crates/project2/src/project2.rs 🔗

@@ -863,7 +863,7 @@ impl Project {
         cx: &mut gpui::TestAppContext,
     ) -> Model<Project> {
         let mut languages = LanguageRegistry::test();
-        languages.set_executor(cx.executor().clone());
+        languages.set_executor(cx.executor());
         let http_client = util::http::FakeHttpClient::with_404_response();
         let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
         let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx));

crates/project2/src/project_tests.rs 🔗

@@ -89,7 +89,7 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
 async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/the-root",
         json!({
@@ -189,7 +189,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
         }))
         .await;
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/the-root",
         json!({
@@ -547,7 +547,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
         }))
         .await;
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/the-root",
         json!({
@@ -734,7 +734,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
 async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -826,7 +826,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
 async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/root",
         json!({
@@ -914,7 +914,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
         }))
         .await;
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -1046,7 +1046,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
         }))
         .await;
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
@@ -1125,7 +1125,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
         }))
         .await;
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
@@ -1215,7 +1215,7 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
         }))
         .await;
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
@@ -1279,7 +1279,7 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
         }))
         .await;
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
         .await;
 
@@ -1401,7 +1401,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
     "
     .unindent();
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree("/dir", json!({ "a.rs": text })).await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
@@ -1671,7 +1671,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
         "let three = 3;\n",
     );
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree("/dir", json!({ "a.rs": text })).await;
 
     let project = Project::test(fs, ["/dir".as_ref()], cx).await;
@@ -1734,7 +1734,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
 async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
         .await;
 
@@ -1813,7 +1813,7 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
     "
     .unindent();
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -1959,7 +1959,7 @@ async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAp
     "
     .unindent();
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -2067,7 +2067,7 @@ async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) {
     "
     .unindent();
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -2187,7 +2187,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
     );
     let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -2299,7 +2299,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
         }))
         .await;
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -2396,7 +2396,7 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
         }))
         .await;
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -2451,7 +2451,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
     );
     let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -2559,7 +2559,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
 async fn test_save_file(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -2591,7 +2591,7 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) {
 async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -2622,7 +2622,7 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
 async fn test_save_as(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree("/dir", json!({})).await;
 
     let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
@@ -2830,7 +2830,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
 async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -2881,7 +2881,7 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
 async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -2927,7 +2927,7 @@ async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
 async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -3074,7 +3074,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
     let initial_contents = "aaa\nbbbbb\nc\n";
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -3154,7 +3154,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
 async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -3216,7 +3216,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
 async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/the-dir",
         json!({
@@ -3479,7 +3479,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
         }))
         .await;
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -3596,7 +3596,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
 async fn test_search(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -3655,7 +3655,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
 
     let search_query = "file";
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -3767,7 +3767,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
 
     let search_query = "file";
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({
@@ -3878,7 +3878,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
 
     let search_query = "file";
 
-    let fs = FakeFs::new(cx.executor().clone());
+    let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
         "/dir",
         json!({

crates/project_panel2/Cargo.toml 🔗

@@ -0,0 +1,41 @@
+[package]
+name = "project_panel2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/project_panel.rs"
+doctest = false
+
+[dependencies]
+context_menu = { path = "../context_menu" }
+collections = { path = "../collections" }
+db = { path = "../db2", package = "db2" }
+editor = { path = "../editor2", package = "editor2" }
+gpui = { path = "../gpui2", package = "gpui2" }
+menu = { path = "../menu2", package = "menu2" }
+project = { path = "../project2", package = "project2" }
+settings = { path = "../settings2", package = "settings2" }
+theme = { path = "../theme2", package = "theme2" }
+ui = { path = "../ui2", package = "ui2" }
+util = { path = "../util" }
+workspace = { path = "../workspace2", package = "workspace2" }
+anyhow.workspace = true
+postage.workspace = true
+futures.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+schemars.workspace = true
+smallvec.workspace = true
+pretty_assertions.workspace = true
+unicase = "2.6"
+
+[dev-dependencies]
+client = { path = "../client2", package = "client2", features = ["test-support"] }
+language = { path = "../language2", package = "language2", features = ["test-support"] }
+editor = { path = "../editor2", package = "editor2", features = ["test-support"] }
+gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
+workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] }
+serde_json.workspace = true

crates/project_panel2/src/file_associations.rs 🔗

@@ -0,0 +1,96 @@
+use std::{path::Path, str, sync::Arc};
+
+use collections::HashMap;
+
+use gpui::{AppContext, AssetSource};
+use serde_derive::Deserialize;
+use util::{maybe, paths::PathExt};
+
+#[derive(Deserialize, Debug)]
+struct TypeConfig {
+    icon: Arc<str>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct FileAssociations {
+    suffixes: HashMap<String, String>,
+    types: HashMap<String, TypeConfig>,
+}
+
+const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder";
+const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder";
+const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron";
+const EXPANDED_CHEVRON_TYPE: &'static str = "expanded_chevron";
+pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json";
+
+pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
+    cx.set_global(FileAssociations::new(assets))
+}
+
+impl FileAssociations {
+    pub fn new(assets: impl AssetSource) -> Self {
+        assets
+            .load("icons/file_icons/file_types.json")
+            .and_then(|file| {
+                serde_json::from_str::<FileAssociations>(str::from_utf8(&file).unwrap())
+                    .map_err(Into::into)
+            })
+            .unwrap_or_else(|_| FileAssociations {
+                suffixes: HashMap::default(),
+                types: HashMap::default(),
+            })
+    }
+
+    pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
+        maybe!({
+            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+
+            // FIXME: Associate a type with the languages and have the file's langauge
+            //        override these associations
+            maybe!({
+                let suffix = path.icon_suffix()?;
+
+                this.suffixes
+                    .get(suffix)
+                    .and_then(|type_str| this.types.get(type_str))
+                    .map(|type_config| type_config.icon.clone())
+            })
+            .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
+        })
+        .unwrap_or_else(|| Arc::from("".to_string()))
+    }
+
+    pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
+        maybe!({
+            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+
+            let key = if expanded {
+                EXPANDED_DIRECTORY_TYPE
+            } else {
+                COLLAPSED_DIRECTORY_TYPE
+            };
+
+            this.types
+                .get(key)
+                .map(|type_config| type_config.icon.clone())
+        })
+        .unwrap_or_else(|| Arc::from("".to_string()))
+    }
+
+    pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
+        maybe!({
+            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+
+            let key = if expanded {
+                EXPANDED_CHEVRON_TYPE
+            } else {
+                COLLAPSED_CHEVRON_TYPE
+            };
+
+            this.types
+                .get(key)
+                .map(|type_config| type_config.icon.clone())
+        })
+        .unwrap_or_else(|| Arc::from("".to_string()))
+    }
+}

crates/project_panel2/src/project_panel.rs 🔗

@@ -0,0 +1,2880 @@
+pub mod file_associations;
+mod project_panel_settings;
+use settings::Settings;
+
+use db::kvp::KEY_VALUE_STORE;
+use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
+use file_associations::FileAssociations;
+
+use anyhow::{anyhow, Result};
+use gpui::{
+    actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
+    ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, InteractiveComponent,
+    Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, Stateful,
+    StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, ViewContext,
+    VisualContext as _, WeakView, WindowContext,
+};
+use menu::{Confirm, SelectNext, SelectPrev};
+use project::{
+    repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath,
+    Worktree, WorktreeId,
+};
+use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
+use serde::{Deserialize, Serialize};
+use std::{
+    cmp::Ordering,
+    collections::{hash_map, HashMap},
+    ffi::OsStr,
+    ops::Range,
+    path::Path,
+    sync::Arc,
+};
+use theme::ActiveTheme as _;
+use ui::{h_stack, v_stack, IconElement, Label};
+use unicase::UniCase;
+use util::{maybe, TryFutureExt};
+use workspace::{
+    dock::{DockPosition, PanelEvent},
+    Workspace,
+};
+
+const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
+const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
+
+pub struct ProjectPanel {
+    project: Model<Project>,
+    fs: Arc<dyn Fs>,
+    list: UniformListScrollHandle,
+    focus_handle: FocusHandle,
+    visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
+    last_worktree_root_id: Option<ProjectEntryId>,
+    expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
+    selection: Option<Selection>,
+    edit_state: Option<EditState>,
+    filename_editor: View<Editor>,
+    clipboard_entry: Option<ClipboardEntry>,
+    _dragged_entry_destination: Option<Arc<Path>>,
+    _workspace: WeakView<Workspace>,
+    has_focus: bool,
+    width: Option<f32>,
+    pending_serialization: Task<Option<()>>,
+}
+
+#[derive(Copy, Clone, Debug)]
+struct Selection {
+    worktree_id: WorktreeId,
+    entry_id: ProjectEntryId,
+}
+
+#[derive(Clone, Debug)]
+struct EditState {
+    worktree_id: WorktreeId,
+    entry_id: ProjectEntryId,
+    is_new_entry: bool,
+    is_dir: bool,
+    processing_filename: Option<String>,
+}
+
+#[derive(Copy, Clone)]
+pub enum ClipboardEntry {
+    Copied {
+        worktree_id: WorktreeId,
+        entry_id: ProjectEntryId,
+    },
+    Cut {
+        worktree_id: WorktreeId,
+        entry_id: ProjectEntryId,
+    },
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct EntryDetails {
+    filename: String,
+    icon: Option<Arc<str>>,
+    path: Arc<Path>,
+    depth: usize,
+    kind: EntryKind,
+    is_ignored: bool,
+    is_expanded: bool,
+    is_selected: bool,
+    is_editing: bool,
+    is_processing: bool,
+    is_cut: bool,
+    git_status: Option<GitFileStatus>,
+}
+
+actions!(
+    ExpandSelectedEntry,
+    CollapseSelectedEntry,
+    CollapseAllEntries,
+    NewDirectory,
+    NewFile,
+    Copy,
+    CopyPath,
+    CopyRelativePath,
+    RevealInFinder,
+    OpenInTerminal,
+    Cut,
+    Paste,
+    Delete,
+    Rename,
+    Open,
+    ToggleFocus,
+    NewSearchInDirectory,
+);
+
+pub fn init_settings(cx: &mut AppContext) {
+    ProjectPanelSettings::register(cx);
+}
+
+pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
+    init_settings(cx);
+    file_associations::init(assets, cx);
+}
+
+#[derive(Debug)]
+pub enum Event {
+    OpenedEntry {
+        entry_id: ProjectEntryId,
+        focus_opened_item: bool,
+    },
+    SplitEntry {
+        entry_id: ProjectEntryId,
+    },
+    DockPositionChanged,
+    Focus,
+    NewSearchInDirectory {
+        dir_entry: Entry,
+    },
+    ActivatePanel,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedProjectPanel {
+    width: Option<f32>,
+}
+
+impl ProjectPanel {
+    fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
+        let project = workspace.project().clone();
+        let project_panel = cx.build_view(|cx: &mut ViewContext<Self>| {
+            cx.observe(&project, |this, _, cx| {
+                this.update_visible_entries(None, cx);
+                cx.notify();
+            })
+            .detach();
+            let focus_handle = cx.focus_handle();
+
+            cx.on_focus(&focus_handle, Self::focus_in).detach();
+            cx.on_blur(&focus_handle, Self::focus_out).detach();
+
+            cx.subscribe(&project, |this, project, event, cx| match event {
+                project::Event::ActiveEntryChanged(Some(entry_id)) => {
+                    if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx)
+                    {
+                        this.expand_entry(worktree_id, *entry_id, cx);
+                        this.update_visible_entries(Some((worktree_id, *entry_id)), cx);
+                        this.autoscroll(cx);
+                        cx.notify();
+                    }
+                }
+                project::Event::ActivateProjectPanel => {
+                    cx.emit(Event::ActivatePanel);
+                }
+                project::Event::WorktreeRemoved(id) => {
+                    this.expanded_dir_ids.remove(id);
+                    this.update_visible_entries(None, cx);
+                    cx.notify();
+                }
+                _ => {}
+            })
+            .detach();
+
+            let filename_editor = cx.build_view(|cx| Editor::single_line(cx));
+
+            cx.subscribe(&filename_editor, |this, _, event, cx| match event {
+                editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => {
+                    this.autoscroll(cx);
+                }
+                editor::Event::Blurred => {
+                    if this
+                        .edit_state
+                        .as_ref()
+                        .map_or(false, |state| state.processing_filename.is_none())
+                    {
+                        this.edit_state = None;
+                        this.update_visible_entries(None, cx);
+                    }
+                }
+                _ => {}
+            })
+            .detach();
+
+            // cx.observe_global::<FileAssociations, _>(|_, cx| {
+            //     cx.notify();
+            // })
+            // .detach();
+
+            let mut this = Self {
+                project: project.clone(),
+                fs: workspace.app_state().fs.clone(),
+                list: UniformListScrollHandle::new(),
+                focus_handle,
+                visible_entries: Default::default(),
+                last_worktree_root_id: Default::default(),
+                expanded_dir_ids: Default::default(),
+                selection: None,
+                edit_state: None,
+                filename_editor,
+                clipboard_entry: None,
+                // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
+                _dragged_entry_destination: None,
+                _workspace: workspace.weak_handle(),
+                has_focus: false,
+                width: None,
+                pending_serialization: Task::ready(None),
+            };
+            this.update_visible_entries(None, cx);
+
+            // Update the dock position when the setting changes.
+            // todo!()
+            // let mut old_dock_position = this.position(cx);
+            // cx.observe_global::<SettingsStore, _>(move |this, cx| {
+            //     let new_dock_position = this.position(cx);
+            //     if new_dock_position != old_dock_position {
+            //         old_dock_position = new_dock_position;
+            //         cx.emit(Event::DockPositionChanged);
+            //     }
+            // })
+            // .detach();
+
+            this
+        });
+
+        cx.subscribe(&project_panel, {
+            let project_panel = project_panel.downgrade();
+            move |workspace, _, event, cx| match event {
+                &Event::OpenedEntry {
+                    entry_id,
+                    focus_opened_item,
+                } => {
+                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
+                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
+                            workspace
+                                .open_path(
+                                    ProjectPath {
+                                        worktree_id: worktree.read(cx).id(),
+                                        path: entry.path.clone(),
+                                    },
+                                    None,
+                                    focus_opened_item,
+                                    cx,
+                                )
+                                .detach_and_log_err(cx);
+                            if !focus_opened_item {
+                                if let Some(project_panel) = project_panel.upgrade() {
+                                    let focus_handle = project_panel.read(cx).focus_handle.clone();
+                                    cx.focus(&focus_handle);
+                                }
+                            }
+                        }
+                    }
+                }
+                &Event::SplitEntry { entry_id } => {
+                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
+                        if let Some(_entry) = worktree.read(cx).entry_for_id(entry_id) {
+                            // workspace
+                            //     .split_path(
+                            //         ProjectPath {
+                            //             worktree_id: worktree.read(cx).id(),
+                            //             path: entry.path.clone(),
+                            //         },
+                            //         cx,
+                            //     )
+                            //     .detach_and_log_err(cx);
+                        }
+                    }
+                }
+                _ => {}
+            }
+        })
+        .detach();
+
+        project_panel
+    }
+
+    pub fn load(
+        workspace: WeakView<Workspace>,
+        cx: AsyncWindowContext,
+    ) -> Task<Result<View<Self>>> {
+        cx.spawn(|mut cx| async move {
+            // let serialized_panel = if let Some(panel) = cx
+            //     .background_executor()
+            //     .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
+            //     .await
+            //     .log_err()
+            //     .flatten()
+            // {
+            //     Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
+            // } else {
+            //     None
+            // };
+            workspace.update(&mut cx, |workspace, cx| {
+                let panel = ProjectPanel::new(workspace, cx);
+                // if let Some(serialized_panel) = serialized_panel {
+                //     panel.update(cx, |panel, cx| {
+                //         panel.width = serialized_panel.width;
+                //         cx.notify();
+                //     });
+                // }
+                panel
+            })
+        })
+    }
+
+    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+        let width = self.width;
+        self.pending_serialization = cx.background_executor().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        PROJECT_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedProjectPanel { width })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
+    }
+
+    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
+        if !self.has_focus {
+            self.has_focus = true;
+            cx.emit(Event::Focus);
+        }
+    }
+
+    fn focus_out(&mut self, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
+
+    fn deploy_context_menu(
+        &mut self,
+        _position: Point<Pixels>,
+        _entry_id: ProjectEntryId,
+        _cx: &mut ViewContext<Self>,
+    ) {
+        todo!()
+        //     let project = self.project.read(cx);
+
+        //     let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
+        //         id
+        //     } else {
+        //         return;
+        //     };
+
+        //     self.selection = Some(Selection {
+        //         worktree_id,
+        //         entry_id,
+        //     });
+
+        //     let mut menu_entries = Vec::new();
+        //     if let Some((worktree, entry)) = self.selected_entry(cx) {
+        //         let is_root = Some(entry) == worktree.root_entry();
+        //         if !project.is_remote() {
+        //             menu_entries.push(ContextMenuItem::action(
+        //                 "Add Folder to Project",
+        //                 workspace::AddFolderToProject,
+        //             ));
+        //             if is_root {
+        //                 let project = self.project.clone();
+        //                 menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| {
+        //                     project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
+        //                 }));
+        //             }
+        //         }
+        //         menu_entries.push(ContextMenuItem::action("New File", NewFile));
+        //         menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory));
+        //         menu_entries.push(ContextMenuItem::Separator);
+        //         menu_entries.push(ContextMenuItem::action("Cut", Cut));
+        //         menu_entries.push(ContextMenuItem::action("Copy", Copy));
+        //         if let Some(clipboard_entry) = self.clipboard_entry {
+        //             if clipboard_entry.worktree_id() == worktree.id() {
+        //                 menu_entries.push(ContextMenuItem::action("Paste", Paste));
+        //             }
+        //         }
+        //         menu_entries.push(ContextMenuItem::Separator);
+        //         menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
+        //         menu_entries.push(ContextMenuItem::action(
+        //             "Copy Relative Path",
+        //             CopyRelativePath,
+        //         ));
+
+        //         if entry.is_dir() {
+        //             menu_entries.push(ContextMenuItem::Separator);
+        //         }
+        //         menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
+        //         if entry.is_dir() {
+        //             menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal));
+        //             menu_entries.push(ContextMenuItem::action(
+        //                 "Search Inside",
+        //                 NewSearchInDirectory,
+        //             ));
+        //         }
+
+        //         menu_entries.push(ContextMenuItem::Separator);
+        //         menu_entries.push(ContextMenuItem::action("Rename", Rename));
+        //         if !is_root {
+        //             menu_entries.push(ContextMenuItem::action("Delete", Delete));
+        //         }
+        //     }
+
+        //     // self.context_menu.update(cx, |menu, cx| {
+        //     //     menu.show(position, AnchorCorner::TopLeft, menu_entries, cx);
+        //     // });
+
+        //     cx.notify();
+    }
+
+    fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
+        if let Some((worktree, entry)) = self.selected_entry(cx) {
+            if entry.is_dir() {
+                let worktree_id = worktree.id();
+                let entry_id = entry.id;
+                let expanded_dir_ids =
+                    if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
+                        expanded_dir_ids
+                    } else {
+                        return;
+                    };
+
+                match expanded_dir_ids.binary_search(&entry_id) {
+                    Ok(_) => self.select_next(&SelectNext, cx),
+                    Err(ix) => {
+                        self.project.update(cx, |project, cx| {
+                            project.expand_entry(worktree_id, entry_id, cx);
+                        });
+
+                        expanded_dir_ids.insert(ix, entry_id);
+                        self.update_visible_entries(None, cx);
+                        cx.notify();
+                    }
+                }
+            }
+        }
+    }
+
+    fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
+        if let Some((worktree, mut entry)) = self.selected_entry(cx) {
+            let worktree_id = worktree.id();
+            let expanded_dir_ids =
+                if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
+                    expanded_dir_ids
+                } else {
+                    return;
+                };
+
+            loop {
+                let entry_id = entry.id;
+                match expanded_dir_ids.binary_search(&entry_id) {
+                    Ok(ix) => {
+                        expanded_dir_ids.remove(ix);
+                        self.update_visible_entries(Some((worktree_id, entry_id)), cx);
+                        cx.notify();
+                        break;
+                    }
+                    Err(_) => {
+                        if let Some(parent_entry) =
+                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
+                        {
+                            entry = parent_entry;
+                        } else {
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
+        self.expanded_dir_ids.clear();
+        self.update_visible_entries(None, cx);
+        cx.notify();
+    }
+
+    fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
+        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
+            if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
+                self.project.update(cx, |project, cx| {
+                    match expanded_dir_ids.binary_search(&entry_id) {
+                        Ok(ix) => {
+                            expanded_dir_ids.remove(ix);
+                        }
+                        Err(ix) => {
+                            project.expand_entry(worktree_id, entry_id, cx);
+                            expanded_dir_ids.insert(ix, entry_id);
+                        }
+                    }
+                });
+                self.update_visible_entries(Some((worktree_id, entry_id)), cx);
+                cx.focus(&self.focus_handle);
+                cx.notify();
+            }
+        }
+    }
+
+    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+        if let Some(selection) = self.selection {
+            let (mut worktree_ix, mut entry_ix, _) =
+                self.index_for_selection(selection).unwrap_or_default();
+            if entry_ix > 0 {
+                entry_ix -= 1;
+            } else if worktree_ix > 0 {
+                worktree_ix -= 1;
+                entry_ix = self.visible_entries[worktree_ix].1.len() - 1;
+            } else {
+                return;
+            }
+
+            let (worktree_id, worktree_entries) = &self.visible_entries[worktree_ix];
+            self.selection = Some(Selection {
+                worktree_id: *worktree_id,
+                entry_id: worktree_entries[entry_ix].id,
+            });
+            self.autoscroll(cx);
+            cx.notify();
+        } else {
+            self.select_first(cx);
+        }
+    }
+
+    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+        if let Some(task) = self.confirm_edit(cx) {
+            task.detach_and_log_err(cx);
+        }
+    }
+
+    fn open_file(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
+        if let Some((_, entry)) = self.selected_entry(cx) {
+            if entry.is_file() {
+                self.open_entry(entry.id, true, cx);
+            }
+        }
+    }
+
+    fn confirm_edit(&mut self, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+        let edit_state = self.edit_state.as_mut()?;
+        cx.focus(&self.focus_handle);
+
+        let worktree_id = edit_state.worktree_id;
+        let is_new_entry = edit_state.is_new_entry;
+        let is_dir = edit_state.is_dir;
+        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
+        let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
+        let filename = self.filename_editor.read(cx).text(cx);
+
+        let path_already_exists = |path| worktree.read(cx).entry_for_path(path).is_some();
+        let edit_task;
+        let edited_entry_id;
+        if is_new_entry {
+            self.selection = Some(Selection {
+                worktree_id,
+                entry_id: NEW_ENTRY_ID,
+            });
+            let new_path = entry.path.join(&filename.trim_start_matches("/"));
+            if path_already_exists(new_path.as_path()) {
+                return None;
+            }
+
+            edited_entry_id = NEW_ENTRY_ID;
+            edit_task = self.project.update(cx, |project, cx| {
+                project.create_entry((worktree_id, &new_path), is_dir, cx)
+            })?;
+        } else {
+            let new_path = if let Some(parent) = entry.path.clone().parent() {
+                parent.join(&filename)
+            } else {
+                filename.clone().into()
+            };
+            if path_already_exists(new_path.as_path()) {
+                return None;
+            }
+
+            edited_entry_id = entry.id;
+            edit_task = self.project.update(cx, |project, cx| {
+                project.rename_entry(entry.id, new_path.as_path(), cx)
+            })?;
+        };
+
+        edit_state.processing_filename = Some(filename);
+        cx.notify();
+
+        Some(cx.spawn(|this, mut cx| async move {
+            let new_entry = edit_task.await;
+            this.update(&mut cx, |this, cx| {
+                this.edit_state.take();
+                cx.notify();
+            })?;
+
+            let new_entry = new_entry?;
+            this.update(&mut cx, |this, cx| {
+                if let Some(selection) = &mut this.selection {
+                    if selection.entry_id == edited_entry_id {
+                        selection.worktree_id = worktree_id;
+                        selection.entry_id = new_entry.id;
+                        this.expand_to_selection(cx);
+                    }
+                }
+                this.update_visible_entries(None, cx);
+                if is_new_entry && !is_dir {
+                    this.open_entry(new_entry.id, true, cx);
+                }
+                cx.notify();
+            })?;
+            Ok(())
+        }))
+    }
+
+    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+        self.edit_state = None;
+        self.update_visible_entries(None, cx);
+        cx.focus(&self.focus_handle);
+        cx.notify();
+    }
+
+    fn open_entry(
+        &mut self,
+        entry_id: ProjectEntryId,
+        focus_opened_item: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        cx.emit(Event::OpenedEntry {
+            entry_id,
+            focus_opened_item,
+        });
+    }
+
+    fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::SplitEntry { entry_id });
+    }
+
+    fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext<Self>) {
+        self.add_entry(false, cx)
+    }
+
+    fn new_directory(&mut self, _: &NewDirectory, cx: &mut ViewContext<Self>) {
+        self.add_entry(true, cx)
+    }
+
+    fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext<Self>) {
+        if let Some(Selection {
+            worktree_id,
+            entry_id,
+        }) = self.selection
+        {
+            let directory_id;
+            if let Some((worktree, expanded_dir_ids)) = self
+                .project
+                .read(cx)
+                .worktree_for_id(worktree_id, cx)
+                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
+            {
+                let worktree = worktree.read(cx);
+                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
+                    loop {
+                        if entry.is_dir() {
+                            if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
+                                expanded_dir_ids.insert(ix, entry.id);
+                            }
+                            directory_id = entry.id;
+                            break;
+                        } else {
+                            if let Some(parent_path) = entry.path.parent() {
+                                if let Some(parent_entry) = worktree.entry_for_path(parent_path) {
+                                    entry = parent_entry;
+                                    continue;
+                                }
+                            }
+                            return;
+                        }
+                    }
+                } else {
+                    return;
+                };
+            } else {
+                return;
+            };
+
+            self.edit_state = Some(EditState {
+                worktree_id,
+                entry_id: directory_id,
+                is_new_entry: true,
+                is_dir,
+                processing_filename: None,
+            });
+            self.filename_editor.update(cx, |editor, cx| {
+                editor.clear(cx);
+                editor.focus(cx);
+            });
+            self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx);
+            self.autoscroll(cx);
+            cx.notify();
+        }
+    }
+
+    fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) {
+        if let Some(Selection {
+            worktree_id,
+            entry_id,
+        }) = self.selection
+        {
+            if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) {
+                if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
+                    self.edit_state = Some(EditState {
+                        worktree_id,
+                        entry_id,
+                        is_new_entry: false,
+                        is_dir: entry.is_dir(),
+                        processing_filename: None,
+                    });
+                    let file_name = entry
+                        .path
+                        .file_name()
+                        .map(|s| s.to_string_lossy())
+                        .unwrap_or_default()
+                        .to_string();
+                    let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
+                    let selection_end =
+                        file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
+                    self.filename_editor.update(cx, |editor, cx| {
+                        editor.set_text(file_name, cx);
+                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                            s.select_ranges([0..selection_end])
+                        });
+                        editor.focus(cx);
+                    });
+                    self.update_visible_entries(None, cx);
+                    self.autoscroll(cx);
+                    cx.notify();
+                }
+            }
+
+            // cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
+            //     drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
+            // })
+        }
+    }
+
+    fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
+        maybe!({
+            let Selection { entry_id, .. } = self.selection?;
+            let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path;
+            let file_name = path.file_name()?;
+
+            let answer = cx.prompt(
+                PromptLevel::Info,
+                &format!("Delete {file_name:?}?"),
+                &["Delete", "Cancel"],
+            );
+
+            cx.spawn(|this, mut cx| async move {
+                if answer.await != Ok(0) {
+                    return Ok(());
+                }
+                this.update(&mut cx, |this, cx| {
+                    this.project
+                        .update(cx, |project, cx| project.delete_entry(entry_id, cx))
+                        .ok_or_else(|| anyhow!("no such entry"))
+                })??
+                .await
+            })
+            .detach_and_log_err(cx);
+            Some(())
+        });
+    }
+
+    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+        if let Some(selection) = self.selection {
+            let (mut worktree_ix, mut entry_ix, _) =
+                self.index_for_selection(selection).unwrap_or_default();
+            if let Some((_, worktree_entries)) = self.visible_entries.get(worktree_ix) {
+                if entry_ix + 1 < worktree_entries.len() {
+                    entry_ix += 1;
+                } else {
+                    worktree_ix += 1;
+                    entry_ix = 0;
+                }
+            }
+
+            if let Some((worktree_id, worktree_entries)) = self.visible_entries.get(worktree_ix) {
+                if let Some(entry) = worktree_entries.get(entry_ix) {
+                    self.selection = Some(Selection {
+                        worktree_id: *worktree_id,
+                        entry_id: entry.id,
+                    });
+                    self.autoscroll(cx);
+                    cx.notify();
+                }
+            }
+        } else {
+            self.select_first(cx);
+        }
+    }
+
+    fn select_first(&mut self, cx: &mut ViewContext<Self>) {
+        let worktree = self
+            .visible_entries
+            .first()
+            .and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx));
+        if let Some(worktree) = worktree {
+            let worktree = worktree.read(cx);
+            let worktree_id = worktree.id();
+            if let Some(root_entry) = worktree.root_entry() {
+                self.selection = Some(Selection {
+                    worktree_id,
+                    entry_id: root_entry.id,
+                });
+                self.autoscroll(cx);
+                cx.notify();
+            }
+        }
+    }
+
+    fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
+            self.list.scroll_to_item(index);
+            cx.notify();
+        }
+    }
+
+    fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
+        if let Some((worktree, entry)) = self.selected_entry(cx) {
+            self.clipboard_entry = Some(ClipboardEntry::Cut {
+                worktree_id: worktree.id(),
+                entry_id: entry.id,
+            });
+            cx.notify();
+        }
+    }
+
+    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
+        if let Some((worktree, entry)) = self.selected_entry(cx) {
+            self.clipboard_entry = Some(ClipboardEntry::Copied {
+                worktree_id: worktree.id(),
+                entry_id: entry.id,
+            });
+            cx.notify();
+        }
+    }
+
+    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+        maybe!({
+            let (worktree, entry) = self.selected_entry(cx)?;
+            let clipboard_entry = self.clipboard_entry?;
+            if clipboard_entry.worktree_id() != worktree.id() {
+                return None;
+            }
+
+            let clipboard_entry_file_name = self
+                .project
+                .read(cx)
+                .path_for_entry(clipboard_entry.entry_id(), cx)?
+                .path
+                .file_name()?
+                .to_os_string();
+
+            let mut new_path = entry.path.to_path_buf();
+            if entry.is_file() {
+                new_path.pop();
+            }
+
+            new_path.push(&clipboard_entry_file_name);
+            let extension = new_path.extension().map(|e| e.to_os_string());
+            let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
+            let mut ix = 0;
+            while worktree.entry_for_path(&new_path).is_some() {
+                new_path.pop();
+
+                let mut new_file_name = file_name_without_extension.to_os_string();
+                new_file_name.push(" copy");
+                if ix > 0 {
+                    new_file_name.push(format!(" {}", ix));
+                }
+                if let Some(extension) = extension.as_ref() {
+                    new_file_name.push(".");
+                    new_file_name.push(extension);
+                }
+
+                new_path.push(new_file_name);
+                ix += 1;
+            }
+
+            if clipboard_entry.is_cut() {
+                if let Some(task) = self.project.update(cx, |project, cx| {
+                    project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
+                }) {
+                    task.detach_and_log_err(cx);
+                }
+            } else if let Some(task) = self.project.update(cx, |project, cx| {
+                project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
+            }) {
+                task.detach_and_log_err(cx);
+            }
+
+            Some(())
+        });
+    }
+
+    fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
+        if let Some((worktree, entry)) = self.selected_entry(cx) {
+            cx.write_to_clipboard(ClipboardItem::new(
+                worktree
+                    .abs_path()
+                    .join(&entry.path)
+                    .to_string_lossy()
+                    .to_string(),
+            ));
+        }
+    }
+
+    fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
+        if let Some((_, entry)) = self.selected_entry(cx) {
+            cx.write_to_clipboard(ClipboardItem::new(entry.path.to_string_lossy().to_string()));
+        }
+    }
+
+    fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
+        if let Some((worktree, entry)) = self.selected_entry(cx) {
+            cx.reveal_path(&worktree.abs_path().join(&entry.path));
+        }
+    }
+
+    fn open_in_terminal(&mut self, _: &OpenInTerminal, _cx: &mut ViewContext<Self>) {
+        todo!()
+        // if let Some((worktree, entry)) = self.selected_entry(cx) {
+        //     let window = cx.window();
+        //     let view_id = cx.view_id();
+        //     let path = worktree.abs_path().join(&entry.path);
+
+        //     cx.app_context()
+        //         .spawn(|mut cx| async move {
+        //             window.dispatch_action(
+        //                 view_id,
+        //                 &workspace::OpenTerminal {
+        //                     working_directory: path,
+        //                 },
+        //                 &mut cx,
+        //             );
+        //         })
+        //         .detach();
+        // }
+    }
+
+    pub fn new_search_in_directory(
+        &mut self,
+        _: &NewSearchInDirectory,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some((_, entry)) = self.selected_entry(cx) {
+            if entry.is_dir() {
+                cx.emit(Event::NewSearchInDirectory {
+                    dir_entry: entry.clone(),
+                });
+            }
+        }
+    }
+
+    // todo!()
+    // fn move_entry(
+    //     &mut self,
+    //     entry_to_move: ProjectEntryId,
+    //     destination: ProjectEntryId,
+    //     destination_is_file: bool,
+    //     cx: &mut ViewContext<Self>,
+    // ) {
+    //     let destination_worktree = self.project.update(cx, |project, cx| {
+    //         let entry_path = project.path_for_entry(entry_to_move, cx)?;
+    //         let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
+
+    //         let mut destination_path = destination_entry_path.as_ref();
+    //         if destination_is_file {
+    //             destination_path = destination_path.parent()?;
+    //         }
+
+    //         let mut new_path = destination_path.to_path_buf();
+    //         new_path.push(entry_path.path.file_name()?);
+    //         if new_path != entry_path.path.as_ref() {
+    //             let task = project.rename_entry(entry_to_move, new_path, cx)?;
+    //             cx.foreground_executor().spawn(task).detach_and_log_err(cx);
+    //         }
+
+    //         Some(project.worktree_id_for_entry(destination, cx)?)
+    //     });
+
+    //     if let Some(destination_worktree) = destination_worktree {
+    //         self.expand_entry(destination_worktree, destination, cx);
+    //     }
+    // }
+
+    fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> {
+        let mut entry_index = 0;
+        let mut visible_entries_index = 0;
+        for (worktree_index, (worktree_id, worktree_entries)) in
+            self.visible_entries.iter().enumerate()
+        {
+            if *worktree_id == selection.worktree_id {
+                for entry in worktree_entries {
+                    if entry.id == selection.entry_id {
+                        return Some((worktree_index, entry_index, visible_entries_index));
+                    } else {
+                        visible_entries_index += 1;
+                        entry_index += 1;
+                    }
+                }
+                break;
+            } else {
+                visible_entries_index += worktree_entries.len();
+            }
+        }
+        None
+    }
+
+    pub fn selected_entry<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> Option<(&'a Worktree, &'a project::Entry)> {
+        let (worktree, entry) = self.selected_entry_handle(cx)?;
+        Some((worktree.read(cx), entry))
+    }
+
+    fn selected_entry_handle<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> Option<(Model<Worktree>, &'a project::Entry)> {
+        let selection = self.selection?;
+        let project = self.project.read(cx);
+        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
+        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
+        Some((worktree, entry))
+    }
+
+    fn expand_to_selection(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+        let (worktree, entry) = self.selected_entry(cx)?;
+        let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
+
+        for path in entry.path.ancestors() {
+            let Some(entry) = worktree.entry_for_path(path) else {
+                continue;
+            };
+            if entry.is_dir() {
+                if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
+                    expanded_dir_ids.insert(idx, entry.id);
+                }
+            }
+        }
+
+        Some(())
+    }
+
+    fn update_visible_entries(
+        &mut self,
+        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let project = self.project.read(cx);
+        self.last_worktree_root_id = project
+            .visible_worktrees(cx)
+            .rev()
+            .next()
+            .and_then(|worktree| worktree.read(cx).root_entry())
+            .map(|entry| entry.id);
+
+        self.visible_entries.clear();
+        for worktree in project.visible_worktrees(cx) {
+            let snapshot = worktree.read(cx).snapshot();
+            let worktree_id = snapshot.id();
+
+            let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
+                hash_map::Entry::Occupied(e) => e.into_mut(),
+                hash_map::Entry::Vacant(e) => {
+                    // The first time a worktree's root entry becomes available,
+                    // mark that root entry as expanded.
+                    if let Some(entry) = snapshot.root_entry() {
+                        e.insert(vec![entry.id]).as_slice()
+                    } else {
+                        &[]
+                    }
+                }
+            };
+
+            let mut new_entry_parent_id = None;
+            let mut new_entry_kind = EntryKind::Dir;
+            if let Some(edit_state) = &self.edit_state {
+                if edit_state.worktree_id == worktree_id && edit_state.is_new_entry {
+                    new_entry_parent_id = Some(edit_state.entry_id);
+                    new_entry_kind = if edit_state.is_dir {
+                        EntryKind::Dir
+                    } else {
+                        EntryKind::File(Default::default())
+                    };
+                }
+            }
+
+            let mut visible_worktree_entries = Vec::new();
+            let mut entry_iter = snapshot.entries(true);
+
+            while let Some(entry) = entry_iter.entry() {
+                visible_worktree_entries.push(entry.clone());
+                if Some(entry.id) == new_entry_parent_id {
+                    visible_worktree_entries.push(Entry {
+                        id: NEW_ENTRY_ID,
+                        kind: new_entry_kind,
+                        path: entry.path.join("\0").into(),
+                        inode: 0,
+                        mtime: entry.mtime,
+                        is_symlink: false,
+                        is_ignored: false,
+                        is_external: false,
+                        git_status: entry.git_status,
+                    });
+                }
+                if expanded_dir_ids.binary_search(&entry.id).is_err()
+                    && entry_iter.advance_to_sibling()
+                {
+                    continue;
+                }
+                entry_iter.advance();
+            }
+
+            snapshot.propagate_git_statuses(&mut visible_worktree_entries);
+
+            visible_worktree_entries.sort_by(|entry_a, entry_b| {
+                let mut components_a = entry_a.path.components().peekable();
+                let mut components_b = entry_b.path.components().peekable();
+                loop {
+                    match (components_a.next(), components_b.next()) {
+                        (Some(component_a), Some(component_b)) => {
+                            let a_is_file = components_a.peek().is_none() && entry_a.is_file();
+                            let b_is_file = components_b.peek().is_none() && entry_b.is_file();
+                            let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
+                                let name_a =
+                                    UniCase::new(component_a.as_os_str().to_string_lossy());
+                                let name_b =
+                                    UniCase::new(component_b.as_os_str().to_string_lossy());
+                                name_a.cmp(&name_b)
+                            });
+                            if !ordering.is_eq() {
+                                return ordering;
+                            }
+                        }
+                        (Some(_), None) => break Ordering::Greater,
+                        (None, Some(_)) => break Ordering::Less,
+                        (None, None) => break Ordering::Equal,
+                    }
+                }
+            });
+            self.visible_entries
+                .push((worktree_id, visible_worktree_entries));
+        }
+
+        if let Some((worktree_id, entry_id)) = new_selected_entry {
+            self.selection = Some(Selection {
+                worktree_id,
+                entry_id,
+            });
+        }
+    }
+
+    fn expand_entry(
+        &mut self,
+        worktree_id: WorktreeId,
+        entry_id: ProjectEntryId,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.project.update(cx, |project, cx| {
+            if let Some((worktree, expanded_dir_ids)) = project
+                .worktree_for_id(worktree_id, cx)
+                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
+            {
+                project.expand_entry(worktree_id, entry_id, cx);
+                let worktree = worktree.read(cx);
+
+                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
+                    loop {
+                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
+                            expanded_dir_ids.insert(ix, entry.id);
+                        }
+
+                        if let Some(parent_entry) =
+                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
+                        {
+                            entry = parent_entry;
+                        } else {
+                            break;
+                        }
+                    }
+                }
+            }
+        });
+    }
+
+    fn for_each_visible_entry(
+        &self,
+        range: Range<usize>,
+        cx: &mut ViewContext<ProjectPanel>,
+        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
+    ) {
+        let mut ix = 0;
+        for (worktree_id, visible_worktree_entries) in &self.visible_entries {
+            if ix >= range.end {
+                return;
+            }
+
+            if ix + visible_worktree_entries.len() <= range.start {
+                ix += visible_worktree_entries.len();
+                continue;
+            }
+
+            let end_ix = range.end.min(ix + visible_worktree_entries.len());
+            let (git_status_setting, show_file_icons, show_folder_icons) = {
+                let settings = ProjectPanelSettings::get_global(cx);
+                (
+                    settings.git_status,
+                    settings.file_icons,
+                    settings.folder_icons,
+                )
+            };
+            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
+                let snapshot = worktree.read(cx).snapshot();
+                let root_name = OsStr::new(snapshot.root_name());
+                let expanded_entry_ids = self
+                    .expanded_dir_ids
+                    .get(&snapshot.id())
+                    .map(Vec::as_slice)
+                    .unwrap_or(&[]);
+
+                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
+                for entry in visible_worktree_entries[entry_range].iter() {
+                    let status = git_status_setting.then(|| entry.git_status).flatten();
+                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
+                    let icon = match entry.kind {
+                        EntryKind::File(_) => {
+                            if show_file_icons {
+                                Some(FileAssociations::get_icon(&entry.path, cx))
+                            } else {
+                                None
+                            }
+                        }
+                        _ => {
+                            if show_folder_icons {
+                                Some(FileAssociations::get_folder_icon(is_expanded, cx))
+                            } else {
+                                Some(FileAssociations::get_chevron_icon(is_expanded, cx))
+                            }
+                        }
+                    };
+
+                    let mut details = EntryDetails {
+                        filename: entry
+                            .path
+                            .file_name()
+                            .unwrap_or(root_name)
+                            .to_string_lossy()
+                            .to_string(),
+                        icon,
+                        path: entry.path.clone(),
+                        depth: entry.path.components().count(),
+                        kind: entry.kind,
+                        is_ignored: entry.is_ignored,
+                        is_expanded,
+                        is_selected: self.selection.map_or(false, |e| {
+                            e.worktree_id == snapshot.id() && e.entry_id == entry.id
+                        }),
+                        is_editing: false,
+                        is_processing: false,
+                        is_cut: self
+                            .clipboard_entry
+                            .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
+                        git_status: status,
+                    };
+
+                    if let Some(edit_state) = &self.edit_state {
+                        let is_edited_entry = if edit_state.is_new_entry {
+                            entry.id == NEW_ENTRY_ID
+                        } else {
+                            entry.id == edit_state.entry_id
+                        };
+
+                        if is_edited_entry {
+                            if let Some(processing_filename) = &edit_state.processing_filename {
+                                details.is_processing = true;
+                                details.filename.clear();
+                                details.filename.push_str(processing_filename);
+                            } else {
+                                if edit_state.is_new_entry {
+                                    details.filename.clear();
+                                }
+                                details.is_editing = true;
+                            }
+                        }
+                    }
+
+                    callback(entry.id, details, cx);
+                }
+            }
+            ix = end_ix;
+        }
+    }
+
+    fn render_entry_visual_element(
+        details: &EntryDetails,
+        editor: Option<&View<Editor>>,
+        padding: Pixels,
+        cx: &mut ViewContext<Self>,
+    ) -> Div<Self> {
+        let show_editor = details.is_editing && !details.is_processing;
+
+        let theme = cx.theme();
+        let filename_text_color = details
+            .git_status
+            .as_ref()
+            .map(|status| match status {
+                GitFileStatus::Added => theme.status().created,
+                GitFileStatus::Modified => theme.status().modified,
+                GitFileStatus::Conflict => theme.status().conflict,
+            })
+            .unwrap_or(theme.status().info);
+
+        h_stack()
+            .child(if let Some(icon) = &details.icon {
+                div().child(IconElement::from_path(icon.to_string()))
+            } else {
+                div()
+            })
+            .child(
+                if let (Some(editor), true) = (editor, show_editor) {
+                    div().w_full().child(editor.clone())
+                } else {
+                    div()
+                        .text_color(filename_text_color)
+                        .child(Label::new(details.filename.clone()))
+                }
+                .ml_1(),
+            )
+            .pl(padding)
+    }
+
+    fn render_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        details: EntryDetails,
+        // dragged_entry_destination: &mut Option<Arc<Path>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Stateful<Self, Div<Self>> {
+        let kind = details.kind;
+        let settings = ProjectPanelSettings::get_global(cx);
+        const INDENT_SIZE: Pixels = px(16.0);
+        let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size);
+        let show_editor = details.is_editing && !details.is_processing;
+        let is_selected = self
+            .selection
+            .map_or(false, |selection| selection.entry_id == entry_id);
+
+        Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx)
+            .id(entry_id.to_proto() as usize)
+            .w_full()
+            .cursor_pointer()
+            .when(is_selected, |this| {
+                this.bg(cx.theme().colors().element_selected)
+            })
+            .hover(|style| style.bg(cx.theme().colors().element_hover))
+            .on_click(move |this, event, cx| {
+                if !show_editor {
+                    if kind.is_dir() {
+                        this.toggle_expanded(entry_id, cx);
+                    } else {
+                        if event.down.modifiers.command {
+                            this.split_entry(entry_id, cx);
+                        } else {
+                            this.open_entry(entry_id, event.up.click_count > 1, cx);
+                        }
+                    }
+                }
+            })
+            .on_mouse_down(MouseButton::Right, move |this, event, cx| {
+                this.deploy_context_menu(event.position, entry_id, cx);
+            })
+        // .on_drop::<ProjectEntryId>(|this, event, cx| {
+        //     this.move_entry(
+        //         *dragged_entry,
+        //         entry_id,
+        //         matches!(details.kind, EntryKind::File(_)),
+        //         cx,
+        //     );
+        // })
+    }
+}
+
+impl Render for ProjectPanel {
+    type Element = Focusable<Self, Stateful<Self, Div<Self>>>;
+
+    fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+        let has_worktree = self.visible_entries.len() != 0;
+
+        if has_worktree {
+            div()
+                .id("project-panel")
+                .size_full()
+                .key_context("ProjectPanel")
+                .on_action(Self::select_next)
+                .on_action(Self::select_prev)
+                .on_action(Self::expand_selected_entry)
+                .on_action(Self::collapse_selected_entry)
+                .on_action(Self::collapse_all_entries)
+                .on_action(Self::new_file)
+                .on_action(Self::new_directory)
+                .on_action(Self::rename)
+                .on_action(Self::delete)
+                .on_action(Self::confirm)
+                .on_action(Self::open_file)
+                .on_action(Self::cancel)
+                .on_action(Self::cut)
+                .on_action(Self::copy)
+                .on_action(Self::copy_path)
+                .on_action(Self::copy_relative_path)
+                .on_action(Self::paste)
+                .on_action(Self::reveal_in_finder)
+                .on_action(Self::open_in_terminal)
+                .on_action(Self::new_search_in_directory)
+                .track_focus(&self.focus_handle)
+                .child(
+                    uniform_list(
+                        "entries",
+                        self.visible_entries
+                            .iter()
+                            .map(|(_, worktree_entries)| worktree_entries.len())
+                            .sum(),
+                        |this: &mut Self, range, cx| {
+                            let mut items = Vec::new();
+                            this.for_each_visible_entry(range, cx, |id, details, cx| {
+                                items.push(this.render_entry(id, details, cx));
+                            });
+                            items
+                        },
+                    )
+                    .size_full()
+                    .track_scroll(self.list.clone()),
+                )
+        } else {
+            v_stack()
+                .id("empty-project_panel")
+                .track_focus(&self.focus_handle)
+        }
+    }
+}
+
+impl EventEmitter<Event> for ProjectPanel {}
+
+impl EventEmitter<PanelEvent> for ProjectPanel {}
+
+impl workspace::dock::Panel for ProjectPanel {
+    fn position(&self, cx: &WindowContext) -> DockPosition {
+        match ProjectPanelSettings::get_global(cx).dock {
+            ProjectPanelDockPosition::Left => DockPosition::Left,
+            ProjectPanelDockPosition::Right => DockPosition::Right,
+        }
+    }
+
+    fn position_is_valid(&self, position: DockPosition) -> bool {
+        matches!(position, DockPosition::Left | DockPosition::Right)
+    }
+
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        settings::update_settings_file::<ProjectPanelSettings>(
+            self.fs.clone(),
+            cx,
+            move |settings| {
+                let dock = match position {
+                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
+                    DockPosition::Right => ProjectPanelDockPosition::Right,
+                };
+                settings.dock = Some(dock);
+            },
+        );
+    }
+
+    fn size(&self, cx: &WindowContext) -> f32 {
+        self.width
+            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
+    }
+
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+        self.width = size;
+        self.serialize(cx);
+        cx.notify();
+    }
+
+    fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
+        Some("icons/project.svg")
+    }
+
+    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
+        ("Project Panel".into(), Some(Box::new(ToggleFocus)))
+    }
+
+    // fn should_change_position_on_event(event: &Self::Event) -> bool {
+    //     matches!(event, Event::DockPositionChanged)
+    // }
+
+    fn has_focus(&self, _: &WindowContext) -> bool {
+        self.has_focus
+    }
+
+    fn persistent_name(&self) -> &'static str {
+        "Project Panel"
+    }
+
+    fn focus_handle(&self, _cx: &WindowContext) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+
+    // fn is_focus_event(event: &Self::Event) -> bool {
+    //     matches!(event, Event::Focus)
+    // }
+}
+
+impl ClipboardEntry {
+    fn is_cut(&self) -> bool {
+        matches!(self, Self::Cut { .. })
+    }
+
+    fn entry_id(&self) -> ProjectEntryId {
+        match self {
+            ClipboardEntry::Copied { entry_id, .. } | ClipboardEntry::Cut { entry_id, .. } => {
+                *entry_id
+            }
+        }
+    }
+
+    fn worktree_id(&self) -> WorktreeId {
+        match self {
+            ClipboardEntry::Copied { worktree_id, .. }
+            | ClipboardEntry::Cut { worktree_id, .. } => *worktree_id,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
+    use pretty_assertions::assert_eq;
+    use project::FakeFs;
+    use serde_json::json;
+    use settings::SettingsStore;
+    use std::{
+        collections::HashSet,
+        path::{Path, PathBuf},
+        sync::atomic::{self, AtomicUsize},
+    };
+    use workspace::{pane, AppState};
+
+    #[gpui::test]
+    async fn test_visible_list(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                ".dockerignore": "",
+                ".git": {
+                    "HEAD": "",
+                },
+                "a": {
+                    "0": { "q": "", "r": "", "s": "" },
+                    "1": { "t": "", "u": "" },
+                    "2": { "v": "", "w": "", "x": "", "y": "" },
+                },
+                "b": {
+                    "3": { "Q": "" },
+                    "4": { "R": "", "S": "", "T": "", "U": "" },
+                },
+                "C": {
+                    "5": {},
+                    "6": { "V": "", "W": "" },
+                    "7": { "X": "" },
+                    "8": { "Y": {}, "Z": "" }
+                }
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            "/root2",
+            json!({
+                "d": {
+                    "9": ""
+                },
+                "e": {}
+            }),
+        )
+        .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 cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        toggle_expand_dir(&panel, "root1/b", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b  <== selected",
+                "        > 3",
+                "        > 4",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 6..9, cx),
+            &[
+                //
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+            ]
+        );
+    }
+
+    #[gpui::test(iterations = 30)]
+    async fn test_editing_files(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                ".dockerignore": "",
+                ".git": {
+                    "HEAD": "",
+                },
+                "a": {
+                    "0": { "q": "", "r": "", "s": "" },
+                    "1": { "t": "", "u": "" },
+                    "2": { "v": "", "w": "", "x": "", "y": "" },
+                },
+                "b": {
+                    "3": { "Q": "" },
+                    "4": { "R": "", "S": "", "T": "", "U": "" },
+                },
+                "C": {
+                    "5": {},
+                    "6": { "V": "", "W": "" },
+                    "7": { "X": "" },
+                    "8": { "Y": {}, "Z": "" }
+                }
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            "/root2",
+            json!({
+                "d": {
+                    "9": ""
+                },
+                "e": {}
+            }),
+        )
+        .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 cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        select_path(&panel, "root1", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1  <== selected",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        // Add a file with the root folder selected. The filename editor is placed
+        // before the first file in the root folder.
+        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+        panel.update(cx, |panel, cx| {
+            assert!(panel.filename_editor.read(cx).is_focused(cx));
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      [EDITOR: '']  <== selected",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        let confirm = panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
+            panel.confirm_edit(cx).unwrap()
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      [PROCESSING: 'the-new-filename']  <== selected",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        confirm.await.unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename  <== selected",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        select_path(&panel, "root1/b", cx);
+        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          [EDITOR: '']  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        panel
+            .update(cx, |panel, cx| {
+                panel
+                    .filename_editor
+                    .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx));
+                panel.confirm_edit(cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          another-filename.txt  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        select_path(&panel, "root1/b/another-filename.txt", cx);
+        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          [EDITOR: 'another-filename.txt']  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        let confirm = panel.update(cx, |panel, cx| {
+            panel.filename_editor.update(cx, |editor, cx| {
+                let file_name_selections = editor.selections.all::<usize>(cx);
+                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
+                let file_name_selection = &file_name_selections[0];
+                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
+                assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
+
+                editor.set_text("a-different-filename.tar.gz", cx)
+            });
+            panel.confirm_edit(cx).unwrap()
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        confirm.await.unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          a-different-filename.tar.gz  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3",
+                "        > 4",
+                "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "      the-new-filename",
+            ]
+        );
+
+        panel.update(cx, |panel, cx| {
+            panel.filename_editor.update(cx, |editor, cx| {
+                let file_name_selections = editor.selections.all::<usize>(cx);
+                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
+                let file_name_selection = &file_name_selections[0];
+                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
+                assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot..");
+
+            });
+            panel.cancel(&Cancel, cx)
+        });
+
+        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > [EDITOR: '']  <== selected",
+                "        > 3",
+                "        > 4",
+                "          a-different-filename.tar.gz",
+                "    > C",
+                "      .dockerignore",
+            ]
+        );
+
+        let confirm = panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("new-dir", cx));
+            panel.confirm_edit(cx).unwrap()
+        });
+        panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > [PROCESSING: 'new-dir']",
+                "        > 3  <== selected",
+                "        > 4",
+                "          a-different-filename.tar.gz",
+                "    > C",
+                "      .dockerignore",
+            ]
+        );
+
+        confirm.await.unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3  <== selected",
+                "        > 4",
+                "        > new-dir",
+                "          a-different-filename.tar.gz",
+                "    > C",
+                "      .dockerignore",
+            ]
+        );
+
+        panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx));
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > [EDITOR: '3']  <== selected",
+                "        > 4",
+                "        > new-dir",
+                "          a-different-filename.tar.gz",
+                "    > C",
+                "      .dockerignore",
+            ]
+        );
+
+        // Dismiss the rename editor when it loses focus.
+        workspace.update(cx, |_, cx| cx.blur()).unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    v b",
+                "        > 3  <== selected",
+                "        > 4",
+                "        > new-dir",
+                "          a-different-filename.tar.gz",
+                "    > C",
+                "      .dockerignore",
+            ]
+        );
+    }
+
+    #[gpui::test(iterations = 10)]
+    async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                ".dockerignore": "",
+                ".git": {
+                    "HEAD": "",
+                },
+                "a": {
+                    "0": { "q": "", "r": "", "s": "" },
+                    "1": { "t": "", "u": "" },
+                    "2": { "v": "", "w": "", "x": "", "y": "" },
+                },
+                "b": {
+                    "3": { "Q": "" },
+                    "4": { "R": "", "S": "", "T": "", "U": "" },
+                },
+                "C": {
+                    "5": {},
+                    "6": { "V": "", "W": "" },
+                    "7": { "X": "" },
+                    "8": { "Y": {}, "Z": "" }
+                }
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            "/root2",
+            json!({
+                "d": {
+                    "9": ""
+                },
+                "e": {}
+            }),
+        )
+        .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 cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        select_path(&panel, "root1", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1  <== selected",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        // Add a file with the root folder selected. The filename editor is placed
+        // before the first file in the root folder.
+        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+        panel.update(cx, |panel, cx| {
+            assert!(panel.filename_editor.read(cx).is_focused(cx));
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      [EDITOR: '']  <== selected",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        let confirm = panel.update(cx, |panel, cx| {
+            panel.filename_editor.update(cx, |editor, cx| {
+                editor.set_text("/bdir1/dir2/the-new-filename", cx)
+            });
+            panel.confirm_edit(cx).unwrap()
+        });
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        confirm.await.unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..13, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    v bdir1",
+                "        v dir2",
+                "              the-new-filename  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                "one.two.txt": "",
+                "one.txt": ""
+            }),
+        )
+        .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 cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        panel.update(cx, |panel, cx| {
+            panel.select_next(&Default::default(), cx);
+            panel.select_next(&Default::default(), cx);
+        });
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                //
+                "v root1",
+                "      one.two.txt  <== selected",
+                "      one.txt",
+            ]
+        );
+
+        // Regression test - file name is created correctly when
+        // the copied file's name contains multiple dots.
+        panel.update(cx, |panel, cx| {
+            panel.copy(&Default::default(), cx);
+            panel.paste(&Default::default(), cx);
+        });
+        cx.executor().run_until_parked();
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                //
+                "v root1",
+                "      one.two copy.txt",
+                "      one.two.txt  <== selected",
+                "      one.txt",
+            ]
+        );
+
+        panel.update(cx, |panel, cx| {
+            panel.paste(&Default::default(), cx);
+        });
+        cx.executor().run_until_parked();
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                //
+                "v root1",
+                "      one.two copy 1.txt",
+                "      one.two copy.txt",
+                "      one.two.txt  <== selected",
+                "      one.txt",
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/src",
+            json!({
+                "test": {
+                    "first.rs": "// First Rust file",
+                    "second.rs": "// Second Rust file",
+                    "third.rs": "// Third Rust file",
+                }
+            }),
+        )
+        .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 cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        toggle_expand_dir(&panel, "src/test", cx);
+        select_path(&panel, "src/test/first.rs", cx);
+        panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          first.rs  <== selected",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
+
+        submit_deletion(&panel, cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          second.rs",
+                "          third.rs"
+            ],
+            "Project panel should have no deleted file, no other file is selected in it"
+        );
+        ensure_no_open_items_and_panes(&workspace, cx);
+
+        select_path(&panel, "src/test/second.rs", cx);
+        panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          second.rs  <== selected",
+                "          third.rs"
+            ]
+        );
+        ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
+
+        workspace
+            .update(cx, |workspace, cx| {
+                let active_items = workspace
+                    .panes()
+                    .iter()
+                    .filter_map(|pane| pane.read(cx).active_item())
+                    .collect::<Vec<_>>();
+                assert_eq!(active_items.len(), 1);
+                let open_editor = active_items
+                    .into_iter()
+                    .next()
+                    .unwrap()
+                    .downcast::<Editor>()
+                    .expect("Open item should be an editor");
+                open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
+            })
+            .unwrap();
+        submit_deletion(&panel, cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v src", "    v test", "          third.rs"],
+            "Project panel should have no deleted file, with one last file remaining"
+        );
+        ensure_no_open_items_and_panes(&workspace, cx);
+    }
+
+    #[gpui::test]
+    async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/src",
+            json!({
+                "test": {
+                    "first.rs": "// First Rust file",
+                    "second.rs": "// Second Rust file",
+                    "third.rs": "// Third Rust file",
+                }
+            }),
+        )
+        .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 cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        select_path(&panel, "src/", cx);
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                //
+                "v src  <== selected",
+                "    > test"
+            ]
+        );
+        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
+        panel.update(cx, |panel, cx| {
+            assert!(panel.filename_editor.read(cx).is_focused(cx));
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                //
+                "v src",
+                "    > [EDITOR: '']  <== selected",
+                "    > test"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("test", cx));
+            assert!(
+                panel.confirm_edit(cx).is_none(),
+                "Should not allow to confirm on conflicting new directory name"
+            )
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                //
+                "v src",
+                "    > test"
+            ],
+            "File list should be unchanged after failed folder create confirmation"
+        );
+
+        select_path(&panel, "src/test/", cx);
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                //
+                "v src",
+                "    > test  <== selected"
+            ]
+        );
+        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+        panel.update(cx, |panel, cx| {
+            assert!(panel.filename_editor.read(cx).is_focused(cx));
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          [EDITOR: '']  <== selected",
+                "          first.rs",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("first.rs", cx));
+            assert!(
+                panel.confirm_edit(cx).is_none(),
+                "Should not allow to confirm on conflicting new file name"
+            )
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          first.rs",
+                "          second.rs",
+                "          third.rs"
+            ],
+            "File list should be unchanged after failed file create confirmation"
+        );
+
+        select_path(&panel, "src/test/first.rs", cx);
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          first.rs  <== selected",
+                "          second.rs",
+                "          third.rs"
+            ],
+        );
+        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+        panel.update(cx, |panel, cx| {
+            assert!(panel.filename_editor.read(cx).is_focused(cx));
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          [EDITOR: 'first.rs']  <== selected",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("second.rs", cx));
+            assert!(
+                panel.confirm_edit(cx).is_none(),
+                "Should not allow to confirm on conflicting file rename"
+            )
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          first.rs  <== selected",
+                "          second.rs",
+                "          third.rs"
+            ],
+            "File list should be unchanged after failed rename confirmation"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/src",
+            json!({
+                "test": {
+                    "first.rs": "// First Rust file",
+                    "second.rs": "// Second Rust file",
+                    "third.rs": "// Third Rust file",
+                }
+            }),
+        )
+        .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 cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        let new_search_events_count = Arc::new(AtomicUsize::new(0));
+        let _subscription = panel.update(cx, |_, cx| {
+            let subcription_count = Arc::clone(&new_search_events_count);
+            let view = cx.view().clone();
+            cx.subscribe(&view, move |_, _, event, _| {
+                if matches!(event, Event::NewSearchInDirectory { .. }) {
+                    subcription_count.fetch_add(1, atomic::Ordering::SeqCst);
+                }
+            })
+        });
+
+        toggle_expand_dir(&panel, "src/test", cx);
+        select_path(&panel, "src/test/first.rs", cx);
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test",
+                "          first.rs  <== selected",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel.new_search_in_directory(&NewSearchInDirectory, cx)
+        });
+        assert_eq!(
+            new_search_events_count.load(atomic::Ordering::SeqCst),
+            0,
+            "Should not trigger new search in directory when called on a file"
+        );
+
+        select_path(&panel, "src/test", cx);
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v src",
+                "    v test  <== selected",
+                "          first.rs",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel.new_search_in_directory(&NewSearchInDirectory, cx)
+        });
+        assert_eq!(
+            new_search_events_count.load(atomic::Ordering::SeqCst),
+            1,
+            "Should trigger new search in directory when called on a directory"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/project_root",
+            json!({
+                "dir_1": {
+                    "nested_dir": {
+                        "file_a.py": "# File contents",
+                        "file_b.py": "# File contents",
+                        "file_c.py": "# File contents",
+                    },
+                    "file_1.py": "# File contents",
+                    "file_2.py": "# File contents",
+                    "file_3.py": "# File contents",
+                },
+                "dir_2": {
+                    "file_1.py": "# File contents",
+                    "file_2.py": "# File contents",
+                    "file_3.py": "# File contents",
+                }
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        panel.update(cx, |panel, cx| {
+            panel.collapse_all_entries(&CollapseAllEntries, cx)
+        });
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v project_root", "    > dir_1", "    > dir_2",]
+        );
+
+        // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
+        toggle_expand_dir(&panel, "project_root/dir_1", cx);
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v project_root",
+                "    v dir_1  <== selected",
+                "        > nested_dir",
+                "          file_1.py",
+                "          file_2.py",
+                "          file_3.py",
+                "    > dir_2",
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.as_fake().insert_tree("/root", json!({})).await;
+        let project = Project::test(fs, ["/root".as_ref()], cx).await;
+        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace
+            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+            .unwrap();
+
+        // Make a new buffer with no backing file
+        workspace
+            .update(cx, |workspace, cx| {
+                Editor::new_file(workspace, &Default::default(), cx)
+            })
+            .unwrap();
+
+        // "Save as"" the buffer, creating a new backing file for it
+        let save_task = workspace
+            .update(cx, |workspace, cx| {
+                workspace.save_active_item(workspace::SaveIntent::Save, cx)
+            })
+            .unwrap();
+
+        cx.executor().run_until_parked();
+        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new")));
+        save_task.await.unwrap();
+
+        // Rename the file
+        select_path(&panel, "root/new", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v root", "      new  <== selected"]
+        );
+        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+        panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("newer", cx));
+        });
+        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+
+        cx.executor().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v root", "      newer  <== selected"]
+        );
+
+        workspace
+            .update(cx, |workspace, cx| {
+                workspace.save_active_item(workspace::SaveIntent::Save, cx)
+            })
+            .unwrap()
+            .await
+            .unwrap();
+
+        cx.executor().run_until_parked();
+        // assert that saving the file doesn't restore "new"
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v root", "      newer  <== selected"]
+        );
+    }
+
+    fn toggle_expand_dir(
+        panel: &View<ProjectPanel>,
+        path: impl AsRef<Path>,
+        cx: &mut VisualTestContext,
+    ) {
+        let path = path.as_ref();
+        panel.update(cx, |panel, cx| {
+            for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
+                let worktree = worktree.read(cx);
+                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
+                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
+                    panel.toggle_expanded(entry_id, cx);
+                    return;
+                }
+            }
+            panic!("no worktree for path {:?}", path);
+        });
+    }
+
+    fn select_path(panel: &View<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
+        let path = path.as_ref();
+        panel.update(cx, |panel, cx| {
+            for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
+                let worktree = worktree.read(cx);
+                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
+                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
+                    panel.selection = Some(Selection {
+                        worktree_id: worktree.id(),
+                        entry_id,
+                    });
+                    return;
+                }
+            }
+            panic!("no worktree for path {:?}", path);
+        });
+    }
+
+    fn visible_entries_as_strings(
+        panel: &View<ProjectPanel>,
+        range: Range<usize>,
+        cx: &mut VisualTestContext,
+    ) -> Vec<String> {
+        let mut result = Vec::new();
+        let mut project_entries = HashSet::new();
+        let mut has_editor = false;
+
+        panel.update(cx, |panel, cx| {
+            panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
+                if details.is_editing {
+                    assert!(!has_editor, "duplicate editor entry");
+                    has_editor = true;
+                } else {
+                    assert!(
+                        project_entries.insert(project_entry),
+                        "duplicate project entry {:?} {:?}",
+                        project_entry,
+                        details
+                    );
+                }
+
+                let indent = "    ".repeat(details.depth);
+                let icon = if details.kind.is_dir() {
+                    if details.is_expanded {
+                        "v "
+                    } else {
+                        "> "
+                    }
+                } else {
+                    "  "
+                };
+                let name = if details.is_editing {
+                    format!("[EDITOR: '{}']", details.filename)
+                } else if details.is_processing {
+                    format!("[PROCESSING: '{}']", details.filename)
+                } else {
+                    details.filename.clone()
+                };
+                let selected = if details.is_selected {
+                    "  <== selected"
+                } else {
+                    ""
+                };
+                result.push(format!("{indent}{icon}{name}{selected}"));
+            });
+        });
+
+        result
+    }
+
+    fn init_test(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let settings_store = SettingsStore::test(cx);
+            cx.set_global(settings_store);
+            init_settings(cx);
+            theme::init(cx);
+            language::init(cx);
+            editor::init_settings(cx);
+            crate::init((), cx);
+            workspace::init_settings(cx);
+            client::init_settings(cx);
+            Project::init_settings(cx);
+        });
+    }
+
+    fn init_test_with_editor(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let app_state = AppState::test(cx);
+            theme::init(cx);
+            init_settings(cx);
+            language::init(cx);
+            editor::init(cx);
+            pane::init(cx);
+            crate::init((), cx);
+            workspace::init(app_state.clone(), cx);
+            Project::init_settings(cx);
+        });
+    }
+
+    fn ensure_single_file_is_opened(
+        window: &WindowHandle<Workspace>,
+        expected_path: &str,
+        cx: &mut TestAppContext,
+    ) {
+        window
+            .update(cx, |workspace, cx| {
+                let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
+                assert_eq!(worktrees.len(), 1);
+                let worktree_id = worktrees[0].read(cx).id();
+
+                let open_project_paths = workspace
+                    .panes()
+                    .iter()
+                    .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
+                    .collect::<Vec<_>>();
+                assert_eq!(
+                    open_project_paths,
+                    vec![ProjectPath {
+                        worktree_id,
+                        path: Arc::from(Path::new(expected_path))
+                    }],
+                    "Should have opened file, selected in project panel"
+                );
+            })
+            .unwrap();
+    }
+
+    fn submit_deletion(panel: &View<ProjectPanel>, cx: &mut VisualTestContext) {
+        assert!(
+            !cx.has_pending_prompt(),
+            "Should have no prompts before the deletion"
+        );
+        panel.update(cx, |panel, cx| panel.delete(&Delete, cx));
+        assert!(
+            cx.has_pending_prompt(),
+            "Should have a prompt after the deletion"
+        );
+        cx.simulate_prompt_answer(0);
+        assert!(
+            !cx.has_pending_prompt(),
+            "Should have no prompts after prompt was replied to"
+        );
+        cx.executor().run_until_parked();
+    }
+
+    fn ensure_no_open_items_and_panes(
+        workspace: &WindowHandle<Workspace>,
+        cx: &mut VisualTestContext,
+    ) {
+        assert!(
+            !cx.has_pending_prompt(),
+            "Should have no prompts after deletion operation closes the file"
+        );
+        workspace
+            .read_with(cx, |workspace, cx| {
+                let open_project_paths = workspace
+                    .panes()
+                    .iter()
+                    .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
+                    .collect::<Vec<_>>();
+                assert!(
+                    open_project_paths.is_empty(),
+                    "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
+                );
+            })
+            .unwrap();
+    }
+}

crates/project_panel2/src/project_panel_settings.rs 🔗

@@ -0,0 +1,45 @@
+use anyhow;
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Settings;
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ProjectPanelDockPosition {
+    Left,
+    Right,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct ProjectPanelSettings {
+    pub default_width: f32,
+    pub dock: ProjectPanelDockPosition,
+    pub file_icons: bool,
+    pub folder_icons: bool,
+    pub git_status: bool,
+    pub indent_size: f32,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct ProjectPanelSettingsContent {
+    pub default_width: Option<f32>,
+    pub dock: Option<ProjectPanelDockPosition>,
+    pub file_icons: Option<bool>,
+    pub folder_icons: Option<bool>,
+    pub git_status: Option<bool>,
+    pub indent_size: Option<f32>,
+}
+
+impl Settings for ProjectPanelSettings {
+    const KEY: Option<&'static str> = Some("project_panel");
+
+    type FileContent = ProjectPanelSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/rpc2/src/peer.rs 🔗

@@ -577,18 +577,18 @@ mod tests {
         let client2 = Peer::new(0);
 
         let (client1_to_server_conn, server_to_client_1_conn, _kill) =
-            Connection::in_memory(cx.executor().clone());
+            Connection::in_memory(cx.executor());
         let (client1_conn_id, io_task1, client1_incoming) =
-            client1.add_test_connection(client1_to_server_conn, cx.executor().clone());
+            client1.add_test_connection(client1_to_server_conn, cx.executor());
         let (_, io_task2, server_incoming1) =
-            server.add_test_connection(server_to_client_1_conn, cx.executor().clone());
+            server.add_test_connection(server_to_client_1_conn, cx.executor());
 
         let (client2_to_server_conn, server_to_client_2_conn, _kill) =
-            Connection::in_memory(cx.executor().clone());
+            Connection::in_memory(cx.executor());
         let (client2_conn_id, io_task3, client2_incoming) =
-            client2.add_test_connection(client2_to_server_conn, cx.executor().clone());
+            client2.add_test_connection(client2_to_server_conn, cx.executor());
         let (_, io_task4, server_incoming2) =
-            server.add_test_connection(server_to_client_2_conn, cx.executor().clone());
+            server.add_test_connection(server_to_client_2_conn, cx.executor());
 
         executor.spawn(io_task1).detach();
         executor.spawn(io_task2).detach();
@@ -763,16 +763,16 @@ mod tests {
 
     #[gpui::test(iterations = 50)]
     async fn test_dropping_request_before_completion(cx: &mut TestAppContext) {
-        let executor = cx.executor().clone();
+        let executor = cx.executor();
         let server = Peer::new(0);
         let client = Peer::new(0);
 
         let (client_to_server_conn, server_to_client_conn, _kill) =
-            Connection::in_memory(cx.executor().clone());
+            Connection::in_memory(cx.executor());
         let (client_to_server_conn_id, io_task1, mut client_incoming) =
-            client.add_test_connection(client_to_server_conn, cx.executor().clone());
+            client.add_test_connection(client_to_server_conn, cx.executor());
         let (server_to_client_conn_id, io_task2, mut server_incoming) =
-            server.add_test_connection(server_to_client_conn, cx.executor().clone());
+            server.add_test_connection(server_to_client_conn, cx.executor());
 
         executor.spawn(io_task1).detach();
         executor.spawn(io_task2).detach();

crates/search2/src/buffer_search.rs 🔗

@@ -10,16 +10,15 @@ use collections::HashMap;
 use editor::Editor;
 use futures::channel::oneshot;
 use gpui::{
-    action, actions, blue, div, red, rems, white, Action, AnyElement, AnyView, AppContext,
-    Component, Div, Entity, EventEmitter, Hsla, ParentElement as _, Render, Styled, Subscription,
-    Svg, Task, View, ViewContext, VisualContext as _, WindowContext,
+    action, actions, div, red, Action, AppContext, Component, Div, EventEmitter,
+    ParentComponent as _, Render, Styled, Subscription, Task, View, ViewContext,
+    VisualContext as _, WindowContext,
 };
 use project::search::SearchQuery;
-use serde::Deserialize;
 use std::{any::Any, sync::Arc};
 use theme::ActiveTheme;
 
-use ui::{h_stack, Button, ButtonGroup, Icon, IconButton, IconElement, Label, StyledExt};
+use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement};
 use util::ResultExt;
 use workspace::{
     item::ItemHandle,

crates/search2/src/search_bar.rs 🔗

@@ -1,17 +1,10 @@
 use std::{borrow::Cow, sync::Arc};
 
-use gpui::{
-    div, Action, AnyElement, Component, CursorStyle, Element, MouseButton, MouseDownEvent,
-    ParentElement as _, StatelessInteractive, Styled, Svg, View, ViewContext,
-};
-use theme::ActiveTheme;
-use ui::{v_stack, Button, ButtonVariant, Label};
+use gpui::{div, Action, Component, ViewContext};
+use ui::{Button, ButtonVariant};
 use workspace::searchable::Direction;
 
-use crate::{
-    mode::{SearchMode, Side},
-    SelectNextMatch, SelectPrevMatch,
-};
+use crate::mode::{SearchMode, Side};
 
 pub(super) fn render_nav_button<V: 'static>(
     icon: &'static str,

crates/semantic_index/Cargo.toml 🔗

@@ -33,7 +33,7 @@ lazy_static.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 async-trait.workspace = true
-tiktoken-rs = "0.5.0"
+tiktoken-rs.workspace = true
 parking_lot.workspace = true
 rand.workspace = true
 schemars.workspace = true

crates/settings2/src/keymap_file.rs 🔗

@@ -9,7 +9,7 @@ use schemars::{
 };
 use serde::Deserialize;
 use serde_json::Value;
-use util::{asset_str, ResultExt};
+use util::asset_str;
 
 #[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
 #[serde(transparent)]
@@ -86,7 +86,9 @@ impl KeymapFile {
                             "invalid binding value for keystroke {keystroke}, context {context:?}"
                         )
                     })
-                    .log_err()
+                    // todo!()
+                    .ok()
+                    // .log_err()
                     .map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
                 })
                 .collect::<Result<Vec<_>>>()?;

crates/storybook2/src/stories/colors.rs 🔗

@@ -1,5 +1,5 @@
 use crate::story::Story;
-use gpui::{px, Div, Render};
+use gpui::{prelude::*, px, Div, Render};
 use theme2::{default_color_scales, ColorScaleStep};
 use ui::prelude::*;
 

crates/storybook2/src/stories/focus.rs 🔗

@@ -1,12 +1,15 @@
 use gpui::{
-    actions, div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render,
-    StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext, WindowContext,
+    actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View,
+    WindowContext,
 };
 use theme2::ActiveTheme;
 
 actions!(ActionA, ActionB, ActionC);
 
-pub struct FocusStory {}
+pub struct FocusStory {
+    child_1_focus: FocusHandle,
+    child_2_focus: FocusHandle,
+}
 
 impl FocusStory {
     pub fn view(cx: &mut WindowContext) -> View<Self> {
@@ -16,12 +19,15 @@ impl FocusStory {
             KeyBinding::new("cmd-c", ActionC, None),
         ]);
 
-        cx.build_view(move |cx| Self {})
+        cx.build_view(move |cx| Self {
+            child_1_focus: cx.focus_handle(),
+            child_2_focus: cx.focus_handle(),
+        })
     }
 }
 
 impl Render for FocusStory {
-    type Element = Div<Self, StatefulInteractivity<Self>, FocusEnabled<Self>>;
+    type Element = Focusable<Self, Stateful<Self, Div<Self>>>;
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
         let theme = cx.theme();
@@ -31,18 +37,16 @@ impl Render for FocusStory {
         let color_4 = theme.status().conflict;
         let color_5 = theme.status().ignored;
         let color_6 = theme.status().renamed;
-        let child_1 = cx.focus_handle();
-        let child_2 = cx.focus_handle();
 
         div()
             .id("parent")
             .focusable()
-            .context("parent")
+            .key_context("parent")
             .on_action(|_, action: &ActionA, cx| {
-                println!("Action A dispatched on parent during");
+                println!("Action A dispatched on parent");
             })
             .on_action(|_, action: &ActionB, cx| {
-                println!("Action B dispatched on parent during");
+                println!("Action B dispatched on parent");
             })
             .on_focus(|_, _, _| println!("Parent focused"))
             .on_blur(|_, _, _| println!("Parent blurred"))
@@ -56,8 +60,8 @@ impl Render for FocusStory {
             .focus_in(|style| style.bg(color_3))
             .child(
                 div()
-                    .track_focus(&child_1)
-                    .context("child-1")
+                    .track_focus(&self.child_1_focus)
+                    .key_context("child-1")
                     .on_action(|_, action: &ActionB, cx| {
                         println!("Action B dispatched on child 1 during");
                     })
@@ -76,10 +80,10 @@ impl Render for FocusStory {
             )
             .child(
                 div()
-                    .track_focus(&child_2)
-                    .context("child-2")
+                    .track_focus(&self.child_2_focus)
+                    .key_context("child-2")
                     .on_action(|_, action: &ActionC, cx| {
-                        println!("Action C dispatched on child 2 during");
+                        println!("Action C dispatched on child 2");
                     })
                     .w_full()
                     .h_6()

crates/storybook2/src/stories/kitchen_sink.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{story::Story, story_selector::ComponentStory};
-use gpui::{Div, Render, StatefulInteractivity, View, VisualContext};
+use gpui::{prelude::*, Div, Render, Stateful, View};
 use strum::IntoEnumIterator;
 use ui::prelude::*;
 
@@ -12,7 +12,7 @@ impl KitchenSinkStory {
 }
 
 impl Render for KitchenSinkStory {
-    type Element = Div<Self, StatefulInteractivity<Self>>;
+    type Element = Stateful<Self, Div<Self>>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let component_stories = ComponentStory::iter()

crates/storybook2/src/stories/picker.rs 🔗

@@ -1,11 +1,7 @@
-use std::sync::Arc;
-
 use fuzzy::StringMatchCandidate;
-use gpui::{
-    div, Component, Div, KeyBinding, ParentElement, Render, StatelessInteractive, Styled, Task,
-    View, VisualContext, WindowContext,
-};
+use gpui::{div, prelude::*, Div, KeyBinding, Render, Styled, Task, View, WindowContext};
 use picker::{Picker, PickerDelegate};
+use std::sync::Arc;
 use theme2::ActiveTheme;
 
 pub struct PickerStory {
@@ -44,6 +40,10 @@ impl PickerDelegate for Delegate {
         self.candidates.len()
     }
 
+    fn placeholder_text(&self) -> Arc<str> {
+        "Test".into()
+    }
+
     fn render_match(
         &self,
         ix: usize,

crates/storybook2/src/stories/scroll.rs 🔗

@@ -1,7 +1,4 @@
-use gpui::{
-    div, px, Component, Div, ParentElement, Render, SharedString, StatefulInteractivity, Styled,
-    View, VisualContext, WindowContext,
-};
+use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
 use theme2::ActiveTheme;
 
 pub struct ScrollStory;
@@ -13,7 +10,7 @@ impl ScrollStory {
 }
 
 impl Render for ScrollStory {
-    type Element = Div<Self, StatefulInteractivity<Self>>;
+    type Element = Stateful<Self, Div<Self>>;
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
         let theme = cx.theme();

crates/storybook2/src/stories/text.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{div, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext};
+use gpui::{div, white, Div, ParentComponent, Render, Styled, View, VisualContext, WindowContext};
 
 pub struct TextStory;
 

crates/storybook2/src/storybook2.rs 🔗

@@ -48,7 +48,7 @@ fn main() {
     let args = Args::parse();
 
     let story_selector = args.story.clone();
-    let theme_name = args.theme.unwrap_or("Zed Pro Moonlight".to_string());
+    let theme_name = args.theme.unwrap_or("One Dark".to_string());
 
     let asset_source = Arc::new(Assets);
     gpui::App::production(asset_source).run(move |cx| {

crates/theme2/src/default_colors.rs 🔗

@@ -1,261 +1,15 @@
-use gpui::{hsla, Hsla, Rgba};
+use gpui::{Hsla, Rgba};
 
-use crate::colors::{StatusColors, SystemColors, ThemeColors};
 use crate::scale::{ColorScaleSet, ColorScales};
-use crate::syntax::SyntaxTheme;
-use crate::{ColorScale, PlayerColor, PlayerColors};
+use crate::ColorScale;
+use crate::{SystemColors, ThemeColors};
 
-impl Default for PlayerColors {
-    fn default() -> Self {
-        Self(vec![
-            PlayerColor {
-                cursor: blue().dark().step_9(),
-                background: blue().dark().step_5(),
-                selection: blue().dark().step_3(),
-            },
-            PlayerColor {
-                cursor: orange().dark().step_9(),
-                background: orange().dark().step_5(),
-                selection: orange().dark().step_3(),
-            },
-            PlayerColor {
-                cursor: pink().dark().step_9(),
-                background: pink().dark().step_5(),
-                selection: pink().dark().step_3(),
-            },
-            PlayerColor {
-                cursor: lime().dark().step_9(),
-                background: lime().dark().step_5(),
-                selection: lime().dark().step_3(),
-            },
-            PlayerColor {
-                cursor: purple().dark().step_9(),
-                background: purple().dark().step_5(),
-                selection: purple().dark().step_3(),
-            },
-            PlayerColor {
-                cursor: amber().dark().step_9(),
-                background: amber().dark().step_5(),
-                selection: amber().dark().step_3(),
-            },
-            PlayerColor {
-                cursor: jade().dark().step_9(),
-                background: jade().dark().step_5(),
-                selection: jade().dark().step_3(),
-            },
-            PlayerColor {
-                cursor: red().dark().step_9(),
-                background: red().dark().step_5(),
-                selection: red().dark().step_3(),
-            },
-        ])
-    }
-}
-
-impl PlayerColors {
-    pub fn default_light() -> Self {
-        Self(vec![
-            PlayerColor {
-                cursor: blue().light().step_9(),
-                background: blue().light().step_4(),
-                selection: blue().light().step_3(),
-            },
-            PlayerColor {
-                cursor: orange().light().step_9(),
-                background: orange().light().step_4(),
-                selection: orange().light().step_3(),
-            },
-            PlayerColor {
-                cursor: pink().light().step_9(),
-                background: pink().light().step_4(),
-                selection: pink().light().step_3(),
-            },
-            PlayerColor {
-                cursor: lime().light().step_9(),
-                background: lime().light().step_4(),
-                selection: lime().light().step_3(),
-            },
-            PlayerColor {
-                cursor: purple().light().step_9(),
-                background: purple().light().step_4(),
-                selection: purple().light().step_3(),
-            },
-            PlayerColor {
-                cursor: amber().light().step_9(),
-                background: amber().light().step_4(),
-                selection: amber().light().step_3(),
-            },
-            PlayerColor {
-                cursor: jade().light().step_9(),
-                background: jade().light().step_4(),
-                selection: jade().light().step_3(),
-            },
-            PlayerColor {
-                cursor: red().light().step_9(),
-                background: red().light().step_4(),
-                selection: red().light().step_3(),
-            },
-        ])
-    }
-}
-
-fn neutral() -> ColorScaleSet {
+pub(crate) fn neutral() -> ColorScaleSet {
     slate()
 }
 
-impl Default for SystemColors {
-    fn default() -> Self {
-        Self {
-            transparent: hsla(0.0, 0.0, 0.0, 0.0),
-            mac_os_traffic_light_red: hsla(0.0139, 0.79, 0.65, 1.0),
-            mac_os_traffic_light_yellow: hsla(0.114, 0.88, 0.63, 1.0),
-            mac_os_traffic_light_green: hsla(0.313, 0.49, 0.55, 1.0),
-        }
-    }
-}
-
-impl Default for StatusColors {
-    fn default() -> Self {
-        Self {
-            conflict: red().dark().step_9(),
-            created: grass().dark().step_9(),
-            deleted: red().dark().step_9(),
-            error: red().dark().step_9(),
-            hidden: neutral().dark().step_9(),
-            ignored: neutral().dark().step_9(),
-            info: blue().dark().step_9(),
-            modified: yellow().dark().step_9(),
-            renamed: blue().dark().step_9(),
-            success: grass().dark().step_9(),
-            warning: yellow().dark().step_9(),
-        }
-    }
-}
-
-impl SyntaxTheme {
-    pub fn default_light() -> Self {
-        Self {
-            highlights: vec![
-                ("attribute".into(), cyan().light().step_11().into()),
-                ("boolean".into(), tomato().light().step_11().into()),
-                ("comment".into(), neutral().light().step_11().into()),
-                ("comment.doc".into(), iris().light().step_12().into()),
-                ("constant".into(), red().light().step_9().into()),
-                ("constructor".into(), red().light().step_9().into()),
-                ("embedded".into(), red().light().step_9().into()),
-                ("emphasis".into(), red().light().step_9().into()),
-                ("emphasis.strong".into(), red().light().step_9().into()),
-                ("enum".into(), red().light().step_9().into()),
-                ("function".into(), red().light().step_9().into()),
-                ("hint".into(), red().light().step_9().into()),
-                ("keyword".into(), orange().light().step_11().into()),
-                ("label".into(), red().light().step_9().into()),
-                ("link_text".into(), red().light().step_9().into()),
-                ("link_uri".into(), red().light().step_9().into()),
-                ("number".into(), red().light().step_9().into()),
-                ("operator".into(), red().light().step_9().into()),
-                ("predictive".into(), red().light().step_9().into()),
-                ("preproc".into(), red().light().step_9().into()),
-                ("primary".into(), red().light().step_9().into()),
-                ("property".into(), red().light().step_9().into()),
-                ("punctuation".into(), neutral().light().step_11().into()),
-                (
-                    "punctuation.bracket".into(),
-                    neutral().light().step_11().into(),
-                ),
-                (
-                    "punctuation.delimiter".into(),
-                    neutral().light().step_11().into(),
-                ),
-                (
-                    "punctuation.list_marker".into(),
-                    blue().light().step_11().into(),
-                ),
-                ("punctuation.special".into(), red().light().step_9().into()),
-                ("string".into(), jade().light().step_11().into()),
-                ("string.escape".into(), red().light().step_9().into()),
-                ("string.regex".into(), tomato().light().step_11().into()),
-                ("string.special".into(), red().light().step_9().into()),
-                (
-                    "string.special.symbol".into(),
-                    red().light().step_9().into(),
-                ),
-                ("tag".into(), red().light().step_9().into()),
-                ("text.literal".into(), red().light().step_9().into()),
-                ("title".into(), red().light().step_9().into()),
-                ("type".into(), red().light().step_9().into()),
-                ("variable".into(), red().light().step_9().into()),
-                ("variable.special".into(), red().light().step_9().into()),
-                ("variant".into(), red().light().step_9().into()),
-            ],
-            inlay_style: tomato().light().step_1().into(), // todo!("nate: use a proper style")
-            suggestion_style: orange().light().step_1().into(), // todo!("nate: use proper style")
-        }
-    }
-
-    pub fn default_dark() -> Self {
-        Self {
-            highlights: vec![
-                ("attribute".into(), tomato().dark().step_11().into()),
-                ("boolean".into(), tomato().dark().step_11().into()),
-                ("comment".into(), neutral().dark().step_11().into()),
-                ("comment.doc".into(), iris().dark().step_12().into()),
-                ("constant".into(), orange().dark().step_11().into()),
-                ("constructor".into(), gold().dark().step_11().into()),
-                ("embedded".into(), red().dark().step_11().into()),
-                ("emphasis".into(), red().dark().step_11().into()),
-                ("emphasis.strong".into(), red().dark().step_11().into()),
-                ("enum".into(), yellow().dark().step_11().into()),
-                ("function".into(), blue().dark().step_11().into()),
-                ("hint".into(), indigo().dark().step_11().into()),
-                ("keyword".into(), plum().dark().step_11().into()),
-                ("label".into(), red().dark().step_11().into()),
-                ("link_text".into(), red().dark().step_11().into()),
-                ("link_uri".into(), red().dark().step_11().into()),
-                ("number".into(), red().dark().step_11().into()),
-                ("operator".into(), red().dark().step_11().into()),
-                ("predictive".into(), red().dark().step_11().into()),
-                ("preproc".into(), red().dark().step_11().into()),
-                ("primary".into(), red().dark().step_11().into()),
-                ("property".into(), red().dark().step_11().into()),
-                ("punctuation".into(), neutral().dark().step_11().into()),
-                (
-                    "punctuation.bracket".into(),
-                    neutral().dark().step_11().into(),
-                ),
-                (
-                    "punctuation.delimiter".into(),
-                    neutral().dark().step_11().into(),
-                ),
-                (
-                    "punctuation.list_marker".into(),
-                    blue().dark().step_11().into(),
-                ),
-                ("punctuation.special".into(), red().dark().step_11().into()),
-                ("string".into(), lime().dark().step_11().into()),
-                ("string.escape".into(), orange().dark().step_11().into()),
-                ("string.regex".into(), tomato().dark().step_11().into()),
-                ("string.special".into(), red().dark().step_11().into()),
-                (
-                    "string.special.symbol".into(),
-                    red().dark().step_11().into(),
-                ),
-                ("tag".into(), red().dark().step_11().into()),
-                ("text.literal".into(), purple().dark().step_11().into()),
-                ("title".into(), sky().dark().step_11().into()),
-                ("type".into(), mint().dark().step_11().into()),
-                ("variable".into(), red().dark().step_11().into()),
-                ("variable.special".into(), red().dark().step_11().into()),
-                ("variant".into(), red().dark().step_11().into()),
-            ],
-            inlay_style: neutral().dark().step_11().into(), // todo!("nate: use a proper style")
-            suggestion_style: orange().dark().step_11().into(), // todo!("nate: use a proper style")
-        }
-    }
-}
-
 impl ThemeColors {
-    pub fn default_light() -> Self {
+    pub fn light() -> Self {
         let system = SystemColors::default();
 
         Self {
@@ -327,7 +81,7 @@ impl ThemeColors {
         }
     }
 
-    pub fn default_dark() -> Self {
+    pub fn dark() -> Self {
         let system = SystemColors::default();
 
         Self {
@@ -470,7 +224,7 @@ pub fn default_color_scales() -> ColorScales {
     }
 }
 
-fn gray() -> ColorScaleSet {
+pub(crate) fn gray() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Gray",
         light: [
@@ -534,7 +288,7 @@ fn gray() -> ColorScaleSet {
     .unwrap()
 }
 
-fn mauve() -> ColorScaleSet {
+pub(crate) fn mauve() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Mauve",
         light: [
@@ -598,7 +352,7 @@ fn mauve() -> ColorScaleSet {
     .unwrap()
 }
 
-fn slate() -> ColorScaleSet {
+pub(crate) fn slate() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Slate",
         light: [
@@ -662,7 +416,7 @@ fn slate() -> ColorScaleSet {
     .unwrap()
 }
 
-fn sage() -> ColorScaleSet {
+pub(crate) fn sage() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Sage",
         light: [
@@ -726,7 +480,7 @@ fn sage() -> ColorScaleSet {
     .unwrap()
 }
 
-fn olive() -> ColorScaleSet {
+pub(crate) fn olive() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Olive",
         light: [
@@ -790,7 +544,7 @@ fn olive() -> ColorScaleSet {
     .unwrap()
 }
 
-fn sand() -> ColorScaleSet {
+pub(crate) fn sand() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Sand",
         light: [
@@ -854,7 +608,7 @@ fn sand() -> ColorScaleSet {
     .unwrap()
 }
 
-fn gold() -> ColorScaleSet {
+pub(crate) fn gold() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Gold",
         light: [
@@ -918,7 +672,7 @@ fn gold() -> ColorScaleSet {
     .unwrap()
 }
 
-fn bronze() -> ColorScaleSet {
+pub(crate) fn bronze() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Bronze",
         light: [
@@ -982,7 +736,7 @@ fn bronze() -> ColorScaleSet {
     .unwrap()
 }
 
-fn brown() -> ColorScaleSet {
+pub(crate) fn brown() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Brown",
         light: [
@@ -1046,7 +800,7 @@ fn brown() -> ColorScaleSet {
     .unwrap()
 }
 
-fn yellow() -> ColorScaleSet {
+pub(crate) fn yellow() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Yellow",
         light: [
@@ -1110,7 +864,7 @@ fn yellow() -> ColorScaleSet {
     .unwrap()
 }
 
-fn amber() -> ColorScaleSet {
+pub(crate) fn amber() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Amber",
         light: [
@@ -1174,7 +928,7 @@ fn amber() -> ColorScaleSet {
     .unwrap()
 }
 
-fn orange() -> ColorScaleSet {
+pub(crate) fn orange() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Orange",
         light: [
@@ -1238,7 +992,7 @@ fn orange() -> ColorScaleSet {
     .unwrap()
 }
 
-fn tomato() -> ColorScaleSet {
+pub(crate) fn tomato() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Tomato",
         light: [
@@ -1302,7 +1056,7 @@ fn tomato() -> ColorScaleSet {
     .unwrap()
 }
 
-fn red() -> ColorScaleSet {
+pub(crate) fn red() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Red",
         light: [
@@ -1366,7 +1120,7 @@ fn red() -> ColorScaleSet {
     .unwrap()
 }
 
-fn ruby() -> ColorScaleSet {
+pub(crate) fn ruby() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Ruby",
         light: [
@@ -1430,7 +1184,7 @@ fn ruby() -> ColorScaleSet {
     .unwrap()
 }
 
-fn crimson() -> ColorScaleSet {
+pub(crate) fn crimson() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Crimson",
         light: [
@@ -1494,7 +1248,7 @@ fn crimson() -> ColorScaleSet {
     .unwrap()
 }
 
-fn pink() -> ColorScaleSet {
+pub(crate) fn pink() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Pink",
         light: [
@@ -1558,7 +1312,7 @@ fn pink() -> ColorScaleSet {
     .unwrap()
 }
 
-fn plum() -> ColorScaleSet {
+pub(crate) fn plum() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Plum",
         light: [
@@ -1622,7 +1376,7 @@ fn plum() -> ColorScaleSet {
     .unwrap()
 }
 
-fn purple() -> ColorScaleSet {
+pub(crate) fn purple() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Purple",
         light: [
@@ -1686,7 +1440,7 @@ fn purple() -> ColorScaleSet {
     .unwrap()
 }
 
-fn violet() -> ColorScaleSet {
+pub(crate) fn violet() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Violet",
         light: [
@@ -1750,7 +1504,7 @@ fn violet() -> ColorScaleSet {
     .unwrap()
 }
 
-fn iris() -> ColorScaleSet {
+pub(crate) fn iris() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Iris",
         light: [
@@ -1814,7 +1568,7 @@ fn iris() -> ColorScaleSet {
     .unwrap()
 }
 
-fn indigo() -> ColorScaleSet {
+pub(crate) fn indigo() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Indigo",
         light: [
@@ -1878,7 +1632,7 @@ fn indigo() -> ColorScaleSet {
     .unwrap()
 }
 
-fn blue() -> ColorScaleSet {
+pub(crate) fn blue() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Blue",
         light: [
@@ -1942,7 +1696,7 @@ fn blue() -> ColorScaleSet {
     .unwrap()
 }
 
-fn cyan() -> ColorScaleSet {
+pub(crate) fn cyan() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Cyan",
         light: [
@@ -2006,7 +1760,7 @@ fn cyan() -> ColorScaleSet {
     .unwrap()
 }
 
-fn teal() -> ColorScaleSet {
+pub(crate) fn teal() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Teal",
         light: [
@@ -2070,7 +1824,7 @@ fn teal() -> ColorScaleSet {
     .unwrap()
 }
 
-fn jade() -> ColorScaleSet {
+pub(crate) fn jade() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Jade",
         light: [
@@ -2134,7 +1888,7 @@ fn jade() -> ColorScaleSet {
     .unwrap()
 }
 
-fn green() -> ColorScaleSet {
+pub(crate) fn green() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Green",
         light: [
@@ -2198,7 +1952,7 @@ fn green() -> ColorScaleSet {
     .unwrap()
 }
 
-fn grass() -> ColorScaleSet {
+pub(crate) fn grass() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Grass",
         light: [
@@ -2262,7 +2016,7 @@ fn grass() -> ColorScaleSet {
     .unwrap()
 }
 
-fn lime() -> ColorScaleSet {
+pub(crate) fn lime() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Lime",
         light: [
@@ -2326,7 +2080,7 @@ fn lime() -> ColorScaleSet {
     .unwrap()
 }
 
-fn mint() -> ColorScaleSet {
+pub(crate) fn mint() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Mint",
         light: [
@@ -2390,7 +2144,7 @@ fn mint() -> ColorScaleSet {
     .unwrap()
 }
 
-fn sky() -> ColorScaleSet {
+pub(crate) fn sky() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Sky",
         light: [
@@ -2454,7 +2208,7 @@ fn sky() -> ColorScaleSet {
     .unwrap()
 }
 
-fn black() -> ColorScaleSet {
+pub(crate) fn black() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "Black",
         light: [
@@ -2518,7 +2272,7 @@ fn black() -> ColorScaleSet {
     .unwrap()
 }
 
-fn white() -> ColorScaleSet {
+pub(crate) fn white() -> ColorScaleSet {
     StaticColorScaleSet {
         scale: "White",
         light: [

crates/theme2/src/default_theme.rs 🔗

@@ -1,58 +1,56 @@
-use std::sync::Arc;
-
 use crate::{
-    colors::{StatusColors, SystemColors, ThemeColors, ThemeStyles},
-    default_color_scales, Appearance, PlayerColors, SyntaxTheme, Theme, ThemeFamily,
+    one_themes::{one_dark, one_family},
+    Theme, ThemeFamily,
 };
 
-fn zed_pro_daylight() -> Theme {
-    Theme {
-        id: "zed_pro_daylight".to_string(),
-        name: "Zed Pro Daylight".into(),
-        appearance: Appearance::Light,
-        styles: ThemeStyles {
-            system: SystemColors::default(),
-            colors: ThemeColors::default_light(),
-            status: StatusColors::default(),
-            player: PlayerColors::default_light(),
-            syntax: Arc::new(SyntaxTheme::default_light()),
-        },
-    }
-}
+// fn zed_pro_daylight() -> Theme {
+//     Theme {
+//         id: "zed_pro_daylight".to_string(),
+//         name: "Zed Pro Daylight".into(),
+//         appearance: Appearance::Light,
+//         styles: ThemeStyles {
+//             system: SystemColors::default(),
+//             colors: ThemeColors::light(),
+//             status: StatusColors::light(),
+//             player: PlayerColors::light(),
+//             syntax: Arc::new(SyntaxTheme::light()),
+//         },
+//     }
+// }
 
-pub(crate) fn zed_pro_moonlight() -> Theme {
-    Theme {
-        id: "zed_pro_moonlight".to_string(),
-        name: "Zed Pro Moonlight".into(),
-        appearance: Appearance::Dark,
-        styles: ThemeStyles {
-            system: SystemColors::default(),
-            colors: ThemeColors::default_dark(),
-            status: StatusColors::default(),
-            player: PlayerColors::default(),
-            syntax: Arc::new(SyntaxTheme::default_dark()),
-        },
-    }
-}
+// pub(crate) fn zed_pro_moonlight() -> Theme {
+//     Theme {
+//         id: "zed_pro_moonlight".to_string(),
+//         name: "Zed Pro Moonlight".into(),
+//         appearance: Appearance::Dark,
+//         styles: ThemeStyles {
+//             system: SystemColors::default(),
+//             colors: ThemeColors::dark(),
+//             status: StatusColors::dark(),
+//             player: PlayerColors::dark(),
+//             syntax: Arc::new(SyntaxTheme::dark()),
+//         },
+//     }
+// }
 
-pub fn zed_pro_family() -> ThemeFamily {
-    ThemeFamily {
-        id: "zed_pro".to_string(),
-        name: "Zed Pro".into(),
-        author: "Zed Team".into(),
-        themes: vec![zed_pro_daylight(), zed_pro_moonlight()],
-        scales: default_color_scales(),
-    }
-}
+// pub fn zed_pro_family() -> ThemeFamily {
+//     ThemeFamily {
+//         id: "zed_pro".to_string(),
+//         name: "Zed Pro".into(),
+//         author: "Zed Team".into(),
+//         themes: vec![zed_pro_daylight(), zed_pro_moonlight()],
+//         scales: default_color_scales(),
+//     }
+// }
 
 impl Default for ThemeFamily {
     fn default() -> Self {
-        zed_pro_family()
+        one_family()
     }
 }
 
 impl Default for Theme {
     fn default() -> Self {
-        zed_pro_daylight()
+        one_dark()
     }
 }

crates/theme2/src/one_themes.rs 🔗

@@ -0,0 +1,199 @@
+use std::sync::Arc;
+
+use gpui::{hsla, FontStyle, FontWeight, HighlightStyle};
+
+use crate::{
+    default_color_scales, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme,
+    ThemeColors, ThemeFamily, ThemeStyles,
+};
+
+pub fn one_family() -> ThemeFamily {
+    ThemeFamily {
+        id: "one".to_string(),
+        name: "One".into(),
+        author: "".into(),
+        themes: vec![one_dark()],
+        scales: default_color_scales(),
+    }
+}
+
+pub(crate) fn one_dark() -> Theme {
+    let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.);
+    let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.);
+    let elevated_surface = hsla(220. / 360., 12. / 100., 18. / 100., 1.);
+
+    let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0);
+    let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0);
+    let green = hsla(95. / 360., 38. / 100., 62. / 100., 1.0);
+    let orange = hsla(29. / 360., 54. / 100., 61. / 100., 1.0);
+    let purple = hsla(286. / 360., 51. / 100., 64. / 100., 1.0);
+    let red = hsla(355. / 360., 65. / 100., 65. / 100., 1.0);
+    let teal = hsla(187. / 360., 47. / 100., 55. / 100., 1.0);
+    let yellow = hsla(39. / 360., 67. / 100., 69. / 100., 1.0);
+
+    Theme {
+        id: "one_dark".to_string(),
+        name: "One Dark".into(),
+        appearance: Appearance::Dark,
+
+        styles: ThemeStyles {
+            system: SystemColors::default(),
+            colors: ThemeColors {
+                border: hsla(225. / 360., 13. / 100., 12. / 100., 1.),
+                border_variant: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
+                border_focused: hsla(223. / 360., 78. / 100., 65. / 100., 1.),
+                border_selected: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0),
+                border_transparent: SystemColors::default().transparent,
+                border_disabled: hsla(222.0 / 360., 11.6 / 100., 33.7 / 100., 1.0),
+                elevated_surface_background: elevated_surface,
+                surface_background: bg,
+                background: bg,
+                element_background: elevated_surface,
+                element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
+                element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
+                element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
+                element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
+                drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0),
+                ghost_element_background: SystemColors::default().transparent,
+                ghost_element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
+                ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
+                ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
+                ghost_element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
+                text: hsla(222.9 / 360., 9.1 / 100., 84.9 / 100., 1.0),
+                text_muted: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0),
+                text_placeholder: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0),
+                text_disabled: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0),
+                text_accent: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0),
+                icon: hsla(222.9 / 360., 9.9 / 100., 86.1 / 100., 1.0),
+                icon_muted: hsla(220.0 / 360., 12.1 / 100., 66.1 / 100., 1.0),
+                icon_disabled: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0),
+                icon_placeholder: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0),
+                icon_accent: blue.into(),
+                status_bar_background: bg,
+                title_bar_background: bg,
+                toolbar_background: editor,
+                tab_bar_background: bg,
+                tab_inactive_background: bg,
+                tab_active_background: editor,
+                editor_background: editor,
+                editor_gutter_background: editor,
+                editor_subheader_background: bg,
+                editor_active_line_background: hsla(222.9 / 360., 13.5 / 100., 20.4 / 100., 1.0),
+                editor_highlighted_line_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.1),
+                editor_line_number: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0),
+                editor_active_line_number: hsla(216.0 / 360., 5.9 / 100., 49.6 / 100., 1.0),
+                editor_invisible: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0),
+                editor_wrap_guide: gpui::black(),
+                editor_active_wrap_guide: gpui::red(),
+                editor_document_highlight_read_background: hsla(
+                    207.8 / 360.,
+                    81. / 100.,
+                    66. / 100.,
+                    0.2,
+                ),
+                editor_document_highlight_write_background: gpui::red(),
+                terminal_background: bg,
+                // todo!("Use one colors for terminal")
+                terminal_ansi_black: crate::black().dark().step_12(),
+                terminal_ansi_red: crate::red().dark().step_11(),
+                terminal_ansi_green: crate::green().dark().step_11(),
+                terminal_ansi_yellow: crate::yellow().dark().step_11(),
+                terminal_ansi_blue: crate::blue().dark().step_11(),
+                terminal_ansi_magenta: crate::violet().dark().step_11(),
+                terminal_ansi_cyan: crate::cyan().dark().step_11(),
+                terminal_ansi_white: crate::neutral().dark().step_12(),
+                terminal_ansi_bright_black: crate::black().dark().step_11(),
+                terminal_ansi_bright_red: crate::red().dark().step_10(),
+                terminal_ansi_bright_green: crate::green().dark().step_10(),
+                terminal_ansi_bright_yellow: crate::yellow().dark().step_10(),
+                terminal_ansi_bright_blue: crate::blue().dark().step_10(),
+                terminal_ansi_bright_magenta: crate::violet().dark().step_10(),
+                terminal_ansi_bright_cyan: crate::cyan().dark().step_10(),
+                terminal_ansi_bright_white: crate::neutral().dark().step_11(),
+            },
+            status: StatusColors {
+                conflict: yellow,
+                created: green,
+                deleted: red,
+                error: red,
+                hidden: gray,
+                hint: blue,
+                ignored: gray,
+                info: blue,
+                modified: yellow,
+                predictive: gray,
+                renamed: blue,
+                success: green,
+                unreachable: gray,
+                warning: yellow,
+            },
+            player: PlayerColors::dark(),
+            syntax: Arc::new(SyntaxTheme {
+                highlights: vec![
+                    ("attribute".into(), purple.into()),
+                    ("boolean".into(), orange.into()),
+                    ("comment".into(), gray.into()),
+                    ("comment.doc".into(), gray.into()),
+                    ("constant".into(), yellow.into()),
+                    ("constructor".into(), blue.into()),
+                    ("embedded".into(), HighlightStyle::default()),
+                    (
+                        "emphasis".into(),
+                        HighlightStyle {
+                            font_style: Some(FontStyle::Italic),
+                            ..HighlightStyle::default()
+                        },
+                    ),
+                    (
+                        "emphasis.strong".into(),
+                        HighlightStyle {
+                            font_weight: Some(FontWeight::BOLD),
+                            ..HighlightStyle::default()
+                        },
+                    ),
+                    ("enum".into(), HighlightStyle::default()),
+                    ("function".into(), blue.into()),
+                    ("function.method".into(), blue.into()),
+                    ("function.definition".into(), blue.into()),
+                    ("hint".into(), blue.into()),
+                    ("keyword".into(), purple.into()),
+                    ("label".into(), HighlightStyle::default()),
+                    ("link_text".into(), blue.into()),
+                    (
+                        "link_uri".into(),
+                        HighlightStyle {
+                            color: Some(teal.into()),
+                            font_style: Some(FontStyle::Italic),
+                            ..HighlightStyle::default()
+                        },
+                    ),
+                    ("number".into(), orange.into()),
+                    ("operator".into(), HighlightStyle::default()),
+                    ("predictive".into(), HighlightStyle::default()),
+                    ("preproc".into(), HighlightStyle::default()),
+                    ("primary".into(), HighlightStyle::default()),
+                    ("property".into(), red.into()),
+                    ("punctuation".into(), HighlightStyle::default()),
+                    ("punctuation.bracket".into(), HighlightStyle::default()),
+                    ("punctuation.delimiter".into(), HighlightStyle::default()),
+                    ("punctuation.list_marker".into(), HighlightStyle::default()),
+                    ("punctuation.special".into(), HighlightStyle::default()),
+                    ("string".into(), green.into()),
+                    ("string.escape".into(), HighlightStyle::default()),
+                    ("string.regex".into(), red.into()),
+                    ("string.special".into(), HighlightStyle::default()),
+                    ("string.special.symbol".into(), HighlightStyle::default()),
+                    ("tag".into(), HighlightStyle::default()),
+                    ("text.literal".into(), HighlightStyle::default()),
+                    ("title".into(), HighlightStyle::default()),
+                    ("type".into(), teal.into()),
+                    ("variable".into(), HighlightStyle::default()),
+                    ("variable.special".into(), red.into()),
+                    ("variant".into(), HighlightStyle::default()),
+                ],
+                inlay_style: HighlightStyle::default(),
+                suggestion_style: HighlightStyle::default(),
+            }),
+        },
+    }
+}

crates/theme2/src/registry.rs 🔗

@@ -6,8 +6,8 @@ use gpui::{HighlightStyle, SharedString};
 use refineable::Refineable;
 
 use crate::{
-    zed_pro_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme,
-    ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily,
+    one_themes::one_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors,
+    Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily,
 };
 
 pub struct ThemeRegistry {
@@ -38,17 +38,17 @@ impl ThemeRegistry {
     fn insert_user_themes(&mut self, themes: impl IntoIterator<Item = UserTheme>) {
         self.insert_themes(themes.into_iter().map(|user_theme| {
             let mut theme_colors = match user_theme.appearance {
-                Appearance::Light => ThemeColors::default_light(),
-                Appearance::Dark => ThemeColors::default_dark(),
+                Appearance::Light => ThemeColors::light(),
+                Appearance::Dark => ThemeColors::dark(),
             };
             theme_colors.refine(&user_theme.styles.colors);
 
-            let mut status_colors = StatusColors::default();
+            let mut status_colors = StatusColors::dark();
             status_colors.refine(&user_theme.styles.status);
 
             let mut syntax_colors = match user_theme.appearance {
-                Appearance::Light => SyntaxTheme::default_light(),
-                Appearance::Dark => SyntaxTheme::default_dark(),
+                Appearance::Light => SyntaxTheme::light(),
+                Appearance::Dark => SyntaxTheme::dark(),
             };
             if let Some(user_syntax) = user_theme.styles.syntax {
                 syntax_colors.highlights = user_syntax
@@ -76,7 +76,10 @@ impl ThemeRegistry {
                     system: SystemColors::default(),
                     colors: theme_colors,
                     status: status_colors,
-                    player: PlayerColors::default(),
+                    player: match user_theme.appearance {
+                        Appearance::Light => PlayerColors::light(),
+                        Appearance::Dark => PlayerColors::dark(),
+                    },
                     syntax: Arc::new(syntax_colors),
                 },
             }
@@ -105,7 +108,7 @@ impl Default for ThemeRegistry {
             themes: HashMap::default(),
         };
 
-        this.insert_theme_families([zed_pro_family()]);
+        this.insert_theme_families([one_family()]);
 
         #[cfg(not(feature = "importing-themes"))]
         this.insert_user_theme_familes(crate::all_user_themes());

crates/theme2/src/settings.rs 🔗

@@ -1,3 +1,4 @@
+use crate::one_themes::one_dark;
 use crate::{Theme, ThemeRegistry};
 use anyhow::Result;
 use gpui::{px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels};
@@ -18,6 +19,7 @@ const MIN_LINE_HEIGHT: f32 = 1.0;
 #[derive(Clone)]
 pub struct ThemeSettings {
     pub ui_font_size: Pixels,
+    pub ui_font: Font,
     pub buffer_font: Font,
     pub buffer_font_size: Pixels,
     pub buffer_line_height: BufferLineHeight,
@@ -119,6 +121,12 @@ impl settings::Settings for ThemeSettings {
 
         let mut this = Self {
             ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(),
+            ui_font: Font {
+                family: "Helvetica".into(),
+                features: Default::default(),
+                weight: Default::default(),
+                style: Default::default(),
+            },
             buffer_font: Font {
                 family: defaults.buffer_font_family.clone().unwrap().into(),
                 features: defaults.buffer_font_features.clone().unwrap(),
@@ -129,7 +137,7 @@ impl settings::Settings for ThemeSettings {
             buffer_line_height: defaults.buffer_line_height.unwrap(),
             active_theme: themes
                 .get(defaults.theme.as_ref().unwrap())
-                .or(themes.get("Zed Pro Moonlight"))
+                .or(themes.get(&one_dark().name))
                 .unwrap(),
         };
 

crates/theme2/src/story.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{div, Component, Div, ParentElement, Styled, ViewContext};
+use gpui::{div, Component, Div, ParentComponent, Styled, ViewContext};
 
 use crate::ActiveTheme;
 

crates/theme2/src/styles.rs 🔗

@@ -0,0 +1,11 @@
+mod colors;
+mod players;
+mod status;
+mod syntax;
+mod system;
+
+pub use colors::*;
+pub use players::*;
+pub use status::*;
+pub use syntax::*;
+pub use system::*;

crates/theme2/src/colors.rs → crates/theme2/src/styles/colors.rs 🔗

@@ -1,31 +1,8 @@
-use crate::{PlayerColors, SyntaxTheme};
 use gpui::Hsla;
 use refineable::Refineable;
 use std::sync::Arc;
 
-#[derive(Clone)]
-pub struct SystemColors {
-    pub transparent: Hsla,
-    pub mac_os_traffic_light_red: Hsla,
-    pub mac_os_traffic_light_yellow: Hsla,
-    pub mac_os_traffic_light_green: Hsla,
-}
-
-#[derive(Refineable, Clone, Debug)]
-#[refineable(Debug, serde::Deserialize)]
-pub struct StatusColors {
-    pub conflict: Hsla,
-    pub created: Hsla,
-    pub deleted: Hsla,
-    pub error: Hsla,
-    pub hidden: Hsla,
-    pub ignored: Hsla,
-    pub info: Hsla,
-    pub modified: Hsla,
-    pub renamed: Hsla,
-    pub success: Hsla,
-    pub warning: Hsla,
-}
+use crate::{PlayerColors, StatusColors, SyntaxTheme, SystemColors};
 
 #[derive(Refineable, Clone, Debug)]
 #[refineable(Debug, serde::Deserialize)]
@@ -259,7 +236,7 @@ mod tests {
 
     #[test]
     fn override_a_single_theme_color() {
-        let mut colors = ThemeColors::default_light();
+        let mut colors = ThemeColors::light();
 
         let magenta: Hsla = gpui::rgb(0xff00ff);
 
@@ -277,7 +254,7 @@ mod tests {
 
     #[test]
     fn override_multiple_theme_colors() {
-        let mut colors = ThemeColors::default_light();
+        let mut colors = ThemeColors::light();
 
         let magenta: Hsla = gpui::rgb(0xff00ff);
         let green: Hsla = gpui::rgb(0x00ff00);

crates/theme2/src/players.rs → crates/theme2/src/styles/players.rs 🔗

@@ -16,6 +16,107 @@ pub struct PlayerColor {
 #[derive(Clone)]
 pub struct PlayerColors(pub Vec<PlayerColor>);
 
+impl Default for PlayerColors {
+    /// Don't use this!
+    /// We have to have a default to be `[refineable::Refinable]`.
+    /// todo!("Find a way to not need this for Refinable")
+    fn default() -> Self {
+        Self::dark()
+    }
+}
+
+impl PlayerColors {
+    pub fn dark() -> Self {
+        Self(vec![
+            PlayerColor {
+                cursor: blue().dark().step_9(),
+                background: blue().dark().step_5(),
+                selection: blue().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: orange().dark().step_9(),
+                background: orange().dark().step_5(),
+                selection: orange().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: pink().dark().step_9(),
+                background: pink().dark().step_5(),
+                selection: pink().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: lime().dark().step_9(),
+                background: lime().dark().step_5(),
+                selection: lime().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: purple().dark().step_9(),
+                background: purple().dark().step_5(),
+                selection: purple().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: amber().dark().step_9(),
+                background: amber().dark().step_5(),
+                selection: amber().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: jade().dark().step_9(),
+                background: jade().dark().step_5(),
+                selection: jade().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: red().dark().step_9(),
+                background: red().dark().step_5(),
+                selection: red().dark().step_3(),
+            },
+        ])
+    }
+
+    pub fn light() -> Self {
+        Self(vec![
+            PlayerColor {
+                cursor: blue().light().step_9(),
+                background: blue().light().step_4(),
+                selection: blue().light().step_3(),
+            },
+            PlayerColor {
+                cursor: orange().light().step_9(),
+                background: orange().light().step_4(),
+                selection: orange().light().step_3(),
+            },
+            PlayerColor {
+                cursor: pink().light().step_9(),
+                background: pink().light().step_4(),
+                selection: pink().light().step_3(),
+            },
+            PlayerColor {
+                cursor: lime().light().step_9(),
+                background: lime().light().step_4(),
+                selection: lime().light().step_3(),
+            },
+            PlayerColor {
+                cursor: purple().light().step_9(),
+                background: purple().light().step_4(),
+                selection: purple().light().step_3(),
+            },
+            PlayerColor {
+                cursor: amber().light().step_9(),
+                background: amber().light().step_4(),
+                selection: amber().light().step_3(),
+            },
+            PlayerColor {
+                cursor: jade().light().step_9(),
+                background: jade().light().step_4(),
+                selection: jade().light().step_3(),
+            },
+            PlayerColor {
+                cursor: red().light().step_9(),
+                background: red().light().step_4(),
+                selection: red().light().step_3(),
+            },
+        ])
+    }
+}
+
 impl PlayerColors {
     pub fn local(&self) -> PlayerColor {
         // todo!("use a valid color");
@@ -36,11 +137,13 @@ impl PlayerColors {
 #[cfg(feature = "stories")]
 pub use stories::*;
 
+use crate::{amber, blue, jade, lime, orange, pink, purple, red};
+
 #[cfg(feature = "stories")]
 mod stories {
     use super::*;
     use crate::{ActiveTheme, Story};
-    use gpui::{div, img, px, Div, ParentElement, Render, Styled, ViewContext};
+    use gpui::{div, img, px, Div, ParentComponent, Render, Styled, ViewContext};
 
     pub struct PlayerStory;
 

crates/theme2/src/styles/status.rs 🔗

@@ -0,0 +1,134 @@
+use gpui::Hsla;
+use refineable::Refineable;
+
+use crate::{blue, grass, neutral, red, yellow};
+
+#[derive(Refineable, Clone, Debug)]
+#[refineable(Debug, serde::Deserialize)]
+pub struct StatusColors {
+    /// Indicates some kind of conflict, like a file changed on disk while it was open, or
+    /// merge conflicts in a Git repository.
+    pub conflict: Hsla,
+
+    /// Indicates something new, like a new file added to a Git repository.
+    pub created: Hsla,
+
+    /// Indicates that something no longer exists, like a deleted file.
+    pub deleted: Hsla,
+
+    /// Indicates a system error, a failed operation or a diagnostic error.
+    pub error: Hsla,
+
+    /// Represents a hidden status, such as a file being hidden in a file tree.
+    pub hidden: Hsla,
+
+    /// Indicates a hint or some kind of additional information.
+    pub hint: Hsla,
+
+    /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git.
+    pub ignored: Hsla,
+
+    /// Represents informational status updates or messages.
+    pub info: Hsla,
+
+    /// Indicates a changed or altered status, like a file that has been edited.
+    pub modified: Hsla,
+
+    /// Indicates something that is predicted, like automatic code completion, or generated code.
+    pub predictive: Hsla,
+
+    /// Represents a renamed status, such as a file that has been renamed.
+    pub renamed: Hsla,
+
+    /// Indicates a successful operation or task completion.
+    pub success: Hsla,
+
+    /// Indicates some kind of unreachable status, like a block of code that can never be reached.
+    pub unreachable: Hsla,
+
+    /// Represents a warning status, like an operation that is about to fail.
+    pub warning: Hsla,
+}
+
+impl Default for StatusColors {
+    /// Don't use this!
+    /// We have to have a default to be `[refineable::Refinable]`.
+    /// todo!("Find a way to not need this for Refinable")
+    fn default() -> Self {
+        Self::dark()
+    }
+}
+
+pub struct DiagnosticColors {
+    pub error: Hsla,
+    pub warning: Hsla,
+    pub info: Hsla,
+}
+
+pub struct GitStatusColors {
+    pub created: Hsla,
+    pub deleted: Hsla,
+    pub modified: Hsla,
+    pub renamed: Hsla,
+    pub conflict: Hsla,
+    pub ignored: Hsla,
+}
+
+impl StatusColors {
+    pub fn dark() -> Self {
+        Self {
+            conflict: red().dark().step_9(),
+            created: grass().dark().step_9(),
+            deleted: red().dark().step_9(),
+            error: red().dark().step_9(),
+            hidden: neutral().dark().step_9(),
+            hint: blue().dark().step_9(),
+            ignored: neutral().dark().step_9(),
+            info: blue().dark().step_9(),
+            modified: yellow().dark().step_9(),
+            predictive: neutral().dark_alpha().step_9(),
+            renamed: blue().dark().step_9(),
+            success: grass().dark().step_9(),
+            unreachable: neutral().dark().step_10(),
+            warning: yellow().dark().step_9(),
+        }
+    }
+
+    pub fn light() -> Self {
+        Self {
+            conflict: red().light().step_9(),
+            created: grass().light().step_9(),
+            deleted: red().light().step_9(),
+            error: red().light().step_9(),
+            hidden: neutral().light().step_9(),
+            hint: blue().light().step_9(),
+            ignored: neutral().light().step_9(),
+            info: blue().light().step_9(),
+            modified: yellow().light().step_9(),
+            predictive: neutral().light_alpha().step_9(),
+            renamed: blue().light().step_9(),
+            success: grass().light().step_9(),
+            unreachable: neutral().light().step_10(),
+            warning: yellow().light().step_9(),
+        }
+    }
+
+    pub fn diagnostic(&self) -> DiagnosticColors {
+        DiagnosticColors {
+            error: self.error,
+            warning: self.warning,
+            info: self.info,
+        }
+    }
+
+    pub fn git(&self) -> GitStatusColors {
+        GitStatusColors {
+            created: self.created,
+            deleted: self.deleted,
+            modified: self.modified,
+            renamed: self.renamed,
+            conflict: self.conflict,
+            ignored: self.ignored,
+        }
+    }
+}

crates/theme2/src/styles/syntax.rs 🔗

@@ -0,0 +1,170 @@
+use gpui::{HighlightStyle, Hsla};
+
+use crate::{
+    blue, cyan, gold, indigo, iris, jade, lime, mint, neutral, orange, plum, purple, red, sky,
+    tomato, yellow,
+};
+
+#[derive(Clone, Default)]
+pub struct SyntaxTheme {
+    pub highlights: Vec<(String, HighlightStyle)>,
+    // todo!("Remove this in favor of StatusColor.hint")
+    // If this should be overridable we should move it to ThemeColors
+    pub inlay_style: HighlightStyle,
+    // todo!("Remove this in favor of StatusColor.prediction")
+    // If this should be overridable we should move it to ThemeColors
+    pub suggestion_style: HighlightStyle,
+}
+
+impl SyntaxTheme {
+    pub fn light() -> Self {
+        Self {
+            highlights: vec![
+                ("attribute".into(), cyan().light().step_11().into()),
+                ("boolean".into(), tomato().light().step_11().into()),
+                ("comment".into(), neutral().light().step_11().into()),
+                ("comment.doc".into(), iris().light().step_12().into()),
+                ("constant".into(), red().light().step_9().into()),
+                ("constructor".into(), red().light().step_9().into()),
+                ("embedded".into(), red().light().step_9().into()),
+                ("emphasis".into(), red().light().step_9().into()),
+                ("emphasis.strong".into(), red().light().step_9().into()),
+                ("enum".into(), red().light().step_9().into()),
+                ("function".into(), red().light().step_9().into()),
+                ("hint".into(), red().light().step_9().into()),
+                ("keyword".into(), orange().light().step_11().into()),
+                ("label".into(), red().light().step_9().into()),
+                ("link_text".into(), red().light().step_9().into()),
+                ("link_uri".into(), red().light().step_9().into()),
+                ("number".into(), red().light().step_9().into()),
+                ("operator".into(), red().light().step_9().into()),
+                ("predictive".into(), red().light().step_9().into()),
+                ("preproc".into(), red().light().step_9().into()),
+                ("primary".into(), red().light().step_9().into()),
+                ("property".into(), red().light().step_9().into()),
+                ("punctuation".into(), neutral().light().step_11().into()),
+                (
+                    "punctuation.bracket".into(),
+                    neutral().light().step_11().into(),
+                ),
+                (
+                    "punctuation.delimiter".into(),
+                    neutral().light().step_11().into(),
+                ),
+                (
+                    "punctuation.list_marker".into(),
+                    blue().light().step_11().into(),
+                ),
+                ("punctuation.special".into(), red().light().step_9().into()),
+                ("string".into(), jade().light().step_11().into()),
+                ("string.escape".into(), red().light().step_9().into()),
+                ("string.regex".into(), tomato().light().step_11().into()),
+                ("string.special".into(), red().light().step_9().into()),
+                (
+                    "string.special.symbol".into(),
+                    red().light().step_9().into(),
+                ),
+                ("tag".into(), red().light().step_9().into()),
+                ("text.literal".into(), red().light().step_9().into()),
+                ("title".into(), red().light().step_9().into()),
+                ("type".into(), red().light().step_9().into()),
+                ("variable".into(), red().light().step_9().into()),
+                ("variable.special".into(), red().light().step_9().into()),
+                ("variant".into(), red().light().step_9().into()),
+            ],
+            inlay_style: tomato().light().step_1().into(), // todo!("nate: use a proper style")
+            suggestion_style: orange().light().step_1().into(), // todo!("nate: use proper style")
+        }
+    }
+
+    pub fn dark() -> Self {
+        Self {
+            highlights: vec![
+                ("attribute".into(), tomato().dark().step_11().into()),
+                ("boolean".into(), tomato().dark().step_11().into()),
+                ("comment".into(), neutral().dark().step_11().into()),
+                ("comment.doc".into(), iris().dark().step_12().into()),
+                ("constant".into(), orange().dark().step_11().into()),
+                ("constructor".into(), gold().dark().step_11().into()),
+                ("embedded".into(), red().dark().step_11().into()),
+                ("emphasis".into(), red().dark().step_11().into()),
+                ("emphasis.strong".into(), red().dark().step_11().into()),
+                ("enum".into(), yellow().dark().step_11().into()),
+                ("function".into(), blue().dark().step_11().into()),
+                ("hint".into(), indigo().dark().step_11().into()),
+                ("keyword".into(), plum().dark().step_11().into()),
+                ("label".into(), red().dark().step_11().into()),
+                ("link_text".into(), red().dark().step_11().into()),
+                ("link_uri".into(), red().dark().step_11().into()),
+                ("number".into(), red().dark().step_11().into()),
+                ("operator".into(), red().dark().step_11().into()),
+                ("predictive".into(), red().dark().step_11().into()),
+                ("preproc".into(), red().dark().step_11().into()),
+                ("primary".into(), red().dark().step_11().into()),
+                ("property".into(), red().dark().step_11().into()),
+                ("punctuation".into(), neutral().dark().step_11().into()),
+                (
+                    "punctuation.bracket".into(),
+                    neutral().dark().step_11().into(),
+                ),
+                (
+                    "punctuation.delimiter".into(),
+                    neutral().dark().step_11().into(),
+                ),
+                (
+                    "punctuation.list_marker".into(),
+                    blue().dark().step_11().into(),
+                ),
+                ("punctuation.special".into(), red().dark().step_11().into()),
+                ("string".into(), lime().dark().step_11().into()),
+                ("string.escape".into(), orange().dark().step_11().into()),
+                ("string.regex".into(), tomato().dark().step_11().into()),
+                ("string.special".into(), red().dark().step_11().into()),
+                (
+                    "string.special.symbol".into(),
+                    red().dark().step_11().into(),
+                ),
+                ("tag".into(), red().dark().step_11().into()),
+                ("text.literal".into(), purple().dark().step_11().into()),
+                ("title".into(), sky().dark().step_11().into()),
+                ("type".into(), mint().dark().step_11().into()),
+                ("variable".into(), red().dark().step_11().into()),
+                ("variable.special".into(), red().dark().step_11().into()),
+                ("variant".into(), red().dark().step_11().into()),
+            ],
+            inlay_style: neutral().dark().step_11().into(), // todo!("nate: use a proper style")
+            suggestion_style: orange().dark().step_11().into(), // todo!("nate: use a proper style")
+        }
+    }
+
+    // TOOD: Get this working with `#[cfg(test)]`. Why isn't it?
+    pub fn new_test(colors: impl IntoIterator<Item = (&'static str, Hsla)>) -> Self {
+        SyntaxTheme {
+            highlights: colors
+                .into_iter()
+                .map(|(key, color)| {
+                    (
+                        key.to_owned(),
+                        HighlightStyle {
+                            color: Some(color),
+                            ..Default::default()
+                        },
+                    )
+                })
+                .collect(),
+            inlay_style: HighlightStyle::default(),
+            suggestion_style: HighlightStyle::default(),
+        }
+    }
+
+    pub fn get(&self, name: &str) -> HighlightStyle {
+        self.highlights
+            .iter()
+            .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None })
+            .unwrap_or_default()
+    }
+
+    pub fn color(&self, name: &str) -> Hsla {
+        self.get(name).color.unwrap_or_default()
+    }
+}

crates/theme2/src/styles/system.rs 🔗

@@ -0,0 +1,20 @@
+use gpui::{hsla, Hsla};
+
+#[derive(Clone)]
+pub struct SystemColors {
+    pub transparent: Hsla,
+    pub mac_os_traffic_light_red: Hsla,
+    pub mac_os_traffic_light_yellow: Hsla,
+    pub mac_os_traffic_light_green: Hsla,
+}
+
+impl Default for SystemColors {
+    fn default() -> Self {
+        Self {
+            transparent: hsla(0.0, 0.0, 0.0, 0.0),
+            mac_os_traffic_light_red: hsla(0.0139, 0.79, 0.65, 1.0),
+            mac_os_traffic_light_yellow: hsla(0.114, 0.88, 0.63, 1.0),
+            mac_os_traffic_light_green: hsla(0.313, 0.49, 0.55, 1.0),
+        }
+    }
+}

crates/theme2/src/syntax.rs 🔗

@@ -1,41 +0,0 @@
-use gpui::{HighlightStyle, Hsla};
-
-#[derive(Clone, Default)]
-pub struct SyntaxTheme {
-    pub highlights: Vec<(String, HighlightStyle)>,
-    pub inlay_style: HighlightStyle,
-    pub suggestion_style: HighlightStyle,
-}
-
-impl SyntaxTheme {
-    // TOOD: Get this working with `#[cfg(test)]`. Why isn't it?
-    pub fn new_test(colors: impl IntoIterator<Item = (&'static str, Hsla)>) -> Self {
-        SyntaxTheme {
-            highlights: colors
-                .into_iter()
-                .map(|(key, color)| {
-                    (
-                        key.to_owned(),
-                        HighlightStyle {
-                            color: Some(color),
-                            ..Default::default()
-                        },
-                    )
-                })
-                .collect(),
-            inlay_style: HighlightStyle::default(),
-            suggestion_style: HighlightStyle::default(),
-        }
-    }
-
-    pub fn get(&self, name: &str) -> HighlightStyle {
-        self.highlights
-            .iter()
-            .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None })
-            .unwrap_or_default()
-    }
-
-    pub fn color(&self, name: &str) -> Hsla {
-        self.get(name).color.unwrap_or_default()
-    }
-}

crates/theme2/src/theme2.rs 🔗

@@ -1,11 +1,10 @@
-mod colors;
 mod default_colors;
 mod default_theme;
-mod players;
+mod one_themes;
 mod registry;
 mod scale;
 mod settings;
-mod syntax;
+mod styles;
 #[cfg(not(feature = "importing-themes"))]
 mod themes;
 mod user_theme;
@@ -13,14 +12,12 @@ mod user_theme;
 use std::sync::Arc;
 
 use ::settings::Settings;
-pub use colors::*;
 pub use default_colors::*;
 pub use default_theme::*;
-pub use players::*;
 pub use registry::*;
 pub use scale::*;
 pub use settings::*;
-pub use syntax::*;
+pub use styles::*;
 #[cfg(not(feature = "importing-themes"))]
 pub use themes::*;
 pub use user_theme::*;

crates/theme2/util/hex_to_hsla.py 🔗

@@ -0,0 +1,35 @@
+import colorsys
+import sys
+
+def hex_to_rgb(hex):
+    hex = hex.lstrip('#')
+    if len(hex) == 8: # 8 digit hex color
+        r, g, b, a = (int(hex[i:i+2], 16) for i in (0, 2, 4, 6))
+        return r, g, b, a / 255.0
+    else: # 6 digit hex color
+        return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4)) + (1.0,)
+
+def rgb_to_hsla(rgb):
+    h, l, s = colorsys.rgb_to_hls(rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0)
+    a = rgb[3] # alpha value
+    return (round(h * 360, 1), round(s * 100, 1), round(l * 100, 1), round(a, 3))
+
+def hex_to_hsla(hex):
+    return rgb_to_hsla(hex_to_rgb(hex))
+
+if len(sys.argv) != 2:
+    print("Usage: python util/hex_to_hsla.py <6 or 8 digit hex color or comma-separated list of colors>")
+else:
+    input_arg = sys.argv[1]
+    if ',' in input_arg: # comma-separated list of colors
+        hex_colors = input_arg.split(',')
+        hslas = [] # output array
+        for hex_color in hex_colors:
+            hex_color = hex_color.strip("'\" ")
+            h, s, l, a = hex_to_hsla(hex_color)
+            hslas.append(f"hsla({h} / 360., {s} / 100., {l} / 100., {a})")
+        print(hslas)
+    else: # single color
+        hex_color = input_arg.strip("'\"")
+        h, s, l, a = hex_to_hsla(hex_color)
+        print(f"hsla({h} / 360., {s} / 100., {l} / 100., {a})")

crates/ui2/src/components/button.rs 🔗

@@ -1,11 +1,9 @@
 use std::sync::Arc;
 
-use gpui::{div, DefiniteLength, Hsla, MouseButton, WindowContext};
+use gpui::{div, DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext};
 
-use crate::{
-    h_stack, prelude::*, Icon, IconButton, IconColor, IconElement, Label, LabelColor,
-    LineHeightStyle,
-};
+use crate::prelude::*;
+use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor};
 
 /// Provides the flexibility to use either a standard
 /// button or an icon button in a given context.
@@ -87,6 +85,7 @@ pub struct Button<V: 'static> {
     label: SharedString,
     variant: ButtonVariant,
     width: Option<DefiniteLength>,
+    color: Option<TextColor>,
 }
 
 impl<V: 'static> Button<V> {
@@ -99,6 +98,7 @@ impl<V: 'static> Button<V> {
             label: label.into(),
             variant: Default::default(),
             width: Default::default(),
+            color: None,
         }
     }
 
@@ -139,34 +139,37 @@ impl<V: 'static> Button<V> {
         self
     }
 
-    fn label_color(&self) -> LabelColor {
-        if self.disabled {
-            LabelColor::Disabled
-        } else {
-            Default::default()
-        }
+    pub fn color(mut self, color: Option<TextColor>) -> Self {
+        self.color = color;
+        self
     }
 
-    fn icon_color(&self) -> IconColor {
+    pub fn label_color(&self, color: Option<TextColor>) -> TextColor {
         if self.disabled {
-            IconColor::Disabled
+            TextColor::Disabled
+        } else if let Some(color) = color {
+            color
         } else {
             Default::default()
         }
     }
 
-    fn render_label(&self) -> Label {
+    fn render_label(&self, color: TextColor) -> Label {
         Label::new(self.label.clone())
-            .color(self.label_color())
+            .color(color)
             .line_height_style(LineHeightStyle::UILabel)
     }
 
-    fn render_icon(&self, icon_color: IconColor) -> Option<IconElement> {
+    fn render_icon(&self, icon_color: TextColor) -> Option<IconElement> {
         self.icon.map(|i| IconElement::new(i).color(icon_color))
     }
 
     pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        let icon_color = self.icon_color();
+        let (icon_color, label_color) = match (self.disabled, self.color) {
+            (true, _) => (TextColor::Disabled, TextColor::Disabled),
+            (_, None) => (TextColor::Default, TextColor::Default),
+            (_, Some(color)) => (TextColor::from(color), color),
+        };
 
         let mut button = h_stack()
             .id(SharedString::from(format!("{}", self.label)))
@@ -182,16 +185,16 @@ impl<V: 'static> Button<V> {
             (Some(_), Some(IconPosition::Left)) => {
                 button = button
                     .gap_1()
-                    .child(self.render_label())
+                    .child(self.render_label(label_color))
                     .children(self.render_icon(icon_color))
             }
             (Some(_), Some(IconPosition::Right)) => {
                 button = button
                     .gap_1()
                     .children(self.render_icon(icon_color))
-                    .child(self.render_label())
+                    .child(self.render_label(label_color))
             }
-            (_, _) => button = button.child(self.render_label()),
+            (_, _) => button = button.child(self.render_label(label_color)),
         }
 
         if let Some(width) = self.width {
@@ -235,7 +238,7 @@ pub use stories::*;
 #[cfg(feature = "stories")]
 mod stories {
     use super::*;
-    use crate::{h_stack, v_stack, LabelColor, Story};
+    use crate::{h_stack, v_stack, Story, TextColor};
     use gpui::{rems, Div, Render};
     use strum::IntoEnumIterator;
 
@@ -260,7 +263,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
@@ -271,7 +274,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")
@@ -285,7 +288,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")
@@ -302,7 +305,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
@@ -313,7 +316,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")
@@ -327,7 +330,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")
@@ -344,7 +347,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")
@@ -358,7 +361,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")
@@ -374,7 +377,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")

crates/ui2/src/components/checkbox.rs 🔗

@@ -1,12 +1,8 @@
+use gpui::{div, prelude::*, Component, ElementId, Styled, ViewContext};
 use std::sync::Arc;
-
-use gpui::{
-    div, Component, ElementId, ParentElement, StatefulInteractive, StatelessInteractive, Styled,
-    ViewContext,
-};
 use theme2::ActiveTheme;
 
-use crate::{Icon, IconColor, IconElement, Selection};
+use crate::{Icon, IconElement, Selection, TextColor};
 
 pub type CheckHandler<V> = Arc<dyn Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync>;
 
@@ -58,9 +54,9 @@ impl<V: 'static> Checkbox<V> {
                         .color(
                             // If the checkbox is disabled we change the color of the icon.
                             if self.disabled {
-                                IconColor::Disabled
+                                TextColor::Disabled
                             } else {
-                                IconColor::Selected
+                                TextColor::Selected
                             },
                         ),
                 )
@@ -73,9 +69,9 @@ impl<V: 'static> Checkbox<V> {
                         .color(
                             // If the checkbox is disabled we change the color of the icon.
                             if self.disabled {
-                                IconColor::Disabled
+                                TextColor::Disabled
                             } else {
-                                IconColor::Selected
+                                TextColor::Selected
                             },
                         ),
                 )

crates/ui2/src/components/elevated_surface.rs 🔗

@@ -23,6 +23,6 @@ pub fn elevated_surface<V: 'static>(level: ElevationIndex, cx: &mut ViewContext<
         .shadow(level.shadow())
 }
 
-pub fn modal<V>(cx: &mut ViewContext<V>) -> Div<V> {
+pub fn modal<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
     elevated_surface(ElevationIndex::ModalSurface, cx)
 }

crates/ui2/src/components/icon.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{rems, svg, Hsla};
+use gpui::{rems, svg};
 use strum::EnumIter;
 
 use crate::prelude::*;
@@ -10,38 +10,6 @@ pub enum IconSize {
     Medium,
 }
 
-#[derive(Default, PartialEq, Copy, Clone)]
-pub enum IconColor {
-    #[default]
-    Default,
-    Muted,
-    Disabled,
-    Placeholder,
-    Accent,
-    Error,
-    Warning,
-    Success,
-    Info,
-    Selected,
-}
-
-impl IconColor {
-    pub fn color(self, cx: &WindowContext) -> Hsla {
-        match self {
-            IconColor::Default => cx.theme().colors().icon,
-            IconColor::Muted => cx.theme().colors().icon_muted,
-            IconColor::Disabled => cx.theme().colors().icon_disabled,
-            IconColor::Placeholder => cx.theme().colors().icon_placeholder,
-            IconColor::Accent => cx.theme().colors().icon_accent,
-            IconColor::Error => cx.theme().status().error,
-            IconColor::Warning => cx.theme().status().warning,
-            IconColor::Success => cx.theme().status().success,
-            IconColor::Info => cx.theme().status().info,
-            IconColor::Selected => cx.theme().colors().icon_accent,
-        }
-    }
-}
-
 #[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
 pub enum Icon {
     Ai,
@@ -165,21 +133,29 @@ impl Icon {
 
 #[derive(Component)]
 pub struct IconElement {
-    icon: Icon,
-    color: IconColor,
+    path: SharedString,
+    color: TextColor,
     size: IconSize,
 }
 
 impl IconElement {
     pub fn new(icon: Icon) -> Self {
         Self {
-            icon,
-            color: IconColor::default(),
+            path: icon.path().into(),
+            color: TextColor::default(),
+            size: IconSize::default(),
+        }
+    }
+
+    pub fn from_path(path: impl Into<SharedString>) -> Self {
+        Self {
+            path: path.into(),
+            color: TextColor::default(),
             size: IconSize::default(),
         }
     }
 
-    pub fn color(mut self, color: IconColor) -> Self {
+    pub fn color(mut self, color: TextColor) -> Self {
         self.color = color;
         self
     }
@@ -198,7 +174,7 @@ impl IconElement {
         svg()
             .size(svg_size)
             .flex_none()
-            .path(self.icon.path())
+            .path(self.path)
             .text_color(self.color.color(cx))
     }
 }

crates/ui2/src/components/icon_button.rs 🔗

@@ -1,10 +1,7 @@
+use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement, TextTooltip};
+use gpui::{prelude::*, MouseButton, VisualContext};
 use std::sync::Arc;
 
-use gpui::{rems, MouseButton};
-
-use crate::{h_stack, prelude::*};
-use crate::{ClickHandler, Icon, IconColor, IconElement};
-
 struct IconButtonHandlers<V: 'static> {
     click: Option<ClickHandler<V>>,
 }
@@ -19,9 +16,10 @@ impl<V: 'static> Default for IconButtonHandlers<V> {
 pub struct IconButton<V: 'static> {
     id: ElementId,
     icon: Icon,
-    color: IconColor,
+    color: TextColor,
     variant: ButtonVariant,
     state: InteractionState,
+    tooltip: Option<SharedString>,
     handlers: IconButtonHandlers<V>,
 }
 
@@ -30,9 +28,10 @@ impl<V: 'static> IconButton<V> {
         Self {
             id: id.into(),
             icon,
-            color: IconColor::default(),
+            color: TextColor::default(),
             variant: ButtonVariant::default(),
             state: InteractionState::default(),
+            tooltip: None,
             handlers: IconButtonHandlers::default(),
         }
     }
@@ -42,7 +41,7 @@ impl<V: 'static> IconButton<V> {
         self
     }
 
-    pub fn color(mut self, color: IconColor) -> Self {
+    pub fn color(mut self, color: TextColor) -> Self {
         self.color = color;
         self
     }
@@ -57,6 +56,11 @@ impl<V: 'static> IconButton<V> {
         self
     }
 
+    pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
+        self.tooltip = Some(tooltip.into());
+        self
+    }
+
     pub fn on_click(
         mut self,
         handler: impl 'static + Fn(&mut V, &mut ViewContext<V>) + Send + Sync,
@@ -67,7 +71,7 @@ impl<V: 'static> IconButton<V> {
 
     fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let icon_color = match (self.state, self.color) {
-            (InteractionState::Disabled, _) => IconColor::Disabled,
+            (InteractionState::Disabled, _) => TextColor::Disabled,
             _ => self.color,
         };
 
@@ -88,9 +92,7 @@ impl<V: 'static> IconButton<V> {
             .id(self.id.clone())
             .justify_center()
             .rounded_md()
-            // todo!("Where do these numbers come from?")
-            .py(rems(0.21875))
-            .px(rems(0.375))
+            .p_1()
             .bg(bg_color)
             .hover(|style| style.bg(bg_hover_color))
             .active(|style| style.bg(bg_active_color))
@@ -103,6 +105,11 @@ impl<V: 'static> IconButton<V> {
             });
         }
 
+        if let Some(tooltip) = self.tooltip.clone() {
+            button =
+                button.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(tooltip.clone())));
+        }
+
         button
     }
 }

crates/ui2/src/components/input.rs 🔗

@@ -1,6 +1,5 @@
-use crate::prelude::*;
-use crate::Label;
-use crate::LabelColor;
+use crate::{prelude::*, Label};
+use gpui::prelude::*;
 
 #[derive(Default, PartialEq)]
 pub enum InputVariant {
@@ -71,15 +70,15 @@ impl Input {
         };
 
         let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
-            LabelColor::Disabled
+            TextColor::Disabled
         } else {
-            LabelColor::Placeholder
+            TextColor::Placeholder
         });
 
         let label = Label::new(self.value.clone()).color(if self.disabled {
-            LabelColor::Disabled
+            TextColor::Disabled
         } else {
-            LabelColor::Default
+            TextColor::Default
         });
 
         div()

crates/ui2/src/components/keybinding.rs 🔗

@@ -1,50 +1,43 @@
-use std::collections::HashSet;
-
-use strum::{EnumIter, IntoEnumIterator};
+use gpui::Action;
+use strum::EnumIter;
 
 use crate::prelude::*;
 
-#[derive(Component)]
-pub struct Keybinding {
+#[derive(Component, Clone)]
+pub struct KeyBinding {
     /// A keybinding consists of a key and a set of modifier keys.
     /// More then one keybinding produces a chord.
     ///
     /// This should always contain at least one element.
-    keybinding: Vec<(String, ModifierKeys)>,
+    key_binding: gpui::KeyBinding,
 }
 
-impl Keybinding {
-    pub fn new(key: String, modifiers: ModifierKeys) -> Self {
-        Self {
-            keybinding: vec![(key, modifiers)],
-        }
+impl KeyBinding {
+    pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<Self> {
+        // todo! this last is arbitrary, we want to prefer users key bindings over defaults,
+        // and vim over normal (in vim mode), etc.
+        let key_binding = cx.bindings_for_action(action).last().cloned()?;
+        Some(Self::new(key_binding))
     }
 
-    pub fn new_chord(
-        first_note: (String, ModifierKeys),
-        second_note: (String, ModifierKeys),
-    ) -> Self {
-        Self {
-            keybinding: vec![first_note, second_note],
-        }
+    pub fn new(key_binding: gpui::KeyBinding) -> Self {
+        Self { key_binding }
     }
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         div()
             .flex()
             .gap_2()
-            .children(self.keybinding.iter().map(|(key, modifiers)| {
+            .children(self.key_binding.keystrokes().iter().map(|keystroke| {
                 div()
                     .flex()
                     .gap_1()
-                    .children(ModifierKey::iter().filter_map(|modifier| {
-                        if modifiers.0.contains(&modifier) {
-                            Some(Key::new(modifier.glyph().to_string()))
-                        } else {
-                            None
-                        }
-                    }))
-                    .child(Key::new(key.clone()))
+                    .when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
+                    .when(keystroke.modifiers.control, |el| el.child(Key::new("^")))
+                    .when(keystroke.modifiers.alt, |el| el.child(Key::new("⌥")))
+                    .when(keystroke.modifiers.command, |el| el.child(Key::new("⌘")))
+                    .when(keystroke.modifiers.shift, |el| el.child(Key::new("⇧")))
+                    .child(Key::new(keystroke.key.clone()))
             }))
     }
 }
@@ -81,76 +74,6 @@ pub enum ModifierKey {
     Shift,
 }
 
-impl ModifierKey {
-    /// Returns the glyph for the [`ModifierKey`].
-    pub fn glyph(&self) -> char {
-        match self {
-            Self::Control => '^',
-            Self::Alt => '⌥',
-            Self::Command => '⌘',
-            Self::Shift => '⇧',
-        }
-    }
-}
-
-#[derive(Clone)]
-pub struct ModifierKeys(HashSet<ModifierKey>);
-
-impl ModifierKeys {
-    pub fn new() -> Self {
-        Self(HashSet::new())
-    }
-
-    pub fn all() -> Self {
-        Self(HashSet::from_iter(ModifierKey::iter()))
-    }
-
-    pub fn add(mut self, modifier: ModifierKey) -> Self {
-        self.0.insert(modifier);
-        self
-    }
-
-    pub fn control(mut self, control: bool) -> Self {
-        if control {
-            self.0.insert(ModifierKey::Control);
-        } else {
-            self.0.remove(&ModifierKey::Control);
-        }
-
-        self
-    }
-
-    pub fn alt(mut self, alt: bool) -> Self {
-        if alt {
-            self.0.insert(ModifierKey::Alt);
-        } else {
-            self.0.remove(&ModifierKey::Alt);
-        }
-
-        self
-    }
-
-    pub fn command(mut self, command: bool) -> Self {
-        if command {
-            self.0.insert(ModifierKey::Command);
-        } else {
-            self.0.remove(&ModifierKey::Command);
-        }
-
-        self
-    }
-
-    pub fn shift(mut self, shift: bool) -> Self {
-        if shift {
-            self.0.insert(ModifierKey::Shift);
-        } else {
-            self.0.remove(&ModifierKey::Shift);
-        }
-
-        self
-    }
-}
-
 #[cfg(feature = "stories")]
 pub use stories::*;
 
@@ -158,29 +81,38 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::Story;
-    use gpui::{Div, Render};
+    use gpui::{action, Div, Render};
     use itertools::Itertools;
 
     pub struct KeybindingStory;
 
+    #[action]
+    struct NoAction {}
+
+    pub fn binding(key: &str) -> gpui::KeyBinding {
+        gpui::KeyBinding::new(key, NoAction {}, None)
+    }
+
     impl Render for KeybindingStory {
         type Element = Div<Self>;
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-            let all_modifier_permutations = ModifierKey::iter().permutations(2);
+            let all_modifier_permutations =
+                ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
 
             Story::container(cx)
-                .child(Story::title_for::<_, Keybinding>(cx))
+                .child(Story::title_for::<_, KeyBinding>(cx))
                 .child(Story::label(cx, "Single Key"))
-                .child(Keybinding::new("Z".to_string(), ModifierKeys::new()))
+                .child(KeyBinding::new(binding("Z")))
                 .child(Story::label(cx, "Single Key with Modifier"))
                 .child(
                     div()
                         .flex()
                         .gap_3()
-                        .children(ModifierKey::iter().map(|modifier| {
-                            Keybinding::new("C".to_string(), ModifierKeys::new().add(modifier))
-                        })),
+                        .child(KeyBinding::new(binding("ctrl-c")))
+                        .child(KeyBinding::new(binding("alt-c")))
+                        .child(KeyBinding::new(binding("cmd-c")))
+                        .child(KeyBinding::new(binding("shift-c"))),
                 )
                 .child(Story::label(cx, "Single Key with Modifier (Permuted)"))
                 .child(
@@ -194,29 +126,18 @@ mod stories {
                                     .gap_4()
                                     .py_3()
                                     .children(chunk.map(|permutation| {
-                                        let mut modifiers = ModifierKeys::new();
-
-                                        for modifier in permutation {
-                                            modifiers = modifiers.add(modifier);
-                                        }
-
-                                        Keybinding::new("X".to_string(), modifiers)
+                                        KeyBinding::new(binding(&*(permutation.join("-") + "-x")))
                                     }))
                             }),
                     ),
                 )
                 .child(Story::label(cx, "Single Key with All Modifiers"))
-                .child(Keybinding::new("Z".to_string(), ModifierKeys::all()))
+                .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
                 .child(Story::label(cx, "Chord"))
-                .child(Keybinding::new_chord(
-                    ("A".to_string(), ModifierKeys::new()),
-                    ("Z".to_string(), ModifierKeys::new()),
-                ))
+                .child(KeyBinding::new(binding("a z")))
                 .child(Story::label(cx, "Chord with Modifier"))
-                .child(Keybinding::new_chord(
-                    ("A".to_string(), ModifierKeys::new().control(true)),
-                    ("Z".to_string(), ModifierKeys::new().shift(true)),
-                ))
+                .child(KeyBinding::new(binding("ctrl-a shift-z")))
+                .child(KeyBinding::new(binding("fn-s")))
         }
     }
 }

crates/ui2/src/components/label.rs 🔗

@@ -1,35 +1,53 @@
-use gpui::{relative, Hsla, WindowContext};
-use smallvec::SmallVec;
+use gpui::{relative, Hsla, Text, TextRun, WindowContext};
 
 use crate::prelude::*;
 use crate::styled_ext::StyledExt;
 
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
+pub enum LabelSize {
+    #[default]
+    Default,
+    Small,
+}
+
 #[derive(Default, PartialEq, Copy, Clone)]
-pub enum LabelColor {
+pub enum TextColor {
     #[default]
     Default,
-    Muted,
+    Accent,
     Created,
-    Modified,
     Deleted,
     Disabled,
+    Error,
     Hidden,
+    Info,
+    Modified,
+    Muted,
     Placeholder,
-    Accent,
+    Player(u32),
+    Selected,
+    Success,
+    Warning,
 }
 
-impl LabelColor {
-    pub fn hsla(&self, cx: &WindowContext) -> Hsla {
+impl TextColor {
+    pub fn color(&self, cx: &WindowContext) -> Hsla {
         match self {
-            Self::Default => cx.theme().colors().text,
-            Self::Muted => cx.theme().colors().text_muted,
-            Self::Created => cx.theme().status().created,
-            Self::Modified => cx.theme().status().modified,
-            Self::Deleted => cx.theme().status().deleted,
-            Self::Disabled => cx.theme().colors().text_disabled,
-            Self::Hidden => cx.theme().status().hidden,
-            Self::Placeholder => cx.theme().colors().text_placeholder,
-            Self::Accent => cx.theme().colors().text_accent,
+            TextColor::Default => cx.theme().colors().text,
+            TextColor::Muted => cx.theme().colors().text_muted,
+            TextColor::Created => cx.theme().status().created,
+            TextColor::Modified => cx.theme().status().modified,
+            TextColor::Deleted => cx.theme().status().deleted,
+            TextColor::Disabled => cx.theme().colors().text_disabled,
+            TextColor::Hidden => cx.theme().status().hidden,
+            TextColor::Info => cx.theme().status().info,
+            TextColor::Placeholder => cx.theme().colors().text_placeholder,
+            TextColor::Accent => cx.theme().colors().text_accent,
+            TextColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor,
+            TextColor::Error => cx.theme().status().error,
+            TextColor::Selected => cx.theme().colors().text_accent,
+            TextColor::Success => cx.theme().status().success,
+            TextColor::Warning => cx.theme().status().warning,
         }
     }
 }
@@ -45,8 +63,9 @@ pub enum LineHeightStyle {
 #[derive(Component)]
 pub struct Label {
     label: SharedString,
+    size: LabelSize,
     line_height_style: LineHeightStyle,
-    color: LabelColor,
+    color: TextColor,
     strikethrough: bool,
 }
 
@@ -54,13 +73,19 @@ impl Label {
     pub fn new(label: impl Into<SharedString>) -> Self {
         Self {
             label: label.into(),
+            size: LabelSize::Default,
             line_height_style: LineHeightStyle::default(),
-            color: LabelColor::Default,
+            color: TextColor::Default,
             strikethrough: false,
         }
     }
 
-    pub fn color(mut self, color: LabelColor) -> Self {
+    pub fn size(mut self, size: LabelSize) -> Self {
+        self.size = size;
+        self
+    }
+
+    pub fn color(mut self, color: TextColor) -> Self {
         self.color = color;
         self
     }
@@ -84,14 +109,17 @@ impl Label {
                         .top_1_2()
                         .w_full()
                         .h_px()
-                        .bg(LabelColor::Hidden.hsla(cx)),
+                        .bg(TextColor::Hidden.color(cx)),
                 )
             })
-            .text_ui()
+            .map(|this| match self.size {
+                LabelSize::Default => this.text_ui(),
+                LabelSize::Small => this.text_ui_sm(),
+            })
             .when(self.line_height_style == LineHeightStyle::UILabel, |this| {
                 this.line_height(relative(1.))
             })
-            .text_color(self.color.hsla(cx))
+            .text_color(self.color.color(cx))
             .child(self.label.clone())
     }
 }
@@ -99,22 +127,31 @@ impl Label {
 #[derive(Component)]
 pub struct HighlightedLabel {
     label: SharedString,
-    color: LabelColor,
+    size: LabelSize,
+    color: TextColor,
     highlight_indices: Vec<usize>,
     strikethrough: bool,
 }
 
 impl HighlightedLabel {
+    /// shows a label with the given characters highlighted.
+    /// characters are identified by utf8 byte position.
     pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
         Self {
             label: label.into(),
-            color: LabelColor::Default,
+            size: LabelSize::Default,
+            color: TextColor::Default,
             highlight_indices,
             strikethrough: false,
         }
     }
 
-    pub fn color(mut self, color: LabelColor) -> Self {
+    pub fn size(mut self, size: LabelSize) -> Self {
+        self.size = size;
+        self
+    }
+
+    pub fn color(mut self, color: TextColor) -> Self {
         self.color = color;
         self
     }
@@ -126,27 +163,26 @@ impl HighlightedLabel {
 
     fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let highlight_color = cx.theme().colors().text_accent;
+        let mut text_style = cx.text_style().clone();
 
         let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
 
-        let mut runs: SmallVec<[Run; 8]> = SmallVec::new();
+        let mut runs: Vec<TextRun> = Vec::new();
 
         for (char_ix, char) in self.label.char_indices() {
-            let mut color = self.color.hsla(cx);
+            let mut color = self.color.color(cx);
 
             if let Some(highlight_ix) = highlight_indices.peek() {
                 if char_ix == *highlight_ix {
                     color = highlight_color;
-
                     highlight_indices.next();
                 }
             }
 
             let last_run = runs.last_mut();
-
             let start_new_run = if let Some(last_run) = last_run {
                 if color == last_run.color {
-                    last_run.text.push(char);
+                    last_run.len += char.len_utf8();
                     false
                 } else {
                     true
@@ -156,10 +192,8 @@ impl HighlightedLabel {
             };
 
             if start_new_run {
-                runs.push(Run {
-                    text: char.to_string(),
-                    color,
-                });
+                text_style.color = color;
+                runs.push(text_style.to_run(char.len_utf8()))
             }
         }
 
@@ -173,13 +207,14 @@ impl HighlightedLabel {
                         .my_auto()
                         .w_full()
                         .h_px()
-                        .bg(LabelColor::Hidden.hsla(cx)),
+                        .bg(TextColor::Hidden.color(cx)),
                 )
             })
-            .children(
-                runs.into_iter()
-                    .map(|run| div().text_color(run.color).child(run.text)),
-            )
+            .map(|this| match self.size {
+                LabelSize::Default => this.text_ui(),
+                LabelSize::Small => this.text_ui_sm(),
+            })
+            .child(Text::styled(self.label, runs))
     }
 }
 
@@ -213,6 +248,10 @@ mod stories {
                     "Hello, world!",
                     vec![0, 1, 2, 7, 8, 12],
                 ))
+                .child(HighlightedLabel::new(
+                    "Héllo, world!",
+                    vec![0, 1, 3, 8, 9, 13],
+                ))
         }
     }
 }

crates/ui2/src/components/list.rs 🔗

@@ -1,11 +1,11 @@
 use gpui::div;
 
+use crate::prelude::*;
 use crate::settings::user_settings;
 use crate::{
-    disclosure_control, h_stack, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label,
-    LabelColor, Toggle,
+    disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label,
+    TextColor, Toggle,
 };
-use crate::{prelude::*, GraphicSlot};
 
 #[derive(Clone, Copy, Default, Debug, PartialEq)]
 pub enum ListItemVariant {
@@ -68,7 +68,7 @@ impl ListHeader {
                     .items_center()
                     .children(icons.into_iter().map(|i| {
                         IconElement::new(i)
-                            .color(IconColor::Muted)
+                            .color(TextColor::Muted)
                             .size(IconSize::Small)
                     })),
             ),
@@ -106,10 +106,10 @@ impl ListHeader {
                                     .items_center()
                                     .children(self.left_icon.map(|i| {
                                         IconElement::new(i)
-                                            .color(IconColor::Muted)
+                                            .color(TextColor::Muted)
                                             .size(IconSize::Small)
                                     }))
-                                    .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
+                                    .child(Label::new(self.label.clone()).color(TextColor::Muted)),
                             )
                             .child(disclosure_control),
                     )
@@ -157,10 +157,10 @@ impl ListSubHeader {
                         .items_center()
                         .children(self.left_icon.map(|i| {
                             IconElement::new(i)
-                                .color(IconColor::Muted)
+                                .color(TextColor::Muted)
                                 .size(IconSize::Small)
                         }))
-                        .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
+                        .child(Label::new(self.label.clone()).color(TextColor::Muted)),
                 ),
         )
     }
@@ -291,7 +291,7 @@ impl ListEntry {
                 h_stack().child(
                     IconElement::new(i)
                         .size(IconSize::Small)
-                        .color(IconColor::Muted),
+                        .color(TextColor::Muted),
                 ),
             ),
             Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
@@ -394,14 +394,14 @@ impl List {
             (false, _) => div().children(self.items),
             (true, Toggle::Toggled(false)) => div(),
             (true, _) => {
-                div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
+                div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
             }
         };
 
         v_stack()
             .w_full()
             .py_1()
-            .children(self.header.map(|header| header))
+            .children(self.header)
             .child(list_content)
     }
 }

crates/ui2/src/components/modal.rs 🔗

@@ -74,7 +74,7 @@ impl<V: 'static> Modal<V> {
     }
 }
 
-impl<V: 'static> ParentElement<V> for Modal<V> {
+impl<V: 'static> ParentComponent<V> for Modal<V> {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
         &mut self.children
     }

crates/ui2/src/components/palette.rs 🔗

@@ -1,5 +1,5 @@
-use crate::prelude::*;
-use crate::{h_stack, v_stack, Keybinding, Label, LabelColor};
+use crate::{h_stack, prelude::*, v_stack, KeyBinding, Label};
+use gpui::prelude::*;
 
 #[derive(Component)]
 pub struct Palette {
@@ -54,7 +54,7 @@ impl Palette {
                 v_stack()
                     .gap_px()
                     .child(v_stack().py_0p5().px_1().child(div().px_2().py_0p5().child(
-                        Label::new(self.input_placeholder.clone()).color(LabelColor::Placeholder),
+                        Label::new(self.input_placeholder.clone()).color(TextColor::Placeholder),
                     )))
                     .child(
                         div()
@@ -75,7 +75,7 @@ impl Palette {
                                     Some(
                                         h_stack().justify_between().px_2().py_1().child(
                                             Label::new(self.empty_string.clone())
-                                                .color(LabelColor::Muted),
+                                                .color(TextColor::Muted),
                                         ),
                                     )
                                 } else {
@@ -108,7 +108,7 @@ impl Palette {
 pub struct PaletteItem {
     pub label: SharedString,
     pub sublabel: Option<SharedString>,
-    pub keybinding: Option<Keybinding>,
+    pub key_binding: Option<KeyBinding>,
 }
 
 impl PaletteItem {
@@ -116,7 +116,7 @@ impl PaletteItem {
         Self {
             label: label.into(),
             sublabel: None,
-            keybinding: None,
+            key_binding: None,
         }
     }
 
@@ -130,11 +130,8 @@ impl PaletteItem {
         self
     }
 
-    pub fn keybinding<K>(mut self, keybinding: K) -> Self
-    where
-        K: Into<Option<Keybinding>>,
-    {
-        self.keybinding = keybinding.into();
+    pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
+        self.key_binding = key_binding.into();
         self
     }
 
@@ -149,7 +146,7 @@ impl PaletteItem {
                     .child(Label::new(self.label.clone()))
                     .children(self.sublabel.clone().map(|sublabel| Label::new(sublabel))),
             )
-            .children(self.keybinding)
+            .children(self.key_binding)
     }
 }
 
@@ -161,7 +158,7 @@ pub use stories::*;
 mod stories {
     use gpui::{Div, Render};
 
-    use crate::{ModifierKeys, Story};
+    use crate::{binding, Story};
 
     use super::*;
 
@@ -181,46 +178,24 @@ mod stories {
                         Palette::new("palette-2")
                             .placeholder("Execute a command...")
                             .items(vec![
-                                PaletteItem::new("theme selector: toggle").keybinding(
-                                    Keybinding::new_chord(
-                                        ("k".to_string(), ModifierKeys::new().command(true)),
-                                        ("t".to_string(), ModifierKeys::new().command(true)),
-                                    ),
-                                ),
-                                PaletteItem::new("assistant: inline assist").keybinding(
-                                    Keybinding::new(
-                                        "enter".to_string(),
-                                        ModifierKeys::new().command(true),
-                                    ),
-                                ),
-                                PaletteItem::new("assistant: quote selection").keybinding(
-                                    Keybinding::new(
-                                        ">".to_string(),
-                                        ModifierKeys::new().command(true),
-                                    ),
-                                ),
-                                PaletteItem::new("assistant: toggle focus").keybinding(
-                                    Keybinding::new(
-                                        "?".to_string(),
-                                        ModifierKeys::new().command(true),
-                                    ),
-                                ),
+                                PaletteItem::new("theme selector: toggle")
+                                    .key_binding(KeyBinding::new(binding("cmd-k cmd-t"))),
+                                PaletteItem::new("assistant: inline assist")
+                                    .key_binding(KeyBinding::new(binding("cmd-enter"))),
+                                PaletteItem::new("assistant: quote selection")
+                                    .key_binding(KeyBinding::new(binding("cmd-<"))),
+                                PaletteItem::new("assistant: toggle focus")
+                                    .key_binding(KeyBinding::new(binding("cmd-?"))),
                                 PaletteItem::new("auto update: check"),
                                 PaletteItem::new("auto update: view release notes"),
-                                PaletteItem::new("branches: open recent").keybinding(
-                                    Keybinding::new(
-                                        "b".to_string(),
-                                        ModifierKeys::new().command(true).alt(true),
-                                    ),
-                                ),
+                                PaletteItem::new("branches: open recent")
+                                    .key_binding(KeyBinding::new(binding("cmd-alt-b"))),
                                 PaletteItem::new("chat panel: toggle focus"),
                                 PaletteItem::new("cli: install"),
                                 PaletteItem::new("client: sign in"),
                                 PaletteItem::new("client: sign out"),
-                                PaletteItem::new("editor: cancel").keybinding(Keybinding::new(
-                                    "escape".to_string(),
-                                    ModifierKeys::new(),
-                                )),
+                                PaletteItem::new("editor: cancel")
+                                    .key_binding(KeyBinding::new(binding("escape"))),
                             ]),
                     )
             }

crates/ui2/src/components/panel.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{AbsoluteLength, AnyElement};
+use gpui::{prelude::*, AbsoluteLength, AnyElement};
 use smallvec::SmallVec;
 
 use crate::prelude::*;
@@ -113,7 +113,7 @@ impl<V: 'static> Panel<V> {
     }
 }
 
-impl<V: 'static> ParentElement<V> for Panel<V> {
+impl<V: 'static> ParentComponent<V> for Panel<V> {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
         &mut self.children
     }
@@ -126,7 +126,7 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::{Label, Story};
-    use gpui::{Div, Render};
+    use gpui::{Div, InteractiveComponent, Render};
 
     pub struct PanelStory;
 

crates/ui2/src/components/tab.rs 🔗

@@ -1,6 +1,6 @@
 use crate::prelude::*;
-use crate::{Icon, IconColor, IconElement, Label, LabelColor};
-use gpui::{red, Div, ElementId, Render, View, VisualContext};
+use crate::{Icon, IconElement, Label, TextColor};
+use gpui::{prelude::*, red, Div, ElementId, Render, View};
 
 #[derive(Component, Clone)]
 pub struct Tab {
@@ -92,20 +92,18 @@ impl Tab {
 
         let label = match (self.git_status, is_deleted) {
             (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
-                .color(LabelColor::Hidden)
+                .color(TextColor::Hidden)
                 .set_strikethrough(true),
             (GitStatus::None, false) => Label::new(self.title.clone()),
-            (GitStatus::Created, false) => {
-                Label::new(self.title.clone()).color(LabelColor::Created)
-            }
+            (GitStatus::Created, false) => Label::new(self.title.clone()).color(TextColor::Created),
             (GitStatus::Modified, false) => {
-                Label::new(self.title.clone()).color(LabelColor::Modified)
+                Label::new(self.title.clone()).color(TextColor::Modified)
             }
-            (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent),
+            (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(TextColor::Accent),
             (GitStatus::Conflict, false) => Label::new(self.title.clone()),
         };
 
-        let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted);
+        let close_icon = || IconElement::new(Icon::Close).color(TextColor::Muted);
 
         let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
             false => (
@@ -148,7 +146,7 @@ impl Tab {
                     .children(has_fs_conflict.then(|| {
                         IconElement::new(Icon::ExclamationTriangle)
                             .size(crate::IconSize::Small)
-                            .color(IconColor::Warning)
+                            .color(TextColor::Warning)
                     }))
                     .children(self.icon.map(IconElement::new))
                     .children(if self.close_side == IconSide::Left {

crates/ui2/src/components/toast.rs 🔗

@@ -1,7 +1,6 @@
-use gpui::AnyElement;
-use smallvec::SmallVec;
-
 use crate::prelude::*;
+use gpui::{prelude::*, AnyElement};
+use smallvec::SmallVec;
 
 #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
 pub enum ToastOrigin {
@@ -59,7 +58,7 @@ impl<V: 'static> Toast<V> {
     }
 }
 
-impl<V: 'static> ParentElement<V> for Toast<V> {
+impl<V: 'static> ParentComponent<V> for Toast<V> {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
         &mut self.children
     }

crates/ui2/src/components/toggle.rs 🔗

@@ -1,6 +1,6 @@
-use gpui::{div, Component, ParentElement};
+use gpui::{div, Component, ParentComponent};
 
-use crate::{Icon, IconColor, IconElement, IconSize};
+use crate::{Icon, IconElement, IconSize, TextColor};
 
 /// Whether the entry is toggleable, and if so, whether it is currently toggled.
 ///
@@ -49,12 +49,12 @@ pub fn disclosure_control<V: 'static>(toggle: Toggle) -> impl Component<V> {
         (false, _) => div(),
         (_, true) => div().child(
             IconElement::new(Icon::ChevronDown)
-                .color(IconColor::Muted)
+                .color(TextColor::Muted)
                 .size(IconSize::Small),
         ),
         (_, false) => div().child(
             IconElement::new(Icon::ChevronRight)
-                .color(IconColor::Muted)
+                .color(TextColor::Muted)
                 .size(IconSize::Small),
         ),
     }

crates/ui2/src/components/tooltip.rs 🔗

@@ -1,14 +1,33 @@
-use gpui::{div, Div, ParentElement, Render, SharedString, Styled, ViewContext};
-use theme2::ActiveTheme;
+use gpui::{Div, Render};
+use settings2::Settings;
+use theme2::{ActiveTheme, ThemeSettings};
+
+use crate::prelude::*;
+use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
 
-#[derive(Clone, Debug)]
 pub struct TextTooltip {
     title: SharedString,
+    meta: Option<SharedString>,
+    key_binding: Option<KeyBinding>,
 }
 
 impl TextTooltip {
-    pub fn new(str: SharedString) -> Self {
-        Self { title: str }
+    pub fn new(title: impl Into<SharedString>) -> Self {
+        Self {
+            title: title.into(),
+            meta: None,
+            key_binding: None,
+        }
+    }
+
+    pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
+        self.meta = Some(meta.into());
+        self
+    }
+
+    pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
+        self.key_binding = key_binding.into();
+        self
     }
 }
 
@@ -16,16 +35,27 @@ impl Render for TextTooltip {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        let theme = cx.theme();
-        div()
-            .bg(theme.colors().background)
-            .rounded_lg()
-            .border()
-            .font("Zed Sans")
-            .border_color(theme.colors().border)
-            .text_color(theme.colors().text)
-            .pl_2()
-            .pr_2()
-            .child(self.title.clone())
+        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
+        v_stack()
+            .elevation_2(cx)
+            .font(ui_font)
+            .text_ui_sm()
+            .text_color(cx.theme().colors().text)
+            .py_1()
+            .px_2()
+            .child(
+                h_stack()
+                    .child(self.title.clone())
+                    .when_some(self.key_binding.clone(), |this, key_binding| {
+                        this.justify_between().child(key_binding)
+                    }),
+            )
+            .when_some(self.meta.clone(), |this, meta| {
+                this.child(
+                    Label::new(meta)
+                        .size(LabelSize::Small)
+                        .color(TextColor::Muted),
+                )
+            })
     }
 }

crates/ui2/src/prelude.rs 🔗

@@ -1,13 +1,13 @@
 use gpui::rems;
 use gpui::Rems;
 pub use gpui::{
-    div, Component, Element, ElementId, ParentElement, SharedString, StatefulInteractive,
-    StatelessInteractive, Styled, ViewContext, WindowContext,
+    div, Component, Element, ElementId, InteractiveComponent, ParentComponent, SharedString,
+    Styled, ViewContext, WindowContext,
 };
 
 pub use crate::elevation::*;
-pub use crate::ButtonVariant;
 pub use crate::StyledExt;
+pub use crate::{ButtonVariant, TextColor};
 pub use theme2::ActiveTheme;
 
 use gpui::Hsla;

crates/ui2/src/static_data.rs 🔗

@@ -7,12 +7,12 @@ use gpui::{AppContext, ViewContext};
 use rand::Rng;
 use theme2::ActiveTheme;
 
-use crate::HighlightedText;
+use crate::{binding, HighlightedText};
 use crate::{
     Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
-    HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream,
-    MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus,
-    PlayerWithCallStatus, PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus,
+    HighlightedLine, Icon, KeyBinding, Label, ListEntry, ListEntrySize, Livestream, MicStatus,
+    Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, PublicPlayer,
+    ScreenShareStatus, Symbol, Tab, TextColor, Toggle, VideoStatus,
 };
 use crate::{ListItem, NotificationAction};
 
@@ -490,20 +490,20 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
         ListEntry::new(Label::new(".config"))
             .left_icon(Icon::Folder.into())
             .indent_level(1),
-        ListEntry::new(Label::new(".git").color(LabelColor::Hidden))
+        ListEntry::new(Label::new(".git").color(TextColor::Hidden))
             .left_icon(Icon::Folder.into())
             .indent_level(1),
         ListEntry::new(Label::new(".cargo"))
             .left_icon(Icon::Folder.into())
             .indent_level(1),
-        ListEntry::new(Label::new(".idea").color(LabelColor::Hidden))
+        ListEntry::new(Label::new(".idea").color(TextColor::Hidden))
             .left_icon(Icon::Folder.into())
             .indent_level(1),
         ListEntry::new(Label::new("assets"))
             .left_icon(Icon::Folder.into())
             .indent_level(1)
             .toggle(Toggle::Toggled(true)),
-        ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden))
+        ListEntry::new(Label::new("cargo-target").color(TextColor::Hidden))
             .left_icon(Icon::Folder.into())
             .indent_level(1),
         ListEntry::new(Label::new("crates"))
@@ -528,7 +528,7 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
         ListEntry::new(Label::new("call"))
             .left_icon(Icon::Folder.into())
             .indent_level(2),
-        ListEntry::new(Label::new("sqlez").color(LabelColor::Modified))
+        ListEntry::new(Label::new("sqlez").color(TextColor::Modified))
             .left_icon(Icon::Folder.into())
             .indent_level(2)
             .toggle(Toggle::Toggled(false)),
@@ -543,45 +543,45 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
         ListEntry::new(Label::new("derive_element.rs"))
             .left_icon(Icon::FileRust.into())
             .indent_level(4),
-        ListEntry::new(Label::new("storybook").color(LabelColor::Modified))
+        ListEntry::new(Label::new("storybook").color(TextColor::Modified))
             .left_icon(Icon::FolderOpen.into())
             .indent_level(1)
             .toggle(Toggle::Toggled(true)),
-        ListEntry::new(Label::new("docs").color(LabelColor::Default))
+        ListEntry::new(Label::new("docs").color(TextColor::Default))
             .left_icon(Icon::Folder.into())
             .indent_level(2)
             .toggle(Toggle::Toggled(true)),
-        ListEntry::new(Label::new("src").color(LabelColor::Modified))
+        ListEntry::new(Label::new("src").color(TextColor::Modified))
             .left_icon(Icon::FolderOpen.into())
             .indent_level(3)
             .toggle(Toggle::Toggled(true)),
-        ListEntry::new(Label::new("ui").color(LabelColor::Modified))
+        ListEntry::new(Label::new("ui").color(TextColor::Modified))
             .left_icon(Icon::FolderOpen.into())
             .indent_level(4)
             .toggle(Toggle::Toggled(true)),
-        ListEntry::new(Label::new("component").color(LabelColor::Created))
+        ListEntry::new(Label::new("component").color(TextColor::Created))
             .left_icon(Icon::FolderOpen.into())
             .indent_level(5)
             .toggle(Toggle::Toggled(true)),
-        ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default))
+        ListEntry::new(Label::new("facepile.rs").color(TextColor::Default))
             .left_icon(Icon::FileRust.into())
             .indent_level(6),
-        ListEntry::new(Label::new("follow_group.rs").color(LabelColor::Default))
+        ListEntry::new(Label::new("follow_group.rs").color(TextColor::Default))
             .left_icon(Icon::FileRust.into())
             .indent_level(6),
-        ListEntry::new(Label::new("list_item.rs").color(LabelColor::Created))
+        ListEntry::new(Label::new("list_item.rs").color(TextColor::Created))
             .left_icon(Icon::FileRust.into())
             .indent_level(6),
-        ListEntry::new(Label::new("tab.rs").color(LabelColor::Default))
+        ListEntry::new(Label::new("tab.rs").color(TextColor::Default))
             .left_icon(Icon::FileRust.into())
             .indent_level(6),
-        ListEntry::new(Label::new("target").color(LabelColor::Hidden))
+        ListEntry::new(Label::new("target").color(TextColor::Hidden))
             .left_icon(Icon::Folder.into())
             .indent_level(1),
         ListEntry::new(Label::new(".dockerignore"))
             .left_icon(Icon::FileGeneric.into())
             .indent_level(1),
-        ListEntry::new(Label::new(".DS_Store").color(LabelColor::Hidden))
+        ListEntry::new(Label::new(".DS_Store").color(TextColor::Hidden))
             .left_icon(Icon::FileGeneric.into())
             .indent_level(1),
         ListEntry::new(Label::new("Cargo.lock"))
@@ -701,46 +701,16 @@ pub fn static_collab_panel_channels() -> Vec<ListItem> {
 
 pub fn example_editor_actions() -> Vec<PaletteItem> {
     vec![
-        PaletteItem::new("New File").keybinding(Keybinding::new(
-            "N".to_string(),
-            ModifierKeys::new().command(true),
-        )),
-        PaletteItem::new("Open File").keybinding(Keybinding::new(
-            "O".to_string(),
-            ModifierKeys::new().command(true),
-        )),
-        PaletteItem::new("Save File").keybinding(Keybinding::new(
-            "S".to_string(),
-            ModifierKeys::new().command(true),
-        )),
-        PaletteItem::new("Cut").keybinding(Keybinding::new(
-            "X".to_string(),
-            ModifierKeys::new().command(true),
-        )),
-        PaletteItem::new("Copy").keybinding(Keybinding::new(
-            "C".to_string(),
-            ModifierKeys::new().command(true),
-        )),
-        PaletteItem::new("Paste").keybinding(Keybinding::new(
-            "V".to_string(),
-            ModifierKeys::new().command(true),
-        )),
-        PaletteItem::new("Undo").keybinding(Keybinding::new(
-            "Z".to_string(),
-            ModifierKeys::new().command(true),
-        )),
-        PaletteItem::new("Redo").keybinding(Keybinding::new(
-            "Z".to_string(),
-            ModifierKeys::new().command(true).shift(true),
-        )),
-        PaletteItem::new("Find").keybinding(Keybinding::new(
-            "F".to_string(),
-            ModifierKeys::new().command(true),
-        )),
-        PaletteItem::new("Replace").keybinding(Keybinding::new(
-            "R".to_string(),
-            ModifierKeys::new().command(true),
-        )),
+        PaletteItem::new("New File").key_binding(KeyBinding::new(binding("cmd-n"))),
+        PaletteItem::new("Open File").key_binding(KeyBinding::new(binding("cmd-o"))),
+        PaletteItem::new("Save File").key_binding(KeyBinding::new(binding("cmd-s"))),
+        PaletteItem::new("Cut").key_binding(KeyBinding::new(binding("cmd-x"))),
+        PaletteItem::new("Copy").key_binding(KeyBinding::new(binding("cmd-c"))),
+        PaletteItem::new("Paste").key_binding(KeyBinding::new(binding("cmd-v"))),
+        PaletteItem::new("Undo").key_binding(KeyBinding::new(binding("cmd-z"))),
+        PaletteItem::new("Redo").key_binding(KeyBinding::new(binding("cmd-shift-z"))),
+        PaletteItem::new("Find").key_binding(KeyBinding::new(binding("cmd-f"))),
+        PaletteItem::new("Replace").key_binding(KeyBinding::new(binding("cmd-r"))),
         PaletteItem::new("Jump to Line"),
         PaletteItem::new("Select All"),
         PaletteItem::new("Deselect All"),

crates/ui2/src/styled_ext.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{Div, ElementFocus, ElementInteractivity, Styled, UniformList, ViewContext};
+use gpui::{Styled, ViewContext};
 use theme2::ActiveTheme;
 
 use crate::{ElevationIndex, UITextSize};
@@ -12,31 +12,22 @@ fn elevated<E: Styled, V: 'static>(this: E, cx: &mut ViewContext<V>, index: Elev
 }
 
 /// Extends [`Styled`](gpui::Styled) with Zed specific styling methods.
-pub trait StyledExt: Styled {
+pub trait StyledExt: Styled + Sized {
     /// Horizontally stacks elements.
     ///
     /// Sets `flex()`, `flex_row()`, `items_center()`
-    fn h_flex(self) -> Self
-    where
-        Self: Sized,
-    {
+    fn h_flex(self) -> Self {
         self.flex().flex_row().items_center()
     }
 
     /// Vertically stacks elements.
     ///
     /// Sets `flex()`, `flex_col()`
-    fn v_flex(self) -> Self
-    where
-        Self: Sized,
-    {
+    fn v_flex(self) -> Self {
         self.flex().flex_col()
     }
 
-    fn text_ui_size(self, size: UITextSize) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_ui_size(self, size: UITextSize) -> Self {
         let size = size.rems();
 
         self.text_size(size)
@@ -49,10 +40,7 @@ pub trait StyledExt: Styled {
     /// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
     ///
     /// Use [`text_ui_sm`] for regular-sized text.
-    fn text_ui(self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_ui(self) -> Self {
         let size = UITextSize::default().rems();
 
         self.text_size(size)
@@ -65,10 +53,7 @@ pub trait StyledExt: Styled {
     /// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
     ///
     /// Use [`text_ui`] for regular-sized text.
-    fn text_ui_sm(self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_ui_sm(self) -> Self {
         let size = UITextSize::Small.rems();
 
         self.text_size(size)
@@ -79,10 +64,7 @@ pub trait StyledExt: Styled {
     /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
     ///
     /// Example Elements: Title Bar, Panel, Tab Bar, Editor
-    fn elevation_1<V: 'static>(self, cx: &mut ViewContext<V>) -> Self
-    where
-        Self: Styled + Sized,
-    {
+    fn elevation_1<V: 'static>(self, cx: &mut ViewContext<V>) -> Self {
         elevated(self, cx, ElevationIndex::Surface)
     }
 
@@ -91,10 +73,7 @@ pub trait StyledExt: Styled {
     /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
     ///
     /// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels
-    fn elevation_2<V: 'static>(self, cx: &mut ViewContext<V>) -> Self
-    where
-        Self: Styled + Sized,
-    {
+    fn elevation_2<V: 'static>(self, cx: &mut ViewContext<V>) -> Self {
         elevated(self, cx, ElevationIndex::ElevatedSurface)
     }
 
@@ -109,19 +88,9 @@ pub trait StyledExt: Styled {
     /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
     ///
     /// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs
-    fn elevation_4<V: 'static>(self, cx: &mut ViewContext<V>) -> Self
-    where
-        Self: Styled + Sized,
-    {
+    fn elevation_4<V: 'static>(self, cx: &mut ViewContext<V>) -> Self {
         elevated(self, cx, ElevationIndex::ModalSurface)
     }
 }
 
-impl<V, I, F> StyledExt for Div<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: ElementFocus<V>,
-{
-}
-
-impl<V> StyledExt for UniformList<V> {}
+impl<E: Styled> StyledExt for E {}

crates/ui2/src/to_extract/assistant_panel.rs 🔗

@@ -1,6 +1,6 @@
 use crate::prelude::*;
 use crate::{Icon, IconButton, Label, Panel, PanelSide};
-use gpui::{rems, AbsoluteLength};
+use gpui::{prelude::*, rems, AbsoluteLength};
 
 #[derive(Component)]
 pub struct AssistantPanel {

crates/ui2/src/to_extract/breadcrumb.rs 🔗

@@ -1,9 +1,7 @@
+use crate::{h_stack, prelude::*, HighlightedText};
+use gpui::{prelude::*, Div};
 use std::path::PathBuf;
 
-use crate::prelude::*;
-use crate::{h_stack, HighlightedText};
-use gpui::Div;
-
 #[derive(Clone)]
 pub struct Symbol(pub Vec<HighlightedText>);
 

crates/ui2/src/to_extract/buffer_search.rs 🔗

@@ -1,7 +1,7 @@
 use gpui::{Div, Render, View, VisualContext};
 
 use crate::prelude::*;
-use crate::{h_stack, Icon, IconButton, IconColor, Input};
+use crate::{h_stack, Icon, IconButton, Input, TextColor};
 
 #[derive(Clone)]
 pub struct BufferSearch {
@@ -36,7 +36,7 @@ impl Render for BufferSearch {
             .child(
                 h_stack().child(Input::new("Search")).child(
                     IconButton::<Self>::new("replace", Icon::Replace)
-                        .when(self.is_replace_open, |this| this.color(IconColor::Accent))
+                        .when(self.is_replace_open, |this| this.color(TextColor::Accent))
                         .on_click(|buffer_search, cx| {
                             buffer_search.toggle_replace(cx);
                         }),

crates/ui2/src/to_extract/chat_panel.rs 🔗

@@ -1,7 +1,6 @@
+use crate::{prelude::*, Icon, IconButton, Input, Label};
 use chrono::NaiveDateTime;
-
-use crate::prelude::*;
-use crate::{Icon, IconButton, Input, Label, LabelColor};
+use gpui::prelude::*;
 
 #[derive(Component)]
 pub struct ChatPanel {
@@ -95,7 +94,7 @@ impl ChatMessage {
                     .child(Label::new(self.author.clone()))
                     .child(
                         Label::new(self.sent_at.format("%m/%d/%Y").to_string())
-                            .color(LabelColor::Muted),
+                            .color(TextColor::Muted),
                     ),
             )
             .child(div().child(Label::new(self.text.clone())))

crates/ui2/src/to_extract/collab_panel.rs 🔗

@@ -1,7 +1,8 @@
-use crate::{prelude::*, Toggle};
 use crate::{
-    static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List, ListHeader,
+    prelude::*, static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon,
+    List, ListHeader, Toggle,
 };
+use gpui::prelude::*;
 
 #[derive(Component)]
 pub struct CollabPanel {

crates/ui2/src/to_extract/copilot.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{prelude::*, Button, Label, LabelColor, Modal};
+use crate::{prelude::*, Button, Label, Modal, TextColor};
 
 #[derive(Component)]
 pub struct CopilotModal {
@@ -14,7 +14,7 @@ impl CopilotModal {
         div().id(self.id.clone()).child(
             Modal::new("some-id")
                 .title("Connect Copilot to Zed")
-                .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(LabelColor::Muted))
+                .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(TextColor::Muted))
                 .primary_action(Button::new("Connect to Github").variant(ButtonVariant::Filled)),
         )
     }

crates/ui2/src/to_extract/editor_pane.rs 🔗

@@ -5,7 +5,7 @@ use gpui::{Div, Render, View, VisualContext};
 use crate::prelude::*;
 use crate::{
     hello_world_rust_editor_with_status_example, v_stack, Breadcrumb, Buffer, BufferSearch, Icon,
-    IconButton, IconColor, Symbol, Tab, TabBar, Toolbar,
+    IconButton, Symbol, Tab, TabBar, TextColor, Toolbar,
 };
 
 #[derive(Clone)]
@@ -60,12 +60,12 @@ impl Render for EditorPane {
                 Toolbar::new()
                     .left_item(Breadcrumb::new(self.path.clone(), self.symbols.clone()))
                     .right_items(vec![
-                        IconButton::new("toggle_inlay_hints", Icon::InlayHint),
+                        IconButton::<Self>::new("toggle_inlay_hints", Icon::InlayHint),
                         IconButton::<Self>::new("buffer_search", Icon::MagnifyingGlass)
                             .when(self.is_buffer_search_open, |this| {
-                                this.color(IconColor::Accent)
+                                this.color(TextColor::Accent)
                             })
-                            .on_click(|editor, cx| {
+                            .on_click(|editor: &mut Self, cx| {
                                 editor.toggle_buffer_search(cx);
                             }),
                         IconButton::new("inline_assist", Icon::MagicWand),

crates/ui2/src/to_extract/notifications_panel.rs 🔗

@@ -1,10 +1,9 @@
-use crate::utils::naive_format_distance_from_now;
 use crate::{
-    h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, ButtonOrIconButton,
-    Icon, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator,
-    PublicPlayer, UnreadIndicator,
+    h_stack, prelude::*, static_new_notification_items_2, utils::naive_format_distance_from_now,
+    v_stack, Avatar, ButtonOrIconButton, ClickHandler, Icon, IconElement, Label, LineHeightStyle,
+    ListHeader, ListHeaderMeta, ListSeparator, PublicPlayer, TextColor, UnreadIndicator,
 };
-use crate::{ClickHandler, ListHeader};
+use gpui::prelude::*;
 
 #[derive(Component)]
 pub struct NotificationsPanel {
@@ -48,7 +47,7 @@ impl NotificationsPanel {
                             .border_color(cx.theme().colors().border_variant)
                             .child(
                                 Label::new("Search...")
-                                    .color(LabelColor::Placeholder)
+                                    .color(TextColor::Placeholder)
                                     .line_height_style(LineHeightStyle::UILabel),
                             ),
                     )
@@ -252,7 +251,7 @@ impl<V> Notification<V> {
                         if let Some(icon) = icon {
                             meta_el = meta_el.child(IconElement::new(icon.clone()));
                         }
-                        meta_el.child(Label::new(text.clone()).color(LabelColor::Muted))
+                        meta_el.child(Label::new(text.clone()).color(TextColor::Muted))
                     })
                     .collect::<Vec<_>>(),
             )
@@ -311,7 +310,7 @@ impl<V> Notification<V> {
                                             true,
                                             true,
                                         ))
-                                        .color(LabelColor::Muted),
+                                        .color(TextColor::Muted),
                                     )
                                     .child(self.render_meta_items(cx)),
                             )
@@ -321,11 +320,11 @@ impl<V> Notification<V> {
                                 // Show the taken_message
                                 (Some(_), Some(action_taken)) => h_stack()
                                     .children(action_taken.taken_message.0.map(|icon| {
-                                        IconElement::new(icon).color(crate::IconColor::Muted)
+                                        IconElement::new(icon).color(crate::TextColor::Muted)
                                     }))
                                     .child(
                                         Label::new(action_taken.taken_message.1.clone())
-                                            .color(LabelColor::Muted),
+                                            .color(TextColor::Muted),
                                     ),
                                 // Show the actions
                                 (Some(actions), None) => {

crates/ui2/src/to_extract/panes.rs 🔗

@@ -59,7 +59,7 @@ impl<V: 'static> Pane<V> {
     }
 }
 
-impl<V: 'static> ParentElement<V> for Pane<V> {
+impl<V: 'static> ParentComponent<V> for Pane<V> {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
         &mut self.children
     }

crates/ui2/src/to_extract/project_panel.rs 🔗

@@ -1,7 +1,8 @@
-use crate::prelude::*;
 use crate::{
-    static_project_panel_project_items, static_project_panel_single_items, Input, List, ListHeader,
+    prelude::*, static_project_panel_project_items, static_project_panel_single_items, Input, List,
+    ListHeader,
 };
+use gpui::prelude::*;
 
 #[derive(Component)]
 pub struct ProjectPanel {

crates/ui2/src/to_extract/status_bar.rs 🔗

@@ -1,7 +1,7 @@
 use std::sync::Arc;
 
 use crate::prelude::*;
-use crate::{Button, Icon, IconButton, IconColor, ToolDivider, Workspace};
+use crate::{Button, Icon, IconButton, TextColor, ToolDivider, Workspace};
 
 #[derive(Default, PartialEq)]
 pub enum Tool {
@@ -110,18 +110,18 @@ impl StatusBar {
             .child(
                 IconButton::<Workspace>::new("project_panel", Icon::FileTree)
                     .when(workspace.is_project_panel_open(), |this| {
-                        this.color(IconColor::Accent)
+                        this.color(TextColor::Accent)
                     })
-                    .on_click(|workspace, cx| {
+                    .on_click(|workspace: &mut Workspace, cx| {
                         workspace.toggle_project_panel(cx);
                     }),
             )
             .child(
                 IconButton::<Workspace>::new("collab_panel", Icon::Hash)
                     .when(workspace.is_collab_panel_open(), |this| {
-                        this.color(IconColor::Accent)
+                        this.color(TextColor::Accent)
                     })
-                    .on_click(|workspace, cx| {
+                    .on_click(|workspace: &mut Workspace, cx| {
                         workspace.toggle_collab_panel();
                     }),
             )
@@ -174,27 +174,27 @@ impl StatusBar {
                     .child(
                         IconButton::<Workspace>::new("terminal", Icon::Terminal)
                             .when(workspace.is_terminal_open(), |this| {
-                                this.color(IconColor::Accent)
+                                this.color(TextColor::Accent)
                             })
-                            .on_click(|workspace, cx| {
+                            .on_click(|workspace: &mut Workspace, cx| {
                                 workspace.toggle_terminal(cx);
                             }),
                     )
                     .child(
                         IconButton::<Workspace>::new("chat_panel", Icon::MessageBubbles)
                             .when(workspace.is_chat_panel_open(), |this| {
-                                this.color(IconColor::Accent)
+                                this.color(TextColor::Accent)
                             })
-                            .on_click(|workspace, cx| {
+                            .on_click(|workspace: &mut Workspace, cx| {
                                 workspace.toggle_chat_panel(cx);
                             }),
                     )
                     .child(
                         IconButton::<Workspace>::new("assistant_panel", Icon::Ai)
                             .when(workspace.is_assistant_panel_open(), |this| {
-                                this.color(IconColor::Accent)
+                                this.color(TextColor::Accent)
                             })
-                            .on_click(|workspace, cx| {
+                            .on_click(|workspace: &mut Workspace, cx| {
                                 workspace.toggle_assistant_panel(cx);
                             }),
                     ),

crates/ui2/src/to_extract/tab_bar.rs 🔗

@@ -1,5 +1,5 @@
-use crate::prelude::*;
-use crate::{Icon, IconButton, Tab};
+use crate::{prelude::*, Icon, IconButton, Tab};
+use gpui::prelude::*;
 
 #[derive(Component)]
 pub struct TabBar {

crates/ui2/src/to_extract/title_bar.rs 🔗

@@ -6,8 +6,8 @@ use gpui::{Div, Render, View, VisualContext};
 use crate::prelude::*;
 use crate::settings::user_settings;
 use crate::{
-    Avatar, Button, Icon, IconButton, IconColor, MicStatus, PlayerStack, PlayerWithCallStatus,
-    ScreenShareStatus, ToolDivider, TrafficLights,
+    Avatar, Button, Icon, IconButton, MicStatus, PlayerStack, PlayerWithCallStatus,
+    ScreenShareStatus, TextColor, ToolDivider, TrafficLights,
 };
 
 #[derive(Clone)]
@@ -152,21 +152,25 @@ impl Render for TitleBar {
                             .gap_1()
                             .child(
                                 IconButton::<TitleBar>::new("toggle_mic_status", Icon::Mic)
-                                    .when(self.is_mic_muted(), |this| this.color(IconColor::Error))
-                                    .on_click(|title_bar, cx| title_bar.toggle_mic_status(cx)),
+                                    .when(self.is_mic_muted(), |this| this.color(TextColor::Error))
+                                    .on_click(|title_bar: &mut TitleBar, cx| {
+                                        title_bar.toggle_mic_status(cx)
+                                    }),
                             )
                             .child(
                                 IconButton::<TitleBar>::new("toggle_deafened", Icon::AudioOn)
-                                    .when(self.is_deafened, |this| this.color(IconColor::Error))
-                                    .on_click(|title_bar, cx| title_bar.toggle_deafened(cx)),
+                                    .when(self.is_deafened, |this| this.color(TextColor::Error))
+                                    .on_click(|title_bar: &mut TitleBar, cx| {
+                                        title_bar.toggle_deafened(cx)
+                                    }),
                             )
                             .child(
                                 IconButton::<TitleBar>::new("toggle_screen_share", Icon::Screen)
                                     .when(
                                         self.screen_share_status == ScreenShareStatus::Shared,
-                                        |this| this.color(IconColor::Accent),
+                                        |this| this.color(TextColor::Accent),
                                     )
-                                    .on_click(|title_bar, cx| {
+                                    .on_click(|title_bar: &mut TitleBar, cx| {
                                         title_bar.toggle_screen_share_status(cx)
                                     }),
                             ),

crates/ui2/src/to_extract/workspace.rs 🔗

@@ -206,13 +206,14 @@ impl Render for Workspace {
             .child(self.editor_1.clone())],
             SplitDirection::Horizontal,
         );
+        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
 
         div()
             .relative()
             .size_full()
             .flex()
             .flex_col()
-            .font("Zed Sans")
+            .font(ui_font)
             .gap_0()
             .justify_start()
             .items_start()

crates/workspace2/src/dock.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{status_bar::StatusItemView, Axis, Workspace};
 use gpui::{
-    div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, ParentElement, Render,
-    Subscription, View, ViewContext, WeakView, WindowContext,
+    div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter,
+    FocusHandle, ParentComponent, Render, Styled, Subscription, View, ViewContext, WeakView,
+    WindowContext,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -34,6 +35,7 @@ pub trait Panel: Render + EventEmitter<PanelEvent> {
     fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
     fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
     fn has_focus(&self, cx: &WindowContext) -> bool;
+    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
 }
 
 pub trait PanelHandle: Send + Sync {
@@ -51,6 +53,7 @@ pub trait PanelHandle: Send + Sync {
     fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
     fn icon_label(&self, cx: &WindowContext) -> Option<String>;
     fn has_focus(&self, cx: &WindowContext) -> bool;
+    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
     fn to_any(&self) -> AnyView;
 }
 
@@ -117,6 +120,10 @@ where
     fn to_any(&self) -> AnyView {
         self.clone().into()
     }
+
+    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
+        self.read(cx).focus_handle(cx).clone()
+    }
 }
 
 impl From<&dyn PanelHandle> for AnyView {
@@ -422,7 +429,18 @@ impl Render for Dock {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        todo!()
+        if let Some(entry) = self.visible_entry() {
+            let size = entry.panel.size(cx);
+
+            div()
+                .map(|this| match self.position().axis() {
+                    Axis::Horizontal => this.w(px(size)).h_full(),
+                    Axis::Vertical => this.h(px(size)).w_full(),
+                })
+                .child(entry.panel.to_any())
+        } else {
+            div()
+        }
     }
 }
 
@@ -728,5 +746,9 @@ pub mod test {
         fn has_focus(&self, _cx: &WindowContext) -> bool {
             self.has_focus
         }
+
+        fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
+            unimplemented!()
+        }
     }
 }

crates/workspace2/src/modal_layer.rs 🔗

@@ -1,8 +1,8 @@
 use gpui::{
-    div, px, AnyView, Div, EventEmitter, FocusHandle, ParentElement, Render, StatelessInteractive,
-    Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
+    div, prelude::*, px, AnyView, Div, EventEmitter, FocusHandle, Render, Subscription, View,
+    ViewContext, WindowContext,
 };
-use ui::v_stack;
+use ui::{h_stack, v_stack};
 
 pub struct ActiveModal {
     modal: AnyView,
@@ -33,8 +33,6 @@ impl ModalLayer {
         V: Modal,
         B: FnOnce(&mut ViewContext<V>) -> V,
     {
-        let previous_focus = cx.focused();
-
         if let Some(active_modal) = &self.active_modal {
             let is_close = active_modal.modal.clone().downcast::<V>().is_ok();
             self.hide_modal(cx);
@@ -73,6 +71,14 @@ impl ModalLayer {
 
         cx.notify();
     }
+
+    pub fn current_modal<V>(&self) -> Option<View<V>>
+    where
+        V: 'static,
+    {
+        let active_modal = self.active_modal.as_ref()?;
+        active_modal.modal.clone().downcast::<V>().ok()
+    }
 }
 
 impl Render for ModalLayer {
@@ -85,9 +91,6 @@ impl Render for ModalLayer {
 
         div()
             .absolute()
-            .flex()
-            .flex_col()
-            .items_center()
             .size_full()
             .top_0()
             .left_0()
@@ -96,11 +99,21 @@ impl Render for ModalLayer {
                 v_stack()
                     .h(px(0.0))
                     .top_20()
+                    .flex()
+                    .flex_col()
+                    .items_center()
                     .track_focus(&active_modal.focus_handle)
-                    .on_mouse_down_out(|this: &mut Self, event, cx| {
-                        this.hide_modal(cx);
-                    })
-                    .child(active_modal.modal.clone()),
+                    .child(
+                        h_stack()
+                            // needed to prevent mouse events leaking to the
+                            // UI below. // todo! for gpui3.
+                            .on_any_mouse_down(|_, _, cx| cx.stop_propagation())
+                            .on_any_mouse_up(|_, _, cx| cx.stop_propagation())
+                            .on_mouse_down_out(|this: &mut Self, event, cx| {
+                                this.hide_modal(cx);
+                            })
+                            .child(active_modal.modal.clone()),
+                    ),
             )
     }
 }

crates/workspace2/src/pane.rs 🔗

@@ -1,5 +1,3 @@
-// mod dragged_item_receiver;
-
 use crate::{
     item::{Item, ItemHandle, ItemSettings, WeakItemHandle},
     toolbar::Toolbar,
@@ -9,7 +7,7 @@ use crate::{
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
-    actions, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
+    actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
     EventEmitter, FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext,
     WeakView, WindowContext,
 };
@@ -27,7 +25,7 @@ use std::{
     },
 };
 use ui::v_stack;
-use ui::{prelude::*, Icon, IconButton, IconColor, IconElement, TextTooltip};
+use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, TextTooltip};
 use util::truncate_and_remove_front;
 
 #[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
@@ -733,21 +731,21 @@ impl Pane {
     //         self.activate_item(index, activate_pane, activate_pane, cx);
     //     }
 
-    //     pub fn close_active_item(
-    //         &mut self,
-    //         action: &CloseActiveItem,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         if self.items.is_empty() {
-    //             return None;
-    //         }
-    //         let active_item_id = self.items[self.active_item_index].id();
-    //         Some(self.close_item_by_id(
-    //             active_item_id,
-    //             action.save_intent.unwrap_or(SaveIntent::Close),
-    //             cx,
-    //         ))
-    //     }
+    pub fn close_active_item(
+        &mut self,
+        action: &CloseActiveItem,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        if self.items.is_empty() {
+            return None;
+        }
+        let active_item_id = self.items[self.active_item_index].id();
+        Some(self.close_item_by_id(
+            active_item_id,
+            action.save_intent.unwrap_or(SaveIntent::Close),
+            cx,
+        ))
+    }
 
     pub fn close_item_by_id(
         &mut self,
@@ -1401,32 +1399,44 @@ impl Pane {
             // .on_drop(|_view, state: View<DraggedTab>, cx| {
             //     eprintln!("{:?}", state.read(cx));
             // })
-            .px_2()
-            .py_0p5()
             .flex()
             .items_center()
             .justify_center()
+            // todo!("Nate - I need to do some work to balance all the items in the tab once things stablize")
+            .map(|this| {
+                if close_right {
+                    this.pl_3().pr_1()
+                } else {
+                    this.pr_1().pr_3()
+                }
+            })
+            .py_1()
             .bg(tab_bg)
-            .hover(|h| h.bg(tab_hover_bg))
-            .active(|a| a.bg(tab_active_bg))
+            .border_color(cx.theme().colors().border)
+            .map(|this| match ix.cmp(&self.active_item_index) {
+                cmp::Ordering::Less => this.border_l(),
+                cmp::Ordering::Equal => this.border_r(),
+                cmp::Ordering::Greater => this.border_l().border_r(),
+            })
+            // .hover(|h| h.bg(tab_hover_bg))
+            // .active(|a| a.bg(tab_active_bg))
             .child(
                 div()
-                    .px_1()
                     .flex()
                     .items_center()
-                    .gap_1p5()
+                    .gap_1()
                     .text_color(text_color)
                     .children(if item.has_conflict(cx) {
                         Some(
                             IconElement::new(Icon::ExclamationTriangle)
                                 .size(ui::IconSize::Small)
-                                .color(IconColor::Warning),
+                                .color(TextColor::Warning),
                         )
                     } else if item.is_dirty(cx) {
                         Some(
                             IconElement::new(Icon::ExclamationTriangle)
                                 .size(ui::IconSize::Small)
-                                .color(IconColor::Info),
+                                .color(TextColor::Info),
                         )
                     } else {
                         None
@@ -1907,7 +1917,12 @@ impl Render for Pane {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         v_stack()
+            .key_context("Pane")
             .size_full()
+            .on_action(|pane: &mut Self, action, cx| {
+                pane.close_active_item(action, cx)
+                    .map(|task| task.detach_and_log_err(cx));
+            })
             .child(self.render_tab_bar(cx))
             .child(self.toolbar.clone())
             .child(if let Some(item) = self.active_item() {

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

@@ -2,7 +2,7 @@ use super::DraggedItem;
 use crate::{Pane, SplitDirection, Workspace};
 use gpui::{
     color::Color,
-    elements::{Canvas, MouseEventHandler, ParentElement, Stack},
+    elements::{Canvas, MouseEventHandler, ParentComponent, Stack},
     geometry::{rect::RectF, vector::Vector2F},
     platform::MouseButton,
     scene::MouseUp,

crates/workspace2/src/status_bar.rs 🔗

@@ -2,7 +2,7 @@ use std::any::TypeId;
 
 use crate::{ItemHandle, Pane};
 use gpui::{
-    div, AnyView, Component, Div, ParentElement, Render, Styled, Subscription, View, ViewContext,
+    div, AnyView, Component, Div, ParentComponent, Render, Styled, Subscription, View, ViewContext,
     WindowContext,
 };
 use theme2::ActiveTheme;

crates/workspace2/src/toolbar.rs 🔗

@@ -1,10 +1,10 @@
 use crate::ItemHandle;
 use gpui::{
-    div, AnyView, Component, Div, Entity, EntityId, EventEmitter, ParentElement, Render, Styled,
-    View, ViewContext, WindowContext,
+    AnyView, Div, Entity, EntityId, EventEmitter, ParentComponent, Render, Styled, View,
+    ViewContext, WindowContext,
 };
 use theme2::ActiveTheme;
-use ui::{h_stack, v_stack, Button, Icon, IconButton, Label, LabelColor, StyledExt};
+use ui::{h_stack, v_stack, Button, Icon, IconButton, Label, TextColor};
 
 pub enum ToolbarItemEvent {
     ChangeLocation(ToolbarItemLocation),
@@ -92,7 +92,7 @@ impl Render for Toolbar {
                         h_stack()
                             .p_1()
                             .child(Button::new("crates"))
-                            .child(Label::new("/").color(LabelColor::Muted))
+                            .child(Label::new("/").color(TextColor::Muted))
                             .child(Button::new("workspace2")),
                     )
                     // Toolbar right side

crates/workspace2/src/workspace2.rs 🔗

@@ -29,23 +29,22 @@ use client2::{
     Client, TypedEnvelope, UserStore,
 };
 use collections::{hash_map, HashMap, HashSet};
-use dock::{Dock, DockPosition, PanelButtons};
+use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle as _};
 use futures::{
     channel::{mpsc, oneshot},
     future::try_join_all,
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
-    AsyncAppContext, AsyncWindowContext, Bounds, Component, DispatchContext, Div, Entity, EntityId,
-    EventEmitter, FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render,
-    Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Subscription,
-    Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle,
-    WindowOptions,
+    actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView,
+    AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId,
+    EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent,
+    Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds,
+    WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
-use language2::LanguageRegistry;
+use language2::{LanguageRegistry, Rope};
 use lazy_static::lazy_static;
 pub use modal_layer::*;
 use node_runtime::NodeRuntime;
@@ -67,12 +66,13 @@ use std::{
     sync::{atomic::AtomicUsize, Arc},
     time::Duration,
 };
-use theme2::ActiveTheme;
+use theme2::{ActiveTheme, ThemeSettings};
 pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
-use ui::{h_stack, Label};
+use ui::TextColor;
+use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextTooltip};
 use util::ResultExt;
 use uuid::Uuid;
-use workspace_settings::{AutosaveSetting, WorkspaceSettings};
+pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
 
 lazy_static! {
     static ref ZED_WINDOW_SIZE: Option<Size<GlobalPixels>> = env::var("ZED_WINDOW_SIZE")
@@ -248,102 +248,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     //             }
     //         }
     //     });
-    //     cx.add_async_action(Workspace::open);
-
-    //     cx.add_async_action(Workspace::follow_next_collaborator);
-    //     cx.add_async_action(Workspace::close);
-    //     cx.add_async_action(Workspace::close_inactive_items_and_panes);
-    //     cx.add_async_action(Workspace::close_all_items_and_panes);
-    //     cx.add_global_action(Workspace::close_global);
-    //     cx.add_global_action(restart);
-    //     cx.add_async_action(Workspace::save_all);
-    //     cx.add_action(Workspace::add_folder_to_project);
-    //     cx.add_action(
-    //         |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
-    //             let pane = workspace.active_pane().clone();
-    //             workspace.unfollow(&pane, cx);
-    //         },
-    //     );
-    //     cx.add_action(
-    //         |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
-    //             workspace
-    //                 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
-    //                 .detach_and_log_err(cx);
-    //         },
-    //     );
-    //     cx.add_action(
-    //         |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
-    //             workspace
-    //                 .save_active_item(SaveIntent::SaveAs, cx)
-    //                 .detach_and_log_err(cx);
-    //         },
-    //     );
-    //     cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
-    //         workspace.activate_previous_pane(cx)
-    //     });
-    //     cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
-    //         workspace.activate_next_pane(cx)
-    //     });
-
-    //     cx.add_action(
-    //         |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
-    //             workspace.activate_pane_in_direction(action.0, cx)
-    //         },
-    //     );
-
-    //     cx.add_action(
-    //         |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
-    //             workspace.swap_pane_in_direction(action.0, cx)
-    //         },
-    //     );
-
-    //     cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
-    //         workspace.toggle_dock(DockPosition::Left, cx);
-    //     });
-    //     cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
-    //         workspace.toggle_dock(DockPosition::Right, cx);
-    //     });
-    //     cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
-    //         workspace.toggle_dock(DockPosition::Bottom, cx);
-    //     });
-    //     cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
-    //         workspace.close_all_docks(cx);
-    //     });
-    //     cx.add_action(Workspace::activate_pane_at_index);
-    //     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
-    //         workspace.reopen_closed_item(cx).detach();
-    //     });
-    //     cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
-    //         workspace
-    //             .go_back(workspace.active_pane().downgrade(), cx)
-    //             .detach();
-    //     });
-    //     cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
-    //         workspace
-    //             .go_forward(workspace.active_pane().downgrade(), cx)
-    //             .detach();
-    //     });
-
-    //     cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
-    //         cx.spawn(|workspace, mut cx| async move {
-    //             let err = install_cli::install_cli(&cx)
-    //                 .await
-    //                 .context("Failed to create CLI symlink");
-
-    //             workspace.update(&mut cx, |workspace, cx| {
-    //                 if matches!(err, Err(_)) {
-    //                     err.notify_err(workspace, cx);
-    //                 } else {
-    //                     workspace.show_notification(1, cx, |cx| {
-    //                         cx.build_view(|_| {
-    //                             MessageNotification::new("Successfully installed the `zed` binary")
-    //                         })
-    //                     });
-    //                 }
-    //             })
-    //         })
-    //         .detach();
-    //     });
 }
 
 type ProjectItemBuilders =
@@ -443,7 +347,6 @@ struct Follower {
 impl AppState {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut AppContext) -> Arc<Self> {
-        use gpui::Context;
         use node_runtime::FakeNodeRuntime;
         use settings2::SettingsStore;
 
@@ -531,13 +434,7 @@ pub enum Event {
 pub struct Workspace {
     weak_self: WeakView<Self>,
     focus_handle: FocusHandle,
-    workspace_actions: Vec<
-        Box<
-            dyn Fn(
-                Div<Workspace, StatefulInteractivity<Workspace>>,
-            ) -> Div<Workspace, StatefulInteractivity<Workspace>>,
-        >,
-    >,
+    workspace_actions: Vec<Box<dyn Fn(Div<Workspace>) -> Div<Workspace>>>,
     zoomed: Option<AnyWeakView>,
     zoomed_position: Option<DockPosition>,
     center: PaneGroup,
@@ -942,108 +839,15 @@ impl Workspace {
         &self.right_dock
     }
 
-    //     pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>)
-    //     where
-    //         T::Event: std::fmt::Debug,
-    //     {
-    //         self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {})
-    //     }
-
-    //     pub fn add_panel_with_extra_event_handler<T: Panel, F>(
-    //         &mut self,
-    //         panel: View<T>,
-    //         cx: &mut ViewContext<Self>,
-    //         handler: F,
-    //     ) where
-    //         T::Event: std::fmt::Debug,
-    //         F: Fn(&mut Self, &View<T>, &T::Event, &mut ViewContext<Self>) + 'static,
-    //     {
-    //         let dock = match panel.position(cx) {
-    //             DockPosition::Left => &self.left_dock,
-    //             DockPosition::Bottom => &self.bottom_dock,
-    //             DockPosition::Right => &self.right_dock,
-    //         };
-
-    //         self.subscriptions.push(cx.subscribe(&panel, {
-    //             let mut dock = dock.clone();
-    //             let mut prev_position = panel.position(cx);
-    //             move |this, panel, event, cx| {
-    //                 if T::should_change_position_on_event(event) {
-    //                     THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION
-    //                     See: Dock::add_panel
-    //
-    //                     let new_position = panel.read(cx).position(cx);
-    //                     let mut was_visible = false;
-    //                     dock.update(cx, |dock, cx| {
-    //                         prev_position = new_position;
-
-    //                         was_visible = dock.is_open()
-    //                             && dock
-    //                                 .visible_panel()
-    //                                 .map_or(false, |active_panel| active_panel.id() == panel.id());
-    //                         dock.remove_panel(&panel, cx);
-    //                     });
-
-    //                     if panel.is_zoomed(cx) {
-    //                         this.zoomed_position = Some(new_position);
-    //                     }
-
-    //                     dock = match panel.read(cx).position(cx) {
-    //                         DockPosition::Left => &this.left_dock,
-    //                         DockPosition::Bottom => &this.bottom_dock,
-    //                         DockPosition::Right => &this.right_dock,
-    //                     }
-    //                     .clone();
-    //                     dock.update(cx, |dock, cx| {
-    //                         dock.add_panel(panel.clone(), cx);
-    //                         if was_visible {
-    //                             dock.set_open(true, cx);
-    //                             dock.activate_panel(dock.panels_len() - 1, cx);
-    //                         }
-    //                     });
-    //                 } else if T::should_zoom_in_on_event(event) {
-    //                     THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION
-    //                     See: Dock::add_panel
-    //
-    //                     dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
-    //                     if !panel.has_focus(cx) {
-    //                         cx.focus(&panel);
-    //                     }
-    //                     this.zoomed = Some(panel.downgrade().into_any());
-    //                     this.zoomed_position = Some(panel.read(cx).position(cx));
-    //                 } else if T::should_zoom_out_on_event(event) {
-    //                     THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION
-    //                     See: Dock::add_panel
-    //
-    //                     dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
-    //                     if this.zoomed_position == Some(prev_position) {
-    //                         this.zoomed = None;
-    //                         this.zoomed_position = None;
-    //                     }
-    //                     cx.notify();
-    //                 } else if T::is_focus_event(event) {
-    //                     THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION
-    //                     See: Dock::add_panel
-    //
-    //                     let position = panel.read(cx).position(cx);
-    //                     this.dismiss_zoomed_items_to_reveal(Some(position), cx);
-    //                     if panel.is_zoomed(cx) {
-    //                         this.zoomed = Some(panel.downgrade().into_any());
-    //                         this.zoomed_position = Some(position);
-    //                     } else {
-    //                         this.zoomed = None;
-    //                         this.zoomed_position = None;
-    //                     }
-    //                     this.update_active_view_for_followers(cx);
-    //                     cx.notify();
-    //                 } else {
-    //                     handler(this, &panel, event, cx)
-    //                 }
-    //             }
-    //         }));
+    pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
+        let dock = match panel.position(cx) {
+            DockPosition::Left => &self.left_dock,
+            DockPosition::Bottom => &self.bottom_dock,
+            DockPosition::Right => &self.right_dock,
+        };
 
-    //         dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
-    //     }
+        dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
+    }
 
     pub fn status_bar(&self) -> &View<StatusBar> {
         &self.status_bar
@@ -1241,29 +1045,29 @@ impl Workspace {
     //     self.titlebar_item.clone()
     // }
 
-    //     /// Call the given callback with a workspace whose project is local.
-    //     ///
-    //     /// If the given workspace has a local project, then it will be passed
-    //     /// to the callback. Otherwise, a new empty window will be created.
-    //     pub fn with_local_workspace<T, F>(
-    //         &mut self,
-    //         cx: &mut ViewContext<Self>,
-    //         callback: F,
-    //     ) -> Task<Result<T>>
-    //     where
-    //         T: 'static,
-    //         F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
-    //     {
-    //         if self.project.read(cx).is_local() {
-    //             Task::Ready(Some(Ok(callback(self, cx))))
-    //         } else {
-    //             let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
-    //             cx.spawn(|_vh, mut cx| async move {
-    //                 let (workspace, _) = task.await;
-    //                 workspace.update(&mut cx, callback)
-    //             })
-    //         }
-    //     }
+    /// Call the given callback with a workspace whose project is local.
+    ///
+    /// If the given workspace has a local project, then it will be passed
+    /// to the callback. Otherwise, a new empty window will be created.
+    pub fn with_local_workspace<T, F>(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+        callback: F,
+    ) -> Task<Result<T>>
+    where
+        T: 'static,
+        F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
+    {
+        if self.project.read(cx).is_local() {
+            Task::Ready(Some(Ok(callback(self, cx))))
+        } else {
+            let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
+            cx.spawn(|_vh, mut cx| async move {
+                let (workspace, _) = task.await?;
+                workspace.update(&mut cx, callback)
+            })
+        }
+    }
 
     pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Model<Worktree>> {
         self.project.read(cx).worktrees()
@@ -1319,53 +1123,56 @@ impl Workspace {
     //         }))
     //     }
 
-    //     pub fn prepare_to_close(
-    //         &mut self,
-    //         quitting: bool,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<Result<bool>> {
-    //         let active_call = self.active_call().cloned();
-    //         let window = cx.window();
-
-    //         cx.spawn(|this, mut cx| async move {
-    //             let workspace_count = cx
-    //                 .windows()
-    //                 .into_iter()
-    //                 .filter(|window| window.root_is::<Workspace>())
-    //                 .count();
-
-    //             if let Some(active_call) = active_call {
-    //                 if !quitting
-    //                     && workspace_count == 1
-    //                     && active_call.read_with(&cx, |call, _| call.room().is_some())
-    //                 {
-    //                     let answer = window.prompt(
-    //                         PromptLevel::Warning,
-    //                         "Do you want to leave the current call?",
-    //                         &["Close window and hang up", "Cancel"],
-    //                         &mut cx,
-    //                     );
-
-    //                     if let Some(mut answer) = answer {
-    //                         if answer.next().await == Some(1) {
-    //                             return anyhow::Ok(false);
-    //                         } else {
-    //                             active_call
-    //                                 .update(&mut cx, |call, cx| call.hang_up(cx))
-    //                                 .await
-    //                                 .log_err();
-    //                         }
-    //                     }
-    //                 }
-    //             }
+    pub fn prepare_to_close(
+        &mut self,
+        quitting: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<bool>> {
+        //todo!(saveing)
+        // let active_call = self.active_call().cloned();
+        // let window = cx.window();
+
+        cx.spawn(|this, mut cx| async move {
+            // let workspace_count = cx
+            //     .windows()
+            //     .into_iter()
+            //     .filter(|window| window.root_is::<Workspace>())
+            //     .count();
+
+            // if let Some(active_call) = active_call {
+            //     if !quitting
+            //         && workspace_count == 1
+            //         && active_call.read_with(&cx, |call, _| call.room().is_some())
+            //     {
+            //         let answer = window.prompt(
+            //             PromptLevel::Warning,
+            //             "Do you want to leave the current call?",
+            //             &["Close window and hang up", "Cancel"],
+            //             &mut cx,
+            //         );
+
+            //         if let Some(mut answer) = answer {
+            //             if answer.next().await == Some(1) {
+            //                 return anyhow::Ok(false);
+            //             } else {
+            //                 active_call
+            //                     .update(&mut cx, |call, cx| call.hang_up(cx))
+            //                     .await
+            //                     .log_err();
+            //             }
+            //         }
+            //     }
+            // }
 
-    //             Ok(this
-    //                 .update(&mut cx, |this, cx| {
-    //                     this.save_all_internal(SaveIntent::Close, cx)
-    //                 })?
-    //                 .await?)
-    //         })
-    //     }
+            Ok(
+                false, // this
+                      // .update(&mut cx, |this, cx| {
+                      //     this.save_all_internal(SaveIntent::Close, cx)
+                      // })?
+                      // .await?
+            )
+        })
+    }
 
     //     fn save_all(
     //         &mut self,
@@ -1732,42 +1539,43 @@ impl Workspace {
     //         }
     //     }
 
-    //     pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
-    //         let dock = match dock_side {
-    //             DockPosition::Left => &self.left_dock,
-    //             DockPosition::Bottom => &self.bottom_dock,
-    //             DockPosition::Right => &self.right_dock,
-    //         };
-    //         let mut focus_center = false;
-    //         let mut reveal_dock = false;
-    //         dock.update(cx, |dock, cx| {
-    //             let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
-    //             let was_visible = dock.is_open() && !other_is_zoomed;
-    //             dock.set_open(!was_visible, cx);
-
-    //             if let Some(active_panel) = dock.active_panel() {
-    //                 if was_visible {
-    //                     if active_panel.has_focus(cx) {
-    //                         focus_center = true;
-    //                     }
-    //                 } else {
-    //                     cx.focus(active_panel.as_any());
-    //                     reveal_dock = true;
-    //                 }
-    //             }
-    //         });
+    pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
+        let dock = match dock_side {
+            DockPosition::Left => &self.left_dock,
+            DockPosition::Bottom => &self.bottom_dock,
+            DockPosition::Right => &self.right_dock,
+        };
+        let mut focus_center = false;
+        let mut reveal_dock = false;
+        dock.update(cx, |dock, cx| {
+            let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
+            let was_visible = dock.is_open() && !other_is_zoomed;
+            dock.set_open(!was_visible, cx);
+
+            if let Some(active_panel) = dock.active_panel() {
+                if was_visible {
+                    if active_panel.has_focus(cx) {
+                        focus_center = true;
+                    }
+                } else {
+                    let focus_handle = &active_panel.focus_handle(cx);
+                    cx.focus(focus_handle);
+                    reveal_dock = true;
+                }
+            }
+        });
 
-    //         if reveal_dock {
-    //             self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
-    //         }
+        if reveal_dock {
+            self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
+        }
 
-    //         if focus_center {
-    //             cx.focus_self();
-    //         }
+        if focus_center {
+            cx.focus(&self.focus_handle);
+        }
 
-    //         cx.notify();
-    //         self.serialize_workspace(cx);
-    //     }
+        cx.notify();
+        self.serialize_workspace(cx);
+    }
 
     pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
         let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
@@ -1958,50 +1766,50 @@ impl Workspace {
         })
     }
 
-    //     pub fn open_abs_path(
-    //         &mut self,
-    //         abs_path: PathBuf,
-    //         visible: bool,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
-    //         cx.spawn(|workspace, mut cx| async move {
-    //             let open_paths_task_result = workspace
-    //                 .update(&mut cx, |workspace, cx| {
-    //                     workspace.open_paths(vec![abs_path.clone()], visible, cx)
-    //                 })
-    //                 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
-    //                 .await;
-    //             anyhow::ensure!(
-    //                 open_paths_task_result.len() == 1,
-    //                 "open abs path {abs_path:?} task returned incorrect number of results"
-    //             );
-    //             match open_paths_task_result
-    //                 .into_iter()
-    //                 .next()
-    //                 .expect("ensured single task result")
-    //             {
-    //                 Some(open_result) => {
-    //                     open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
-    //                 }
-    //                 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
-    //             }
-    //         })
-    //     }
+    pub fn open_abs_path(
+        &mut self,
+        abs_path: PathBuf,
+        visible: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+        cx.spawn(|workspace, mut cx| async move {
+            let open_paths_task_result = workspace
+                .update(&mut cx, |workspace, cx| {
+                    workspace.open_paths(vec![abs_path.clone()], visible, cx)
+                })
+                .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
+                .await;
+            anyhow::ensure!(
+                open_paths_task_result.len() == 1,
+                "open abs path {abs_path:?} task returned incorrect number of results"
+            );
+            match open_paths_task_result
+                .into_iter()
+                .next()
+                .expect("ensured single task result")
+            {
+                Some(open_result) => {
+                    open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
+                }
+                None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
+            }
+        })
+    }
 
-    //     pub fn split_abs_path(
-    //         &mut self,
-    //         abs_path: PathBuf,
-    //         visible: bool,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
-    //         let project_path_task =
-    //             Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
-    //         cx.spawn(|this, mut cx| async move {
-    //             let (_, path) = project_path_task.await?;
-    //             this.update(&mut cx, |this, cx| this.split_path(path, cx))?
-    //                 .await
-    //         })
-    //     }
+    pub fn split_abs_path(
+        &mut self,
+        abs_path: PathBuf,
+        visible: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+        let project_path_task =
+            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
+        cx.spawn(|this, mut cx| async move {
+            let (_, path) = project_path_task.await?;
+            this.update(&mut cx, |this, cx| this.split_path(path, cx))?
+                .await
+        })
+    }
 
     pub fn open_path(
         &mut self,
@@ -2028,37 +1836,37 @@ impl Workspace {
         })
     }
 
-    //     pub fn split_path(
-    //         &mut self,
-    //         path: impl Into<ProjectPath>,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
-    //         let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
-    //             self.panes
-    //                 .first()
-    //                 .expect("There must be an active pane")
-    //                 .downgrade()
-    //         });
+    pub fn split_path(
+        &mut self,
+        path: impl Into<ProjectPath>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
+        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
+            self.panes
+                .first()
+                .expect("There must be an active pane")
+                .downgrade()
+        });
 
-    //         if let Member::Pane(center_pane) = &self.center.root {
-    //             if center_pane.read(cx).items_len() == 0 {
-    //                 return self.open_path(path, Some(pane), true, cx);
-    //             }
-    //         }
+        if let Member::Pane(center_pane) = &self.center.root {
+            if center_pane.read(cx).items_len() == 0 {
+                return self.open_path(path, Some(pane), true, cx);
+            }
+        }
 
-    //         let task = self.load_path(path.into(), cx);
-    //         cx.spawn(|this, mut cx| async move {
-    //             let (project_entry_id, build_item) = task.await?;
-    //             this.update(&mut cx, move |this, cx| -> Option<_> {
-    //                 let pane = pane.upgrade(cx)?;
-    //                 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
-    //                 new_pane.update(cx, |new_pane, cx| {
-    //                     Some(new_pane.open_item(project_entry_id, true, cx, build_item))
-    //                 })
-    //             })
-    //             .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
-    //         })
-    //     }
+        let task = self.load_path(path.into(), cx);
+        cx.spawn(|this, mut cx| async move {
+            let (project_entry_id, build_item) = task.await?;
+            this.update(&mut cx, move |this, cx| -> Option<_> {
+                let pane = pane.upgrade()?;
+                let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
+                new_pane.update(cx, |new_pane, cx| {
+                    Some(new_pane.open_item(project_entry_id, true, cx, build_item))
+                })
+            })
+            .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
+        })
+    }
 
     pub(crate) fn load_path(
         &mut self,
@@ -2641,19 +2449,68 @@ impl Workspace {
         h_stack()
             .id("titlebar")
             .justify_between()
-            .w_full()
-            .h(rems(1.75))
-            .bg(cx.theme().colors().title_bar_background)
             .when(
                 !matches!(cx.window_bounds(), WindowBounds::Fullscreen),
                 |s| s.pl_20(),
             )
+            .w_full()
+            .h(rems(1.75))
+            .bg(cx.theme().colors().title_bar_background)
             .on_click(|_, event, cx| {
                 if event.up.click_count == 2 {
                     cx.zoom_window();
                 }
             })
-            .child(h_stack().child(Label::new("Left side titlebar item"))) // self.titlebar_item
+            .child(
+                h_stack()
+                    // TODO - Add player menu
+                    .child(
+                        div()
+                            .id("project_owner_indicator")
+                            .child(
+                                Button::new("player")
+                                    .variant(ButtonVariant::Ghost)
+                                    .color(Some(TextColor::Player(0))),
+                            )
+                            .tooltip(move |_, cx| {
+                                cx.build_view(|cx| TextTooltip::new("Toggle following"))
+                            }),
+                    )
+                    // TODO - Add project menu
+                    .child(
+                        div()
+                            .id("titlebar_project_menu_button")
+                            .child(Button::new("project_name").variant(ButtonVariant::Ghost))
+                            .tooltip(move |_, cx| {
+                                cx.build_view(|cx| TextTooltip::new("Recent Projects"))
+                            }),
+                    )
+                    // TODO - Add git menu
+                    .child(
+                        div()
+                            .id("titlebar_git_menu_button")
+                            .child(
+                                Button::new("branch_name")
+                                    .variant(ButtonVariant::Ghost)
+                                    .color(Some(TextColor::Muted)),
+                            )
+                            .tooltip(move |_, cx| {
+                                // todo!() Replace with real action.
+                                #[gpui::action]
+                                struct NoAction {}
+
+                                cx.build_view(|cx| {
+                                    TextTooltip::new("Recent Branches")
+                                        .key_binding(KeyBinding::new(gpui::KeyBinding::new(
+                                            "cmd-b",
+                                            NoAction {},
+                                            None,
+                                        )))
+                                        .meta("Only local branches shown")
+                                })
+                            }),
+                    ),
+            ) // self.titlebar_item
             .child(h_stack().child(Label::new("Right side titlebar item")))
     }
 
@@ -3173,10 +3030,10 @@ impl Workspace {
 
     fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
         self.panes.retain(|p| p != pane);
-        if true {
-            todo!()
-            // cx.focus(self.panes.last().unwrap());
-        }
+        self.panes
+            .last()
+            .unwrap()
+            .update(cx, |pane, cx| pane.focus(cx));
         if self.last_active_center_pane == Some(pane.downgrade()) {
             self.last_active_center_pane = None;
         }
@@ -3448,9 +3305,105 @@ impl Workspace {
         })
     }
 
+    fn actions(div: Div<Self>) -> Div<Self> {
+        div
+            //     cx.add_async_action(Workspace::open);
+            //     cx.add_async_action(Workspace::follow_next_collaborator);
+            //     cx.add_async_action(Workspace::close);
+            //     cx.add_async_action(Workspace::close_inactive_items_and_panes);
+            //     cx.add_async_action(Workspace::close_all_items_and_panes);
+            //     cx.add_global_action(Workspace::close_global);
+            //     cx.add_global_action(restart);
+            //     cx.add_async_action(Workspace::save_all);
+            //     cx.add_action(Workspace::add_folder_to_project);
+            //     cx.add_action(
+            //         |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
+            //             let pane = workspace.active_pane().clone();
+            //             workspace.unfollow(&pane, cx);
+            //         },
+            //     );
+            //     cx.add_action(
+            //         |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
+            //             workspace
+            //                 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
+            //                 .detach_and_log_err(cx);
+            //         },
+            //     );
+            //     cx.add_action(
+            //         |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
+            //             workspace
+            //                 .save_active_item(SaveIntent::SaveAs, cx)
+            //                 .detach_and_log_err(cx);
+            //         },
+            //     );
+            //     cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
+            //         workspace.activate_previous_pane(cx)
+            //     });
+            //     cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
+            //         workspace.activate_next_pane(cx)
+            //     });
+            //     cx.add_action(
+            //         |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
+            //             workspace.activate_pane_in_direction(action.0, cx)
+            //         },
+            //     );
+            //     cx.add_action(
+            //         |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
+            //             workspace.swap_pane_in_direction(action.0, cx)
+            //         },
+            //     );
+            .on_action(|this, e: &ToggleLeftDock, cx| {
+                println!("TOGGLING DOCK");
+                this.toggle_dock(DockPosition::Left, cx);
+            })
+        //     cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
+        //         workspace.toggle_dock(DockPosition::Right, cx);
+        //     });
+        //     cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
+        //         workspace.toggle_dock(DockPosition::Bottom, cx);
+        //     });
+        //     cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
+        //         workspace.close_all_docks(cx);
+        //     });
+        //     cx.add_action(Workspace::activate_pane_at_index);
+        //     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
+        //         workspace.reopen_closed_item(cx).detach();
+        //     });
+        //     cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
+        //         workspace
+        //             .go_back(workspace.active_pane().downgrade(), cx)
+        //             .detach();
+        //     });
+        //     cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
+        //         workspace
+        //             .go_forward(workspace.active_pane().downgrade(), cx)
+        //             .detach();
+        //     });
+
+        //     cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
+        //         cx.spawn(|workspace, mut cx| async move {
+        //             let err = install_cli::install_cli(&cx)
+        //                 .await
+        //                 .context("Failed to create CLI symlink");
+
+        //             workspace.update(&mut cx, |workspace, cx| {
+        //                 if matches!(err, Err(_)) {
+        //                     err.notify_err(workspace, cx);
+        //                 } else {
+        //                     workspace.show_notification(1, cx, |cx| {
+        //                         cx.build_view(|_| {
+        //                             MessageNotification::new("Successfully installed the `zed` binary")
+        //                         })
+        //                     });
+        //                 }
+        //             })
+        //         })
+        //         .detach();
+        //     });
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
-        use gpui::Context;
         use node_runtime::FakeNodeRuntime;
 
         let client = project.read(cx).client();
@@ -3467,7 +3420,9 @@ impl Workspace {
             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
             node_runtime: FakeNodeRuntime::new(),
         });
-        Self::new(0, project, app_state, cx)
+        let workspace = Self::new(0, project, app_state, cx);
+        workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
+        workspace
     }
 
     //     fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
@@ -3503,25 +3458,27 @@ impl Workspace {
     pub fn register_action<A: Action>(
         &mut self,
         callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
-    ) {
+    ) -> &mut Self {
         let callback = Arc::new(callback);
 
         self.workspace_actions.push(Box::new(move |div| {
             let callback = callback.clone();
             div.on_action(move |workspace, event, cx| (callback.clone())(workspace, event, cx))
         }));
+        self
     }
 
-    fn add_workspace_actions_listeners(
-        &self,
-        mut div: Div<Workspace, StatefulInteractivity<Workspace>>,
-    ) -> Div<Workspace, StatefulInteractivity<Workspace>> {
+    fn add_workspace_actions_listeners(&self, mut div: Div<Workspace>) -> Div<Workspace> {
         for action in self.workspace_actions.iter() {
             div = (action)(div)
         }
         div
     }
 
+    pub fn current_modal<V: Modal + 'static>(&mut self, cx: &ViewContext<Self>) -> Option<View<V>> {
+        self.modal_layer.read(cx).current_modal()
+    }
+
     pub fn toggle_modal<V: Modal, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
     where
         B: FnOnce(&mut ViewContext<V>) -> V,
@@ -3743,158 +3700,123 @@ impl Render for Workspace {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        let mut context = DispatchContext::default();
-        context.insert("Workspace");
-        cx.with_key_dispatch_context(context, |cx| {
-            div()
-                .relative()
-                .size_full()
-                .flex()
-                .flex_col()
-                .font("Zed Sans")
-                .gap_0()
-                .justify_start()
-                .items_start()
-                .text_color(cx.theme().colors().text)
-                .bg(cx.theme().colors().background)
-                .child(self.render_titlebar(cx))
-                .child(
-                    // todo! should this be a component a view?
-                    self.add_workspace_actions_listeners(div().id("workspace"))
-                        .relative()
-                        .flex_1()
-                        .w_full()
-                        .flex()
-                        .overflow_hidden()
-                        .border_t()
-                        .border_b()
-                        .border_color(cx.theme().colors().border)
-                        .child(self.modal_layer.clone())
-                        // .children(
-                        //     Some(
-                        //         Panel::new("project-panel-outer", cx)
-                        //             .side(PanelSide::Left)
-                        //             .child(ProjectPanel::new("project-panel-inner")),
-                        //     )
-                        //     .filter(|_| self.is_project_panel_open()),
-                        // )
-                        // .children(
-                        //     Some(
-                        //         Panel::new("collab-panel-outer", cx)
-                        //             .child(CollabPanel::new("collab-panel-inner"))
-                        //             .side(PanelSide::Left),
-                        //     )
-                        //     .filter(|_| self.is_collab_panel_open()),
-                        // )
-                        // .child(NotificationToast::new(
-                        //     "maxbrunsfeld has requested to add you as a contact.".into(),
-                        // ))
-                        .child(
-                            div().flex().flex_col().flex_1().h_full().child(
-                                div().flex().flex_1().child(self.center.render(
-                                    &self.project,
-                                    &self.follower_states,
-                                    self.active_call(),
-                                    &self.active_pane,
-                                    self.zoomed.as_ref(),
-                                    &self.app_state,
-                                    cx,
-                                )),
-                            ), // .children(
-                               //     Some(
-                               //         Panel::new("terminal-panel", cx)
-                               //             .child(Terminal::new())
-                               //             .allowed_sides(PanelAllowedSides::BottomOnly)
-                               //             .side(PanelSide::Bottom),
-                               //     )
-                               //     .filter(|_| self.is_terminal_open()),
-                               // ),
-                        ), // .children(
-                           //     Some(
-                           //         Panel::new("chat-panel-outer", cx)
-                           //             .side(PanelSide::Right)
-                           //             .child(ChatPanel::new("chat-panel-inner").messages(vec![
-                           //                 ChatMessage::new(
-                           //                     "osiewicz".to_string(),
-                           //                     "is this thing on?".to_string(),
-                           //                     DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
-                           //                         .unwrap()
-                           //                         .naive_local(),
-                           //                 ),
-                           //                 ChatMessage::new(
-                           //                     "maxdeviant".to_string(),
-                           //                     "Reading you loud and clear!".to_string(),
-                           //                     DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
-                           //                         .unwrap()
-                           //                         .naive_local(),
-                           //                 ),
-                           //             ])),
-                           //     )
-                           //     .filter(|_| self.is_chat_panel_open()),
-                           // )
-                           // .children(
-                           //     Some(
-                           //         Panel::new("notifications-panel-outer", cx)
-                           //             .side(PanelSide::Right)
-                           //             .child(NotificationsPanel::new("notifications-panel-inner")),
-                           //     )
-                           //     .filter(|_| self.is_notifications_panel_open()),
-                           // )
-                           // .children(
-                           //     Some(
-                           //         Panel::new("assistant-panel-outer", cx)
-                           //             .child(AssistantPanel::new("assistant-panel-inner")),
-                           //     )
-                           //     .filter(|_| self.is_assistant_panel_open()),
-                           // ),
-                )
-                .child(self.status_bar.clone())
-                // .when(self.debug.show_toast, |this| {
-                //     this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
-                // })
-                // .children(
-                //     Some(
-                //         div()
-                //             .absolute()
-                //             .top(px(50.))
-                //             .left(px(640.))
-                //             .z_index(8)
-                //             .child(LanguageSelector::new("language-selector")),
-                //     )
-                //     .filter(|_| self.is_language_selector_open()),
-                // )
-                .z_index(8)
-                // Debug
-                .child(
-                    div()
-                        .flex()
-                        .flex_col()
-                        .z_index(9)
-                        .absolute()
-                        .top_20()
-                        .left_1_4()
-                        .w_40()
-                        .gap_2(), // .when(self.show_debug, |this| {
-                                  //     this.child(Button::<Workspace>::new("Toggle User Settings").on_click(
-                                  //         Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)),
-                                  //     ))
-                                  //     .child(
-                                  //         Button::<Workspace>::new("Toggle Toasts").on_click(Arc::new(
-                                  //             |workspace, cx| workspace.debug_toggle_toast(cx),
-                                  //         )),
-                                  //     )
-                                  //     .child(
-                                  //         Button::<Workspace>::new("Toggle Livestream").on_click(Arc::new(
-                                  //             |workspace, cx| workspace.debug_toggle_livestream(cx),
-                                  //         )),
-                                  //     )
-                                  // })
-                                  // .child(
-                                  //     Button::<Workspace>::new("Toggle Debug")
-                                  //         .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))),
-                                  // ),
-                )
-        })
+        let mut context = KeyContext::default();
+        context.add("Workspace");
+        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
+
+        self.add_workspace_actions_listeners(div())
+            .key_context(context)
+            .relative()
+            .size_full()
+            .flex()
+            .flex_col()
+            .font(ui_font)
+            .gap_0()
+            .justify_start()
+            .items_start()
+            .text_color(cx.theme().colors().text)
+            .bg(cx.theme().colors().background)
+            .child(self.render_titlebar(cx))
+            .child(
+                // todo! should this be a component a view?
+                div()
+                    .id("workspace")
+                    .relative()
+                    .flex_1()
+                    .w_full()
+                    .flex()
+                    .overflow_hidden()
+                    .border_t()
+                    .border_b()
+                    .border_color(cx.theme().colors().border)
+                    .child(self.modal_layer.clone())
+                    .child(
+                        div()
+                            .flex()
+                            .flex_row()
+                            .flex_1()
+                            .h_full()
+                            // Left Dock
+                            .child(
+                                div()
+                                    .flex()
+                                    .flex_none()
+                                    .overflow_hidden()
+                                    .child(self.left_dock.clone()),
+                            )
+                            // Panes
+                            .child(
+                                div()
+                                    .flex()
+                                    .flex_col()
+                                    .flex_1()
+                                    .child(self.center.render(
+                                        &self.project,
+                                        &self.follower_states,
+                                        self.active_call(),
+                                        &self.active_pane,
+                                        self.zoomed.as_ref(),
+                                        &self.app_state,
+                                        cx,
+                                    ))
+                                    .child(div().flex().flex_1().child(self.bottom_dock.clone())),
+                            )
+                            // Right Dock
+                            .child(
+                                div()
+                                    .flex()
+                                    .flex_none()
+                                    .overflow_hidden()
+                                    .child(self.right_dock.clone()),
+                            ),
+                    ),
+            )
+            .child(self.status_bar.clone())
+            // .when(self.debug.show_toast, |this| {
+            //     this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
+            // })
+            // .children(
+            //     Some(
+            //         div()
+            //             .absolute()
+            //             .top(px(50.))
+            //             .left(px(640.))
+            //             .z_index(8)
+            //             .child(LanguageSelector::new("language-selector")),
+            //     )
+            //     .filter(|_| self.is_language_selector_open()),
+            // )
+            .z_index(8)
+            // Debug
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .z_index(9)
+                    .absolute()
+                    .top_20()
+                    .left_1_4()
+                    .w_40()
+                    .gap_2(), // .when(self.show_debug, |this| {
+                              //     this.child(Button::<Workspace>::new("Toggle User Settings").on_click(
+                              //         Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)),
+                              //     ))
+                              //     .child(
+                              //         Button::<Workspace>::new("Toggle Toasts").on_click(Arc::new(
+                              //             |workspace, cx| workspace.debug_toggle_toast(cx),
+                              //         )),
+                              //     )
+                              //     .child(
+                              //         Button::<Workspace>::new("Toggle Livestream").on_click(Arc::new(
+                              //             |workspace, cx| workspace.debug_toggle_livestream(cx),
+                              //         )),
+                              //     )
+                              // })
+                              // .child(
+                              //     Button::<Workspace>::new("Toggle Debug")
+                              //         .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))),
+                              // ),
+            )
     }
 }
 // todo!()

crates/zed2/Cargo.toml 🔗

@@ -36,14 +36,14 @@ copilot = { package = "copilot2", path = "../copilot2" }
 db = { package = "db2", path = "../db2" }
 editor = { package="editor2", path = "../editor2" }
 # feedback = { path = "../feedback" }
-# file_finder = { path = "../file_finder" }
+file_finder = { package="file_finder2", path = "../file_finder2" }
 search = { package = "search2", path = "../search2" }
 fs = { package = "fs2", path = "../fs2" }
 fsevent = { path = "../fsevent" }
 fuzzy = { path = "../fuzzy" }
 go_to_line = { package = "go_to_line2", path = "../go_to_line2" }
 gpui = { package = "gpui2", path = "../gpui2" }
-install_cli = { path = "../install_cli" }
+install_cli = { package = "install_cli2", path = "../install_cli2" }
 journal = { package = "journal2", path = "../journal2" }
 language = { package = "language2", path = "../language2" }
 # language_selector = { path = "../language_selector" }
@@ -55,7 +55,7 @@ node_runtime = { path = "../node_runtime" }
 # outline = { path = "../outline" }
 # plugin_runtime = { path = "../plugin_runtime",optional = true }
 project = { package = "project2", path = "../project2" }
-# project_panel = { path = "../project_panel" }
+project_panel = { package = "project_panel2", path = "../project_panel2" }
 # project_symbols = { path = "../project_symbols" }
 # quick_action_bar = { path = "../quick_action_bar" }
 # recent_projects = { path = "../recent_projects" }

crates/zed2/src/main.rs 🔗

@@ -9,6 +9,7 @@ use backtrace::Backtrace;
 use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
 use client::UserStore;
 use db::kvp::KEY_VALUE_STORE;
+use editor::Editor;
 use fs::RealFs;
 use futures::StreamExt;
 use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
@@ -49,17 +50,16 @@ use util::{
 use uuid::Uuid;
 use workspace::{AppState, WorkspaceStore};
 use zed2::{
-    build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
-    languages, Assets, IsOnlyInstance, OpenListener, OpenRequest,
+    build_window_options, ensure_only_instance, handle_cli_connection, init_zed_actions,
+    initialize_workspace, languages, Assets, IsOnlyInstance, OpenListener, OpenRequest,
 };
 
 mod open_listener;
 
 fn main() {
-    //TODO!(figure out what the linker issues are here)
-    // https://github.com/rust-lang/rust/issues/47384
-    // https://github.com/mmastrac/rust-ctor/issues/280
-    menu::unused();
+    menu::init();
+    zed_actions::init();
+
     let http = http::client();
     init_paths();
     init_logger();
@@ -98,7 +98,7 @@ fn main() {
     let (listener, mut open_rx) = OpenListener::new();
     let listener = Arc::new(listener);
     let open_listener = listener.clone();
-    app.on_open_urls(move |urls, _| open_listener.open_urls(urls));
+    app.on_open_urls(move |urls, _| open_listener.open_urls(&urls));
     app.on_reopen(move |_cx| {
         // todo!("workspace")
         // if cx.has_global::<Weak<AppState>>() {
@@ -113,6 +113,8 @@ fn main() {
 
     app.run(move |cx| {
         cx.set_global(*RELEASE_CHANNEL);
+        cx.set_global(listener.clone());
+
         load_embedded_fonts(cx);
 
         let mut store = SettingsStore::default();
@@ -188,10 +190,10 @@ fn main() {
         // recent_projects::init(cx);
 
         go_to_line::init(cx);
-        // file_finder::init(cx);
+        file_finder::init(cx);
         // outline::init(cx);
         // project_symbols::init(cx);
-        // project_panel::init(Assets, cx);
+        project_panel::init(Assets, cx);
         // channel::init(&client, user_store.clone(), cx);
         // diagnostics::init(cx);
         search::init(cx);
@@ -211,12 +213,13 @@ fn main() {
         // zed::init(&app_state, cx);
 
         // cx.set_menus(menus::menus());
+        init_zed_actions(app_state.clone(), cx);
 
         if stdout_is_a_pty() {
             cx.activate(true);
             let urls = collect_url_args();
             if !urls.is_empty() {
-                listener.open_urls(urls)
+                listener.open_urls(&urls)
             }
         } else {
             upload_previous_panics(http.clone(), cx);
@@ -226,7 +229,7 @@ fn main() {
             if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some()
                 && !listener.triggered.load(Ordering::Acquire)
             {
-                listener.open_urls(collect_url_args())
+                listener.open_urls(&collect_url_args())
             }
         }
 
@@ -357,8 +360,7 @@ async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncApp
         } else {
             cx.update(|cx| {
                 workspace::open_new(app_state, cx, |workspace, cx| {
-                    // todo!(editor)
-                    // Editor::new_file(workspace, &Default::default(), cx)
+                    Editor::new_file(workspace, &Default::default(), cx)
                 })
                 .detach();
             })?;

crates/zed2/src/open_listener.rs 🔗

@@ -54,7 +54,7 @@ impl OpenListener {
         )
     }
 
-    pub fn open_urls(&self, urls: Vec<String>) {
+    pub fn open_urls(&self, urls: &[String]) {
         self.triggered.store(true, Ordering::Release);
         let request = if let Some(server_name) =
             urls.first().and_then(|url| url.strip_prefix("zed-cli://"))
@@ -101,7 +101,7 @@ impl OpenListener {
         None
     }
 
-    fn handle_file_urls(&self, urls: Vec<String>) -> Option<OpenRequest> {
+    fn handle_file_urls(&self, urls: &[String]) -> Option<OpenRequest> {
         let paths: Vec<_> = urls
             .iter()
             .flat_map(|url| url.strip_prefix("file://"))

crates/zed2/src/zed2.rs 🔗

@@ -1,5 +1,5 @@
-#![allow(unused_variables, dead_code, unused_mut)]
-// todo!() this is to make transition easier.
+#![allow(unused_variables, unused_mut)]
+//todo!()
 
 mod assets;
 pub mod languages;
@@ -7,17 +7,56 @@ mod only_instance;
 mod open_listener;
 
 pub use assets::*;
+use collections::VecDeque;
+use editor::{Editor, MultiBuffer};
 use gpui::{
-    point, px, AppContext, AsyncWindowContext, Task, TitlebarOptions, VisualContext as _, WeakView,
-    WindowBounds, WindowKind, WindowOptions,
+    actions, point, px, AppContext, AsyncWindowContext, Context, PromptLevel, Task,
+    TitlebarOptions, ViewContext, VisualContext, WeakView, WindowBounds, WindowKind, WindowOptions,
 };
 pub use only_instance::*;
 pub use open_listener::*;
 
-use anyhow::Result;
-use std::sync::Arc;
+use anyhow::{anyhow, Context as _, Result};
+use project_panel::ProjectPanel;
+use settings::{initial_local_settings_content, Settings};
+use std::{borrow::Cow, ops::Deref, sync::Arc};
+use util::{
+    asset_str,
+    channel::ReleaseChannel,
+    paths::{self, LOCAL_SETTINGS_RELATIVE_PATH},
+    ResultExt,
+};
 use uuid::Uuid;
-use workspace::{AppState, Workspace};
+use workspace::{
+    create_and_open_local_file, dock::PanelHandle,
+    notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile,
+    NewWindow, Workspace, WorkspaceSettings,
+};
+use zed_actions::{OpenBrowser, OpenZedURL};
+
+actions!(
+    About,
+    DebugElements,
+    DecreaseBufferFontSize,
+    Hide,
+    HideOthers,
+    IncreaseBufferFontSize,
+    Minimize,
+    OpenDefaultKeymap,
+    OpenDefaultSettings,
+    OpenKeymap,
+    OpenLicenses,
+    OpenLocalSettings,
+    OpenLog,
+    OpenSettings,
+    OpenTelemetryLog,
+    Quit,
+    ResetBufferFontSize,
+    ResetDatabase,
+    ShowAll,
+    ToggleFullScreen,
+    Zoom,
+);
 
 pub fn build_window_options(
     bounds: Option<WindowBounds>,
@@ -47,6 +86,211 @@ pub fn build_window_options(
     }
 }
 
+pub fn init_zed_actions(app_state: Arc<AppState>, cx: &mut AppContext) {
+    cx.observe_new_views(move |workspace: &mut Workspace, _cx| {
+        workspace
+            .register_action(about)
+            .register_action(|_, _: &Hide, cx| {
+                cx.hide();
+            })
+            .register_action(|_, _: &HideOthers, cx| {
+                cx.hide_other_apps();
+            })
+            .register_action(|_, _: &ShowAll, cx| {
+                cx.unhide_other_apps();
+            })
+            .register_action(|_, _: &Minimize, cx| {
+                cx.minimize_window();
+            })
+            .register_action(|_, _: &Zoom, cx| {
+                cx.zoom_window();
+            })
+            .register_action(|_, _: &ToggleFullScreen, cx| {
+                cx.toggle_full_screen();
+            })
+            .register_action(quit)
+            .register_action(|_, action: &OpenZedURL, cx| {
+                cx.global::<Arc<OpenListener>>()
+                    .open_urls(&[action.url.clone()])
+            })
+            .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url))
+            //todo!(buffer font size)
+            // cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
+            //     theme::adjust_font_size(cx, |size| *size += 1.0)
+            // });
+            // cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
+            //     theme::adjust_font_size(cx, |size| *size -= 1.0)
+            // });
+            // cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx));
+            .register_action(|_, _: &install_cli::Install, cx| {
+                cx.spawn(|_, cx| async move {
+                    install_cli::install_cli(cx.deref())
+                        .await
+                        .context("error creating CLI symlink")
+                })
+                .detach_and_log_err(cx);
+            })
+            .register_action(|workspace, _: &OpenLog, cx| {
+                open_log_file(workspace, cx);
+            })
+            .register_action(|workspace, _: &OpenLicenses, cx| {
+                open_bundled_file(
+                    workspace,
+                    asset_str::<Assets>("licenses.md"),
+                    "Open Source License Attribution",
+                    "Markdown",
+                    cx,
+                );
+            })
+            .register_action(
+                move |workspace: &mut Workspace,
+                      _: &OpenTelemetryLog,
+                      cx: &mut ViewContext<Workspace>| {
+                    open_telemetry_log_file(workspace, cx);
+                },
+            )
+            .register_action(
+                move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
+                    create_and_open_local_file(&paths::KEYMAP, cx, Default::default)
+                        .detach_and_log_err(cx);
+                },
+            )
+            .register_action(
+                move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
+                    create_and_open_local_file(&paths::SETTINGS, cx, || {
+                        settings::initial_user_settings_content().as_ref().into()
+                    })
+                    .detach_and_log_err(cx);
+                },
+            )
+            .register_action(open_local_settings_file)
+            .register_action(
+                move |workspace: &mut Workspace,
+                      _: &OpenDefaultKeymap,
+                      cx: &mut ViewContext<Workspace>| {
+                    open_bundled_file(
+                        workspace,
+                        settings::default_keymap(),
+                        "Default Key Bindings",
+                        "JSON",
+                        cx,
+                    );
+                },
+            )
+            .register_action(
+                move |workspace: &mut Workspace,
+                      _: &OpenDefaultSettings,
+                      cx: &mut ViewContext<Workspace>| {
+                    open_bundled_file(
+                        workspace,
+                        settings::default_settings(),
+                        "Default Settings",
+                        "JSON",
+                        cx,
+                    );
+                },
+            )
+            //todo!()
+            // cx.add_action({
+            //     move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
+            //         let app_state = workspace.app_state().clone();
+            //         let markdown = app_state.languages.language_for_name("JSON");
+            //         let window = cx.window();
+            //         cx.spawn(|workspace, mut cx| async move {
+            //             let markdown = markdown.await.log_err();
+            //             let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| {
+            //                 anyhow!("could not debug elements for window {}", window.id())
+            //             })?)
+            //             .unwrap();
+            //             workspace
+            //                 .update(&mut cx, |workspace, cx| {
+            //                     workspace.with_local_workspace(cx, move |workspace, cx| {
+            //                         let project = workspace.project().clone();
+            //                         let buffer = project
+            //                             .update(cx, |project, cx| {
+            //                                 project.create_buffer(&content, markdown, cx)
+            //                             })
+            //                             .expect("creating buffers on a local workspace always succeeds");
+            //                         let buffer = cx.add_model(|cx| {
+            //                             MultiBuffer::singleton(buffer, cx)
+            //                                 .with_title("Debug Elements".into())
+            //                         });
+            //                         workspace.add_item(
+            //                             Box::new(cx.add_view(|cx| {
+            //                                 Editor::for_multibuffer(buffer, Some(project.clone()), cx)
+            //                             })),
+            //                             cx,
+            //                         );
+            //                     })
+            //                 })?
+            //                 .await
+            //         })
+            //         .detach_and_log_err(cx);
+            //     }
+            // });
+            // .register_action(
+            //     |workspace: &mut Workspace,
+            //      _: &project_panel::ToggleFocus,
+            //      cx: &mut ViewContext<Workspace>| {
+            //         workspace.toggle_panel_focus::<ProjectPanel>(cx);
+            //     },
+            // );
+            // cx.add_action(
+            //     |workspace: &mut Workspace,
+            //      _: &collab_ui::collab_panel::ToggleFocus,
+            //      cx: &mut ViewContext<Workspace>| {
+            //         workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
+            //     },
+            // );
+            // cx.add_action(
+            //     |workspace: &mut Workspace,
+            //      _: &collab_ui::chat_panel::ToggleFocus,
+            //      cx: &mut ViewContext<Workspace>| {
+            //         workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(cx);
+            //     },
+            // );
+            // cx.add_action(
+            //     |workspace: &mut Workspace,
+            //      _: &collab_ui::notification_panel::ToggleFocus,
+            //      cx: &mut ViewContext<Workspace>| {
+            //         workspace.toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(cx);
+            //     },
+            // );
+            // cx.add_action(
+            //     |workspace: &mut Workspace,
+            //      _: &terminal_panel::ToggleFocus,
+            //      cx: &mut ViewContext<Workspace>| {
+            //         workspace.toggle_panel_focus::<TerminalPanel>(cx);
+            //     },
+            // );
+            .register_action({
+                let app_state = Arc::downgrade(&app_state);
+                move |_, _: &NewWindow, cx| {
+                    if let Some(app_state) = app_state.upgrade() {
+                        open_new(&app_state, cx, |workspace, cx| {
+                            Editor::new_file(workspace, &Default::default(), cx)
+                        })
+                        .detach();
+                    }
+                }
+            })
+            .register_action({
+                let app_state = Arc::downgrade(&app_state);
+                move |_, _: &NewFile, cx| {
+                    if let Some(app_state) = app_state.upgrade() {
+                        open_new(&app_state, cx, |workspace, cx| {
+                            Editor::new_file(workspace, &Default::default(), cx)
+                        })
+                        .detach();
+                    }
+                }
+            });
+        //todo!()
+        // load_default_keymap(cx);
+    })
+    .detach();
+}
+
 pub fn initialize_workspace(
     workspace_handle: WeakView<Workspace>,
     was_deserialized: bool,
@@ -55,7 +299,7 @@ pub fn initialize_workspace(
 ) -> Task<Result<()>> {
     cx.spawn(|mut cx| async move {
         workspace_handle.update(&mut cx, |workspace, cx| {
-            let workspace_handle = cx.view();
+            let workspace_handle = cx.view().clone();
             cx.subscribe(&workspace_handle, {
                 move |workspace, _, event, cx| {
                     if let workspace::Event::PaneAdded(pane) = event {
@@ -138,49 +382,38 @@ pub fn initialize_workspace(
             //         }
             //         false
             //     });
-            // })?;
-
-            // let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
-            // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
-            // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
-            // let channels_panel =
-            //     collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
-            // let chat_panel =
-            //     collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
-            // let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
-            //     workspace_handle.clone(),
-            //     cx.clone(),
-            // );
-            // let (
-            //     project_panel,
+        })?;
+
+        let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
+        // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
+        // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
+        // let channels_panel =
+        //     collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
+        // let chat_panel =
+        //     collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
+        // let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
+        //     workspace_handle.clone(),
+        //     cx.clone(),
+        // );
+        let (
+            project_panel,
             //     terminal_panel,
             //     assistant_panel,
             //     channels_panel,
             //     chat_panel,
             //     notification_panel,
-            // ) = futures::try_join!(
-            //     project_panel,
+        ) = futures::try_join!(
+            project_panel,
             //     terminal_panel,
             //     assistant_panel,
             //     channels_panel,
             //     chat_panel,
             //     notification_panel,
-            // )?;
-            // workspace_handle.update(&mut cx, |workspace, cx| {
-            //     let project_panel_position = project_panel.position(cx);
-            //     workspace.add_panel_with_extra_event_handler(
-            //         project_panel,
-            //         cx,
-            //         |workspace, _, event, cx| match event {
-            //             project_panel::Event::NewSearchInDirectory { dir_entry } => {
-            //                 search::ProjectSearchView::new_search_in_directory(workspace, dir_entry, cx)
-            //             }
-            //             project_panel::Event::ActivatePanel => {
-            //                 workspace.focus_panel::<ProjectPanel>(cx);
-            //             }
-            //             _ => {}
-            //         },
-            //     );
+        )?;
+
+        workspace_handle.update(&mut cx, |workspace, cx| {
+            let project_panel_position = project_panel.position(cx);
+            workspace.add_panel(project_panel, cx);
             //     workspace.add_panel(terminal_panel, cx);
             //     workspace.add_panel(assistant_panel, cx);
             //     workspace.add_panel(channels_panel, cx);
@@ -198,10 +431,286 @@ pub fn initialize_workspace(
             //                     .map_or(false, |entry| entry.is_dir())
             //             })
             //     {
-            //         workspace.toggle_dock(project_panel_position, cx);
+            workspace.toggle_dock(project_panel_position, cx);
             //     }
-            //     cx.focus_self();
+            // cx.focus_self();
         })?;
         Ok(())
     })
 }
+
+fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
+    let app_name = cx.global::<ReleaseChannel>().display_name();
+    let version = env!("CARGO_PKG_VERSION");
+    let prompt = cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]);
+    cx.foreground_executor()
+        .spawn(async {
+            prompt.await.ok();
+        })
+        .detach();
+}
+
+fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext<Workspace>) {
+    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
+    cx.spawn(|_, mut cx| async move {
+        let mut workspace_windows = cx.update(|_, cx| {
+            cx.windows()
+                .into_iter()
+                .filter_map(|window| window.downcast::<Workspace>())
+                .collect::<Vec<_>>()
+        })?;
+
+        // If multiple windows have unsaved changes, and need a save prompt,
+        // prompt in the active window before switching to a different window.
+        cx.update(|_, cx| {
+            workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
+        })
+        .log_err();
+
+        if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) {
+            let answer = cx
+                .update(|_, cx| {
+                    cx.prompt(
+                        PromptLevel::Info,
+                        "Are you sure you want to quit?",
+                        &["Quit", "Cancel"],
+                    )
+                })
+                .log_err();
+
+            if let Some(mut answer) = answer {
+                let answer = answer.await.ok();
+                if answer != Some(0) {
+                    return Ok(());
+                }
+            }
+        }
+
+        // If the user cancels any save prompt, then keep the app open.
+        for window in workspace_windows {
+            if let Some(should_close) = window
+                .update(&mut cx, |workspace, cx| {
+                    workspace.prepare_to_close(true, cx)
+                })
+                .log_err()
+            {
+                if !should_close.await? {
+                    return Ok(());
+                }
+            }
+        }
+        cx.update(|_, cx| {
+            cx.quit();
+        })?;
+        anyhow::Ok(())
+    })
+    .detach_and_log_err(cx);
+}
+
+fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+    const MAX_LINES: usize = 1000;
+    workspace
+        .with_local_workspace(cx, move |workspace, cx| {
+            let fs = workspace.app_state().fs.clone();
+            cx.spawn(|workspace, mut cx| async move {
+                let (old_log, new_log) =
+                    futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG));
+
+                let mut lines = VecDeque::with_capacity(MAX_LINES);
+                for line in old_log
+                    .iter()
+                    .flat_map(|log| log.lines())
+                    .chain(new_log.iter().flat_map(|log| log.lines()))
+                {
+                    if lines.len() == MAX_LINES {
+                        lines.pop_front();
+                    }
+                    lines.push_back(line);
+                }
+                let log = lines
+                    .into_iter()
+                    .flat_map(|line| [line, "\n"])
+                    .collect::<String>();
+
+                workspace
+                    .update(&mut cx, |workspace, cx| {
+                        let project = workspace.project().clone();
+                        let buffer = project
+                            .update(cx, |project, cx| project.create_buffer("", None, cx))
+                            .expect("creating buffers on a local workspace always succeeds");
+                        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
+
+                        let buffer = cx.build_model(|cx| {
+                            MultiBuffer::singleton(buffer, cx).with_title("Log".into())
+                        });
+                        workspace.add_item(
+                            Box::new(cx.build_view(|cx| {
+                                Editor::for_multibuffer(buffer, Some(project), cx)
+                            })),
+                            cx,
+                        );
+                    })
+                    .log_err();
+            })
+            .detach();
+        })
+        .detach();
+}
+
+fn open_local_settings_file(
+    workspace: &mut Workspace,
+    _: &OpenLocalSettings,
+    cx: &mut ViewContext<Workspace>,
+) {
+    let project = workspace.project().clone();
+    let worktree = project
+        .read(cx)
+        .visible_worktrees(cx)
+        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
+    if let Some(worktree) = worktree {
+        let tree_id = worktree.read(cx).id();
+        cx.spawn(|workspace, mut cx| async move {
+            let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH;
+
+            if let Some(dir_path) = file_path.parent() {
+                if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
+                    project
+                        .update(&mut cx, |project, cx| {
+                            project.create_entry((tree_id, dir_path), true, cx)
+                        })?
+                        .ok_or_else(|| anyhow!("worktree was removed"))?
+                        .await?;
+                }
+            }
+
+            if worktree.update(&mut cx, |tree, _| tree.entry_for_path(file_path).is_none())? {
+                project
+                    .update(&mut cx, |project, cx| {
+                        project.create_entry((tree_id, file_path), false, cx)
+                    })?
+                    .ok_or_else(|| anyhow!("worktree was removed"))?
+                    .await?;
+            }
+
+            let editor = workspace
+                .update(&mut cx, |workspace, cx| {
+                    workspace.open_path((tree_id, file_path), None, true, cx)
+                })?
+                .await?
+                .downcast::<Editor>()
+                .ok_or_else(|| anyhow!("unexpected item type"))?;
+
+            editor
+                .downgrade()
+                .update(&mut cx, |editor, cx| {
+                    if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
+                        if buffer.read(cx).is_empty() {
+                            buffer.update(cx, |buffer, cx| {
+                                buffer.edit([(0..0, initial_local_settings_content())], None, cx)
+                            });
+                        }
+                    }
+                })
+                .ok();
+
+            anyhow::Ok(())
+        })
+        .detach();
+    } else {
+        workspace.show_notification(0, cx, |cx| {
+            cx.build_view(|_| MessageNotification::new("This project has no folders open."))
+        })
+    }
+}
+
+fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+    workspace.with_local_workspace(cx, move |workspace, cx| {
+        let app_state = workspace.app_state().clone();
+        cx.spawn(|workspace, mut cx| async move {
+            async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
+                let path = app_state.client.telemetry().log_file_path()?;
+                app_state.fs.load(&path).await.log_err()
+            }
+
+            let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
+
+            const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
+            let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
+            if let Some(newline_offset) = log[start_offset..].find('\n') {
+                start_offset += newline_offset + 1;
+            }
+            let log_suffix = &log[start_offset..];
+            let json = app_state.languages.language_for_name("JSON").await.log_err();
+
+            workspace.update(&mut cx, |workspace, cx| {
+                let project = workspace.project().clone();
+                let buffer = project
+                    .update(cx, |project, cx| project.create_buffer("", None, cx))
+                    .expect("creating buffers on a local workspace always succeeds");
+                buffer.update(cx, |buffer, cx| {
+                    buffer.set_language(json, cx);
+                    buffer.edit(
+                        [(
+                            0..0,
+                            concat!(
+                                "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
+                                "// Telemetry can be disabled via the `settings.json` file.\n",
+                                "// Here is the data that has been reported for the current session:\n",
+                                "\n"
+                            ),
+                        )],
+                        None,
+                        cx,
+                    );
+                    buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
+                });
+
+                let buffer = cx.build_model(|cx| {
+                    MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
+                });
+                workspace.add_item(
+                    Box::new(cx.build_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
+                    cx,
+                );
+            }).log_err()?;
+
+            Some(())
+        })
+        .detach();
+    }).detach();
+}
+
+fn open_bundled_file(
+    workspace: &mut Workspace,
+    text: Cow<'static, str>,
+    title: &'static str,
+    language: &'static str,
+    cx: &mut ViewContext<Workspace>,
+) {
+    let language = workspace.app_state().languages.language_for_name(language);
+    cx.spawn(|workspace, mut cx| async move {
+        let language = language.await.log_err();
+        workspace
+            .update(&mut cx, |workspace, cx| {
+                workspace.with_local_workspace(cx, |workspace, cx| {
+                    let project = workspace.project();
+                    let buffer = project.update(cx, move |project, cx| {
+                        project
+                            .create_buffer(text.as_ref(), language, cx)
+                            .expect("creating buffers on a local workspace always succeeds")
+                    });
+                    let buffer = cx.build_model(|cx| {
+                        MultiBuffer::singleton(buffer, cx).with_title(title.into())
+                    });
+                    workspace.add_item(
+                        Box::new(cx.build_view(|cx| {
+                            Editor::for_multibuffer(buffer, Some(project.clone()), cx)
+                        })),
+                        cx,
+                    );
+                })
+            })?
+            .await
+    })
+    .detach_and_log_err(cx);
+}

crates/zed_actions2/src/lib.rs 🔗

@@ -1,33 +1,19 @@
-use gpui::{action, actions};
+use gpui::action;
 
-actions!(
-    About,
-    DebugElements,
-    DecreaseBufferFontSize,
-    Hide,
-    HideOthers,
-    IncreaseBufferFontSize,
-    Minimize,
-    OpenDefaultKeymap,
-    OpenDefaultSettings,
-    OpenKeymap,
-    OpenLicenses,
-    OpenLocalSettings,
-    OpenLog,
-    OpenSettings,
-    OpenTelemetryLog,
-    Quit,
-    ResetBufferFontSize,
-    ResetDatabase,
-    ShowAll,
-    ToggleFullScreen,
-    Zoom,
-);
+// If the zed binary doesn't use anything in this crate, it will be optimized away
+// and the actions won't initialize. So we just provide an empty initialization function
+// to be called from main.
+//
+// These may provide relevant context:
+// https://github.com/rust-lang/rust/issues/47384
+// https://github.com/mmastrac/rust-ctor/issues/280
+pub fn init() {}
 
 #[action]
 pub struct OpenBrowser {
     pub url: String,
 }
+
 #[action]
 pub struct OpenZedURL {
     pub url: String,