Merge branch 'main' into feedback-2

Joseph T. Lyons created

Change summary

Cargo.lock                                                     |  137 
Cargo.toml                                                     |    6 
assets/icons/copy.svg                                          |    1 
assets/keymaps/default.json                                    |    7 
crates/ai2/src/auth.rs                                         |    2 
crates/ai2/src/providers/open_ai/embedding.rs                  |    4 
crates/assistant/src/assistant_panel.rs                        |   39 
crates/auto_update2/src/auto_update.rs                         |    2 
crates/breadcrumbs2/Cargo.toml                                 |   28 
crates/breadcrumbs2/src/breadcrumbs.rs                         |  204 
crates/call2/Cargo.toml                                        |    4 
crates/call2/src/call2.rs                                      |  257 
crates/call2/src/participant.rs                                |    2 
crates/client/src/client.rs                                    |    2 
crates/collab/Cargo.toml                                       |    2 
crates/collab/src/db/queries/channels.rs                       |    7 
crates/collab/src/db/tests/channel_tests.rs                    |   14 
crates/collab/src/tests/following_tests.rs                     |  137 
crates/collab/src/tests/integration_tests.rs                   |   27 
crates/collab/src/tests/random_project_collaboration_tests.rs  |    1 
crates/collab2/src/db/queries/channels.rs                      |    7 
crates/collab2/src/db/tests/channel_tests.rs                   |   16 
crates/collab2/src/tests/channel_tests.rs                      |   37 
crates/collab2/src/tests/following_tests.rs                    |  425 
crates/collab2/src/tests/integration_tests.rs                  |   43 
crates/collab2/src/tests/random_project_collaboration_tests.rs |    1 
crates/collab2/src/tests/test_server.rs                        |    1 
crates/collab_ui2/src/collab_panel.rs                          | 1034 
crates/collab_ui2/src/collab_panel/channel_modal.rs            |  738 
crates/collab_ui2/src/collab_titlebar_item.rs                  |  413 
crates/collab_ui2/src/collab_ui.rs                             |  115 
crates/command_palette2/src/command_palette.rs                 |    6 
crates/copilot2/Cargo.toml                                     |    2 
crates/copilot2/src/copilot2.rs                                |  454 
crates/copilot_button2/Cargo.toml                              |   27 
crates/copilot_button2/src/copilot_button.rs                   |  370 
crates/diagnostics2/src/diagnostics.rs                         |   72 
crates/editor2/src/display_map.rs                              | 1768 
crates/editor2/src/display_map/block_map.rs                    | 1338 
crates/editor2/src/display_map/wrap_map.rs                     |  752 
crates/editor2/src/editor.rs                                   |  596 
crates/editor2/src/editor_tests.rs                             | 1588 
crates/editor2/src/element.rs                                  | 1167 
crates/editor2/src/git.rs                                      |  364 
crates/editor2/src/highlight_matching_bracket.rs               |  200 
crates/editor2/src/inlay_hint_cache.rs                         |  173 
crates/editor2/src/items.rs                                    |  744 
crates/editor2/src/link_go_to_definition.rs                    | 1337 
crates/editor2/src/mouse_context_menu.rs                       |  168 
crates/editor2/src/movement.rs                                 |  952 
crates/editor2/src/selections_collection.rs                    |    3 
crates/file_finder2/src/file_finder.rs                         |    2 
crates/gpui2/src/app.rs                                        |   37 
crates/gpui2/src/app/entity_map.rs                             |   19 
crates/gpui2/src/app/model_context.rs                          |   24 
crates/gpui2/src/app/test_context.rs                           |  102 
crates/gpui2/src/elements/canvas.rs                            |   48 
crates/gpui2/src/elements/div.rs                               |   40 
crates/gpui2/src/elements/mod.rs                               |    2 
crates/gpui2/src/elements/text.rs                              |   38 
crates/gpui2/src/elements/uniform_list.rs                      |    5 
crates/gpui2/src/executor.rs                                   |   42 
crates/gpui2/src/geometry.rs                                   |   14 
crates/gpui2/src/gpui2.rs                                      |    2 
crates/gpui2/src/key_dispatch.rs                               |   20 
crates/gpui2/src/platform.rs                                   |   27 
crates/gpui2/src/platform/mac/platform.rs                      |   29 
crates/gpui2/src/platform/test/dispatcher.rs                   |   17 
crates/gpui2/src/platform/test/platform.rs                     |   22 
crates/gpui2/src/platform/test/window.rs                       |   24 
crates/gpui2/src/scene.rs                                      |    4 
crates/gpui2/src/style.rs                                      |    3 
crates/gpui2/src/styled.rs                                     |  119 
crates/gpui2/src/subscription.rs                               |   50 
crates/gpui2/src/taffy.rs                                      |   49 
crates/gpui2/src/test.rs                                       |   31 
crates/gpui2/src/text_system.rs                                |   32 
crates/gpui2/src/text_system/line.rs                           |    4 
crates/gpui2/src/view.rs                                       |    8 
crates/gpui2/src/window.rs                                     |  160 
crates/language2/src/outline.rs                                |    1 
crates/language_selector2/Cargo.toml                           |   26 
crates/language_selector2/src/active_buffer_language.rs        |   82 
crates/language_selector2/src/language_selector.rs             |  232 
crates/outline2/Cargo.toml                                     |   29 
crates/outline2/src/outline.rs                                 |  276 
crates/picker2/src/picker2.rs                                  |    9 
crates/project/src/project.rs                                  |  206 
crates/project/src/project_tests.rs                            |   88 
crates/project/src/search.rs                                   |   22 
crates/project/src/worktree.rs                                 |  173 
crates/project/src/worktree_tests.rs                           |   36 
crates/project2/src/project2.rs                                |  207 
crates/project2/src/project_tests.rs                           |   88 
crates/project2/src/search.rs                                  |   22 
crates/project2/src/worktree.rs                                |  172 
crates/project2/src/worktree_tests.rs                          |   36 
crates/project_panel/src/project_panel.rs                      |   69 
crates/project_panel2/src/project_panel.rs                     |  104 
crates/rpc/proto/zed.proto                                     |    4 
crates/rpc/src/rpc.rs                                          |    2 
crates/rpc2/proto/zed.proto                                    |    4 
crates/rpc2/src/rpc.rs                                         |    2 
crates/semantic_index2/Cargo.toml                              |   69 
crates/semantic_index2/README.md                               |   20 
crates/semantic_index2/eval/gpt-engineer.json                  |  114 
crates/semantic_index2/eval/tree-sitter.json                   |  104 
crates/semantic_index2/src/db.rs                               |  603 
crates/semantic_index2/src/embedding_queue.rs                  |  169 
crates/semantic_index2/src/parsing.rs                          |  414 
crates/semantic_index2/src/semantic_index.rs                   | 1280 
crates/semantic_index2/src/semantic_index_settings.rs          |   28 
crates/semantic_index2/src/semantic_index_tests.rs             | 1697 +
crates/storybook2/src/stories.rs                               |    4 
crates/storybook2/src/stories/auto_height_editor.rs            |   34 
crates/storybook2/src/stories/cursor.rs                        |  111 
crates/storybook2/src/stories/text.rs                          |   14 
crates/storybook2/src/story_selector.rs                        |    6 
crates/terminal_view/src/terminal_view.rs                      |    1 
crates/terminal_view2/src/terminal_view.rs                     |    8 
crates/theme2/src/one_themes.rs                                |    4 
crates/theme_selector2/src/theme_selector.rs                   |   14 
crates/ui2/src/components.rs                                   |    4 
crates/ui2/src/components/button.rs                            |    1 
crates/ui2/src/components/button/button.rs                     |   88 
crates/ui2/src/components/button/button_icon.rs                |   84 
crates/ui2/src/components/button/button_like.rs                |  105 
crates/ui2/src/components/button/icon_button.rs                |   41 
crates/ui2/src/components/context_menu.rs                      |  290 
crates/ui2/src/components/icon.rs                              |    4 
crates/ui2/src/components/keybinding.rs                        |   15 
crates/ui2/src/components/label.rs                             |   54 
crates/ui2/src/components/list.rs                              |   66 
crates/ui2/src/components/list/list.rs                         |   60 
crates/ui2/src/components/list/list_header.rs                  |   42 
crates/ui2/src/components/list/list_item.rs                    |   11 
crates/ui2/src/components/popover_menu.rs                      |  231 
crates/ui2/src/components/right_click_menu.rs                  |  185 
crates/ui2/src/components/stories.rs                           |    2 
crates/ui2/src/components/stories/button.rs                    |   20 
crates/ui2/src/components/stories/context_menu.rs              |   42 
crates/ui2/src/components/stories/icon_button.rs               |    8 
crates/ui2/src/components/stories/list.rs                      |    4 
crates/ui2/src/components/stories/list_header.rs               |   33 
crates/ui2/src/styled_ext.rs                                   |   14 
crates/util/src/paths.rs                                       |   18 
crates/workspace/src/pane.rs                                   |   17 
crates/workspace/src/workspace.rs                              |    4 
crates/workspace2/Cargo.toml                                   |   44 
crates/workspace2/src/dock.rs                                  |   12 
crates/workspace2/src/item.rs                                  |  664 
crates/workspace2/src/notifications.rs                         |   57 
crates/workspace2/src/pane.rs                                  |  374 
crates/workspace2/src/pane_group.rs                            |  109 
crates/workspace2/src/persistence.rs                           |    4 
crates/workspace2/src/persistence/model.rs                     |    4 
crates/workspace2/src/searchable.rs                            |    2 
crates/workspace2/src/shared_screen.rs                         |    9 
crates/workspace2/src/status_bar.rs                            |   45 
crates/workspace2/src/toolbar.rs                               |   17 
crates/workspace2/src/workspace2.rs                            |  692 
crates/workspace2/src/workspace_settings.rs                    |    2 
crates/zed/Cargo.toml                                          |    2 
crates/zed/resources/app-icon-nightly.png                      |    0 
crates/zed/resources/app-icon-nightly@2x.png                   |    0 
crates/zed/src/only_instance.rs                                |    2 
crates/zed/src/zed.rs                                          |  126 
crates/zed2/Cargo.toml                                         |   10 
crates/zed2/resources/app-icon-nightly.png                     |    0 
crates/zed2/resources/app-icon-nightly@2x.png                  |    0 
crates/zed2/src/main.rs                                        |    5 
crates/zed2/src/only_instance.rs                               |    2 
crates/zed2/src/zed2.rs                                        | 1876 +
test.rs                                                        | 5618 ----
174 files changed, 18,975 insertions(+), 15,496 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1095,6 +1095,23 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "breadcrumbs2"
+version = "0.1.0"
+dependencies = [
+ "collections",
+ "editor2",
+ "gpui2",
+ "itertools 0.10.5",
+ "language2",
+ "project2",
+ "search2",
+ "settings2",
+ "theme2",
+ "ui2",
+ "workspace2",
+]
+
 [[package]]
 name = "bromberg_sl2"
 version = "0.6.0"
@@ -1205,7 +1222,6 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-broadcast",
- "async-trait",
  "audio2",
  "client2",
  "collections",
@@ -1225,9 +1241,7 @@ dependencies = [
  "serde_json",
  "settings2",
  "smallvec",
- "ui2",
  "util",
- "workspace2",
 ]
 
 [[package]]
@@ -1688,7 +1702,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.29.0"
+version = "0.29.1"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -2098,7 +2112,7 @@ dependencies = [
  "lsp2",
  "node_runtime",
  "parking_lot 0.11.2",
- "rpc",
+ "rpc2",
  "serde",
  "serde_derive",
  "settings2",
@@ -2126,6 +2140,25 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "copilot_button2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "copilot2",
+ "editor2",
+ "fs2",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "settings2",
+ "smol",
+ "theme2",
+ "util",
+ "workspace2",
+ "zed_actions2",
+]
+
 [[package]]
 name = "core-foundation"
 version = "0.9.3"
@@ -4805,6 +4838,24 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "language_selector2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "editor2",
+ "fuzzy2",
+ "gpui2",
+ "language2",
+ "picker2",
+ "project2",
+ "settings2",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "language_tools"
 version = "0.1.0"
@@ -6141,6 +6192,26 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "outline2"
+version = "0.1.0"
+dependencies = [
+ "editor2",
+ "fuzzy2",
+ "gpui2",
+ "language2",
+ "ordered-float 2.10.0",
+ "picker2",
+ "postage",
+ "settings2",
+ "smol",
+ "text2",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "overload"
 version = "0.1.1"
@@ -8192,6 +8263,57 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "semantic_index2"
+version = "0.1.0"
+dependencies = [
+ "ai2",
+ "anyhow",
+ "async-trait",
+ "client2",
+ "collections",
+ "ctor",
+ "env_logger 0.9.3",
+ "futures 0.3.28",
+ "globset",
+ "gpui2",
+ "language2",
+ "lazy_static",
+ "log",
+ "ndarray",
+ "node_runtime",
+ "ordered-float 2.10.0",
+ "parking_lot 0.11.2",
+ "postage",
+ "pretty_assertions",
+ "project2",
+ "rand 0.8.5",
+ "rpc2",
+ "rusqlite",
+ "rust-embed",
+ "schemars",
+ "serde",
+ "serde_json",
+ "settings2",
+ "sha1",
+ "smol",
+ "tempdir",
+ "tiktoken-rs",
+ "tree-sitter",
+ "tree-sitter-cpp",
+ "tree-sitter-elixir",
+ "tree-sitter-json 0.20.0",
+ "tree-sitter-lua",
+ "tree-sitter-php",
+ "tree-sitter-ruby",
+ "tree-sitter-rust",
+ "tree-sitter-toml",
+ "tree-sitter-typescript",
+ "unindent",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "semver"
 version = "1.0.18"
@@ -11491,7 +11613,6 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-recursion 1.0.5",
- "async-trait",
  "bincode",
  "call2",
  "client2",
@@ -11757,6 +11878,7 @@ dependencies = [
  "audio2",
  "auto_update2",
  "backtrace",
+ "breadcrumbs2",
  "call2",
  "channel2",
  "chrono",
@@ -11766,6 +11888,7 @@ dependencies = [
  "collections",
  "command_palette2",
  "copilot2",
+ "copilot_button2",
  "ctor",
  "db2",
  "diagnostics2",
@@ -11786,6 +11909,7 @@ dependencies = [
  "isahc",
  "journal2",
  "language2",
+ "language_selector2",
  "lazy_static",
  "libc",
  "log",
@@ -11793,6 +11917,7 @@ dependencies = [
  "menu2",
  "node_runtime",
  "num_cpus",
+ "outline2",
  "parking_lot 0.11.2",
  "postage",
  "project2",

Cargo.toml 🔗

@@ -9,6 +9,7 @@ members = [
     "crates/auto_update",
     "crates/auto_update2",
     "crates/breadcrumbs",
+    "crates/breadcrumbs2",
     "crates/call",
     "crates/call2",
     "crates/channel",
@@ -60,6 +61,7 @@ members = [
     "crates/language",
     "crates/language2",
     "crates/language_selector",
+    "crates/language_selector2",
     "crates/language_tools",
     "crates/live_kit_client",
     "crates/live_kit_server",
@@ -74,6 +76,7 @@ members = [
     "crates/notifications",
     "crates/notifications2",
     "crates/outline",
+    "crates/outline2",
     "crates/picker",
     "crates/picker2",
     "crates/plugin",
@@ -92,6 +95,8 @@ members = [
     "crates/rpc2",
     "crates/search",
     "crates/search2",
+    "crates/semantic_index",
+    "crates/semantic_index2",
     "crates/settings",
     "crates/settings2",
     "crates/snippet",
@@ -111,7 +116,6 @@ members = [
     "crates/theme_selector2",
     "crates/ui2",
     "crates/util",
-    "crates/semantic_index",
     "crates/story",
     "crates/vim",
     "crates/vcs_menu",

assets/icons/copy.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>

assets/keymaps/default.json 🔗

@@ -530,12 +530,17 @@
       "alt-cmd-shift-c": "project_panel::CopyRelativePath",
       "f2": "project_panel::Rename",
       "enter": "project_panel::Rename",
-      "space": "project_panel::Open",
       "backspace": "project_panel::Delete",
       "alt-cmd-r": "project_panel::RevealInFinder",
       "alt-shift-f": "project_panel::NewSearchInDirectory"
     }
   },
+  {
+    "context": "ProjectPanel && not_editing",
+    "bindings": {
+      "space": "project_panel::Open"
+    }
+  },
   {
     "context": "CollabPanel && not_editing",
     "bindings": {

crates/ai2/src/auth.rs 🔗

@@ -7,7 +7,7 @@ pub enum ProviderCredential {
     NotNeeded,
 }
 
-pub trait CredentialProvider {
+pub trait CredentialProvider: Send + Sync {
     fn has_credentials(&self) -> bool;
     fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential;
     fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential);

crates/ai2/src/providers/open_ai/embedding.rs 🔗

@@ -35,7 +35,7 @@ pub struct OpenAIEmbeddingProvider {
     model: OpenAILanguageModel,
     credential: Arc<RwLock<ProviderCredential>>,
     pub client: Arc<dyn HttpClient>,
-    pub executor: Arc<BackgroundExecutor>,
+    pub executor: BackgroundExecutor,
     rate_limit_count_rx: watch::Receiver<Option<Instant>>,
     rate_limit_count_tx: Arc<Mutex<watch::Sender<Option<Instant>>>>,
 }
@@ -66,7 +66,7 @@ struct OpenAIEmbeddingUsage {
 }
 
 impl OpenAIEmbeddingProvider {
-    pub fn new(client: Arc<dyn HttpClient>, executor: Arc<BackgroundExecutor>) -> Self {
+    pub fn new(client: Arc<dyn HttpClient>, executor: BackgroundExecutor) -> Self {
         let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
         let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
 

crates/assistant/src/assistant_panel.rs 🔗

@@ -1218,6 +1218,31 @@ impl View for AssistantPanel {
         let style = &theme.assistant;
         if let Some(api_key_editor) = self.api_key_editor.as_ref() {
             Flex::column()
+                .with_child(
+                    Text::new(
+                        "To use the assistant panel or inline assistant, you need to add your OpenAI api key.",
+                        style.api_key_prompt.text.clone(),
+                    ),
+                )
+                .with_child(
+                    Text::new(
+                        " - Having a subscription for another service like GitHub Copilot won't work.",
+                        style.api_key_prompt.text.clone(),
+                    ),
+                )
+                .with_child(
+                    Text::new(
+                        " - You can create a api key at: platform.openai.com/api-keys",
+                        style.api_key_prompt.text.clone(),
+                    ),
+                )
+                .with_child(
+                    Text::new(
+                        " ",
+                        style.api_key_prompt.text.clone(),
+                    )
+                    .aligned(),
+                )
                 .with_child(
                     Text::new(
                         "Paste your OpenAI API key and press Enter to use the assistant",
@@ -1231,6 +1256,20 @@ impl View for AssistantPanel {
                         .with_style(style.api_key_editor.container)
                         .aligned(),
                 )
+                .with_child(
+                    Text::new(
+                        " ",
+                        style.api_key_prompt.text.clone(),
+                    )
+                    .aligned(),
+                )
+                .with_child(
+                    Text::new(
+                        "Click on the Z button in the status bar to close this panel.",
+                        style.api_key_prompt.text.clone(),
+                    )
+                    .aligned(),
+                )
                 .contained()
                 .with_style(style.api_key_prompt.container)
                 .aligned()

crates/auto_update2/src/auto_update.rs 🔗

@@ -102,7 +102,7 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
     })
     .detach();
 
-    if let Some(version) = *ZED_APP_VERSION {
+    if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
         let auto_updater = cx.build_model(|cx| {
             let updater = AutoUpdater::new(version, http_client, server_url);
 

crates/breadcrumbs2/Cargo.toml 🔗

@@ -0,0 +1,28 @@
+[package]
+name = "breadcrumbs2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/breadcrumbs.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+editor = { package = "editor2", path = "../editor2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
+language = { package = "language2", path = "../language2" }
+project = { package = "project2", path = "../project2" }
+search = { package = "search2", path = "../search2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+# outline = { path = "../outline" }
+itertools = "0.10"
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }

crates/breadcrumbs2/src/breadcrumbs.rs 🔗

@@ -0,0 +1,204 @@
+use gpui::{
+    Component, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
+    ViewContext, WeakView,
+};
+use itertools::Itertools;
+use theme::ActiveTheme;
+use ui::{ButtonCommon, ButtonLike, ButtonStyle, Clickable, Disableable, Label};
+use workspace::{
+    item::{ItemEvent, ItemHandle},
+    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
+};
+
+pub enum Event {
+    UpdateLocation,
+}
+
+pub struct Breadcrumbs {
+    pane_focused: bool,
+    active_item: Option<Box<dyn ItemHandle>>,
+    subscription: Option<Subscription>,
+    _workspace: WeakView<Workspace>,
+}
+
+impl Breadcrumbs {
+    pub fn new(workspace: &Workspace) -> Self {
+        Self {
+            pane_focused: false,
+            active_item: Default::default(),
+            subscription: Default::default(),
+            _workspace: workspace.weak_handle(),
+        }
+    }
+}
+
+impl EventEmitter<Event> for Breadcrumbs {}
+impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
+
+impl Render for Breadcrumbs {
+    type Element = Component<ButtonLike>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let button = ButtonLike::new("breadcrumbs")
+            .style(ButtonStyle::Transparent)
+            .disabled(true);
+
+        let active_item = match &self.active_item {
+            Some(active_item) => active_item,
+            None => return button.into_element(),
+        };
+        let not_editor = active_item.downcast::<editor::Editor>().is_none();
+
+        let breadcrumbs = match active_item.breadcrumbs(cx.theme(), cx) {
+            Some(breadcrumbs) => breadcrumbs,
+            None => return button.into_element(),
+        }
+        .into_iter()
+        .map(|breadcrumb| {
+            StyledText::new(breadcrumb.text)
+                .with_highlights(&cx.text_style(), breadcrumb.highlights.unwrap_or_default())
+                .into_any()
+        });
+
+        let button = button.children(Itertools::intersperse_with(breadcrumbs, || {
+            Label::new(" › ").into_any_element()
+        }));
+
+        if not_editor || !self.pane_focused {
+            return button.into_element();
+        }
+
+        // let this = cx.view().downgrade();
+        button
+            .style(ButtonStyle::Filled)
+            .disabled(false)
+            .on_click(move |_, _cx| {
+                todo!("outline::toggle");
+                // this.update(cx, |this, cx| {
+                //     if let Some(workspace) = this.workspace.upgrade() {
+                //         workspace.update(cx, |_workspace, _cx| {
+                //             outline::toggle(workspace, &Default::default(), cx)
+                //         })
+                //     }
+                // })
+                // .ok();
+            })
+            .into_element()
+    }
+}
+
+// impl View for Breadcrumbs {
+//     fn ui_name() -> &'static str {
+//         "Breadcrumbs"
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         let active_item = match &self.active_item {
+//             Some(active_item) => active_item,
+//             None => return Empty::new().into_any(),
+//         };
+//         let not_editor = active_item.downcast::<editor::Editor>().is_none();
+
+//         let theme = theme::current(cx).clone();
+//         let style = &theme.workspace.toolbar.breadcrumbs;
+
+//         let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
+//             Some(breadcrumbs) => breadcrumbs,
+//             None => return Empty::new().into_any(),
+//         }
+//         .into_iter()
+//         .map(|breadcrumb| {
+//             Text::new(
+//                 breadcrumb.text,
+//                 theme.workspace.toolbar.breadcrumbs.default.text.clone(),
+//             )
+//             .with_highlights(breadcrumb.highlights.unwrap_or_default())
+//             .into_any()
+//         });
+
+//         let crumbs = Flex::row()
+//             .with_children(Itertools::intersperse_with(breadcrumbs, || {
+//                 Label::new(" › ", style.default.text.clone()).into_any()
+//             }))
+//             .constrained()
+//             .with_height(theme.workspace.toolbar.breadcrumb_height)
+//             .contained();
+
+//         if not_editor || !self.pane_focused {
+//             return crumbs
+//                 .with_style(style.default.container)
+//                 .aligned()
+//                 .left()
+//                 .into_any();
+//         }
+
+//         MouseEventHandler::new::<Breadcrumbs, _>(0, cx, |state, _| {
+//             let style = style.style_for(state);
+//             crumbs.with_style(style.container)
+//         })
+//         .on_click(MouseButton::Left, |_, this, cx| {
+//             if let Some(workspace) = this.workspace.upgrade(cx) {
+//                 workspace.update(cx, |workspace, cx| {
+//                     outline::toggle(workspace, &Default::default(), cx)
+//                 })
+//             }
+//         })
+//         .with_tooltip::<Breadcrumbs>(
+//             0,
+//             "Show symbol outline".to_owned(),
+//             Some(Box::new(outline::Toggle)),
+//             theme.tooltip.clone(),
+//             cx,
+//         )
+//         .aligned()
+//         .left()
+//         .into_any()
+//     }
+// }
+
+impl ToolbarItemView for Breadcrumbs {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) -> ToolbarItemLocation {
+        cx.notify();
+        self.active_item = None;
+        if let Some(item) = active_pane_item {
+            let this = cx.view().downgrade();
+            self.subscription = Some(item.subscribe_to_item_events(
+                cx,
+                Box::new(move |event, cx| {
+                    if let ItemEvent::UpdateBreadcrumbs = event {
+                        this.update(cx, |_, cx| {
+                            cx.emit(Event::UpdateLocation);
+                            cx.notify();
+                        })
+                        .ok();
+                    }
+                }),
+            ));
+            self.active_item = Some(item.boxed_clone());
+            item.breadcrumb_location(cx)
+        } else {
+            ToolbarItemLocation::Hidden
+        }
+    }
+
+    // fn location_for_event(
+    //     &self,
+    //     _: &Event,
+    //     current_location: ToolbarItemLocation,
+    //     cx: &AppContext,
+    // ) -> ToolbarItemLocation {
+    //     if let Some(active_item) = self.active_item.as_ref() {
+    //         active_item.breadcrumb_location(cx)
+    //     } else {
+    //         current_location
+    //     }
+    // }
+
+    fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
+        self.pane_focused = pane_focused;
+    }
+}

crates/call2/Cargo.toml 🔗

@@ -31,9 +31,7 @@ media = { path = "../media" }
 project = { package = "project2", path = "../project2" }
 settings = { package = "settings2", path = "../settings2" }
 util = { path = "../util" }
-ui = {package = "ui2", path = "../ui2"}
-workspace = {package = "workspace2", path = "../workspace2"}
-async-trait.workspace = true
+
 anyhow.workspace = true
 async-broadcast = "0.4"
 futures.workspace = true

crates/call2/src/call2.rs 🔗

@@ -1,32 +1,25 @@
 pub mod call_settings;
 pub mod participant;
 pub mod room;
-mod shared_screen;
 
 use anyhow::{anyhow, Result};
-use async_trait::async_trait;
 use audio::Audio;
 use call_settings::CallSettings;
-use client::{
-    proto::{self, PeerId},
-    Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE,
-};
+use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
 use collections::HashSet;
 use futures::{channel::oneshot, future::Shared, Future, FutureExt};
 use gpui::{
-    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
-    Subscription, Task, View, ViewContext, VisualContext, WeakModel, WindowHandle,
+    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
+    WeakModel,
 };
-pub use participant::ParticipantLocation;
 use postage::watch;
 use project::Project;
 use room::Event;
-pub use room::Room;
 use settings::Settings;
-use shared_screen::SharedScreen;
 use std::sync::Arc;
-use util::ResultExt;
-use workspace::{item::ItemHandle, CallHandler, Pane, Workspace};
+
+pub use participant::ParticipantLocation;
+pub use room::Room;
 
 pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
     CallSettings::register(cx);
@@ -334,55 +327,12 @@ impl ActiveCall {
     pub fn join_channel(
         &mut self,
         channel_id: u64,
-        requesting_window: Option<WindowHandle<Workspace>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Model<Room>>>> {
         if let Some(room) = self.room().cloned() {
             if room.read(cx).channel_id() == Some(channel_id) {
-                return cx.spawn(|_, _| async move {
-                    todo!();
-                    // let future = room.update(&mut cx, |room, cx| {
-                    //     room.most_active_project(cx).map(|(host, project)| {
-                    //         room.join_project(project, host, app_state.clone(), cx)
-                    //     })
-                    // })
-
-                    // if let Some(future) = future {
-                    //     future.await?;
-                    // }
-
-                    // Ok(Some(room))
-                });
-            }
-
-            let should_prompt = room.update(cx, |room, _| {
-                room.channel_id().is_some()
-                    && room.is_sharing_project()
-                    && room.remote_participants().len() > 0
-            });
-            if should_prompt && requesting_window.is_some() {
-                return cx.spawn(|this, mut cx| async move {
-                    let answer = requesting_window.unwrap().update(&mut cx, |_, cx| {
-                        cx.prompt(
-                            PromptLevel::Warning,
-                            "Leaving this call will unshare your current project.\nDo you want to switch channels?",
-                            &["Yes, Join Channel", "Cancel"],
-                        )
-                    })?;
-                    if answer.await? == 1 {
-                        return Ok(None);
-                    }
-
-                    room.update(&mut cx, |room, cx| room.clear_state(cx))?;
-
-                    this.update(&mut cx, |this, cx| {
-                        this.join_channel(channel_id, requesting_window, cx)
-                    })?
-                    .await
-                });
-            }
-
-            if room.read(cx).channel_id().is_some() {
+                return Task::ready(Ok(Some(room)));
+            } else {
                 room.update(cx, |room, cx| room.clear_state(cx));
             }
         }
@@ -555,197 +505,6 @@ pub fn report_call_event_for_channel(
     )
 }
 
-pub struct Call {
-    active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
-}
-
-impl Call {
-    pub fn new(cx: &mut ViewContext<'_, Workspace>) -> Box<dyn CallHandler> {
-        let mut active_call = None;
-        if cx.has_global::<Model<ActiveCall>>() {
-            let call = cx.global::<Model<ActiveCall>>().clone();
-            let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
-            active_call = Some((call, subscriptions));
-        }
-        Box::new(Self { active_call })
-    }
-    fn on_active_call_event(
-        workspace: &mut Workspace,
-        _: Model<ActiveCall>,
-        event: &room::Event,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        match event {
-            room::Event::ParticipantLocationChanged { participant_id }
-            | room::Event::RemoteVideoTracksChanged { participant_id } => {
-                workspace.leader_updated(*participant_id, cx);
-            }
-            _ => {}
-        }
-    }
-}
-
-#[async_trait(?Send)]
-impl CallHandler for Call {
-    fn peer_state(
-        &mut self,
-        leader_id: PeerId,
-        project: &Model<Project>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<(bool, bool)> {
-        let (call, _) = self.active_call.as_ref()?;
-        let room = call.read(cx).room()?.read(cx);
-        let participant = room.remote_participant_for_peer_id(leader_id)?;
-
-        let leader_in_this_app;
-        let leader_in_this_project;
-        match participant.location {
-            ParticipantLocation::SharedProject { project_id } => {
-                leader_in_this_app = true;
-                leader_in_this_project = Some(project_id) == project.read(cx).remote_id();
-            }
-            ParticipantLocation::UnsharedProject => {
-                leader_in_this_app = true;
-                leader_in_this_project = false;
-            }
-            ParticipantLocation::External => {
-                leader_in_this_app = false;
-                leader_in_this_project = false;
-            }
-        };
-
-        Some((leader_in_this_project, leader_in_this_app))
-    }
-
-    fn shared_screen_for_peer(
-        &self,
-        peer_id: PeerId,
-        pane: &View<Pane>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<Box<dyn ItemHandle>> {
-        let (call, _) = self.active_call.as_ref()?;
-        let room = call.read(cx).room()?.read(cx);
-        let participant = room.remote_participant_for_peer_id(peer_id)?;
-        let track = participant.video_tracks.values().next()?.clone();
-        let user = participant.user.clone();
-        for item in pane.read(cx).items_of_type::<SharedScreen>() {
-            if item.read(cx).peer_id == peer_id {
-                return Some(Box::new(item));
-            }
-        }
-
-        Some(Box::new(cx.build_view(|cx| {
-            SharedScreen::new(&track, peer_id, user.clone(), cx)
-        })))
-    }
-    fn room_id(&self, cx: &AppContext) -> Option<u64> {
-        Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
-    }
-    fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
-        let Some((call, _)) = self.active_call.as_ref() else {
-            return Task::ready(Err(anyhow!("Cannot exit a call; not in a call")));
-        };
-
-        call.update(cx, |this, cx| this.hang_up(cx))
-    }
-    fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
-        ActiveCall::global(cx).read(cx).location().cloned()
-    }
-    fn invite(
-        &mut self,
-        called_user_id: u64,
-        initial_project: Option<Model<Project>>,
-        cx: &mut AppContext,
-    ) -> Task<Result<()>> {
-        ActiveCall::global(cx).update(cx, |this, cx| {
-            this.invite(called_user_id, initial_project, cx)
-        })
-    }
-    fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
-        self.active_call
-            .as_ref()
-            .map(|call| {
-                call.0.read(cx).room().map(|room| {
-                    room.read(cx)
-                        .remote_participants()
-                        .iter()
-                        .map(|participant| {
-                            (participant.1.user.clone(), participant.1.peer_id.clone())
-                        })
-                        .collect()
-                })
-            })
-            .flatten()
-    }
-    fn is_muted(&self, cx: &AppContext) -> Option<bool> {
-        self.active_call
-            .as_ref()
-            .map(|call| {
-                call.0
-                    .read(cx)
-                    .room()
-                    .map(|room| room.read(cx).is_muted(cx))
-            })
-            .flatten()
-    }
-    fn toggle_mute(&self, cx: &mut AppContext) {
-        self.active_call.as_ref().map(|call| {
-            call.0.update(cx, |this, cx| {
-                this.room().map(|room| {
-                    let room = room.clone();
-                    cx.spawn(|_, mut cx| async move {
-                        room.update(&mut cx, |this, cx| this.toggle_mute(cx))??
-                            .await
-                    })
-                    .detach_and_log_err(cx);
-                })
-            })
-        });
-    }
-    fn toggle_screen_share(&self, cx: &mut AppContext) {
-        self.active_call.as_ref().map(|call| {
-            call.0.update(cx, |this, cx| {
-                this.room().map(|room| {
-                    room.update(cx, |this, cx| {
-                        if this.is_screen_sharing() {
-                            this.unshare_screen(cx).log_err();
-                        } else {
-                            let t = this.share_screen(cx);
-                            cx.spawn(move |_, _| async move {
-                                t.await.log_err();
-                            })
-                            .detach();
-                        }
-                    })
-                })
-            })
-        });
-    }
-    fn toggle_deafen(&self, cx: &mut AppContext) {
-        self.active_call.as_ref().map(|call| {
-            call.0.update(cx, |this, cx| {
-                this.room().map(|room| {
-                    room.update(cx, |this, cx| {
-                        this.toggle_deafen(cx).log_err();
-                    })
-                })
-            })
-        });
-    }
-    fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
-        self.active_call
-            .as_ref()
-            .map(|call| {
-                call.0
-                    .read(cx)
-                    .room()
-                    .map(|room| room.read(cx).is_deafened())
-            })
-            .flatten()
-            .flatten()
-    }
-}
-
 #[cfg(test)]
 mod test {
     use gpui::TestAppContext;

crates/call2/src/participant.rs 🔗

@@ -4,7 +4,7 @@ use client::{proto, User};
 use collections::HashMap;
 use gpui::WeakModel;
 pub use live_kit_client::Frame;
-pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
+pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
 use project::Project;
 use std::sync::Arc;
 

crates/client/src/client.rs 🔗

@@ -346,7 +346,7 @@ impl<T: Entity> Drop for PendingEntitySubscription<T> {
     }
 }
 
-#[derive(Copy, Clone)]
+#[derive(Debug, Copy, Clone)]
 pub struct TelemetrySettings {
     pub diagnostics: bool,
     pub metrics: bool,

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.29.0"
+version = "0.29.1"
 publish = false
 
 [[bin]]

crates/collab/src/db/queries/channels.rs 🔗

@@ -1220,6 +1220,13 @@ impl Database {
                 self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
                     .await?;
 
+                if new_parent
+                    .ancestors_including_self()
+                    .any(|id| id == channel.id)
+                {
+                    Err(anyhow!("cannot move a channel into one of its descendants"))?;
+                }
+
                 new_parent_path = new_parent.path();
                 new_parent_channel = Some(new_parent);
             } else {

crates/collab/src/db/tests/channel_tests.rs 🔗

@@ -450,6 +450,20 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
             (livestreaming_id, &[projects_id]),
         ],
     );
+
+    // Can't move a channel into its ancestor
+    db.move_channel(projects_id, Some(livestreaming_id), user_id)
+        .await
+        .unwrap_err();
+    let result = db.get_channels_for_user(user_id).await.unwrap();
+    assert_channel_tree(
+        result.channels,
+        &[
+            (zed_id, &[]),
+            (projects_id, &[]),
+            (livestreaming_id, &[projects_id]),
+        ],
+    );
 }
 
 test_both_dbs!(

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

@@ -4,8 +4,10 @@ use collab_ui::notifications::project_shared_notification::ProjectSharedNotifica
 use editor::{Editor, ExcerptRange, MultiBuffer};
 use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
 use live_kit_client::MacOSDisplay;
+use project::project_settings::ProjectSettings;
 use rpc::proto::PeerId;
 use serde_json::json;
+use settings::SettingsStore;
 use std::{borrow::Cow, sync::Arc};
 use workspace::{
     dock::{test::TestPanel, DockPosition},
@@ -1602,6 +1604,141 @@ async fn test_following_across_workspaces(
     });
 }
 
+#[gpui::test]
+async fn test_following_into_excluded_file(
+    deterministic: Arc<Deterministic>,
+    mut cx_a: &mut TestAppContext,
+    mut cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    for cx in [&mut cx_a, &mut cx_b] {
+        cx.update(|cx| {
+            cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+                    project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
+                });
+            });
+        });
+    }
+    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);
+
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                ".git": {
+                    "COMMIT_EDITMSG": "write your commit message here",
+                },
+                "1.txt": "one\none\none",
+                "2.txt": "two\ntwo\ntwo",
+                "3.txt": "three\nthree\nthree",
+            }),
+        )
+        .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 window_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = window_a.root(cx_a);
+    let peer_id_a = client_a.peer_id().unwrap();
+    let window_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = window_b.root(cx_b);
+
+    // Client A opens editors for a regular file and an excluded file.
+    let editor_for_regular = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+    let editor_for_excluded_a = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client A updates their selections in those editors
+    editor_for_regular.update(cx_a, |editor, cx| {
+        editor.handle_input("a", cx);
+        editor.handle_input("b", cx);
+        editor.handle_input("c", cx);
+        editor.select_left(&Default::default(), cx);
+        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+    });
+    editor_for_excluded_a.update(cx_a, |editor, cx| {
+        editor.select_all(&Default::default(), cx);
+        editor.handle_input("new commit message", cx);
+        editor.select_left(&Default::default(), cx);
+        assert_eq!(editor.selections.ranges(cx), vec![18..17]);
+    });
+
+    // When client B starts following client A, currently visible file is replicated
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.follow(peer_id_a, cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    let editor_for_excluded_b = workspace_b.read_with(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    assert_eq!(
+        cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
+        Some((worktree_id, ".git/COMMIT_EDITMSG").into())
+    );
+    assert_eq!(
+        editor_for_excluded_b.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        vec![18..17]
+    );
+
+    // Changes from B to the excluded file are replicated in A's editor
+    editor_for_excluded_b.update(cx_b, |editor, cx| {
+        editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
+    });
+    deterministic.run_until_parked();
+    editor_for_excluded_a.update(cx_a, |editor, cx| {
+        assert_eq!(
+            editor.text(cx),
+            "new commit messag\nCo-Authored-By: B <b@b.b>"
+        );
+    });
+}
+
 fn visible_push_notifications(
     cx: &mut TestAppContext,
 ) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {

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

@@ -2981,11 +2981,10 @@ async fn test_fs_operations(
 
     let entry = project_b
         .update(cx_b, |project, cx| {
-            project
-                .create_entry((worktree_id, "c.txt"), false, cx)
-                .unwrap()
+            project.create_entry((worktree_id, "c.txt"), false, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     worktree_a.read_with(cx_a, |worktree, _| {
         assert_eq!(
@@ -3010,7 +3009,6 @@ async fn test_fs_operations(
         .update(cx_b, |project, cx| {
             project.rename_entry(entry.id, Path::new("d.txt"), cx)
         })
-        .unwrap()
         .await
         .unwrap();
     worktree_a.read_with(cx_a, |worktree, _| {
@@ -3034,11 +3032,10 @@ async fn test_fs_operations(
 
     let dir_entry = project_b
         .update(cx_b, |project, cx| {
-            project
-                .create_entry((worktree_id, "DIR"), true, cx)
-                .unwrap()
+            project.create_entry((worktree_id, "DIR"), true, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     worktree_a.read_with(cx_a, |worktree, _| {
         assert_eq!(
@@ -3061,25 +3058,19 @@ async fn test_fs_operations(
 
     project_b
         .update(cx_b, |project, cx| {
-            project
-                .create_entry((worktree_id, "DIR/e.txt"), false, cx)
-                .unwrap()
+            project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
         })
         .await
         .unwrap();
     project_b
         .update(cx_b, |project, cx| {
-            project
-                .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
-                .unwrap()
+            project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
         })
         .await
         .unwrap();
     project_b
         .update(cx_b, |project, cx| {
-            project
-                .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
-                .unwrap()
+            project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
         })
         .await
         .unwrap();
@@ -3120,9 +3111,7 @@ async fn test_fs_operations(
 
     project_b
         .update(cx_b, |project, cx| {
-            project
-                .copy_entry(entry.id, Path::new("f.txt"), cx)
-                .unwrap()
+            project.copy_entry(entry.id, Path::new("f.txt"), cx)
         })
         .await
         .unwrap();

crates/collab2/src/db/queries/channels.rs 🔗

@@ -1220,6 +1220,13 @@ impl Database {
                 self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
                     .await?;
 
+                if new_parent
+                    .ancestors_including_self()
+                    .any(|id| id == channel.id)
+                {
+                    Err(anyhow!("cannot move a channel into one of its descendants"))?;
+                }
+
                 new_parent_path = new_parent.path();
                 new_parent_channel = Some(new_parent);
             } else {

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

@@ -420,8 +420,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
         .await
         .unwrap();
 
-    // Dag is: zed - projects - livestreaming
-
     // Move to same parent should be a no-op
     assert!(db
         .move_channel(projects_id, Some(zed_id), user_id)
@@ -450,6 +448,20 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
             (livestreaming_id, &[projects_id]),
         ],
     );
+
+    // Can't move a channel into its ancestor
+    db.move_channel(projects_id, Some(livestreaming_id), user_id)
+        .await
+        .unwrap_err();
+    let result = db.get_channels_for_user(user_id).await.unwrap();
+    assert_channel_tree(
+        result.channels,
+        &[
+            (zed_id, &[]),
+            (projects_id, &[]),
+            (livestreaming_id, &[projects_id]),
+        ],
+    );
 }
 
 test_both_dbs!(

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

@@ -364,8 +364,7 @@ async fn test_joining_channel_ancestor_member(
     let active_call_b = cx_b.read(ActiveCall::global);
 
     assert!(active_call_b
-        .update(cx_b, |active_call, cx| active_call
-            .join_channel(sub_id, None, cx))
+        .update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
         .await
         .is_ok());
 }
@@ -395,9 +394,7 @@ async fn test_channel_room(
     let active_call_b = cx_b.read(ActiveCall::global);
 
     active_call_a
-        .update(cx_a, |active_call, cx| {
-            active_call.join_channel(zed_id, None, cx)
-        })
+        .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
         .await
         .unwrap();
 
@@ -445,9 +442,7 @@ async fn test_channel_room(
     });
 
     active_call_b
-        .update(cx_b, |active_call, cx| {
-            active_call.join_channel(zed_id, None, cx)
-        })
+        .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
         .await
         .unwrap();
 
@@ -564,16 +559,12 @@ async fn test_channel_room(
     });
 
     active_call_a
-        .update(cx_a, |active_call, cx| {
-            active_call.join_channel(zed_id, None, cx)
-        })
+        .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
         .await
         .unwrap();
 
     active_call_b
-        .update(cx_b, |active_call, cx| {
-            active_call.join_channel(zed_id, None, cx)
-        })
+        .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
         .await
         .unwrap();
 
@@ -617,9 +608,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
     let active_call_a = cx_a.read(ActiveCall::global);
 
     active_call_a
-        .update(cx_a, |active_call, cx| {
-            active_call.join_channel(zed_id, None, cx)
-        })
+        .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
         .await
         .unwrap();
 
@@ -638,7 +627,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
 
     active_call_a
         .update(cx_a, |active_call, cx| {
-            active_call.join_channel(rust_id, None, cx)
+            active_call.join_channel(rust_id, cx)
         })
         .await
         .unwrap();
@@ -804,7 +793,7 @@ async fn test_call_from_channel(
     let active_call_b = cx_b.read(ActiveCall::global);
 
     active_call_a
-        .update(cx_a, |call, cx| call.join_channel(channel_id, None, cx))
+        .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
         .await
         .unwrap();
 
@@ -1297,7 +1286,7 @@ async fn test_guest_access(
 
     // Non-members should not be allowed to join
     assert!(active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_a, cx))
         .await
         .is_err());
 
@@ -1319,7 +1308,7 @@ async fn test_guest_access(
 
     // Client B joins channel A as a guest
     active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_a, cx))
         .await
         .unwrap();
 
@@ -1352,7 +1341,7 @@ async fn test_guest_access(
     assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
 
     active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_b, None, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_b, cx))
         .await
         .unwrap();
 
@@ -1383,7 +1372,7 @@ async fn test_invite_access(
 
     // should not be allowed to join
     assert!(active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
         .await
         .is_err());
 
@@ -1401,7 +1390,7 @@ async fn test_invite_access(
         .unwrap();
 
     active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
         .await
         .unwrap();
 

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

@@ -4,10 +4,12 @@
 // use call::ActiveCall;
 // use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
 // use editor::{Editor, ExcerptRange, MultiBuffer};
-// use gpui::{BackgroundExecutor, TestAppContext, View};
+// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext};
 // use live_kit_client::MacOSDisplay;
+// use project::project_settings::ProjectSettings;
 // use rpc::proto::PeerId;
 // use serde_json::json;
+// use settings::SettingsStore;
 // use std::borrow::Cow;
 // use workspace::{
 //     dock::{test::TestPanel, DockPosition},
@@ -24,7 +26,7 @@
 //     cx_c: &mut TestAppContext,
 //     cx_d: &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;
@@ -71,12 +73,22 @@
 //         .unwrap();
 
 //     let window_a = client_a.build_workspace(&project_a, cx_a);
-//     let workspace_a = window_a.root(cx_a);
+//     let workspace_a = window_a.root(cx_a).unwrap();
 //     let window_b = client_b.build_workspace(&project_b, cx_b);
-//     let workspace_b = window_b.root(cx_b);
+//     let workspace_b = window_b.root(cx_b).unwrap();
+
+//     todo!("could be wrong")
+//     let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
+//     let cx_a = &mut cx_a;
+//     let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
+//     let cx_b = &mut cx_b;
+//     let mut cx_c = VisualTestContext::from_window(*window_c, cx_c);
+//     let cx_c = &mut cx_c;
+//     let mut cx_d = VisualTestContext::from_window(*window_d, cx_d);
+//     let cx_d = &mut cx_d;
 
 //     // Client A opens some editors.
-//     let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+//     let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
 //     let editor_a1 = workspace_a
 //         .update(cx_a, |workspace, cx| {
 //             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -132,8 +144,8 @@
 //         .await
 //         .unwrap();
 
-//     cx_c.foreground().run_until_parked();
-//     let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+//     cx_c.executor().run_until_parked();
+//     let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
 //         workspace
 //             .active_item(cx)
 //             .unwrap()
@@ -145,19 +157,19 @@
 //         Some((worktree_id, "2.txt").into())
 //     );
 //     assert_eq!(
-//         editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+//         editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
 //         vec![2..1]
 //     );
 //     assert_eq!(
-//         editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+//         editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
 //         vec![3..2]
 //     );
 
-//     cx_c.foreground().run_until_parked();
+//     cx_c.executor().run_until_parked();
 //     let active_call_c = cx_c.read(ActiveCall::global);
 //     let project_c = client_c.build_remote_project(project_id, cx_c).await;
 //     let window_c = client_c.build_workspace(&project_c, cx_c);
-//     let workspace_c = window_c.root(cx_c);
+//     let workspace_c = window_c.root(cx_c).unwrap();
 //     active_call_c
 //         .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
 //         .await
@@ -172,10 +184,13 @@
 //         .await
 //         .unwrap();
 
-//     cx_d.foreground().run_until_parked();
+//     cx_d.executor().run_until_parked();
 //     let active_call_d = cx_d.read(ActiveCall::global);
 //     let project_d = client_d.build_remote_project(project_id, cx_d).await;
-//     let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
+//     let workspace_d = client_d
+//         .build_workspace(&project_d, cx_d)
+//         .root(cx_d)
+//         .unwrap();
 //     active_call_d
 //         .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
 //         .await
@@ -183,7 +198,7 @@
 //     drop(project_d);
 
 //     // All clients see that clients B and C are following client A.
-//     cx_c.foreground().run_until_parked();
+//     cx_c.executor().run_until_parked();
 //     for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
 //         assert_eq!(
 //             followers_by_leader(project_id, cx),
@@ -198,7 +213,7 @@
 //     });
 
 //     // All clients see that clients B is following client A.
-//     cx_c.foreground().run_until_parked();
+//     cx_c.executor().run_until_parked();
 //     for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
 //         assert_eq!(
 //             followers_by_leader(project_id, cx),
@@ -216,7 +231,7 @@
 //         .unwrap();
 
 //     // All clients see that clients B and C are following client A.
-//     cx_c.foreground().run_until_parked();
+//     cx_c.executor().run_until_parked();
 //     for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
 //         assert_eq!(
 //             followers_by_leader(project_id, cx),
@@ -240,7 +255,7 @@
 //         .unwrap();
 
 //     // All clients see that D is following C
-//     cx_d.foreground().run_until_parked();
+//     cx_d.executor().run_until_parked();
 //     for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
 //         assert_eq!(
 //             followers_by_leader(project_id, cx),
@@ -257,7 +272,7 @@
 //     cx_c.drop_last(workspace_c);
 
 //     // Clients A and B see that client B is following A, and client C is not present in the followers.
-//     cx_c.foreground().run_until_parked();
+//     cx_c.executor().run_until_parked();
 //     for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
 //         assert_eq!(
 //             followers_by_leader(project_id, cx),
@@ -271,12 +286,15 @@
 //         workspace.activate_item(&editor_a1, cx)
 //     });
 //     executor.run_until_parked();
-//     workspace_b.read_with(cx_b, |workspace, cx| {
-//         assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+//     workspace_b.update(cx_b, |workspace, cx| {
+//         assert_eq!(
+//             workspace.active_item(cx).unwrap().item_id(),
+//             editor_b1.item_id()
+//         );
 //     });
 
 //     // When client A opens a multibuffer, client B does so as well.
-//     let multibuffer_a = cx_a.add_model(|cx| {
+//     let multibuffer_a = cx_a.build_model(|cx| {
 //         let buffer_a1 = project_a.update(cx, |project, cx| {
 //             project
 //                 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
@@ -308,12 +326,12 @@
 //     });
 //     let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
 //         let editor =
-//             cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
+//             cx.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
 //         workspace.add_item(Box::new(editor.clone()), cx);
 //         editor
 //     });
 //     executor.run_until_parked();
-//     let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
+//     let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
 //         workspace
 //             .active_item(cx)
 //             .unwrap()
@@ -321,8 +339,8 @@
 //             .unwrap()
 //     });
 //     assert_eq!(
-//         multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
-//         multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
+//         multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
+//         multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
 //     );
 
 //     // When client A navigates back and forth, client B does so as well.
@@ -333,8 +351,11 @@
 //         .await
 //         .unwrap();
 //     executor.run_until_parked();
-//     workspace_b.read_with(cx_b, |workspace, cx| {
-//         assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+//     workspace_b.update(cx_b, |workspace, cx| {
+//         assert_eq!(
+//             workspace.active_item(cx).unwrap().item_id(),
+//             editor_b1.item_id()
+//         );
 //     });
 
 //     workspace_a
@@ -344,8 +365,11 @@
 //         .await
 //         .unwrap();
 //     executor.run_until_parked();
-//     workspace_b.read_with(cx_b, |workspace, cx| {
-//         assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
+//     workspace_b.update(cx_b, |workspace, cx| {
+//         assert_eq!(
+//             workspace.active_item(cx).unwrap().item_id(),
+//             editor_b2.item_id()
+//         );
 //     });
 
 //     workspace_a
@@ -355,8 +379,11 @@
 //         .await
 //         .unwrap();
 //     executor.run_until_parked();
-//     workspace_b.read_with(cx_b, |workspace, cx| {
-//         assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+//     workspace_b.update(cx_b, |workspace, cx| {
+//         assert_eq!(
+//             workspace.active_item(cx).unwrap().item_id(),
+//             editor_b1.item_id()
+//         );
 //     });
 
 //     // Changes to client A's editor are reflected on client B.
@@ -364,20 +391,20 @@
 //         editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
 //     });
 //     executor.run_until_parked();
-//     editor_b1.read_with(cx_b, |editor, cx| {
+//     editor_b1.update(cx_b, |editor, cx| {
 //         assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
 //     });
 
 //     editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
 //     executor.run_until_parked();
-//     editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
+//     editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
 
 //     editor_a1.update(cx_a, |editor, cx| {
 //         editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
-//         editor.set_scroll_position(vec2f(0., 100.), cx);
+//         editor.set_scroll_position(point(0., 100.), cx);
 //     });
 //     executor.run_until_parked();
-//     editor_b1.read_with(cx_b, |editor, cx| {
+//     editor_b1.update(cx_b, |editor, cx| {
 //         assert_eq!(editor.selections.ranges(cx), &[3..3]);
 //     });
 
@@ -390,11 +417,11 @@
 //     });
 //     executor.run_until_parked();
 //     assert_eq!(
-//         workspace_b.read_with(cx_b, |workspace, cx| workspace
+//         workspace_b.update(cx_b, |workspace, cx| workspace
 //             .active_item(cx)
 //             .unwrap()
-//             .id()),
-//         editor_b1.id()
+//             .item_id()),
+//         editor_b1.item_id()
 //     );
 
 //     // Client A starts following client B.
@@ -405,15 +432,15 @@
 //         .await
 //         .unwrap();
 //     assert_eq!(
-//         workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+//         workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
 //         Some(peer_id_b)
 //     );
 //     assert_eq!(
-//         workspace_a.read_with(cx_a, |workspace, cx| workspace
+//         workspace_a.update(cx_a, |workspace, cx| workspace
 //             .active_item(cx)
 //             .unwrap()
-//             .id()),
-//         editor_a1.id()
+//             .item_id()),
+//         editor_a1.item_id()
 //     );
 
 //     // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
@@ -432,7 +459,7 @@
 //         .await
 //         .unwrap();
 //     executor.run_until_parked();
-//     let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
+//     let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
 //         workspace
 //             .active_item(cx)
 //             .expect("no active item")
@@ -446,8 +473,11 @@
 //         .await
 //         .unwrap();
 //     executor.run_until_parked();
-//     workspace_a.read_with(cx_a, |workspace, cx| {
-//         assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
+//     workspace_a.update(cx_a, |workspace, cx| {
+//         assert_eq!(
+//             workspace.active_item(cx).unwrap().item_id(),
+//             editor_a1.item_id()
+//         )
 //     });
 
 //     // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
@@ -455,26 +485,26 @@
 //         workspace.activate_item(&multibuffer_editor_b, cx)
 //     });
 //     executor.run_until_parked();
-//     workspace_a.read_with(cx_a, |workspace, cx| {
+//     workspace_a.update(cx_a, |workspace, cx| {
 //         assert_eq!(
-//             workspace.active_item(cx).unwrap().id(),
-//             multibuffer_editor_a.id()
+//             workspace.active_item(cx).unwrap().item_id(),
+//             multibuffer_editor_a.item_id()
 //         )
 //     });
 
 //     // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
-//     let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
+//     let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left));
 //     workspace_b.update(cx_b, |workspace, cx| {
 //         workspace.add_panel(panel, cx);
 //         workspace.toggle_panel_focus::<TestPanel>(cx);
 //     });
 //     executor.run_until_parked();
 //     assert_eq!(
-//         workspace_a.read_with(cx_a, |workspace, cx| workspace
+//         workspace_a.update(cx_a, |workspace, cx| workspace
 //             .active_item(cx)
 //             .unwrap()
-//             .id()),
-//         shared_screen.id()
+//             .item_id()),
+//         shared_screen.item_id()
 //     );
 
 //     // Toggling the focus back to the pane causes client A to return to the multibuffer.
@@ -482,16 +512,16 @@
 //         workspace.toggle_panel_focus::<TestPanel>(cx);
 //     });
 //     executor.run_until_parked();
-//     workspace_a.read_with(cx_a, |workspace, cx| {
+//     workspace_a.update(cx_a, |workspace, cx| {
 //         assert_eq!(
-//             workspace.active_item(cx).unwrap().id(),
-//             multibuffer_editor_a.id()
+//             workspace.active_item(cx).unwrap().item_id(),
+//             multibuffer_editor_a.item_id()
 //         )
 //     });
 
 //     // Client B activates an item that doesn't implement following,
 //     // so the previously-opened screen-sharing item gets activated.
-//     let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
+//     let unfollowable_item = window_b.build_view(cx_b, |_| TestItem::new());
 //     workspace_b.update(cx_b, |workspace, cx| {
 //         workspace.active_pane().update(cx, |pane, cx| {
 //             pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
@@ -499,18 +529,18 @@
 //     });
 //     executor.run_until_parked();
 //     assert_eq!(
-//         workspace_a.read_with(cx_a, |workspace, cx| workspace
+//         workspace_a.update(cx_a, |workspace, cx| workspace
 //             .active_item(cx)
 //             .unwrap()
-//             .id()),
-//         shared_screen.id()
+//             .item_id()),
+//         shared_screen.item_id()
 //     );
 
 //     // Following interrupts when client B disconnects.
 //     client_b.disconnect(&cx_b.to_async());
 //     executor.advance_clock(RECONNECT_TIMEOUT);
 //     assert_eq!(
-//         workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+//         workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
 //         None
 //     );
 // }
@@ -521,7 +551,7 @@
 //     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;
 //     server
@@ -560,13 +590,19 @@
 //         .await
 //         .unwrap();
 
-//     let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-//     let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+//     let workspace_a = client_a
+//         .build_workspace(&project_a, cx_a)
+//         .root(cx_a)
+//         .unwrap();
+//     let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
 
-//     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-//     let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+//     let workspace_b = client_b
+//         .build_workspace(&project_b, cx_b)
+//         .root(cx_b)
+//         .unwrap();
+//     let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
 
-//     let client_b_id = project_a.read_with(cx_a, |project, _| {
+//     let client_b_id = project_a.update(cx_a, |project, _| {
 //         project.collaborators().values().next().unwrap().peer_id
 //     });
 
@@ -584,7 +620,7 @@
 //         .await
 //         .unwrap();
 
-//     let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
+//     let pane_paths = |pane: &View<workspace::Pane>, cx: &mut TestAppContext| {
 //         pane.update(cx, |pane, cx| {
 //             pane.items()
 //                 .map(|item| {
@@ -642,7 +678,7 @@
 //     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;
 //     server
@@ -685,7 +721,10 @@
 //         .unwrap();
 
 //     // Client A opens a file.
-//     let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+//     let workspace_a = client_a
+//         .build_workspace(&project_a, cx_a)
+//         .root(cx_a)
+//         .unwrap();
 //     workspace_a
 //         .update(cx_a, |workspace, cx| {
 //             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -696,7 +735,10 @@
 //         .unwrap();
 
 //     // Client B opens a different file.
-//     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+//     let workspace_b = client_b
+//         .build_workspace(&project_b, cx_b)
+//         .root(cx_b)
+//         .unwrap();
 //     workspace_b
 //         .update(cx_b, |workspace, cx| {
 //             workspace.open_path((worktree_id, "2.txt"), None, true, cx)
@@ -1167,7 +1209,7 @@
 //     cx_b: &mut TestAppContext,
 // ) {
 //     // 2 clients connect to a server.
-//     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;
 //     server
@@ -1207,8 +1249,17 @@
 //         .await
 //         .unwrap();
 
+//     todo!("could be wrong")
+//     let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
+//     let cx_a = &mut cx_a;
+//     let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
+//     let cx_b = &mut cx_b;
+
 //     // Client A opens some editors.
-//     let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+//     let workspace_a = client_a
+//         .build_workspace(&project_a, cx_a)
+//         .root(cx_a)
+//         .unwrap();
 //     let _editor_a1 = workspace_a
 //         .update(cx_a, |workspace, cx| {
 //             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -1219,9 +1270,12 @@
 //         .unwrap();
 
 //     // Client B starts following client A.
-//     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-//     let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
-//     let leader_id = project_b.read_with(cx_b, |project, _| {
+//     let workspace_b = client_b
+//         .build_workspace(&project_b, cx_b)
+//         .root(cx_b)
+//         .unwrap();
+//     let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
+//     let leader_id = project_b.update(cx_b, |project, _| {
 //         project.collaborators().values().next().unwrap().peer_id
 //     });
 //     workspace_b
@@ -1231,10 +1285,10 @@
 //         .await
 //         .unwrap();
 //     assert_eq!(
-//         workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 //         Some(leader_id)
 //     );
-//     let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+//     let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
 //         workspace
 //             .active_item(cx)
 //             .unwrap()
@@ -1245,7 +1299,7 @@
 //     // When client B moves, it automatically stops following client A.
 //     editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
 //     assert_eq!(
-//         workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 //         None
 //     );
 
@@ -1256,14 +1310,14 @@
 //         .await
 //         .unwrap();
 //     assert_eq!(
-//         workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 //         Some(leader_id)
 //     );
 
 //     // When client B edits, it automatically stops following client A.
 //     editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
 //     assert_eq!(
-//         workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 //         None
 //     );
 
@@ -1274,16 +1328,16 @@
 //         .await
 //         .unwrap();
 //     assert_eq!(
-//         workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 //         Some(leader_id)
 //     );
 
 //     // When client B scrolls, it automatically stops following client A.
 //     editor_b2.update(cx_b, |editor, cx| {
-//         editor.set_scroll_position(vec2f(0., 3.), cx)
+//         editor.set_scroll_position(point(0., 3.), cx)
 //     });
 //     assert_eq!(
-//         workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 //         None
 //     );
 
@@ -1294,7 +1348,7 @@
 //         .await
 //         .unwrap();
 //     assert_eq!(
-//         workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 //         Some(leader_id)
 //     );
 
@@ -1303,13 +1357,13 @@
 //         workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
 //     });
 //     assert_eq!(
-//         workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 //         Some(leader_id)
 //     );
 
 //     workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
 //     assert_eq!(
-//         workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 //         Some(leader_id)
 //     );
 
@@ -1321,7 +1375,7 @@
 //         .await
 //         .unwrap();
 //     assert_eq!(
-//         workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+//         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
 //         None
 //     );
 // }
@@ -1332,7 +1386,7 @@
 //     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;
 //     server
@@ -1345,20 +1399,26 @@
 
 //     client_a.fs().insert_tree("/a", json!({})).await;
 //     let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
-//     let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+//     let workspace_a = client_a
+//         .build_workspace(&project_a, cx_a)
+//         .root(cx_a)
+//         .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;
-//     let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+//     let workspace_b = client_b
+//         .build_workspace(&project_b, cx_b)
+//         .root(cx_b)
+//         .unwrap();
 
 //     executor.run_until_parked();
-//     let client_a_id = project_b.read_with(cx_b, |project, _| {
+//     let client_a_id = project_b.update(cx_b, |project, _| {
 //         project.collaborators().values().next().unwrap().peer_id
 //     });
-//     let client_b_id = project_a.read_with(cx_a, |project, _| {
+//     let client_b_id = project_a.update(cx_a, |project, _| {
 //         project.collaborators().values().next().unwrap().peer_id
 //     });
 
@@ -1370,13 +1430,13 @@
 //     });
 
 //     futures::try_join!(a_follow_b, b_follow_a).unwrap();
-//     workspace_a.read_with(cx_a, |workspace, _| {
+//     workspace_a.update(cx_a, |workspace, _| {
 //         assert_eq!(
 //             workspace.leader_for_pane(workspace.active_pane()),
 //             Some(client_b_id)
 //         );
 //     });
-//     workspace_b.read_with(cx_b, |workspace, _| {
+//     workspace_b.update(cx_b, |workspace, _| {
 //         assert_eq!(
 //             workspace.leader_for_pane(workspace.active_pane()),
 //             Some(client_a_id)
@@ -1398,7 +1458,7 @@
 //     // b opens a different file in project 2, a follows b
 //     // b opens a different file in project 1, a cannot follow b
 //     // b shares the project, a joins the project and follows b
-//     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;
 //     cx_a.update(editor::init);
@@ -1435,8 +1495,14 @@
 //     let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
 //     let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).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_a = client_a
+//         .build_workspace(&project_a, cx_a)
+//         .root(cx_a)
+//         .unwrap();
+//     let workspace_b = client_b
+//         .build_workspace(&project_b, cx_b)
+//         .root(cx_b)
+//         .unwrap();
 
 //     cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
 //     cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
@@ -1455,6 +1521,12 @@
 //         .await
 //         .unwrap();
 
+//     todo!("could be wrong")
+//     let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
+//     let cx_a = &mut cx_a;
+//     let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
+//     let cx_b = &mut cx_b;
+
 //     workspace_a
 //         .update(cx_a, |workspace, cx| {
 //             workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
@@ -1476,11 +1548,12 @@
 //     let workspace_b_project_a = cx_b
 //         .windows()
 //         .iter()
-//         .max_by_key(|window| window.id())
+//         .max_by_key(|window| window.item_id())
 //         .unwrap()
 //         .downcast::<Workspace>()
 //         .unwrap()
-//         .root(cx_b);
+//         .root(cx_b)
+//         .unwrap();
 
 //     // assert that b is following a in project a in w.rs
 //     workspace_b_project_a.update(cx_b, |workspace, cx| {
@@ -1534,7 +1607,7 @@
 //             workspace.leader_for_pane(workspace.active_pane())
 //         );
 //         let item = workspace.active_pane().read(cx).active_item().unwrap();
-//         assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
+//         assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs".into());
 //     });
 
 //     // b moves to y.rs in b's project, a is still following but can't yet see
@@ -1578,11 +1651,12 @@
 //     let workspace_a_project_b = cx_a
 //         .windows()
 //         .iter()
-//         .max_by_key(|window| window.id())
+//         .max_by_key(|window| window.item_id())
 //         .unwrap()
 //         .downcast::<Workspace>()
 //         .unwrap()
-//         .root(cx_a);
+//         .root(cx_a)
+//         .unwrap();
 
 //     workspace_a_project_b.update(cx_a, |workspace, cx| {
 //         assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
@@ -1596,12 +1670,151 @@
 //     });
 // }
 
+// #[gpui::test]
+// async fn test_following_into_excluded_file(
+//     executor: BackgroundExecutor,
+//     mut cx_a: &mut TestAppContext,
+//     mut 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;
+//     for cx in [&mut cx_a, &mut cx_b] {
+//         cx.update(|cx| {
+//             cx.update_global::<SettingsStore, _>(|store, cx| {
+//                 store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+//                     project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
+//                 });
+//             });
+//         });
+//     }
+//     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);
+
+//     client_a
+//         .fs()
+//         .insert_tree(
+//             "/a",
+//             json!({
+//                 ".git": {
+//                     "COMMIT_EDITMSG": "write your commit message here",
+//                 },
+//                 "1.txt": "one\none\none",
+//                 "2.txt": "two\ntwo\ntwo",
+//                 "3.txt": "three\nthree\nthree",
+//             }),
+//         )
+//         .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 window_a = client_a.build_workspace(&project_a, cx_a);
+//     let workspace_a = window_a.root(cx_a).unwrap();
+//     let peer_id_a = client_a.peer_id().unwrap();
+//     let window_b = client_b.build_workspace(&project_b, cx_b);
+//     let workspace_b = window_b.root(cx_b).unwrap();
+
+//     todo!("could be wrong")
+//     let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
+//     let cx_a = &mut cx_a;
+//     let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
+//     let cx_b = &mut cx_b;
+
+//     // Client A opens editors for a regular file and an excluded file.
+//     let editor_for_regular = workspace_a
+//         .update(cx_a, |workspace, cx| {
+//             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+//         })
+//         .await
+//         .unwrap()
+//         .downcast::<Editor>()
+//         .unwrap();
+//     let editor_for_excluded_a = workspace_a
+//         .update(cx_a, |workspace, cx| {
+//             workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
+//         })
+//         .await
+//         .unwrap()
+//         .downcast::<Editor>()
+//         .unwrap();
+
+//     // Client A updates their selections in those editors
+//     editor_for_regular.update(cx_a, |editor, cx| {
+//         editor.handle_input("a", cx);
+//         editor.handle_input("b", cx);
+//         editor.handle_input("c", cx);
+//         editor.select_left(&Default::default(), cx);
+//         assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+//     });
+//     editor_for_excluded_a.update(cx_a, |editor, cx| {
+//         editor.select_all(&Default::default(), cx);
+//         editor.handle_input("new commit message", cx);
+//         editor.select_left(&Default::default(), cx);
+//         assert_eq!(editor.selections.ranges(cx), vec![18..17]);
+//     });
+
+//     // When client B starts following client A, currently visible file is replicated
+//     workspace_b
+//         .update(cx_b, |workspace, cx| {
+//             workspace.follow(peer_id_a, cx).unwrap()
+//         })
+//         .await
+//         .unwrap();
+
+//     let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
+//         workspace
+//             .active_item(cx)
+//             .unwrap()
+//             .downcast::<Editor>()
+//             .unwrap()
+//     });
+//     assert_eq!(
+//         cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
+//         Some((worktree_id, ".git/COMMIT_EDITMSG").into())
+//     );
+//     assert_eq!(
+//         editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
+//         vec![18..17]
+//     );
+
+//     // Changes from B to the excluded file are replicated in A's editor
+//     editor_for_excluded_b.update(cx_b, |editor, cx| {
+//         editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
+//     });
+//     executor.run_until_parked();
+//     editor_for_excluded_a.update(cx_a, |editor, cx| {
+//         assert_eq!(
+//             editor.text(cx),
+//             "new commit messag\nCo-Authored-By: B <b@b.b>"
+//         );
+//     });
+// }
+
 // fn visible_push_notifications(
 //     cx: &mut TestAppContext,
-// ) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
+// ) -> Vec<gpui::View<ProjectSharedNotification>> {
 //     let mut ret = Vec::new();
 //     for window in cx.windows() {
-//         window.read_with(cx, |window| {
+//         window.update(cx, |window| {
 //             if let Some(handle) = window
 //                 .root_view()
 //                 .clone()
@@ -1645,8 +1858,8 @@
 //     })
 // }
 
-// fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
-//     workspace.read_with(cx, |workspace, cx| {
+// fn pane_summaries(workspace: &View<Workspace>, cx: &mut WindowContext<'_>) -> Vec<PaneSummary> {
+//     workspace.update(cx, |workspace, cx| {
 //         let active_pane = workspace.active_pane();
 //         workspace
 //             .panes()

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

@@ -510,10 +510,9 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
 
     // Simultaneously join channel 1 and then channel 2
     active_call_a
-        .update(cx_a, |call, cx| call.join_channel(channel_1, None, cx))
+        .update(cx_a, |call, cx| call.join_channel(channel_1, cx))
         .detach();
-    let join_channel_2 =
-        active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, None, cx));
+    let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx));
 
     join_channel_2.await.unwrap();
 
@@ -539,8 +538,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
         call.invite(client_c.user_id().unwrap(), None, cx)
     });
 
-    let join_channel =
-        active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
+    let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
 
     b_invite.await.unwrap();
     c_invite.await.unwrap();
@@ -569,8 +567,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
         .unwrap();
 
     // Simultaneously join channel 1 and call user B and user C from client A.
-    let join_channel =
-        active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
+    let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
 
     let b_invite = active_call_a.update(cx_a, |call, cx| {
         call.invite(client_b.user_id().unwrap(), None, cx)
@@ -2784,11 +2781,10 @@ async fn test_fs_operations(
 
     let entry = project_b
         .update(cx_b, |project, cx| {
-            project
-                .create_entry((worktree_id, "c.txt"), false, cx)
-                .unwrap()
+            project.create_entry((worktree_id, "c.txt"), false, cx)
         })
         .await
+        .unwrap()
         .unwrap();
 
     worktree_a.read_with(cx_a, |worktree, _| {
@@ -2815,8 +2811,8 @@ async fn test_fs_operations(
         .update(cx_b, |project, cx| {
             project.rename_entry(entry.id, Path::new("d.txt"), cx)
         })
-        .unwrap()
         .await
+        .unwrap()
         .unwrap();
 
     worktree_a.read_with(cx_a, |worktree, _| {
@@ -2841,11 +2837,10 @@ async fn test_fs_operations(
 
     let dir_entry = project_b
         .update(cx_b, |project, cx| {
-            project
-                .create_entry((worktree_id, "DIR"), true, cx)
-                .unwrap()
+            project.create_entry((worktree_id, "DIR"), true, cx)
         })
         .await
+        .unwrap()
         .unwrap();
 
     worktree_a.read_with(cx_a, |worktree, _| {
@@ -2870,27 +2865,24 @@ async fn test_fs_operations(
 
     project_b
         .update(cx_b, |project, cx| {
-            project
-                .create_entry((worktree_id, "DIR/e.txt"), false, cx)
-                .unwrap()
+            project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     project_b
         .update(cx_b, |project, cx| {
-            project
-                .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
-                .unwrap()
+            project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     project_b
         .update(cx_b, |project, cx| {
-            project
-                .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
-                .unwrap()
+            project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
         })
         .await
+        .unwrap()
         .unwrap();
 
     worktree_a.read_with(cx_a, |worktree, _| {
@@ -2931,11 +2923,10 @@ async fn test_fs_operations(
 
     project_b
         .update(cx_b, |project, cx| {
-            project
-                .copy_entry(entry.id, Path::new("f.txt"), cx)
-                .unwrap()
+            project.copy_entry(entry.id, Path::new("f.txt"), cx)
         })
         .await
+        .unwrap()
         .unwrap();
 
     worktree_a.read_with(cx_a, |worktree, _| {

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

@@ -221,7 +221,6 @@ impl TestServer {
             fs: fs.clone(),
             build_window_options: |_, _, _| Default::default(),
             node_runtime: FakeNodeRuntime::new(),
-            call_factory: |_| Box::new(workspace::TestCallHandler),
         });
 
         cx.update(|cx| {

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -1,5 +1,5 @@
 #![allow(unused)]
-// mod channel_modal;
+mod channel_modal;
 mod contact_finder;
 
 // use crate::{
@@ -18,7 +18,7 @@ mod contact_finder;
 // };
 use contact_finder::ContactFinder;
 use menu::{Cancel, Confirm, SelectNext, SelectPrev};
-use rpc::proto;
+use rpc::proto::{self, PeerId};
 use theme::{ActiveTheme, ThemeSettings};
 // use context_menu::{ContextMenu, ContextMenuItem};
 // use db::kvp::KEY_VALUE_STORE;
@@ -169,11 +169,12 @@ use editor::Editor;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext,
-    AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle,
-    Focusable, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent,
-    ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, ScrollHandle, SharedString,
-    Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+    actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, Action,
+    AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
+    FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, Length, Model,
+    MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce,
+    ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View, ViewContext,
+    VisualContext, WeakView,
 };
 use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
@@ -192,6 +193,8 @@ use workspace::{
 
 use crate::{face_pile::FacePile, CollaborationPanelSettings};
 
+use self::channel_modal::ChannelModal;
+
 pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(|workspace: &mut Workspace, _| {
         workspace.register_action(|workspace, _: &ToggleFocus, cx| {
@@ -345,21 +348,21 @@ enum Section {
 #[derive(Clone, Debug)]
 enum ListEntry {
     Header(Section),
-    //     CallParticipant {
-    //         user: Arc<User>,
-    //         peer_id: Option<PeerId>,
-    //         is_pending: bool,
-    //     },
-    //     ParticipantProject {
-    //         project_id: u64,
-    //         worktree_root_names: Vec<String>,
-    //         host_user_id: u64,
-    //         is_last: bool,
-    //     },
-    //     ParticipantScreen {
-    //         peer_id: Option<PeerId>,
-    //         is_last: bool,
-    //     },
+    CallParticipant {
+        user: Arc<User>,
+        peer_id: Option<PeerId>,
+        is_pending: bool,
+    },
+    ParticipantProject {
+        project_id: u64,
+        worktree_root_names: Vec<String>,
+        host_user_id: u64,
+        is_last: bool,
+    },
+    ParticipantScreen {
+        peer_id: Option<PeerId>,
+        is_last: bool,
+    },
     IncomingRequest(Arc<User>),
     OutgoingRequest(Arc<User>),
     //     ChannelInvite(Arc<Channel>),
@@ -368,12 +371,12 @@ enum ListEntry {
         depth: usize,
         has_children: bool,
     },
-    //     ChannelNotes {
-    //         channel_id: ChannelId,
-    //     },
-    //     ChannelChat {
-    //         channel_id: ChannelId,
-    //     },
+    ChannelNotes {
+        channel_id: ChannelId,
+    },
+    ChannelChat {
+        channel_id: ChannelId,
+    },
     ChannelEditor {
         depth: usize,
     },
@@ -706,136 +709,136 @@ impl CollabPanel {
 
         let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
         let old_entries = mem::take(&mut self.entries);
-        let scroll_to_top = false;
-
-        //         if let Some(room) = ActiveCall::global(cx).read(cx).room() {
-        //             self.entries.push(ListEntry::Header(Section::ActiveCall));
-        //             if !old_entries
-        //                 .iter()
-        //                 .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
-        //             {
-        //                 scroll_to_top = true;
-        //             }
+        let mut scroll_to_top = false;
 
-        //             if !self.collapsed_sections.contains(&Section::ActiveCall) {
-        //                 let room = room.read(cx);
+        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
+            self.entries.push(ListEntry::Header(Section::ActiveCall));
+            if !old_entries
+                .iter()
+                .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
+            {
+                scroll_to_top = true;
+            }
 
-        //                 if let Some(channel_id) = room.channel_id() {
-        //                     self.entries.push(ListEntry::ChannelNotes { channel_id });
-        //                     self.entries.push(ListEntry::ChannelChat { channel_id })
-        //                 }
+            if !self.collapsed_sections.contains(&Section::ActiveCall) {
+                let room = room.read(cx);
 
-        //                 // Populate the active user.
-        //                 if let Some(user) = user_store.current_user() {
-        //                     self.match_candidates.clear();
-        //                     self.match_candidates.push(StringMatchCandidate {
-        //                         id: 0,
-        //                         string: user.github_login.clone(),
-        //                         char_bag: user.github_login.chars().collect(),
-        //                     });
-        //                     let matches = executor.block(match_strings(
-        //                         &self.match_candidates,
-        //                         &query,
-        //                         true,
-        //                         usize::MAX,
-        //                         &Default::default(),
-        //                         executor.clone(),
-        //                     ));
-        //                     if !matches.is_empty() {
-        //                         let user_id = user.id;
-        //                         self.entries.push(ListEntry::CallParticipant {
-        //                             user,
-        //                             peer_id: None,
-        //                             is_pending: false,
-        //                         });
-        //                         let mut projects = room.local_participant().projects.iter().peekable();
-        //                         while let Some(project) = projects.next() {
-        //                             self.entries.push(ListEntry::ParticipantProject {
-        //                                 project_id: project.id,
-        //                                 worktree_root_names: project.worktree_root_names.clone(),
-        //                                 host_user_id: user_id,
-        //                                 is_last: projects.peek().is_none() && !room.is_screen_sharing(),
-        //                             });
-        //                         }
-        //                         if room.is_screen_sharing() {
-        //                             self.entries.push(ListEntry::ParticipantScreen {
-        //                                 peer_id: None,
-        //                                 is_last: true,
-        //                             });
-        //                         }
-        //                     }
-        //                 }
+                if let Some(channel_id) = room.channel_id() {
+                    self.entries.push(ListEntry::ChannelNotes { channel_id });
+                    self.entries.push(ListEntry::ChannelChat { channel_id })
+                }
 
-        //                 // Populate remote participants.
-        //                 self.match_candidates.clear();
-        //                 self.match_candidates
-        //                     .extend(room.remote_participants().iter().map(|(_, participant)| {
-        //                         StringMatchCandidate {
-        //                             id: participant.user.id as usize,
-        //                             string: participant.user.github_login.clone(),
-        //                             char_bag: participant.user.github_login.chars().collect(),
-        //                         }
-        //                     }));
-        //                 let matches = executor.block(match_strings(
-        //                     &self.match_candidates,
-        //                     &query,
-        //                     true,
-        //                     usize::MAX,
-        //                     &Default::default(),
-        //                     executor.clone(),
-        //                 ));
-        //                 for mat in matches {
-        //                     let user_id = mat.candidate_id as u64;
-        //                     let participant = &room.remote_participants()[&user_id];
-        //                     self.entries.push(ListEntry::CallParticipant {
-        //                         user: participant.user.clone(),
-        //                         peer_id: Some(participant.peer_id),
-        //                         is_pending: false,
-        //                     });
-        //                     let mut projects = participant.projects.iter().peekable();
-        //                     while let Some(project) = projects.next() {
-        //                         self.entries.push(ListEntry::ParticipantProject {
-        //                             project_id: project.id,
-        //                             worktree_root_names: project.worktree_root_names.clone(),
-        //                             host_user_id: participant.user.id,
-        //                             is_last: projects.peek().is_none()
-        //                                 && participant.video_tracks.is_empty(),
-        //                         });
-        //                     }
-        //                     if !participant.video_tracks.is_empty() {
-        //                         self.entries.push(ListEntry::ParticipantScreen {
-        //                             peer_id: Some(participant.peer_id),
-        //                             is_last: true,
-        //                         });
-        //                     }
-        //                 }
+                // Populate the active user.
+                if let Some(user) = user_store.current_user() {
+                    self.match_candidates.clear();
+                    self.match_candidates.push(StringMatchCandidate {
+                        id: 0,
+                        string: user.github_login.clone(),
+                        char_bag: user.github_login.chars().collect(),
+                    });
+                    let matches = executor.block(match_strings(
+                        &self.match_candidates,
+                        &query,
+                        true,
+                        usize::MAX,
+                        &Default::default(),
+                        executor.clone(),
+                    ));
+                    if !matches.is_empty() {
+                        let user_id = user.id;
+                        self.entries.push(ListEntry::CallParticipant {
+                            user,
+                            peer_id: None,
+                            is_pending: false,
+                        });
+                        let mut projects = room.local_participant().projects.iter().peekable();
+                        while let Some(project) = projects.next() {
+                            self.entries.push(ListEntry::ParticipantProject {
+                                project_id: project.id,
+                                worktree_root_names: project.worktree_root_names.clone(),
+                                host_user_id: user_id,
+                                is_last: projects.peek().is_none() && !room.is_screen_sharing(),
+                            });
+                        }
+                        if room.is_screen_sharing() {
+                            self.entries.push(ListEntry::ParticipantScreen {
+                                peer_id: None,
+                                is_last: true,
+                            });
+                        }
+                    }
+                }
 
-        //                 // Populate pending participants.
-        //                 self.match_candidates.clear();
-        //                 self.match_candidates
-        //                     .extend(room.pending_participants().iter().enumerate().map(
-        //                         |(id, participant)| StringMatchCandidate {
-        //                             id,
-        //                             string: participant.github_login.clone(),
-        //                             char_bag: participant.github_login.chars().collect(),
-        //                         },
-        //                     ));
-        //                 let matches = executor.block(match_strings(
-        //                     &self.match_candidates,
-        //                     &query,
-        //                     true,
-        //                     usize::MAX,
-        //                     &Default::default(),
-        //                     executor.clone(),
-        //                 ));
-        //                 self.entries
-        //                     .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
-        //                         user: room.pending_participants()[mat.candidate_id].clone(),
-        //                         peer_id: None,
-        //                         is_pending: true,
-        //                     }));
-        //             }
-        //         }
+                // Populate remote participants.
+                self.match_candidates.clear();
+                self.match_candidates
+                    .extend(room.remote_participants().iter().map(|(_, participant)| {
+                        StringMatchCandidate {
+                            id: participant.user.id as usize,
+                            string: participant.user.github_login.clone(),
+                            char_bag: participant.user.github_login.chars().collect(),
+                        }
+                    }));
+                let matches = executor.block(match_strings(
+                    &self.match_candidates,
+                    &query,
+                    true,
+                    usize::MAX,
+                    &Default::default(),
+                    executor.clone(),
+                ));
+                for mat in matches {
+                    let user_id = mat.candidate_id as u64;
+                    let participant = &room.remote_participants()[&user_id];
+                    self.entries.push(ListEntry::CallParticipant {
+                        user: participant.user.clone(),
+                        peer_id: Some(participant.peer_id),
+                        is_pending: false,
+                    });
+                    let mut projects = participant.projects.iter().peekable();
+                    while let Some(project) = projects.next() {
+                        self.entries.push(ListEntry::ParticipantProject {
+                            project_id: project.id,
+                            worktree_root_names: project.worktree_root_names.clone(),
+                            host_user_id: participant.user.id,
+                            is_last: projects.peek().is_none()
+                                && participant.video_tracks.is_empty(),
+                        });
+                    }
+                    if !participant.video_tracks.is_empty() {
+                        self.entries.push(ListEntry::ParticipantScreen {
+                            peer_id: Some(participant.peer_id),
+                            is_last: true,
+                        });
+                    }
+                }
+
+                // Populate pending participants.
+                self.match_candidates.clear();
+                self.match_candidates
+                    .extend(room.pending_participants().iter().enumerate().map(
+                        |(id, participant)| StringMatchCandidate {
+                            id,
+                            string: participant.github_login.clone(),
+                            char_bag: participant.github_login.chars().collect(),
+                        },
+                    ));
+                let matches = executor.block(match_strings(
+                    &self.match_candidates,
+                    &query,
+                    true,
+                    usize::MAX,
+                    &Default::default(),
+                    executor.clone(),
+                ));
+                self.entries
+                    .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
+                        user: room.pending_participants()[mat.candidate_id].clone(),
+                        peer_id: None,
+                        is_pending: true,
+                    }));
+            }
+        }
 
         let mut request_entries = Vec::new();
 
@@ -1133,290 +1136,234 @@ impl CollabPanel {
         cx.notify();
     }
 
-    //     fn render_call_participant(
-    //         user: &User,
-    //         peer_id: Option<PeerId>,
-    //         user_store: ModelHandle<UserStore>,
-    //         is_pending: bool,
-    //         is_selected: bool,
-    //         theme: &theme::Theme,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> AnyElement<Self> {
-    //         enum CallParticipant {}
-    //         enum CallParticipantTooltip {}
-    //         enum LeaveCallButton {}
-    //         enum LeaveCallTooltip {}
-
-    //         let collab_theme = &theme.collab_panel;
-
-    //         let is_current_user =
-    //             user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
-
-    //         let content = MouseEventHandler::new::<CallParticipant, _>(
-    //             user.id as usize,
-    //             cx,
-    //             |mouse_state, cx| {
-    //                 let style = if is_current_user {
-    //                     *collab_theme
-    //                         .contact_row
-    //                         .in_state(is_selected)
-    //                         .style_for(&mut Default::default())
-    //                 } else {
-    //                     *collab_theme
-    //                         .contact_row
-    //                         .in_state(is_selected)
-    //                         .style_for(mouse_state)
-    //                 };
-
-    //                 Flex::row()
-    //                     .with_children(user.avatar.clone().map(|avatar| {
-    //                         Image::from_data(avatar)
-    //                             .with_style(collab_theme.contact_avatar)
-    //                             .aligned()
-    //                             .left()
-    //                     }))
-    //                     .with_child(
-    //                         Label::new(
-    //                             user.github_login.clone(),
-    //                             collab_theme.contact_username.text.clone(),
-    //                         )
-    //                         .contained()
-    //                         .with_style(collab_theme.contact_username.container)
-    //                         .aligned()
-    //                         .left()
-    //                         .flex(1., true),
-    //                     )
-    //                     .with_children(if is_pending {
-    //                         Some(
-    //                             Label::new("Calling", collab_theme.calling_indicator.text.clone())
-    //                                 .contained()
-    //                                 .with_style(collab_theme.calling_indicator.container)
-    //                                 .aligned()
-    //                                 .into_any(),
-    //                         )
-    //                     } else if is_current_user {
-    //                         Some(
-    //                             MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
-    //                                 render_icon_button(
-    //                                     theme
-    //                                         .collab_panel
-    //                                         .leave_call_button
-    //                                         .style_for(is_selected, state),
-    //                                     "icons/exit.svg",
-    //                                 )
-    //                             })
-    //                             .with_cursor_style(CursorStyle::PointingHand)
-    //                             .on_click(MouseButton::Left, |_, _, cx| {
-    //                                 Self::leave_call(cx);
-    //                             })
-    //                             .with_tooltip::<LeaveCallTooltip>(
-    //                                 0,
-    //                                 "Leave call",
-    //                                 None,
-    //                                 theme.tooltip.clone(),
-    //                                 cx,
-    //                             )
-    //                             .into_any(),
-    //                         )
-    //                     } else {
-    //                         None
-    //                     })
-    //                     .constrained()
-    //                     .with_height(collab_theme.row_height)
-    //                     .contained()
-    //                     .with_style(style)
-    //             },
-    //         );
-
-    //         if is_current_user || is_pending || peer_id.is_none() {
-    //             return content.into_any();
-    //         }
-
-    //         let tooltip = format!("Follow {}", user.github_login);
-
-    //         content
-    //             .on_click(MouseButton::Left, move |_, this, cx| {
-    //                 if let Some(workspace) = this.workspace.upgrade(cx) {
-    //                     workspace
-    //                         .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
-    //                         .map(|task| task.detach_and_log_err(cx));
-    //                 }
-    //             })
-    //             .with_cursor_style(CursorStyle::PointingHand)
-    //             .with_tooltip::<CallParticipantTooltip>(
-    //                 user.id as usize,
-    //                 tooltip,
-    //                 Some(Box::new(FollowNextCollaborator)),
-    //                 theme.tooltip.clone(),
-    //                 cx,
-    //             )
-    //             .into_any()
-    //     }
+    fn render_call_participant(
+        &self,
+        user: Arc<User>,
+        peer_id: Option<PeerId>,
+        is_pending: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        let is_current_user =
+            self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
+        let tooltip = format!("Follow {}", user.github_login);
 
-    //     fn render_participant_project(
-    //         project_id: u64,
-    //         worktree_root_names: &[String],
-    //         host_user_id: u64,
-    //         is_current: bool,
-    //         is_last: bool,
-    //         is_selected: bool,
-    //         theme: &theme::Theme,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> AnyElement<Self> {
-    //         enum JoinProject {}
-    //         enum JoinProjectTooltip {}
-
-    //         let collab_theme = &theme.collab_panel;
-    //         let host_avatar_width = collab_theme
-    //             .contact_avatar
-    //             .width
-    //             .or(collab_theme.contact_avatar.height)
-    //             .unwrap_or(0.);
-    //         let tree_branch = collab_theme.tree_branch;
-    //         let project_name = if worktree_root_names.is_empty() {
-    //             "untitled".to_string()
-    //         } else {
-    //             worktree_root_names.join(", ")
-    //         };
-
-    //         let content =
-    //             MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
-    //                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
-    //                 let row = if is_current {
-    //                     collab_theme
-    //                         .project_row
-    //                         .in_state(true)
-    //                         .style_for(&mut Default::default())
-    //                 } else {
-    //                     collab_theme
-    //                         .project_row
-    //                         .in_state(is_selected)
-    //                         .style_for(mouse_state)
-    //                 };
-
-    //                 Flex::row()
-    //                     .with_child(render_tree_branch(
-    //                         tree_branch,
-    //                         &row.name.text,
-    //                         is_last,
-    //                         vec2f(host_avatar_width, collab_theme.row_height),
-    //                         cx.font_cache(),
-    //                     ))
-    //                     .with_child(
-    //                         Svg::new("icons/file_icons/folder.svg")
-    //                             .with_color(collab_theme.channel_hash.color)
-    //                             .constrained()
-    //                             .with_width(collab_theme.channel_hash.width)
-    //                             .aligned()
-    //                             .left(),
-    //                     )
-    //                     .with_child(
-    //                         Label::new(project_name.clone(), row.name.text.clone())
-    //                             .aligned()
-    //                             .left()
-    //                             .contained()
-    //                             .with_style(row.name.container)
-    //                             .flex(1., false),
-    //                     )
-    //                     .constrained()
-    //                     .with_height(collab_theme.row_height)
-    //                     .contained()
-    //                     .with_style(row.container)
-    //             });
-
-    //         if is_current {
-    //             return content.into_any();
-    //         }
-
-    //         content
-    //             .with_cursor_style(CursorStyle::PointingHand)
-    //             .on_click(MouseButton::Left, move |_, this, cx| {
-    //                 if let Some(workspace) = this.workspace.upgrade(cx) {
-    //                     let app_state = workspace.read(cx).app_state().clone();
-    //                     workspace::join_remote_project(project_id, host_user_id, app_state, cx)
-    //                         .detach_and_log_err(cx);
-    //                 }
-    //             })
-    //             .with_tooltip::<JoinProjectTooltip>(
-    //                 project_id as usize,
-    //                 format!("Open {}", project_name),
-    //                 None,
-    //                 theme.tooltip.clone(),
-    //                 cx,
-    //             )
-    //             .into_any()
-    //     }
+        ListItem::new(SharedString::from(user.github_login.clone()))
+            .left_child(Avatar::data(user.avatar.clone().unwrap()))
+            .child(
+                h_stack()
+                    .w_full()
+                    .justify_between()
+                    .child(Label::new(user.github_login.clone()))
+                    .child(if is_pending {
+                        Label::new("Calling").color(Color::Muted).into_any_element()
+                    } else if is_current_user {
+                        IconButton::new("leave-call", Icon::ArrowRight)
+                            .on_click(cx.listener(move |this, _, cx| {
+                                Self::leave_call(cx);
+                            }))
+                            .tooltip(|cx| Tooltip::text("Leave Call", cx))
+                            .into_any_element()
+                    } else {
+                        div().into_any_element()
+                    }),
+            )
+            .when_some(peer_id, |this, peer_id| {
+                this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
+                    .on_click(cx.listener(move |this, _, cx| {
+                        this.workspace
+                            .update(cx, |workspace, cx| workspace.follow(peer_id, cx));
+                    }))
+            })
+    }
 
-    //     fn render_participant_screen(
-    //         peer_id: Option<PeerId>,
-    //         is_last: bool,
-    //         is_selected: bool,
-    //         theme: &theme::CollabPanel,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> AnyElement<Self> {
-    //         enum OpenSharedScreen {}
-
-    //         let host_avatar_width = theme
-    //             .contact_avatar
-    //             .width
-    //             .or(theme.contact_avatar.height)
-    //             .unwrap_or(0.);
-    //         let tree_branch = theme.tree_branch;
-
-    //         let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
-    //             peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
-    //             cx,
-    //             |mouse_state, cx| {
-    //                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
-    //                 let row = theme
-    //                     .project_row
-    //                     .in_state(is_selected)
-    //                     .style_for(mouse_state);
-
-    //                 Flex::row()
-    //                     .with_child(render_tree_branch(
-    //                         tree_branch,
-    //                         &row.name.text,
-    //                         is_last,
-    //                         vec2f(host_avatar_width, theme.row_height),
-    //                         cx.font_cache(),
-    //                     ))
-    //                     .with_child(
-    //                         Svg::new("icons/desktop.svg")
-    //                             .with_color(theme.channel_hash.color)
-    //                             .constrained()
-    //                             .with_width(theme.channel_hash.width)
-    //                             .aligned()
-    //                             .left(),
-    //                     )
-    //                     .with_child(
-    //                         Label::new("Screen", row.name.text.clone())
-    //                             .aligned()
-    //                             .left()
-    //                             .contained()
-    //                             .with_style(row.name.container)
-    //                             .flex(1., false),
-    //                     )
-    //                     .constrained()
-    //                     .with_height(theme.row_height)
-    //                     .contained()
-    //                     .with_style(row.container)
-    //             },
-    //         );
-    //         if peer_id.is_none() {
-    //             return handler.into_any();
-    //         }
-    //         handler
-    //             .with_cursor_style(CursorStyle::PointingHand)
-    //             .on_click(MouseButton::Left, move |_, this, cx| {
-    //                 if let Some(workspace) = this.workspace.upgrade(cx) {
-    //                     workspace.update(cx, |workspace, cx| {
-    //                         workspace.open_shared_screen(peer_id.unwrap(), cx)
-    //                     });
-    //                 }
-    //             })
-    //             .into_any()
-    //     }
+    fn render_participant_project(
+        &self,
+        project_id: u64,
+        worktree_root_names: &[String],
+        host_user_id: u64,
+        //         is_current: bool,
+        is_last: bool,
+        //         is_selected: bool,
+        //         theme: &theme::Theme,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        let project_name: SharedString = if worktree_root_names.is_empty() {
+            "untitled".to_string()
+        } else {
+            worktree_root_names.join(", ")
+        }
+        .into();
+
+        let theme = cx.theme();
+
+        ListItem::new(project_id as usize)
+            .on_click(cx.listener(move |this, _, cx| {
+                this.workspace.update(cx, |workspace, cx| {
+                    let app_state = workspace.app_state().clone();
+                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+                        .detach_and_log_err(cx);
+                });
+            }))
+            .left_child(IconButton::new(0, Icon::Folder))
+            .child(
+                h_stack()
+                    .w_full()
+                    .justify_between()
+                    .child(render_tree_branch(is_last, cx))
+                    .child(Label::new(project_name.clone())),
+            )
+            .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
+
+        //         enum JoinProject {}
+        //         enum JoinProjectTooltip {}
+
+        //         let collab_theme = &theme.collab_panel;
+        //         let host_avatar_width = collab_theme
+        //             .contact_avatar
+        //             .width
+        //             .or(collab_theme.contact_avatar.height)
+        //             .unwrap_or(0.);
+        //         let tree_branch = collab_theme.tree_branch;
+
+        //         let content =
+        //             MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
+        //                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+        //                 let row = if is_current {
+        //                     collab_theme
+        //                         .project_row
+        //                         .in_state(true)
+        //                         .style_for(&mut Default::default())
+        //                 } else {
+        //                     collab_theme
+        //                         .project_row
+        //                         .in_state(is_selected)
+        //                         .style_for(mouse_state)
+        //                 };
+
+        //                 Flex::row()
+        //                     .with_child(render_tree_branch(
+        //                         tree_branch,
+        //                         &row.name.text,
+        //                         is_last,
+        //                         vec2f(host_avatar_width, collab_theme.row_height),
+        //                         cx.font_cache(),
+        //                     ))
+        //                     .with_child(
+        //                         Svg::new("icons/file_icons/folder.svg")
+        //                             .with_color(collab_theme.channel_hash.color)
+        //                             .constrained()
+        //                             .with_width(collab_theme.channel_hash.width)
+        //                             .aligned()
+        //                             .left(),
+        //                     )
+        //                     .with_child(
+        //                         Label::new(project_name.clone(), row.name.text.clone())
+        //                             .aligned()
+        //                             .left()
+        //                             .contained()
+        //                             .with_style(row.name.container)
+        //                             .flex(1., false),
+        //                     )
+        //                     .constrained()
+        //                     .with_height(collab_theme.row_height)
+        //                     .contained()
+        //                     .with_style(row.container)
+        //             });
+
+        //         if is_current {
+        //             return content.into_any();
+        //         }
+
+        //         content
+        //             .with_cursor_style(CursorStyle::PointingHand)
+        //             .on_click(MouseButton::Left, move |_, this, cx| {
+        //                 if let Some(workspace) = this.workspace.upgrade(cx) {
+        //                     let app_state = workspace.read(cx).app_state().clone();
+        //                     workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+        //                         .detach_and_log_err(cx);
+        //                 }
+        //             })
+        //             .with_tooltip::<JoinProjectTooltip>(
+        //                 project_id as usize,
+        //                 format!("Open {}", project_name),
+        //                 None,
+        //                 theme.tooltip.clone(),
+        //                 cx,
+        //             )
+        //             .into_any()
+    }
+
+    fn render_participant_screen(
+        &self,
+        peer_id: Option<PeerId>,
+        is_last: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        //         enum OpenSharedScreen {}
+
+        //         let host_avatar_width = theme
+        //             .contact_avatar
+        //             .width
+        //             .or(theme.contact_avatar.height)
+        //             .unwrap_or(0.);
+        //         let tree_branch = theme.tree_branch;
+
+        //         let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
+        //             peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
+        //             cx,
+        //             |mouse_state, cx| {
+        //                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+        //                 let row = theme
+        //                     .project_row
+        //                     .in_state(is_selected)
+        //                     .style_for(mouse_state);
+
+        //                 Flex::row()
+        //                     .with_child(render_tree_branch(
+        //                         tree_branch,
+        //                         &row.name.text,
+        //                         is_last,
+        //                         vec2f(host_avatar_width, theme.row_height),
+        //                         cx.font_cache(),
+        //                     ))
+        //                     .with_child(
+        //                         Svg::new("icons/desktop.svg")
+        //                             .with_color(theme.channel_hash.color)
+        //                             .constrained()
+        //                             .with_width(theme.channel_hash.width)
+        //                             .aligned()
+        //                             .left(),
+        //                     )
+        //                     .with_child(
+        //                         Label::new("Screen", row.name.text.clone())
+        //                             .aligned()
+        //                             .left()
+        //                             .contained()
+        //                             .with_style(row.name.container)
+        //                             .flex(1., false),
+        //                     )
+        //                     .constrained()
+        //                     .with_height(theme.row_height)
+        //                     .contained()
+        //                     .with_style(row.container)
+        //             },
+        //         );
+        //         if peer_id.is_none() {
+        //             return handler.into_any();
+        //         }
+        //         handler
+        //             .with_cursor_style(CursorStyle::PointingHand)
+        //             .on_click(MouseButton::Left, move |_, this, cx| {
+        //                 if let Some(workspace) = this.workspace.upgrade(cx) {
+        //                     workspace.update(cx, |workspace, cx| {
+        //                         workspace.open_shared_screen(peer_id.unwrap(), cx)
+        //                     });
+        //                 }
+        //             })
+        //             .into_any()
+
+        div()
+    }
 
     fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
         if let Some(_) = self.channel_editing_state.take() {
@@ -1463,117 +1410,114 @@ impl CollabPanel {
     //         .into_any()
     //     }
 
-    //     fn render_channel_notes(
-    //         &self,
-    //         channel_id: ChannelId,
-    //         theme: &theme::CollabPanel,
-    //         is_selected: bool,
-    //         ix: usize,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> AnyElement<Self> {
-    //         enum ChannelNotes {}
-    //         let host_avatar_width = theme
-    //             .contact_avatar
-    //             .width
-    //             .or(theme.contact_avatar.height)
-    //             .unwrap_or(0.);
-
-    //         MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
-    //             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
-    //             let row = theme.project_row.in_state(is_selected).style_for(state);
-
-    //             Flex::<Self>::row()
-    //                 .with_child(render_tree_branch(
-    //                     tree_branch,
-    //                     &row.name.text,
-    //                     false,
-    //                     vec2f(host_avatar_width, theme.row_height),
-    //                     cx.font_cache(),
-    //                 ))
-    //                 .with_child(
-    //                     Svg::new("icons/file.svg")
-    //                         .with_color(theme.channel_hash.color)
-    //                         .constrained()
-    //                         .with_width(theme.channel_hash.width)
-    //                         .aligned()
-    //                         .left(),
-    //                 )
-    //                 .with_child(
-    //                     Label::new("notes", theme.channel_name.text.clone())
-    //                         .contained()
-    //                         .with_style(theme.channel_name.container)
-    //                         .aligned()
-    //                         .left()
-    //                         .flex(1., true),
-    //                 )
-    //                 .constrained()
-    //                 .with_height(theme.row_height)
-    //                 .contained()
-    //                 .with_style(*theme.channel_row.style_for(is_selected, state))
-    //                 .with_padding_left(theme.channel_row.default_style().padding.left)
-    //         })
-    //         .on_click(MouseButton::Left, move |_, this, cx| {
-    //             this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
-    //         })
-    //         .with_cursor_style(CursorStyle::PointingHand)
-    //         .into_any()
-    //     }
+    fn render_channel_notes(
+        &self,
+        channel_id: ChannelId,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        //         enum ChannelNotes {}
+        //         let host_avatar_width = theme
+        //             .contact_avatar
+        //             .width
+        //             .or(theme.contact_avatar.height)
+        //             .unwrap_or(0.);
+
+        //         MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
+        //             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+        //             let row = theme.project_row.in_state(is_selected).style_for(state);
+
+        //             Flex::<Self>::row()
+        //                 .with_child(render_tree_branch(
+        //                     tree_branch,
+        //                     &row.name.text,
+        //                     false,
+        //                     vec2f(host_avatar_width, theme.row_height),
+        //                     cx.font_cache(),
+        //                 ))
+        //                 .with_child(
+        //                     Svg::new("icons/file.svg")
+        //                         .with_color(theme.channel_hash.color)
+        //                         .constrained()
+        //                         .with_width(theme.channel_hash.width)
+        //                         .aligned()
+        //                         .left(),
+        //                 )
+        //                 .with_child(
+        //                     Label::new("notes", theme.channel_name.text.clone())
+        //                         .contained()
+        //                         .with_style(theme.channel_name.container)
+        //                         .aligned()
+        //                         .left()
+        //                         .flex(1., true),
+        //                 )
+        //                 .constrained()
+        //                 .with_height(theme.row_height)
+        //                 .contained()
+        //                 .with_style(*theme.channel_row.style_for(is_selected, state))
+        //                 .with_padding_left(theme.channel_row.default_style().padding.left)
+        //         })
+        //         .on_click(MouseButton::Left, move |_, this, cx| {
+        //             this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
+        //         })
+        //         .with_cursor_style(CursorStyle::PointingHand)
+        //         .into_any()
 
-    //     fn render_channel_chat(
-    //         &self,
-    //         channel_id: ChannelId,
-    //         theme: &theme::CollabPanel,
-    //         is_selected: bool,
-    //         ix: usize,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> AnyElement<Self> {
-    //         enum ChannelChat {}
-    //         let host_avatar_width = theme
-    //             .contact_avatar
-    //             .width
-    //             .or(theme.contact_avatar.height)
-    //             .unwrap_or(0.);
-
-    //         MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
-    //             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
-    //             let row = theme.project_row.in_state(is_selected).style_for(state);
-
-    //             Flex::<Self>::row()
-    //                 .with_child(render_tree_branch(
-    //                     tree_branch,
-    //                     &row.name.text,
-    //                     true,
-    //                     vec2f(host_avatar_width, theme.row_height),
-    //                     cx.font_cache(),
-    //                 ))
-    //                 .with_child(
-    //                     Svg::new("icons/conversations.svg")
-    //                         .with_color(theme.channel_hash.color)
-    //                         .constrained()
-    //                         .with_width(theme.channel_hash.width)
-    //                         .aligned()
-    //                         .left(),
-    //                 )
-    //                 .with_child(
-    //                     Label::new("chat", theme.channel_name.text.clone())
-    //                         .contained()
-    //                         .with_style(theme.channel_name.container)
-    //                         .aligned()
-    //                         .left()
-    //                         .flex(1., true),
-    //                 )
-    //                 .constrained()
-    //                 .with_height(theme.row_height)
-    //                 .contained()
-    //                 .with_style(*theme.channel_row.style_for(is_selected, state))
-    //                 .with_padding_left(theme.channel_row.default_style().padding.left)
-    //         })
-    //         .on_click(MouseButton::Left, move |_, this, cx| {
-    //             this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
-    //         })
-    //         .with_cursor_style(CursorStyle::PointingHand)
-    //         .into_any()
-    //     }
+        div()
+    }
+
+    fn render_channel_chat(
+        &self,
+        channel_id: ChannelId,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        //         enum ChannelChat {}
+        //         let host_avatar_width = theme
+        //             .contact_avatar
+        //             .width
+        //             .or(theme.contact_avatar.height)
+        //             .unwrap_or(0.);
+
+        //         MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
+        //             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+        //             let row = theme.project_row.in_state(is_selected).style_for(state);
+
+        //             Flex::<Self>::row()
+        //                 .with_child(render_tree_branch(
+        //                     tree_branch,
+        //                     &row.name.text,
+        //                     true,
+        //                     vec2f(host_avatar_width, theme.row_height),
+        //                     cx.font_cache(),
+        //                 ))
+        //                 .with_child(
+        //                     Svg::new("icons/conversations.svg")
+        //                         .with_color(theme.channel_hash.color)
+        //                         .constrained()
+        //                         .with_width(theme.channel_hash.width)
+        //                         .aligned()
+        //                         .left(),
+        //                 )
+        //                 .with_child(
+        //                     Label::new("chat", theme.channel_name.text.clone())
+        //                         .contained()
+        //                         .with_style(theme.channel_name.container)
+        //                         .aligned()
+        //                         .left()
+        //                         .flex(1., true),
+        //                 )
+        //                 .constrained()
+        //                 .with_height(theme.row_height)
+        //                 .contained()
+        //                 .with_style(*theme.channel_row.style_for(is_selected, state))
+        //                 .with_padding_left(theme.channel_row.default_style().padding.left)
+        //         })
+        //         .on_click(MouseButton::Left, move |_, this, cx| {
+        //             this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
+        //         })
+        //         .with_cursor_style(CursorStyle::PointingHand)
+        //         .into_any()
+        div()
+    }
 
     //     fn render_channel_invite(
     //         channel: Arc<Channel>,

crates/collab_ui2/src/collab_panel/channel_modal.rs 🔗

@@ -3,58 +3,54 @@ use client::{
     proto::{self, ChannelRole, ChannelVisibility},
     User, UserId, UserStore,
 };
-use context_menu::{ContextMenu, ContextMenuItem};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions,
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
-    ViewHandle,
+    actions, div, AppContext, ClipboardItem, DismissEvent, Div, Entity, EventEmitter,
+    FocusableView, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext,
+    WeakView,
 };
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
+use ui::v_stack;
 use util::TryFutureExt;
-use workspace::Modal;
 
 actions!(
-    channel_modal,
-    [
-        SelectNextControl,
-        ToggleMode,
-        ToggleMemberAdmin,
-        RemoveMember
-    ]
+    SelectNextControl,
+    ToggleMode,
+    ToggleMemberAdmin,
+    RemoveMember
 );
 
-pub fn init(cx: &mut AppContext) {
-    Picker::<ChannelModalDelegate>::init(cx);
-    cx.add_action(ChannelModal::toggle_mode);
-    cx.add_action(ChannelModal::toggle_member_admin);
-    cx.add_action(ChannelModal::remove_member);
-    cx.add_action(ChannelModal::dismiss);
-}
+// pub fn init(cx: &mut AppContext) {
+//     Picker::<ChannelModalDelegate>::init(cx);
+//     cx.add_action(ChannelModal::toggle_mode);
+//     cx.add_action(ChannelModal::toggle_member_admin);
+//     cx.add_action(ChannelModal::remove_member);
+//     cx.add_action(ChannelModal::dismiss);
+// }
 
 pub struct ChannelModal {
-    picker: ViewHandle<Picker<ChannelModalDelegate>>,
-    channel_store: ModelHandle<ChannelStore>,
+    picker: View<Picker<ChannelModalDelegate>>,
+    channel_store: Model<ChannelStore>,
     channel_id: ChannelId,
     has_focus: bool,
 }
 
 impl ChannelModal {
     pub fn new(
-        user_store: ModelHandle<UserStore>,
-        channel_store: ModelHandle<ChannelStore>,
+        user_store: Model<UserStore>,
+        channel_store: Model<ChannelStore>,
         channel_id: ChannelId,
         mode: Mode,
         members: Vec<ChannelMembership>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
-        let picker = cx.add_view(|cx| {
+        let channel_modal = cx.view().downgrade();
+        let picker = cx.build_view(|cx| {
             Picker::new(
                 ChannelModalDelegate {
+                    channel_modal,
                     matching_users: Vec::new(),
                     matching_member_indices: Vec::new(),
                     selected_index: 0,
@@ -64,20 +60,17 @@ impl ChannelModal {
                     match_candidates: Vec::new(),
                     members,
                     mode,
-                    context_menu: cx.add_view(|cx| {
-                        let mut menu = ContextMenu::new(cx.view_id(), cx);
-                        menu.set_position_mode(OverlayPositionMode::Local);
-                        menu
-                    }),
+                    // context_menu: cx.add_view(|cx| {
+                    //     let mut menu = ContextMenu::new(cx.view_id(), cx);
+                    //     menu.set_position_mode(OverlayPositionMode::Local);
+                    //     menu
+                    // }),
                 },
                 cx,
             )
-            .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
         });
 
-        cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
-
-        let has_focus = picker.read(cx).has_focus();
+        let has_focus = picker.focus_handle(cx).contains_focused(cx);
 
         Self {
             picker,
@@ -88,7 +81,7 @@ impl ChannelModal {
     }
 
     fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
-        let mode = match self.picker.read(cx).delegate().mode {
+        let mode = match self.picker.read(cx).delegate.mode {
             Mode::ManageMembers => Mode::InviteMembers,
             Mode::InviteMembers => Mode::ManageMembers,
         };
@@ -103,20 +96,20 @@ impl ChannelModal {
                 let mut members = channel_store
                     .update(&mut cx, |channel_store, cx| {
                         channel_store.get_channel_member_details(channel_id, cx)
-                    })
+                    })?
                     .await?;
 
                 members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
 
                 this.update(&mut cx, |this, cx| {
                     this.picker
-                        .update(cx, |picker, _| picker.delegate_mut().members = members);
+                        .update(cx, |picker, _| picker.delegate.members = members);
                 })?;
             }
 
             this.update(&mut cx, |this, cx| {
                 this.picker.update(cx, |picker, cx| {
-                    let delegate = picker.delegate_mut();
+                    let delegate = &mut picker.delegate;
                     delegate.mode = mode;
                     delegate.selected_index = 0;
                     picker.set_query("", cx);
@@ -131,203 +124,194 @@ impl ChannelModal {
 
     fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
         self.picker.update(cx, |picker, cx| {
-            picker.delegate_mut().toggle_selected_member_admin(cx);
+            picker.delegate.toggle_selected_member_admin(cx);
         })
     }
 
     fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
         self.picker.update(cx, |picker, cx| {
-            picker.delegate_mut().remove_selected_member(cx);
+            picker.delegate.remove_selected_member(cx);
         });
     }
 
     fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(PickerEvent::Dismiss);
+        cx.emit(DismissEvent);
     }
 }
 
-impl Entity for ChannelModal {
-    type Event = PickerEvent;
-}
-
-impl View for ChannelModal {
-    fn ui_name() -> &'static str {
-        "ChannelModal"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = &theme::current(cx).collab_panel.tabbed_modal;
-
-        let mode = self.picker.read(cx).delegate().mode;
-        let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
-            return Empty::new().into_any();
-        };
-
-        enum InviteMembers {}
-        enum ManageMembers {}
-
-        fn render_mode_button<T: 'static>(
-            mode: Mode,
-            text: &'static str,
-            current_mode: Mode,
-            theme: &theme::TabbedModal,
-            cx: &mut ViewContext<ChannelModal>,
-        ) -> AnyElement<ChannelModal> {
-            let active = mode == current_mode;
-            MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
-                let contained_text = theme.tab_button.style_for(active, state);
-                Label::new(text, contained_text.text.clone())
-                    .contained()
-                    .with_style(contained_text.container.clone())
-            })
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if !active {
-                    this.set_mode(mode, cx);
-                }
-            })
-            .with_cursor_style(CursorStyle::PointingHand)
-            .into_any()
-        }
-
-        fn render_visibility(
-            channel_id: ChannelId,
-            visibility: ChannelVisibility,
-            theme: &theme::TabbedModal,
-            cx: &mut ViewContext<ChannelModal>,
-        ) -> AnyElement<ChannelModal> {
-            enum TogglePublic {}
-
-            if visibility == ChannelVisibility::Members {
-                return Flex::row()
-                    .with_child(
-                        MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
-                            let style = theme.visibility_toggle.style_for(state);
-                            Label::new(format!("{}", "Public access: OFF"), style.text.clone())
-                                .contained()
-                                .with_style(style.container.clone())
-                        })
-                        .on_click(MouseButton::Left, move |_, this, cx| {
-                            this.channel_store
-                                .update(cx, |channel_store, cx| {
-                                    channel_store.set_channel_visibility(
-                                        channel_id,
-                                        ChannelVisibility::Public,
-                                        cx,
-                                    )
-                                })
-                                .detach_and_log_err(cx);
-                        })
-                        .with_cursor_style(CursorStyle::PointingHand),
-                    )
-                    .into_any();
-            }
-
-            Flex::row()
-                .with_child(
-                    MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
-                        let style = theme.visibility_toggle.style_for(state);
-                        Label::new(format!("{}", "Public access: ON"), style.text.clone())
-                            .contained()
-                            .with_style(style.container.clone())
-                    })
-                    .on_click(MouseButton::Left, move |_, this, cx| {
-                        this.channel_store
-                            .update(cx, |channel_store, cx| {
-                                channel_store.set_channel_visibility(
-                                    channel_id,
-                                    ChannelVisibility::Members,
-                                    cx,
-                                )
-                            })
-                            .detach_and_log_err(cx);
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand),
-                )
-                .with_spacing(14.0)
-                .with_child(
-                    MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
-                        let style = theme.channel_link.style_for(state);
-                        Label::new(format!("{}", "copy link"), style.text.clone())
-                            .contained()
-                            .with_style(style.container.clone())
-                    })
-                    .on_click(MouseButton::Left, move |_, this, cx| {
-                        if let Some(channel) =
-                            this.channel_store.read(cx).channel_for_id(channel_id)
-                        {
-                            let item = ClipboardItem::new(channel.link());
-                            cx.write_to_clipboard(item);
-                        }
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand),
-                )
-                .into_any()
-        }
-
-        Flex::column()
-            .with_child(
-                Flex::column()
-                    .with_child(
-                        Label::new(format!("#{}", channel.name), theme.title.text.clone())
-                            .contained()
-                            .with_style(theme.title.container.clone()),
-                    )
-                    .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
-                    .with_child(Flex::row().with_children([
-                        render_mode_button::<InviteMembers>(
-                            Mode::InviteMembers,
-                            "Invite members",
-                            mode,
-                            theme,
-                            cx,
-                        ),
-                        render_mode_button::<ManageMembers>(
-                            Mode::ManageMembers,
-                            "Manage members",
-                            mode,
-                            theme,
-                            cx,
-                        ),
-                    ]))
-                    .expanded()
-                    .contained()
-                    .with_style(theme.header),
-            )
-            .with_child(
-                ChildView::new(&self.picker, cx)
-                    .contained()
-                    .with_style(theme.body),
-            )
-            .constrained()
-            .with_max_height(theme.max_height)
-            .with_max_width(theme.max_width)
-            .contained()
-            .with_style(theme.modal)
-            .into_any()
-    }
-
-    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-        self.has_focus = true;
-        if cx.is_self_focused() {
-            cx.focus(&self.picker)
-        }
-    }
+impl EventEmitter<DismissEvent> for ChannelModal {}
 
-    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = false;
+impl FocusableView for ChannelModal {
+    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+        self.picker.focus_handle(cx)
     }
 }
 
-impl Modal for ChannelModal {
-    fn has_focus(&self) -> bool {
-        self.has_focus
+impl Render for ChannelModal {
+    type Element = Div;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        v_stack().min_w_96().child(self.picker.clone())
+        // let theme = &theme::current(cx).collab_panel.tabbed_modal;
+
+        // let mode = self.picker.read(cx).delegate().mode;
+        // let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
+        //     return Empty::new().into_any();
+        // };
+
+        // enum InviteMembers {}
+        // enum ManageMembers {}
+
+        // fn render_mode_button<T: 'static>(
+        //     mode: Mode,
+        //     text: &'static str,
+        //     current_mode: Mode,
+        //     theme: &theme::TabbedModal,
+        //     cx: &mut ViewContext<ChannelModal>,
+        // ) -> AnyElement<ChannelModal> {
+        //     let active = mode == current_mode;
+        //     MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
+        //         let contained_text = theme.tab_button.style_for(active, state);
+        //         Label::new(text, contained_text.text.clone())
+        //             .contained()
+        //             .with_style(contained_text.container.clone())
+        //     })
+        //     .on_click(MouseButton::Left, move |_, this, cx| {
+        //         if !active {
+        //             this.set_mode(mode, cx);
+        //         }
+        //     })
+        //     .with_cursor_style(CursorStyle::PointingHand)
+        //     .into_any()
+        // }
+
+        // fn render_visibility(
+        //     channel_id: ChannelId,
+        //     visibility: ChannelVisibility,
+        //     theme: &theme::TabbedModal,
+        //     cx: &mut ViewContext<ChannelModal>,
+        // ) -> AnyElement<ChannelModal> {
+        //     enum TogglePublic {}
+
+        //     if visibility == ChannelVisibility::Members {
+        //         return Flex::row()
+        //             .with_child(
+        //                 MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
+        //                     let style = theme.visibility_toggle.style_for(state);
+        //                     Label::new(format!("{}", "Public access: OFF"), style.text.clone())
+        //                         .contained()
+        //                         .with_style(style.container.clone())
+        //                 })
+        //                 .on_click(MouseButton::Left, move |_, this, cx| {
+        //                     this.channel_store
+        //                         .update(cx, |channel_store, cx| {
+        //                             channel_store.set_channel_visibility(
+        //                                 channel_id,
+        //                                 ChannelVisibility::Public,
+        //                                 cx,
+        //                             )
+        //                         })
+        //                         .detach_and_log_err(cx);
+        //                 })
+        //                 .with_cursor_style(CursorStyle::PointingHand),
+        //             )
+        //             .into_any();
+        //     }
+
+        //     Flex::row()
+        //         .with_child(
+        //             MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
+        //                 let style = theme.visibility_toggle.style_for(state);
+        //                 Label::new(format!("{}", "Public access: ON"), style.text.clone())
+        //                     .contained()
+        //                     .with_style(style.container.clone())
+        //             })
+        //             .on_click(MouseButton::Left, move |_, this, cx| {
+        //                 this.channel_store
+        //                     .update(cx, |channel_store, cx| {
+        //                         channel_store.set_channel_visibility(
+        //                             channel_id,
+        //                             ChannelVisibility::Members,
+        //                             cx,
+        //                         )
+        //                     })
+        //                     .detach_and_log_err(cx);
+        //             })
+        //             .with_cursor_style(CursorStyle::PointingHand),
+        //         )
+        //         .with_spacing(14.0)
+        //         .with_child(
+        //             MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
+        //                 let style = theme.channel_link.style_for(state);
+        //                 Label::new(format!("{}", "copy link"), style.text.clone())
+        //                     .contained()
+        //                     .with_style(style.container.clone())
+        //             })
+        //             .on_click(MouseButton::Left, move |_, this, cx| {
+        //                 if let Some(channel) =
+        //                     this.channel_store.read(cx).channel_for_id(channel_id)
+        //                 {
+        //                     let item = ClipboardItem::new(channel.link());
+        //                     cx.write_to_clipboard(item);
+        //                 }
+        //             })
+        //             .with_cursor_style(CursorStyle::PointingHand),
+        //         )
+        //         .into_any()
+        // }
+
+        // Flex::column()
+        //     .with_child(
+        //         Flex::column()
+        //             .with_child(
+        //                 Label::new(format!("#{}", channel.name), theme.title.text.clone())
+        //                     .contained()
+        //                     .with_style(theme.title.container.clone()),
+        //             )
+        //             .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
+        //             .with_child(Flex::row().with_children([
+        //                 render_mode_button::<InviteMembers>(
+        //                     Mode::InviteMembers,
+        //                     "Invite members",
+        //                     mode,
+        //                     theme,
+        //                     cx,
+        //                 ),
+        //                 render_mode_button::<ManageMembers>(
+        //                     Mode::ManageMembers,
+        //                     "Manage members",
+        //                     mode,
+        //                     theme,
+        //                     cx,
+        //                 ),
+        //             ]))
+        //             .expanded()
+        //             .contained()
+        //             .with_style(theme.header),
+        //     )
+        //     .with_child(
+        //         ChildView::new(&self.picker, cx)
+        //             .contained()
+        //             .with_style(theme.body),
+        //     )
+        //     .constrained()
+        //     .with_max_height(theme.max_height)
+        //     .with_max_width(theme.max_width)
+        //     .contained()
+        //     .with_style(theme.modal)
+        //     .into_any()
     }
 
-    fn dismiss_on_event(event: &Self::Event) -> bool {
-        match event {
-            PickerEvent::Dismiss => true,
-        }
-    }
+    // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+    //     self.has_focus = true;
+    //     if cx.is_self_focused() {
+    //         cx.focus(&self.picker)
+    //     }
+    // }
+
+    // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+    //     self.has_focus = false;
+    // }
 }
 
 #[derive(Copy, Clone, PartialEq)]
@@ -337,19 +321,22 @@ pub enum Mode {
 }
 
 pub struct ChannelModalDelegate {
+    channel_modal: WeakView<ChannelModal>,
     matching_users: Vec<Arc<User>>,
     matching_member_indices: Vec<usize>,
-    user_store: ModelHandle<UserStore>,
-    channel_store: ModelHandle<ChannelStore>,
+    user_store: Model<UserStore>,
+    channel_store: Model<ChannelStore>,
     channel_id: ChannelId,
     selected_index: usize,
     mode: Mode,
     match_candidates: Vec<StringMatchCandidate>,
     members: Vec<ChannelMembership>,
-    context_menu: ViewHandle<ContextMenu>,
+    // context_menu: ViewHandle<ContextMenu>,
 }
 
 impl PickerDelegate for ChannelModalDelegate {
+    type ListItem = Div;
+
     fn placeholder_text(&self) -> Arc<str> {
         "Search collaborator by username...".into()
     }
@@ -382,19 +369,19 @@ impl PickerDelegate for ChannelModalDelegate {
                         }
                     }));
 
-                let matches = cx.background().block(match_strings(
+                let matches = cx.background_executor().block(match_strings(
                     &self.match_candidates,
                     &query,
                     true,
                     usize::MAX,
                     &Default::default(),
-                    cx.background().clone(),
+                    cx.background_executor().clone(),
                 ));
 
                 cx.spawn(|picker, mut cx| async move {
                     picker
                         .update(&mut cx, |picker, cx| {
-                            let delegate = picker.delegate_mut();
+                            let delegate = &mut picker.delegate;
                             delegate.matching_member_indices.clear();
                             delegate
                                 .matching_member_indices
@@ -412,8 +399,7 @@ impl PickerDelegate for ChannelModalDelegate {
                     async {
                         let users = search_users.await?;
                         picker.update(&mut cx, |picker, cx| {
-                            let delegate = picker.delegate_mut();
-                            delegate.matching_users = users;
+                            picker.delegate.matching_users = users;
                             cx.notify();
                         })?;
                         anyhow::Ok(())
@@ -445,138 +431,142 @@ impl PickerDelegate for ChannelModalDelegate {
     }
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        cx.emit(PickerEvent::Dismiss);
+        self.channel_modal
+            .update(cx, |_, cx| {
+                cx.emit(DismissEvent);
+            })
+            .ok();
     }
 
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: &mut MouseState,
         selected: bool,
-        cx: &gpui::AppContext,
-    ) -> AnyElement<Picker<Self>> {
-        let full_theme = &theme::current(cx);
-        let theme = &full_theme.collab_panel.channel_modal;
-        let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
-        let (user, role) = self.user_at_index(ix).unwrap();
-        let request_status = self.member_status(user.id, cx);
-
-        let style = tabbed_modal
-            .picker
-            .item
-            .in_state(selected)
-            .style_for(mouse_state);
-
-        let in_manage = matches!(self.mode, Mode::ManageMembers);
-
-        let mut result = Flex::row()
-            .with_children(user.avatar.clone().map(|avatar| {
-                Image::from_data(avatar)
-                    .with_style(theme.contact_avatar)
-                    .aligned()
-                    .left()
-            }))
-            .with_child(
-                Label::new(user.github_login.clone(), style.label.clone())
-                    .contained()
-                    .with_style(theme.contact_username)
-                    .aligned()
-                    .left(),
-            )
-            .with_children({
-                (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
-                    || {
-                        Label::new("Invited", theme.member_tag.text.clone())
-                            .contained()
-                            .with_style(theme.member_tag.container)
-                            .aligned()
-                            .left()
-                    },
-                )
-            })
-            .with_children(if in_manage && role == Some(ChannelRole::Admin) {
-                Some(
-                    Label::new("Admin", theme.member_tag.text.clone())
-                        .contained()
-                        .with_style(theme.member_tag.container)
-                        .aligned()
-                        .left(),
-                )
-            } else if in_manage && role == Some(ChannelRole::Guest) {
-                Some(
-                    Label::new("Guest", theme.member_tag.text.clone())
-                        .contained()
-                        .with_style(theme.member_tag.container)
-                        .aligned()
-                        .left(),
-                )
-            } else {
-                None
-            })
-            .with_children({
-                let svg = match self.mode {
-                    Mode::ManageMembers => Some(
-                        Svg::new("icons/ellipsis.svg")
-                            .with_color(theme.member_icon.color)
-                            .constrained()
-                            .with_width(theme.member_icon.icon_width)
-                            .aligned()
-                            .constrained()
-                            .with_width(theme.member_icon.button_width)
-                            .with_height(theme.member_icon.button_width)
-                            .contained()
-                            .with_style(theme.member_icon.container),
-                    ),
-                    Mode::InviteMembers => match request_status {
-                        Some(proto::channel_member::Kind::Member) => Some(
-                            Svg::new("icons/check.svg")
-                                .with_color(theme.member_icon.color)
-                                .constrained()
-                                .with_width(theme.member_icon.icon_width)
-                                .aligned()
-                                .constrained()
-                                .with_width(theme.member_icon.button_width)
-                                .with_height(theme.member_icon.button_width)
-                                .contained()
-                                .with_style(theme.member_icon.container),
-                        ),
-                        Some(proto::channel_member::Kind::Invitee) => Some(
-                            Svg::new("icons/check.svg")
-                                .with_color(theme.invitee_icon.color)
-                                .constrained()
-                                .with_width(theme.invitee_icon.icon_width)
-                                .aligned()
-                                .constrained()
-                                .with_width(theme.invitee_icon.button_width)
-                                .with_height(theme.invitee_icon.button_width)
-                                .contained()
-                                .with_style(theme.invitee_icon.container),
-                        ),
-                        Some(proto::channel_member::Kind::AncestorMember) | None => None,
-                    },
-                };
-
-                svg.map(|svg| svg.aligned().flex_float().into_any())
-            })
-            .contained()
-            .with_style(style.container)
-            .constrained()
-            .with_height(tabbed_modal.row_height)
-            .into_any();
-
-        if selected {
-            result = Stack::new()
-                .with_child(result)
-                .with_child(
-                    ChildView::new(&self.context_menu, cx)
-                        .aligned()
-                        .top()
-                        .right(),
-                )
-                .into_any();
-        }
-
-        result
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        None
+        //     let full_theme = &theme::current(cx);
+        //     let theme = &full_theme.collab_panel.channel_modal;
+        //     let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
+        //     let (user, role) = self.user_at_index(ix).unwrap();
+        //     let request_status = self.member_status(user.id, cx);
+
+        //     let style = tabbed_modal
+        //         .picker
+        //         .item
+        //         .in_state(selected)
+        //         .style_for(mouse_state);
+
+        //     let in_manage = matches!(self.mode, Mode::ManageMembers);
+
+        //     let mut result = Flex::row()
+        //         .with_children(user.avatar.clone().map(|avatar| {
+        //             Image::from_data(avatar)
+        //                 .with_style(theme.contact_avatar)
+        //                 .aligned()
+        //                 .left()
+        //         }))
+        //         .with_child(
+        //             Label::new(user.github_login.clone(), style.label.clone())
+        //                 .contained()
+        //                 .with_style(theme.contact_username)
+        //                 .aligned()
+        //                 .left(),
+        //         )
+        //         .with_children({
+        //             (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
+        //                 || {
+        //                     Label::new("Invited", theme.member_tag.text.clone())
+        //                         .contained()
+        //                         .with_style(theme.member_tag.container)
+        //                         .aligned()
+        //                         .left()
+        //                 },
+        //             )
+        //         })
+        //         .with_children(if in_manage && role == Some(ChannelRole::Admin) {
+        //             Some(
+        //                 Label::new("Admin", theme.member_tag.text.clone())
+        //                     .contained()
+        //                     .with_style(theme.member_tag.container)
+        //                     .aligned()
+        //                     .left(),
+        //             )
+        //         } else if in_manage && role == Some(ChannelRole::Guest) {
+        //             Some(
+        //                 Label::new("Guest", theme.member_tag.text.clone())
+        //                     .contained()
+        //                     .with_style(theme.member_tag.container)
+        //                     .aligned()
+        //                     .left(),
+        //             )
+        //         } else {
+        //             None
+        //         })
+        //         .with_children({
+        //             let svg = match self.mode {
+        //                 Mode::ManageMembers => Some(
+        //                     Svg::new("icons/ellipsis.svg")
+        //                         .with_color(theme.member_icon.color)
+        //                         .constrained()
+        //                         .with_width(theme.member_icon.icon_width)
+        //                         .aligned()
+        //                         .constrained()
+        //                         .with_width(theme.member_icon.button_width)
+        //                         .with_height(theme.member_icon.button_width)
+        //                         .contained()
+        //                         .with_style(theme.member_icon.container),
+        //                 ),
+        //                 Mode::InviteMembers => match request_status {
+        //                     Some(proto::channel_member::Kind::Member) => Some(
+        //                         Svg::new("icons/check.svg")
+        //                             .with_color(theme.member_icon.color)
+        //                             .constrained()
+        //                             .with_width(theme.member_icon.icon_width)
+        //                             .aligned()
+        //                             .constrained()
+        //                             .with_width(theme.member_icon.button_width)
+        //                             .with_height(theme.member_icon.button_width)
+        //                             .contained()
+        //                             .with_style(theme.member_icon.container),
+        //                     ),
+        //                     Some(proto::channel_member::Kind::Invitee) => Some(
+        //                         Svg::new("icons/check.svg")
+        //                             .with_color(theme.invitee_icon.color)
+        //                             .constrained()
+        //                             .with_width(theme.invitee_icon.icon_width)
+        //                             .aligned()
+        //                             .constrained()
+        //                             .with_width(theme.invitee_icon.button_width)
+        //                             .with_height(theme.invitee_icon.button_width)
+        //                             .contained()
+        //                             .with_style(theme.invitee_icon.container),
+        //                     ),
+        //                     Some(proto::channel_member::Kind::AncestorMember) | None => None,
+        //                 },
+        //             };
+
+        //             svg.map(|svg| svg.aligned().flex_float().into_any())
+        //         })
+        //         .contained()
+        //         .with_style(style.container)
+        //         .constrained()
+        //         .with_height(tabbed_modal.row_height)
+        //         .into_any();
+
+        //     if selected {
+        //         result = Stack::new()
+        //             .with_child(result)
+        //             .with_child(
+        //                 ChildView::new(&self.context_menu, cx)
+        //                     .aligned()
+        //                     .top()
+        //                     .right(),
+        //             )
+        //             .into_any();
+        //     }
+
+        //     result
     }
 }
 
@@ -623,7 +613,7 @@ impl ChannelModalDelegate {
         cx.spawn(|picker, mut cx| async move {
             update.await?;
             picker.update(&mut cx, |picker, cx| {
-                let this = picker.delegate_mut();
+                let this = &mut picker.delegate;
                 if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
                     member.role = new_role;
                 }
@@ -644,7 +634,7 @@ impl ChannelModalDelegate {
         cx.spawn(|picker, mut cx| async move {
             update.await?;
             picker.update(&mut cx, |picker, cx| {
-                let this = picker.delegate_mut();
+                let this = &mut picker.delegate;
                 if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
                     this.members.remove(ix);
                     this.matching_member_indices.retain_mut(|member_ix| {
@@ -683,7 +673,7 @@ impl ChannelModalDelegate {
                     kind: proto::channel_member::Kind::Invitee,
                     role: ChannelRole::Member,
                 };
-                let members = &mut this.delegate_mut().members;
+                let members = &mut this.delegate.members;
                 match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
                     Ok(ix) | Err(ix) => members.insert(ix, new_member),
                 }
@@ -695,23 +685,23 @@ impl ChannelModalDelegate {
     }
 
     fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
-        self.context_menu.update(cx, |context_menu, cx| {
-            context_menu.show(
-                Default::default(),
-                AnchorCorner::TopRight,
-                vec![
-                    ContextMenuItem::action("Remove", RemoveMember),
-                    ContextMenuItem::action(
-                        if role == ChannelRole::Admin {
-                            "Make non-admin"
-                        } else {
-                            "Make admin"
-                        },
-                        ToggleMemberAdmin,
-                    ),
-                ],
-                cx,
-            )
-        })
+        // self.context_menu.update(cx, |context_menu, cx| {
+        //     context_menu.show(
+        //         Default::default(),
+        //         AnchorCorner::TopRight,
+        //         vec![
+        //             ContextMenuItem::action("Remove", RemoveMember),
+        //             ContextMenuItem::action(
+        //                 if role == ChannelRole::Admin {
+        //                     "Make non-admin"
+        //                 } else {
+        //                     "Make admin"
+        //                 },
+        //                 ToggleMemberAdmin,
+        //             ),
+        //         ],
+        //         cx,
+        //     )
+        // })
     }
 }

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -31,20 +31,31 @@ use std::sync::Arc;
 use call::ActiveCall;
 use client::{Client, UserStore};
 use gpui::{
-    div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton,
-    ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription,
-    ViewContext, VisualContext, WeakView, WindowBounds,
+    actions, div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model,
+    MouseButton, ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled,
+    Subscription, ViewContext, VisualContext, WeakView, WindowBounds,
 };
-use project::Project;
+use project::{Project, RepositoryEntry};
 use theme::ActiveTheme;
-use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle, IconButton, KeyBinding, Tooltip};
+use ui::{
+    h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
+    IconButton, IconElement, KeyBinding, Tooltip,
+};
 use util::ResultExt;
 use workspace::{notifications::NotifyResultExt, Workspace};
 
 use crate::face_pile::FacePile;
 
-// const MAX_PROJECT_NAME_LENGTH: usize = 40;
-// const MAX_BRANCH_NAME_LENGTH: usize = 40;
+const MAX_PROJECT_NAME_LENGTH: usize = 40;
+const MAX_BRANCH_NAME_LENGTH: usize = 40;
+
+actions!(
+    ShareProject,
+    UnshareProject,
+    ToggleUserMenu,
+    ToggleProjectMenu,
+    SwitchBranch
+);
 
 // actions!(
 //     collab,
@@ -88,36 +99,23 @@ impl Render for CollabTitlebarItem {
     type Element = Stateful<Div>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        let is_in_room = self
-            .workspace
-            .update(cx, |this, cx| this.call_state().is_in_room(cx))
-            .unwrap_or_default();
+        let room = ActiveCall::global(cx).read(cx).room();
+        let is_in_room = room.is_some();
         let is_shared = is_in_room && self.project.read(cx).is_shared();
         let current_user = self.user_store.read(cx).current_user();
         let client = self.client.clone();
-        let users = self
-            .workspace
-            .update(cx, |this, cx| this.call_state().remote_participants(cx))
-            .log_err()
-            .flatten();
-        let mic_icon = if self
-            .workspace
-            .update(cx, |this, cx| this.call_state().is_muted(cx))
-            .log_err()
-            .flatten()
-            .unwrap_or_default()
-        {
-            ui::Icon::MicMute
-        } else {
-            ui::Icon::Mic
-        };
-        let speakers_icon = if self
-            .workspace
-            .update(cx, |this, cx| this.call_state().is_deafened(cx))
-            .log_err()
-            .flatten()
-            .unwrap_or_default()
-        {
+        let remote_participants = room.map(|room| {
+            room.read(cx)
+                .remote_participants()
+                .values()
+                .map(|participant| (participant.user.clone(), participant.peer_id))
+                .collect::<Vec<_>>()
+        });
+        let is_muted = room.map_or(false, |room| room.read(cx).is_muted(cx));
+        let is_deafened = room
+            .and_then(|room| room.read(cx).is_deafened())
+            .unwrap_or(false);
+        let speakers_icon = if is_deafened {
             ui::Icon::AudioOff
         } else {
             ui::Icon::AudioOn
@@ -146,59 +144,14 @@ impl Render for CollabTitlebarItem {
             .child(
                 h_stack()
                     .gap_1()
-                    // TODO - Add player menu
-                    .child(
-                        div()
-                            .border()
-                            .border_color(gpui::red())
-                            .id("project_owner_indicator")
-                            .child(
-                                Button::new("player", "player")
-                                    .style(ButtonStyle::Subtle)
-                                    .color(Some(Color::Player(0))),
-                            )
-                            .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
-                    )
-                    // TODO - Add project menu
-                    .child(
-                        div()
-                            .border()
-                            .border_color(gpui::red())
-                            .id("titlebar_project_menu_button")
-                            .child(
-                                Button::new("project_name", "project_name")
-                                    .style(ButtonStyle::Subtle),
-                            )
-                            .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
-                    )
-                    // TODO - Add git menu
-                    .child(
-                        div()
-                            .border()
-                            .border_color(gpui::red())
-                            .id("titlebar_git_menu_button")
-                            .child(
-                                Button::new("branch_name", "branch_name")
-                                    .style(ButtonStyle::Subtle)
-                                    .color(Some(Color::Muted)),
-                            )
-                            .tooltip(move |cx| {
-                                cx.build_view(|_| {
-                                    Tooltip::new("Recent Branches")
-                                        .key_binding(KeyBinding::new(gpui::KeyBinding::new(
-                                            "cmd-b",
-                                            // todo!() Replace with real action.
-                                            gpui::NoAction,
-                                            None,
-                                        )))
-                                        .meta("Only local branches shown")
-                                })
-                                .into()
-                            }),
-                    ),
+                    .when(is_in_room, |this| {
+                        this.children(self.render_project_owner(cx))
+                    })
+                    .child(self.render_project_name(cx))
+                    .children(self.render_project_branch(cx)),
             )
             .when_some(
-                users.zip(current_user.clone()),
+                remote_participants.zip(current_user.clone()),
                 |this, (remote_participants, current_user)| {
                     let mut pile = FacePile::default();
                     pile.extend(
@@ -209,25 +162,30 @@ impl Render for CollabTitlebarItem {
                                 div().child(Avatar::data(avatar.clone())).into_any_element()
                             })
                             .into_iter()
-                            .chain(remote_participants.into_iter().flat_map(|(user, peer_id)| {
-                                user.avatar.as_ref().map(|avatar| {
-                                    div()
-                                        .child(
-                                            Avatar::data(avatar.clone()).into_element().into_any(),
-                                        )
-                                        .on_mouse_down(MouseButton::Left, {
-                                            let workspace = workspace.clone();
-                                            move |_, cx| {
-                                                workspace
-                                                    .update(cx, |this, cx| {
-                                                        this.open_shared_screen(peer_id, cx);
-                                                    })
-                                                    .log_err();
-                                            }
-                                        })
-                                        .into_any_element()
-                                })
-                            })),
+                            .chain(remote_participants.into_iter().filter_map(
+                                |(user, peer_id)| {
+                                    let avatar = user.avatar.as_ref()?;
+                                    Some(
+                                        div()
+                                            .child(
+                                                Avatar::data(avatar.clone())
+                                                    .into_element()
+                                                    .into_any(),
+                                            )
+                                            .on_mouse_down(MouseButton::Left, {
+                                                let workspace = workspace.clone();
+                                                move |_, cx| {
+                                                    workspace
+                                                        .update(cx, |this, cx| {
+                                                            this.open_shared_screen(peer_id, cx);
+                                                        })
+                                                        .log_err();
+                                                }
+                                            })
+                                            .into_any_element(),
+                                    )
+                                },
+                            )),
                     );
                     this.child(pile.render(cx))
                 },
@@ -236,62 +194,112 @@ impl Render for CollabTitlebarItem {
             .when(is_in_room, |this| {
                 this.child(
                     h_stack()
+                        .gap_1()
                         .child(
                             h_stack()
-                                .child(Button::new(
-                                    "toggle_sharing",
-                                    if is_shared { "Unshare" } else { "Share" },
-                                ))
-                                .child(IconButton::new("leave-call", ui::Icon::Exit).on_click({
-                                    let workspace = workspace.clone();
-                                    move |_, cx| {
-                                        workspace
-                                            .update(cx, |this, cx| {
-                                                this.call_state().hang_up(cx).detach();
-                                            })
-                                            .log_err();
-                                    }
-                                })),
+                                .gap_1()
+                                .child(
+                                    Button::new(
+                                        "toggle_sharing",
+                                        if is_shared { "Unshare" } else { "Share" },
+                                    )
+                                    .style(ButtonStyle::Subtle)
+                                    .on_click(cx.listener(
+                                        move |this, _, cx| {
+                                            if is_shared {
+                                                this.unshare_project(&Default::default(), cx);
+                                            } else {
+                                                this.share_project(&Default::default(), cx);
+                                            }
+                                        },
+                                    )),
+                                )
+                                .child(
+                                    IconButton::new("leave-call", ui::Icon::Exit)
+                                        .style(ButtonStyle::Subtle)
+                                        .on_click(move |_, cx| {
+                                            ActiveCall::global(cx)
+                                                .update(cx, |call, cx| call.hang_up(cx))
+                                                .detach_and_log_err(cx);
+                                        }),
+                                ),
                         )
                         .child(
                             h_stack()
-                                .child(IconButton::new("mute-microphone", mic_icon).on_click({
-                                    let workspace = workspace.clone();
-                                    move |_, cx| {
-                                        workspace
-                                            .update(cx, |this, cx| {
-                                                this.call_state().toggle_mute(cx);
-                                            })
-                                            .log_err();
-                                    }
-                                }))
-                                .child(IconButton::new("mute-sound", speakers_icon).on_click({
-                                    let workspace = workspace.clone();
-                                    move |_, cx| {
-                                        workspace
-                                            .update(cx, |this, cx| {
-                                                this.call_state().toggle_deafen(cx);
-                                            })
-                                            .log_err();
-                                    }
-                                }))
-                                .child(IconButton::new("screen-share", ui::Icon::Screen).on_click(
-                                    move |_, cx| {
-                                        workspace
-                                            .update(cx, |this, cx| {
-                                                this.call_state().toggle_screen_share(cx);
-                                            })
-                                            .log_err();
-                                    },
-                                ))
+                                .gap_1()
+                                .child(
+                                    IconButton::new(
+                                        "mute-microphone",
+                                        if is_muted {
+                                            ui::Icon::MicMute
+                                        } else {
+                                            ui::Icon::Mic
+                                        },
+                                    )
+                                    .style(ButtonStyle::Subtle)
+                                    .selected(is_muted)
+                                    .on_click(move |_, cx| {
+                                        crate::toggle_mute(&Default::default(), cx)
+                                    }),
+                                )
+                                .child(
+                                    IconButton::new("mute-sound", speakers_icon)
+                                        .style(ButtonStyle::Subtle)
+                                        .selected(is_deafened.clone())
+                                        .tooltip(move |cx| {
+                                            Tooltip::with_meta(
+                                                "Deafen Audio",
+                                                None,
+                                                "Mic will be muted",
+                                                cx,
+                                            )
+                                        })
+                                        .on_click(move |_, cx| {
+                                            crate::toggle_mute(&Default::default(), cx)
+                                        }),
+                                )
+                                .child(
+                                    IconButton::new("screen-share", ui::Icon::Screen)
+                                        .style(ButtonStyle::Subtle)
+                                        .on_click(move |_, cx| {
+                                            crate::toggle_screen_sharing(&Default::default(), cx)
+                                        }),
+                                )
                                 .pl_2(),
                         ),
                 )
             })
-            .map(|this| {
+            .child(h_stack().px_1p5().map(|this| {
                 if let Some(user) = current_user {
                     this.when_some(user.avatar.clone(), |this, avatar| {
-                        this.child(ui::Avatar::data(avatar))
+                        // TODO: Finish implementing user menu popover
+                        //
+                        this.child(
+                            popover_menu("user-menu")
+                                .menu(|cx| ContextMenu::build(cx, |menu, _| menu.header("ADADA")))
+                                .trigger(
+                                    ButtonLike::new("user-menu")
+                                        .child(
+                                            h_stack().gap_0p5().child(Avatar::data(avatar)).child(
+                                                IconElement::new(Icon::ChevronDown)
+                                                    .color(Color::Muted),
+                                            ),
+                                        )
+                                        .style(ButtonStyle::Subtle)
+                                        .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
+                                )
+                                .anchor(gpui::AnchorCorner::TopRight),
+                        )
+                        // this.child(
+                        //     ButtonLike::new("user-menu")
+                        //         .child(
+                        //             h_stack().gap_0p5().child(Avatar::data(avatar)).child(
+                        //                 IconElement::new(Icon::ChevronDown).color(Color::Muted),
+                        //             ),
+                        //         )
+                        //         .style(ButtonStyle::Subtle)
+                        //         .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
+                        // )
                     })
                 } else {
                     this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| {
@@ -305,7 +313,7 @@ impl Render for CollabTitlebarItem {
                         .detach();
                     }))
                 }
-            })
+            }))
     }
 }
 
@@ -424,6 +432,83 @@ impl CollabTitlebarItem {
         }
     }
 
+    // resolve if you are in a room -> render_project_owner
+    // render_project_owner -> resolve if you are in a room -> Option<foo>
+
+    pub fn render_project_owner(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
+        let host = self.project.read(cx).host()?;
+        let host = self.user_store.read(cx).get_cached_user(host.user_id)?;
+        let participant_index = self
+            .user_store
+            .read(cx)
+            .participant_indices()
+            .get(&host.id)?;
+        Some(
+            div().border().border_color(gpui::red()).child(
+                Button::new("project_owner_trigger", host.github_login.clone())
+                    .color(Color::Player(participant_index.0))
+                    .style(ButtonStyle::Subtle)
+                    .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
+            ),
+        )
+    }
+
+    pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl Element {
+        let name = {
+            let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
+                let worktree = worktree.read(cx);
+                worktree.root_name()
+            });
+
+            names.next().unwrap_or("")
+        };
+
+        let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
+
+        div().border().border_color(gpui::red()).child(
+            Button::new("project_name_trigger", name)
+                .style(ButtonStyle::Subtle)
+                .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
+        )
+    }
+
+    pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
+        let entry = {
+            let mut names_and_branches =
+                self.project.read(cx).visible_worktrees(cx).map(|worktree| {
+                    let worktree = worktree.read(cx);
+                    worktree.root_git_entry()
+                });
+
+            names_and_branches.next().flatten()
+        };
+
+        let branch_name = entry
+            .as_ref()
+            .and_then(RepositoryEntry::branch)
+            .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
+
+        Some(
+            div().border().border_color(gpui::red()).child(
+                Button::new("project_branch_trigger", branch_name)
+                    .style(ButtonStyle::Subtle)
+                    .tooltip(move |cx| {
+                        cx.build_view(|_| {
+                            Tooltip::new("Recent Branches")
+                                .key_binding(KeyBinding::new(gpui::KeyBinding::new(
+                                    "cmd-b",
+                                    // todo!() Replace with real action.
+                                    gpui::NoAction,
+                                    None,
+                                )))
+                                .meta("Local branches only")
+                        })
+                        .into()
+                    }),
+            ),
+        )
+    }
+
     // fn collect_title_root_names(
     //     &self,
     //     theme: Arc<Theme>,
@@ -603,21 +688,21 @@ impl CollabTitlebarItem {
         cx.notify();
     }
 
-    // fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
-    //     let active_call = ActiveCall::global(cx);
-    //     let project = self.project.clone();
-    //     active_call
-    //         .update(cx, |call, cx| call.share_project(project, cx))
-    //         .detach_and_log_err(cx);
-    // }
+    fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
+        let active_call = ActiveCall::global(cx);
+        let project = self.project.clone();
+        active_call
+            .update(cx, |call, cx| call.share_project(project, cx))
+            .detach_and_log_err(cx);
+    }
 
-    // fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
-    //     let active_call = ActiveCall::global(cx);
-    //     let project = self.project.clone();
-    //     active_call
-    //         .update(cx, |call, cx| call.unshare_project(project, cx))
-    //         .log_err();
-    // }
+    fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
+        let active_call = ActiveCall::global(cx);
+        let project = self.project.clone();
+        active_call
+            .update(cx, |call, cx| call.unshare_project(project, cx))
+            .log_err();
+    }
 
     // pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
     //     self.user_menu.update(cx, |user_menu, cx| {

crates/collab_ui2/src/collab_ui.rs 🔗

@@ -9,22 +9,21 @@ mod panel_settings;
 
 use std::{rc::Rc, sync::Arc};
 
+use call::{report_call_event_for_room, ActiveCall, Room};
 pub use collab_panel::CollabPanel;
 pub use collab_titlebar_item::CollabTitlebarItem;
 use gpui::{
-    point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind,
-    WindowOptions,
+    actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
+    WindowKind, WindowOptions,
 };
 pub use panel_settings::{
     ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
 };
 use settings::Settings;
+use util::ResultExt;
 use workspace::AppState;
 
-// actions!(
-//     collab,
-//     [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
-// );
+actions!(ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall);
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     CollaborationPanelSettings::register(cx);
@@ -42,61 +41,61 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     // cx.add_global_action(toggle_deafen);
 }
 
-// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
-//     let call = ActiveCall::global(cx).read(cx);
-//     if let Some(room) = call.room().cloned() {
-//         let client = call.client();
-//         let toggle_screen_sharing = room.update(cx, |room, cx| {
-//             if room.is_screen_sharing() {
-//                 report_call_event_for_room(
-//                     "disable screen share",
-//                     room.id(),
-//                     room.channel_id(),
-//                     &client,
-//                     cx,
-//                 );
-//                 Task::ready(room.unshare_screen(cx))
-//             } else {
-//                 report_call_event_for_room(
-//                     "enable screen share",
-//                     room.id(),
-//                     room.channel_id(),
-//                     &client,
-//                     cx,
-//                 );
-//                 room.share_screen(cx)
-//             }
-//         });
-//         toggle_screen_sharing.detach_and_log_err(cx);
-//     }
-// }
+pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
+    let call = ActiveCall::global(cx).read(cx);
+    if let Some(room) = call.room().cloned() {
+        let client = call.client();
+        let toggle_screen_sharing = room.update(cx, |room, cx| {
+            if room.is_screen_sharing() {
+                report_call_event_for_room(
+                    "disable screen share",
+                    room.id(),
+                    room.channel_id(),
+                    &client,
+                    cx,
+                );
+                Task::ready(room.unshare_screen(cx))
+            } else {
+                report_call_event_for_room(
+                    "enable screen share",
+                    room.id(),
+                    room.channel_id(),
+                    &client,
+                    cx,
+                );
+                room.share_screen(cx)
+            }
+        });
+        toggle_screen_sharing.detach_and_log_err(cx);
+    }
+}
 
-// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
-//     let call = ActiveCall::global(cx).read(cx);
-//     if let Some(room) = call.room().cloned() {
-//         let client = call.client();
-//         room.update(cx, |room, cx| {
-//             let operation = if room.is_muted(cx) {
-//                 "enable microphone"
-//             } else {
-//                 "disable microphone"
-//             };
-//             report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
+pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
+    let call = ActiveCall::global(cx).read(cx);
+    if let Some(room) = call.room().cloned() {
+        let client = call.client();
+        room.update(cx, |room, cx| {
+            let operation = if room.is_muted(cx) {
+                "enable microphone"
+            } else {
+                "disable microphone"
+            };
+            report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
 
-//             room.toggle_mute(cx)
-//         })
-//         .map(|task| task.detach_and_log_err(cx))
-//         .log_err();
-//     }
-// }
+            room.toggle_mute(cx)
+        })
+        .map(|task| task.detach_and_log_err(cx))
+        .log_err();
+    }
+}
 
-// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
-//     if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-//         room.update(cx, Room::toggle_deafen)
-//             .map(|task| task.detach_and_log_err(cx))
-//             .log_err();
-//     }
-// }
+pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
+    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+        room.update(cx, Room::toggle_deafen)
+            .map(|task| task.detach_and_log_err(cx))
+            .log_err();
+    }
+}
 
 fn notification_window_options(
     screen: Rc<dyn PlatformDisplay>,

crates/command_palette2/src/command_palette.rs 🔗

@@ -311,7 +311,11 @@ impl PickerDelegate for CommandPaletteDelegate {
                         command.name.clone(),
                         r#match.positions.clone(),
                     ))
-                    .children(KeyBinding::for_action(&*command.action, cx)),
+                    .children(KeyBinding::for_action_in(
+                        &*command.action,
+                        &self.previous_focus_handle,
+                        cx,
+                    )),
             ),
         )
     }

crates/copilot2/Cargo.toml 🔗

@@ -45,6 +45,6 @@ fs = { path = "../fs", features = ["test-support"] }
 gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
 language = { package = "language2", path = "../language2", features = ["test-support"] }
 lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
-rpc = { path = "../rpc", features = ["test-support"] }
+rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
 settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }

crates/copilot2/src/copilot2.rs 🔗

@@ -1002,229 +1002,231 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use gpui::{executor::Deterministic, TestAppContext};
-
-//     #[gpui::test(iterations = 10)]
-//     async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-//         deterministic.forbid_parking();
-//         let (copilot, mut lsp) = Copilot::fake(cx);
-
-//         let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello"));
-//         let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
-//         copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-//                 .await,
-//             lsp::DidOpenTextDocumentParams {
-//                 text_document: lsp::TextDocumentItem::new(
-//                     buffer_1_uri.clone(),
-//                     "plaintext".into(),
-//                     0,
-//                     "Hello".into()
-//                 ),
-//             }
-//         );
-
-//         let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye"));
-//         let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
-//         copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-//                 .await,
-//             lsp::DidOpenTextDocumentParams {
-//                 text_document: lsp::TextDocumentItem::new(
-//                     buffer_2_uri.clone(),
-//                     "plaintext".into(),
-//                     0,
-//                     "Goodbye".into()
-//                 ),
-//             }
-//         );
-
-//         buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
-//                 .await,
-//             lsp::DidChangeTextDocumentParams {
-//                 text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
-//                 content_changes: vec![lsp::TextDocumentContentChangeEvent {
-//                     range: Some(lsp::Range::new(
-//                         lsp::Position::new(0, 5),
-//                         lsp::Position::new(0, 5)
-//                     )),
-//                     range_length: None,
-//                     text: " world".into(),
-//                 }],
-//             }
-//         );
-
-//         // Ensure updates to the file are reflected in the LSP.
-//         buffer_1
-//             .update(cx, |buffer, cx| {
-//                 buffer.file_updated(
-//                     Arc::new(File {
-//                         abs_path: "/root/child/buffer-1".into(),
-//                         path: Path::new("child/buffer-1").into(),
-//                     }),
-//                     cx,
-//                 )
-//             })
-//             .await;
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
-//                 .await,
-//             lsp::DidCloseTextDocumentParams {
-//                 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
-//             }
-//         );
-//         let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-//                 .await,
-//             lsp::DidOpenTextDocumentParams {
-//                 text_document: lsp::TextDocumentItem::new(
-//                     buffer_1_uri.clone(),
-//                     "plaintext".into(),
-//                     1,
-//                     "Hello world".into()
-//                 ),
-//             }
-//         );
-
-//         // Ensure all previously-registered buffers are closed when signing out.
-//         lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
-//             Ok(request::SignOutResult {})
-//         });
-//         copilot
-//             .update(cx, |copilot, cx| copilot.sign_out(cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
-//                 .await,
-//             lsp::DidCloseTextDocumentParams {
-//                 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
-//             }
-//         );
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
-//                 .await,
-//             lsp::DidCloseTextDocumentParams {
-//                 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
-//             }
-//         );
-
-//         // Ensure all previously-registered buffers are re-opened when signing in.
-//         lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
-//             Ok(request::SignInInitiateResult::AlreadySignedIn {
-//                 user: "user-1".into(),
-//             })
-//         });
-//         copilot
-//             .update(cx, |copilot, cx| copilot.sign_in(cx))
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-//                 .await,
-//             lsp::DidOpenTextDocumentParams {
-//                 text_document: lsp::TextDocumentItem::new(
-//                     buffer_2_uri.clone(),
-//                     "plaintext".into(),
-//                     0,
-//                     "Goodbye".into()
-//                 ),
-//             }
-//         );
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-//                 .await,
-//             lsp::DidOpenTextDocumentParams {
-//                 text_document: lsp::TextDocumentItem::new(
-//                     buffer_1_uri.clone(),
-//                     "plaintext".into(),
-//                     0,
-//                     "Hello world".into()
-//                 ),
-//             }
-//         );
-
-//         // Dropping a buffer causes it to be closed on the LSP side as well.
-//         cx.update(|_| drop(buffer_2));
-//         assert_eq!(
-//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
-//                 .await,
-//             lsp::DidCloseTextDocumentParams {
-//                 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
-//             }
-//         );
-//     }
-
-//     struct File {
-//         abs_path: PathBuf,
-//         path: Arc<Path>,
-//     }
-
-//     impl language2::File for File {
-//         fn as_local(&self) -> Option<&dyn language2::LocalFile> {
-//             Some(self)
-//         }
-
-//         fn mtime(&self) -> std::time::SystemTime {
-//             unimplemented!()
-//         }
-
-//         fn path(&self) -> &Arc<Path> {
-//             &self.path
-//         }
-
-//         fn full_path(&self, _: &AppContext) -> PathBuf {
-//             unimplemented!()
-//         }
-
-//         fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
-//             unimplemented!()
-//         }
-
-//         fn is_deleted(&self) -> bool {
-//             unimplemented!()
-//         }
-
-//         fn as_any(&self) -> &dyn std::any::Any {
-//             unimplemented!()
-//         }
-
-//         fn to_proto(&self) -> rpc::proto::File {
-//             unimplemented!()
-//         }
-
-//         fn worktree_id(&self) -> usize {
-//             0
-//         }
-//     }
-
-//     impl language::LocalFile for File {
-//         fn abs_path(&self, _: &AppContext) -> PathBuf {
-//             self.abs_path.clone()
-//         }
-
-//         fn load(&self, _: &AppContext) -> Task<Result<String>> {
-//             unimplemented!()
-//         }
-
-//         fn buffer_reloaded(
-//             &self,
-//             _: u64,
-//             _: &clock::Global,
-//             _: language::RopeFingerprint,
-//             _: language::LineEnding,
-//             _: std::time::SystemTime,
-//             _: &mut AppContext,
-//         ) {
-//             unimplemented!()
-//         }
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::TestAppContext;
+
+    #[gpui::test(iterations = 10)]
+    async fn test_buffer_management(cx: &mut TestAppContext) {
+        let (copilot, mut lsp) = Copilot::fake(cx);
+
+        let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Hello"));
+        let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
+            .parse()
+            .unwrap();
+        copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_1_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Hello".into()
+                ),
+            }
+        );
+
+        let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Goodbye"));
+        let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
+            .parse()
+            .unwrap();
+        copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_2_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Goodbye".into()
+                ),
+            }
+        );
+
+        buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
+                .await,
+            lsp::DidChangeTextDocumentParams {
+                text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
+                content_changes: vec![lsp::TextDocumentContentChangeEvent {
+                    range: Some(lsp::Range::new(
+                        lsp::Position::new(0, 5),
+                        lsp::Position::new(0, 5)
+                    )),
+                    range_length: None,
+                    text: " world".into(),
+                }],
+            }
+        );
+
+        // Ensure updates to the file are reflected in the LSP.
+        buffer_1.update(cx, |buffer, cx| {
+            buffer.file_updated(
+                Arc::new(File {
+                    abs_path: "/root/child/buffer-1".into(),
+                    path: Path::new("child/buffer-1").into(),
+                }),
+                cx,
+            )
+        });
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
+            }
+        );
+        let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_1_uri.clone(),
+                    "plaintext".into(),
+                    1,
+                    "Hello world".into()
+                ),
+            }
+        );
+
+        // Ensure all previously-registered buffers are closed when signing out.
+        lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
+            Ok(request::SignOutResult {})
+        });
+        copilot
+            .update(cx, |copilot, cx| copilot.sign_out(cx))
+            .await
+            .unwrap();
+        // todo!() po: these notifications now happen in reverse order?
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
+            }
+        );
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
+            }
+        );
+
+        // Ensure all previously-registered buffers are re-opened when signing in.
+        lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
+            Ok(request::SignInInitiateResult::AlreadySignedIn {
+                user: "user-1".into(),
+            })
+        });
+        copilot
+            .update(cx, |copilot, cx| copilot.sign_in(cx))
+            .await
+            .unwrap();
+
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_1_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Hello world".into()
+                ),
+            }
+        );
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_2_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Goodbye".into()
+                ),
+            }
+        );
+        // Dropping a buffer causes it to be closed on the LSP side as well.
+        cx.update(|_| drop(buffer_2));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
+            }
+        );
+    }
+
+    struct File {
+        abs_path: PathBuf,
+        path: Arc<Path>,
+    }
+
+    impl language::File for File {
+        fn as_local(&self) -> Option<&dyn language::LocalFile> {
+            Some(self)
+        }
+
+        fn mtime(&self) -> std::time::SystemTime {
+            unimplemented!()
+        }
+
+        fn path(&self) -> &Arc<Path> {
+            &self.path
+        }
+
+        fn full_path(&self, _: &AppContext) -> PathBuf {
+            unimplemented!()
+        }
+
+        fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
+            unimplemented!()
+        }
+
+        fn is_deleted(&self) -> bool {
+            unimplemented!()
+        }
+
+        fn as_any(&self) -> &dyn std::any::Any {
+            unimplemented!()
+        }
+
+        fn to_proto(&self) -> rpc::proto::File {
+            unimplemented!()
+        }
+
+        fn worktree_id(&self) -> usize {
+            0
+        }
+    }
+
+    impl language::LocalFile for File {
+        fn abs_path(&self, _: &AppContext) -> PathBuf {
+            self.abs_path.clone()
+        }
+
+        fn load(&self, _: &AppContext) -> Task<Result<String>> {
+            unimplemented!()
+        }
+
+        fn buffer_reloaded(
+            &self,
+            _: u64,
+            _: &clock::Global,
+            _: language::RopeFingerprint,
+            _: language::LineEnding,
+            _: std::time::SystemTime,
+            _: &mut AppContext,
+        ) {
+            unimplemented!()
+        }
+    }
+}

crates/copilot_button2/Cargo.toml 🔗

@@ -0,0 +1,27 @@
+[package]
+name = "copilot_button2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/copilot_button.rs"
+doctest = false
+
+[dependencies]
+copilot = { package = "copilot2", path = "../copilot2" }
+editor = { package = "editor2", path = "../editor2" }
+fs = { package = "fs2", path = "../fs2" }
+zed-actions = { package="zed_actions2", path = "../zed_actions2"}
+gpui = { package = "gpui2", path = "../gpui2" }
+language = { package = "language2", path = "../language2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+util = { path = "../util" }
+workspace = { package = "workspace2", path = "../workspace2" }
+anyhow.workspace = true
+smol.workspace = true
+futures.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/copilot_button2/src/copilot_button.rs 🔗

@@ -0,0 +1,370 @@
+#![allow(unused)]
+use anyhow::Result;
+use copilot::{Copilot, SignOut, Status};
+use editor::{scroll::autoscroll::Autoscroll, Editor};
+use fs::Fs;
+use gpui::{
+    div, Action, AnchorCorner, AppContext, AsyncAppContext, AsyncWindowContext, Div, Entity,
+    ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
+};
+use language::{
+    language_settings::{self, all_language_settings, AllLanguageSettings},
+    File, Language,
+};
+use settings::{update_settings_file, Settings, SettingsStore};
+use std::{path::Path, sync::Arc};
+use util::{paths, ResultExt};
+use workspace::{
+    create_and_open_local_file,
+    item::ItemHandle,
+    ui::{
+        popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, PopoverMenu, Tooltip,
+    },
+    StatusItemView, Toast, Workspace,
+};
+use zed_actions::OpenBrowser;
+
+const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
+const COPILOT_STARTING_TOAST_ID: usize = 1337;
+const COPILOT_ERROR_TOAST_ID: usize = 1338;
+
+pub struct CopilotButton {
+    editor_subscription: Option<(Subscription, usize)>,
+    editor_enabled: Option<bool>,
+    language: Option<Arc<Language>>,
+    file: Option<Arc<dyn File>>,
+    fs: Arc<dyn Fs>,
+}
+
+impl Render for CopilotButton {
+    type Element = Div;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let all_language_settings = all_language_settings(None, cx);
+        if !all_language_settings.copilot.feature_enabled {
+            return div();
+        }
+
+        let Some(copilot) = Copilot::global(cx) else {
+            return div();
+        };
+        let status = copilot.read(cx).status();
+
+        let enabled = self
+            .editor_enabled
+            .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
+
+        let icon = match status {
+            Status::Error(_) => Icon::CopilotError,
+            Status::Authorized => {
+                if enabled {
+                    Icon::Copilot
+                } else {
+                    Icon::CopilotDisabled
+                }
+            }
+            _ => Icon::CopilotInit,
+        };
+
+        if let Status::Error(e) = status {
+            return div().child(
+                IconButton::new("copilot-error", icon)
+                    .on_click(cx.listener(move |this, _, cx| {
+                        if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
+                            workspace.update(cx, |workspace, cx| {
+                                workspace.show_toast(
+                                    Toast::new(
+                                        COPILOT_ERROR_TOAST_ID,
+                                        format!("Copilot can't be started: {}", e),
+                                    )
+                                    .on_click(
+                                        "Reinstall Copilot",
+                                        |cx| {
+                                            if let Some(copilot) = Copilot::global(cx) {
+                                                copilot
+                                                    .update(cx, |copilot, cx| copilot.reinstall(cx))
+                                                    .detach();
+                                            }
+                                        },
+                                    ),
+                                    cx,
+                                );
+                            });
+                        }
+                    }))
+                    .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
+            );
+        }
+        let this = cx.view().clone();
+
+        div().child(
+            popover_menu("copilot")
+                .menu(move |cx| match status {
+                    Status::Authorized => this.update(cx, |this, cx| this.build_copilot_menu(cx)),
+                    _ => this.update(cx, |this, cx| this.build_copilot_start_menu(cx)),
+                })
+                .anchor(AnchorCorner::BottomRight)
+                .trigger(
+                    IconButton::new("copilot-icon", icon)
+                        .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
+                ),
+        )
+    }
+}
+
+impl CopilotButton {
+    pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
+        Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
+
+        cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
+            .detach();
+
+        Self {
+            editor_subscription: None,
+            editor_enabled: None,
+            language: None,
+            file: None,
+            fs,
+        }
+    }
+
+    pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
+        let fs = self.fs.clone();
+        ContextMenu::build(cx, |menu, cx| {
+            menu.entry("Sign In", initiate_sign_in)
+                .entry("Disable Copilot", move |cx| hide_copilot(fs.clone(), cx))
+        })
+    }
+
+    pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
+        let fs = self.fs.clone();
+
+        return ContextMenu::build(cx, move |mut menu, cx| {
+            if let Some(language) = self.language.clone() {
+                let fs = fs.clone();
+                let language_enabled =
+                    language_settings::language_settings(Some(&language), None, cx)
+                        .show_copilot_suggestions;
+
+                menu = menu.entry(
+                    format!(
+                        "{} Suggestions for {}",
+                        if language_enabled { "Hide" } else { "Show" },
+                        language.name()
+                    ),
+                    move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
+                );
+            }
+
+            let settings = AllLanguageSettings::get_global(cx);
+
+            if let Some(file) = &self.file {
+                let path = file.path().clone();
+                let path_enabled = settings.copilot_enabled_for_path(&path);
+
+                menu = menu.entry(
+                    format!(
+                        "{} Suggestions for This Path",
+                        if path_enabled { "Hide" } else { "Show" }
+                    ),
+                    move |cx| {
+                        if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
+                            if let Ok(workspace) = workspace.root_view(cx) {
+                                let workspace = workspace.downgrade();
+                                cx.spawn(|cx| {
+                                    configure_disabled_globs(
+                                        workspace,
+                                        path_enabled.then_some(path.clone()),
+                                        cx,
+                                    )
+                                })
+                                .detach_and_log_err(cx);
+                            }
+                        }
+                    },
+                );
+            }
+
+            let globally_enabled = settings.copilot_enabled(None, None);
+            menu.entry(
+                if globally_enabled {
+                    "Hide Suggestions for All Files"
+                } else {
+                    "Show Suggestions for All Files"
+                },
+                move |cx| toggle_copilot_globally(fs.clone(), cx),
+            )
+            .separator()
+            .link(
+                "Copilot Settings",
+                OpenBrowser {
+                    url: COPILOT_SETTINGS_URL.to_string(),
+                }
+                .boxed_clone(),
+            )
+            .action("Sign Out", SignOut.boxed_clone())
+        });
+    }
+
+    pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
+        let editor = editor.read(cx);
+        let snapshot = editor.buffer().read(cx).snapshot(cx);
+        let suggestion_anchor = editor.selections.newest_anchor().start;
+        let language = snapshot.language_at(suggestion_anchor);
+        let file = snapshot.file_at(suggestion_anchor).cloned();
+
+        self.editor_enabled = Some(
+            all_language_settings(self.file.as_ref(), cx)
+                .copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
+        );
+        self.language = language.cloned();
+        self.file = file;
+
+        cx.notify()
+    }
+}
+
+impl StatusItemView for CopilotButton {
+    fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+        if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
+            self.editor_subscription = Some((
+                cx.observe(&editor, Self::update_enabled),
+                editor.entity_id().as_u64() as usize,
+            ));
+            self.update_enabled(editor, cx);
+        } else {
+            self.language = None;
+            self.editor_subscription = None;
+            self.editor_enabled = None;
+        }
+        cx.notify();
+    }
+}
+
+async fn configure_disabled_globs(
+    workspace: WeakView<Workspace>,
+    path_to_disable: Option<Arc<Path>>,
+    mut cx: AsyncWindowContext,
+) -> Result<()> {
+    let settings_editor = workspace
+        .update(&mut cx, |_, cx| {
+            create_and_open_local_file(&paths::SETTINGS, cx, || {
+                settings::initial_user_settings_content().as_ref().into()
+            })
+        })?
+        .await?
+        .downcast::<Editor>()
+        .unwrap();
+
+    settings_editor.downgrade().update(&mut cx, |item, cx| {
+        let text = item.buffer().read(cx).snapshot(cx).text();
+
+        let settings = cx.global::<SettingsStore>();
+        let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
+            let copilot = file.copilot.get_or_insert_with(Default::default);
+            let globs = copilot.disabled_globs.get_or_insert_with(|| {
+                settings
+                    .get::<AllLanguageSettings>(None)
+                    .copilot
+                    .disabled_globs
+                    .iter()
+                    .map(|glob| glob.glob().to_string())
+                    .collect()
+            });
+
+            if let Some(path_to_disable) = &path_to_disable {
+                globs.push(path_to_disable.to_string_lossy().into_owned());
+            } else {
+                globs.clear();
+            }
+        });
+
+        if !edits.is_empty() {
+            item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
+                selections.select_ranges(edits.iter().map(|e| e.0.clone()));
+            });
+
+            // When *enabling* a path, don't actually perform an edit, just select the range.
+            if path_to_disable.is_some() {
+                item.edit(edits.iter().cloned(), cx);
+            }
+        }
+    })?;
+
+    anyhow::Ok(())
+}
+
+fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+    let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
+    update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+        file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
+    });
+}
+
+fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
+    let show_copilot_suggestions =
+        all_language_settings(None, cx).copilot_enabled(Some(&language), None);
+    update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+        file.languages
+            .entry(language.name())
+            .or_default()
+            .show_copilot_suggestions = Some(!show_copilot_suggestions);
+    });
+}
+
+fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+    update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+        file.features.get_or_insert(Default::default()).copilot = Some(false);
+    });
+}
+
+fn initiate_sign_in(cx: &mut WindowContext) {
+    let Some(copilot) = Copilot::global(cx) else {
+        return;
+    };
+    let status = copilot.read(cx).status();
+
+    match status {
+        Status::Starting { task } => {
+            let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
+                return;
+            };
+
+            let Ok(workspace) = workspace.update(cx, |workspace, cx| {
+                workspace.show_toast(
+                    Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
+                    cx,
+                );
+                workspace.weak_handle()
+            }) else {
+                return;
+            };
+
+            cx.spawn(|mut cx| async move {
+                task.await;
+                if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
+                    workspace
+                        .update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
+                            Status::Authorized => workspace.show_toast(
+                                Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
+                                cx,
+                            ),
+                            _ => {
+                                workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
+                                copilot
+                                    .update(cx, |copilot, cx| copilot.sign_in(cx))
+                                    .detach_and_log_err(cx);
+                            }
+                        })
+                        .log_err();
+                }
+            })
+            .detach();
+        }
+        _ => {
+            copilot
+                .update(cx, |copilot, cx| copilot.sign_in(cx))
+                .detach_and_log_err(cx);
+        }
+    }
+}

crates/diagnostics2/src/diagnostics.rs 🔗

@@ -774,24 +774,39 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
     Arc::new(move |_| {
         h_stack()
             .id("diagnostic header")
-            .gap_3()
-            .bg(gpui::red())
-            .map(|stack| {
-                let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
-                    IconElement::new(Icon::XCircle).color(Color::Error)
-                } else {
-                    IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
-                };
-
-                stack.child(div().pl_8().child(icon))
-            })
-            .when_some(diagnostic.source.as_ref(), |stack, source| {
-                stack.child(Label::new(format!("{source}:")).color(Color::Accent))
-            })
-            .child(HighlightedLabel::new(message.clone(), highlights.clone()))
-            .when_some(diagnostic.code.as_ref(), |stack, code| {
-                stack.child(Label::new(code.clone()))
-            })
+            .py_2()
+            .pl_10()
+            .pr_5()
+            .w_full()
+            .justify_between()
+            .gap_2()
+            .child(
+                h_stack()
+                    .gap_3()
+                    .map(|stack| {
+                        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
+                            IconElement::new(Icon::XCircle).color(Color::Error)
+                        } else {
+                            IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
+                        };
+                        stack.child(icon)
+                    })
+                    .child(
+                        h_stack()
+                            .gap_1()
+                            .child(HighlightedLabel::new(message.clone(), highlights.clone()))
+                            .when_some(diagnostic.code.as_ref(), |stack, code| {
+                                stack.child(Label::new(format!("({code})")).color(Color::Muted))
+                            }),
+                    ),
+            )
+            .child(
+                h_stack()
+                    .gap_1()
+                    .when_some(diagnostic.source.as_ref(), |stack, source| {
+                        stack.child(Label::new(format!("{source}")).color(Color::Muted))
+                    }),
+            )
             .into_any_element()
     })
 }
@@ -802,11 +817,22 @@ pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
         label.into_any_element()
     } else {
         h_stack()
-            .bg(gpui::red())
-            .child(IconElement::new(Icon::XCircle))
-            .child(Label::new(summary.error_count.to_string()))
-            .child(IconElement::new(Icon::ExclamationTriangle))
-            .child(Label::new(summary.warning_count.to_string()))
+            .gap_1()
+            .when(summary.error_count > 0, |then| {
+                then.child(
+                    h_stack()
+                        .gap_1()
+                        .child(IconElement::new(Icon::XCircle).color(Color::Error))
+                        .child(Label::new(summary.error_count.to_string())),
+                )
+            })
+            .when(summary.warning_count > 0, |then| {
+                then.child(
+                    h_stack()
+                        .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
+                        .child(Label::new(summary.warning_count.to_string())),
+                )
+            })
             .into_any_element()
     }
 }

crates/editor2/src/display_map.rs 🔗

@@ -990,905 +990,869 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat
     })
 }
 
-// #[cfg(test)]
-// pub mod tests {
-//     use super::*;
-//     use crate::{
-//         movement,
-//         test::{editor_test_context::EditorTestContext, marked_display_snapshot},
-//     };
-//     use gpui::{AppContext, Hsla};
-//     use language::{
-//         language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
-//         Buffer, Language, LanguageConfig, SelectionGoal,
-//     };
-//     use project::Project;
-//     use rand::{prelude::*, Rng};
-//     use settings::SettingsStore;
-//     use smol::stream::StreamExt;
-//     use std::{env, sync::Arc};
-//     use theme::SyntaxTheme;
-//     use util::test::{marked_text_ranges, sample_text};
-//     use Bias::*;
-
-//     #[gpui::test(iterations = 100)]
-//     async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
-//         cx.foreground().set_block_on_ticks(0..=50);
-//         cx.foreground().forbid_parking();
-//         let operations = env::var("OPERATIONS")
-//             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-//             .unwrap_or(10);
-
-//         let font_cache = cx.font_cache().clone();
-//         let mut tab_size = rng.gen_range(1..=4);
-//         let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
-//         let excerpt_header_height = rng.gen_range(1..=5);
-//         let family_id = font_cache
-//             .load_family(&["Helvetica"], &Default::default())
-//             .unwrap();
-//         let font_id = font_cache
-//             .select_font(family_id, &Default::default())
-//             .unwrap();
-//         let font_size = 14.0;
-//         let max_wrap_width = 300.0;
-//         let mut wrap_width = if rng.gen_bool(0.1) {
-//             None
-//         } else {
-//             Some(rng.gen_range(0.0..=max_wrap_width))
-//         };
-
-//         log::info!("tab size: {}", tab_size);
-//         log::info!("wrap width: {:?}", wrap_width);
-
-//         cx.update(|cx| {
-//             init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size));
-//         });
-
-//         let buffer = cx.update(|cx| {
-//             if rng.gen() {
-//                 let len = rng.gen_range(0..10);
-//                 let text = util::RandomCharIter::new(&mut rng)
-//                     .take(len)
-//                     .collect::<String>();
-//                 MultiBuffer::build_simple(&text, cx)
-//             } else {
-//                 MultiBuffer::build_random(&mut rng, cx)
-//             }
-//         });
-
-//         let map = cx.add_model(|cx| {
-//             DisplayMap::new(
-//                 buffer.clone(),
-//                 font_id,
-//                 font_size,
-//                 wrap_width,
-//                 buffer_start_excerpt_header_height,
-//                 excerpt_header_height,
-//                 cx,
-//             )
-//         });
-//         let mut notifications = observe(&map, cx);
-//         let mut fold_count = 0;
-//         let mut blocks = Vec::new();
-
-//         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-//         log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
-//         log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
-//         log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
-//         log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
-//         log::info!("block text: {:?}", snapshot.block_snapshot.text());
-//         log::info!("display text: {:?}", snapshot.text());
-
-//         for _i in 0..operations {
-//             match rng.gen_range(0..100) {
-//                 0..=19 => {
-//                     wrap_width = if rng.gen_bool(0.2) {
-//                         None
-//                     } else {
-//                         Some(rng.gen_range(0.0..=max_wrap_width))
-//                     };
-//                     log::info!("setting wrap width to {:?}", wrap_width);
-//                     map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
-//                 }
-//                 20..=29 => {
-//                     let mut tab_sizes = vec![1, 2, 3, 4];
-//                     tab_sizes.remove((tab_size - 1) as usize);
-//                     tab_size = *tab_sizes.choose(&mut rng).unwrap();
-//                     log::info!("setting tab size to {:?}", tab_size);
-//                     cx.update(|cx| {
-//                         cx.update_global::<SettingsStore, _, _>(|store, cx| {
-//                             store.update_user_settings::<AllLanguageSettings>(cx, |s| {
-//                                 s.defaults.tab_size = NonZeroU32::new(tab_size);
-//                             });
-//                         });
-//                     });
-//                 }
-//                 30..=44 => {
-//                     map.update(cx, |map, cx| {
-//                         if rng.gen() || blocks.is_empty() {
-//                             let buffer = map.snapshot(cx).buffer_snapshot;
-//                             let block_properties = (0..rng.gen_range(1..=1))
-//                                 .map(|_| {
-//                                     let position =
-//                                         buffer.anchor_after(buffer.clip_offset(
-//                                             rng.gen_range(0..=buffer.len()),
-//                                             Bias::Left,
-//                                         ));
-
-//                                     let disposition = if rng.gen() {
-//                                         BlockDisposition::Above
-//                                     } else {
-//                                         BlockDisposition::Below
-//                                     };
-//                                     let height = rng.gen_range(1..5);
-//                                     log::info!(
-//                                         "inserting block {:?} {:?} with height {}",
-//                                         disposition,
-//                                         position.to_point(&buffer),
-//                                         height
-//                                     );
-//                                     BlockProperties {
-//                                         style: BlockStyle::Fixed,
-//                                         position,
-//                                         height,
-//                                         disposition,
-//                                         render: Arc::new(|_| Empty::new().into_any()),
-//                                     }
-//                                 })
-//                                 .collect::<Vec<_>>();
-//                             blocks.extend(map.insert_blocks(block_properties, cx));
-//                         } else {
-//                             blocks.shuffle(&mut rng);
-//                             let remove_count = rng.gen_range(1..=4.min(blocks.len()));
-//                             let block_ids_to_remove = (0..remove_count)
-//                                 .map(|_| blocks.remove(rng.gen_range(0..blocks.len())))
-//                                 .collect();
-//                             log::info!("removing block ids {:?}", block_ids_to_remove);
-//                             map.remove_blocks(block_ids_to_remove, cx);
-//                         }
-//                     });
-//                 }
-//                 45..=79 => {
-//                     let mut ranges = Vec::new();
-//                     for _ in 0..rng.gen_range(1..=3) {
-//                         buffer.read_with(cx, |buffer, cx| {
-//                             let buffer = buffer.read(cx);
-//                             let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
-//                             let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
-//                             ranges.push(start..end);
-//                         });
-//                     }
-
-//                     if rng.gen() && fold_count > 0 {
-//                         log::info!("unfolding ranges: {:?}", ranges);
-//                         map.update(cx, |map, cx| {
-//                             map.unfold(ranges, true, cx);
-//                         });
-//                     } else {
-//                         log::info!("folding ranges: {:?}", ranges);
-//                         map.update(cx, |map, cx| {
-//                             map.fold(ranges, cx);
-//                         });
-//                     }
-//                 }
-//                 _ => {
-//                     buffer.update(cx, |buffer, cx| buffer.randomly_mutate(&mut rng, 5, cx));
-//                 }
-//             }
-
-//             if map.read_with(cx, |map, cx| map.is_rewrapping(cx)) {
-//                 notifications.next().await.unwrap();
-//             }
-
-//             let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-//             fold_count = snapshot.fold_count();
-//             log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
-//             log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
-//             log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
-//             log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
-//             log::info!("block text: {:?}", snapshot.block_snapshot.text());
-//             log::info!("display text: {:?}", snapshot.text());
-
-//             // Line boundaries
-//             let buffer = &snapshot.buffer_snapshot;
-//             for _ in 0..5 {
-//                 let row = rng.gen_range(0..=buffer.max_point().row);
-//                 let column = rng.gen_range(0..=buffer.line_len(row));
-//                 let point = buffer.clip_point(Point::new(row, column), Left);
-
-//                 let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point);
-//                 let (next_buffer_bound, next_display_bound) = snapshot.next_line_boundary(point);
-
-//                 assert!(prev_buffer_bound <= point);
-//                 assert!(next_buffer_bound >= point);
-//                 assert_eq!(prev_buffer_bound.column, 0);
-//                 assert_eq!(prev_display_bound.column(), 0);
-//                 if next_buffer_bound < buffer.max_point() {
-//                     assert_eq!(buffer.chars_at(next_buffer_bound).next(), Some('\n'));
-//                 }
-
-//                 assert_eq!(
-//                     prev_display_bound,
-//                     prev_buffer_bound.to_display_point(&snapshot),
-//                     "row boundary before {:?}. reported buffer row boundary: {:?}",
-//                     point,
-//                     prev_buffer_bound
-//                 );
-//                 assert_eq!(
-//                     next_display_bound,
-//                     next_buffer_bound.to_display_point(&snapshot),
-//                     "display row boundary after {:?}. reported buffer row boundary: {:?}",
-//                     point,
-//                     next_buffer_bound
-//                 );
-//                 assert_eq!(
-//                     prev_buffer_bound,
-//                     prev_display_bound.to_point(&snapshot),
-//                     "row boundary before {:?}. reported display row boundary: {:?}",
-//                     point,
-//                     prev_display_bound
-//                 );
-//                 assert_eq!(
-//                     next_buffer_bound,
-//                     next_display_bound.to_point(&snapshot),
-//                     "row boundary after {:?}. reported display row boundary: {:?}",
-//                     point,
-//                     next_display_bound
-//                 );
-//             }
-
-//             // Movement
-//             let min_point = snapshot.clip_point(DisplayPoint::new(0, 0), Left);
-//             let max_point = snapshot.clip_point(snapshot.max_point(), Right);
-//             for _ in 0..5 {
-//                 let row = rng.gen_range(0..=snapshot.max_point().row());
-//                 let column = rng.gen_range(0..=snapshot.line_len(row));
-//                 let point = snapshot.clip_point(DisplayPoint::new(row, column), Left);
-
-//                 log::info!("Moving from point {:?}", point);
-
-//                 let moved_right = movement::right(&snapshot, point);
-//                 log::info!("Right {:?}", moved_right);
-//                 if point < max_point {
-//                     assert!(moved_right > point);
-//                     if point.column() == snapshot.line_len(point.row())
-//                         || snapshot.soft_wrap_indent(point.row()).is_some()
-//                             && point.column() == snapshot.line_len(point.row()) - 1
-//                     {
-//                         assert!(moved_right.row() > point.row());
-//                     }
-//                 } else {
-//                     assert_eq!(moved_right, point);
-//                 }
-
-//                 let moved_left = movement::left(&snapshot, point);
-//                 log::info!("Left {:?}", moved_left);
-//                 if point > min_point {
-//                     assert!(moved_left < point);
-//                     if point.column() == 0 {
-//                         assert!(moved_left.row() < point.row());
-//                     }
-//                 } else {
-//                     assert_eq!(moved_left, point);
-//                 }
-//             }
-//         }
-//     }
-
-//     #[gpui::test(retries = 5)]
-//     async fn test_soft_wraps(cx: &mut gpui::TestAppContext) {
-//         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
-//         cx.update(|cx| {
-//             init_test(cx, |_| {});
-//         });
-
-//         let mut cx = EditorTestContext::new(cx).await;
-//         let editor = cx.editor.clone();
-//         let window = cx.window.clone();
-
-//         cx.update_window(window, |cx| {
-//             let text_layout_details =
-//                 editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
-
-//             let font_cache = cx.font_cache().clone();
-
-//             let family_id = font_cache
-//                 .load_family(&["Helvetica"], &Default::default())
-//                 .unwrap();
-//             let font_id = font_cache
-//                 .select_font(family_id, &Default::default())
-//                 .unwrap();
-//             let font_size = 12.0;
-//             let wrap_width = Some(64.);
-
-//             let text = "one two three four five\nsix seven eight";
-//             let buffer = MultiBuffer::build_simple(text, cx);
-//             let map = cx.add_model(|cx| {
-//                 DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
-//             });
-
-//             let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-//             assert_eq!(
-//                 snapshot.text_chunks(0).collect::<String>(),
-//                 "one two \nthree four \nfive\nsix seven \neight"
-//             );
-//             assert_eq!(
-//                 snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
-//                 DisplayPoint::new(0, 7)
-//             );
-//             assert_eq!(
-//                 snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
-//                 DisplayPoint::new(1, 0)
-//             );
-//             assert_eq!(
-//                 movement::right(&snapshot, DisplayPoint::new(0, 7)),
-//                 DisplayPoint::new(1, 0)
-//             );
-//             assert_eq!(
-//                 movement::left(&snapshot, DisplayPoint::new(1, 0)),
-//                 DisplayPoint::new(0, 7)
-//             );
-
-//             let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details);
-//             assert_eq!(
-//                 movement::up(
-//                     &snapshot,
-//                     DisplayPoint::new(1, 10),
-//                     SelectionGoal::None,
-//                     false,
-//                     &text_layout_details,
-//                 ),
-//                 (
-//                     DisplayPoint::new(0, 7),
-//                     SelectionGoal::HorizontalPosition(x)
-//                 )
-//             );
-//             assert_eq!(
-//                 movement::down(
-//                     &snapshot,
-//                     DisplayPoint::new(0, 7),
-//                     SelectionGoal::HorizontalPosition(x),
-//                     false,
-//                     &text_layout_details
-//                 ),
-//                 (
-//                     DisplayPoint::new(1, 10),
-//                     SelectionGoal::HorizontalPosition(x)
-//                 )
-//             );
-//             assert_eq!(
-//                 movement::down(
-//                     &snapshot,
-//                     DisplayPoint::new(1, 10),
-//                     SelectionGoal::HorizontalPosition(x),
-//                     false,
-//                     &text_layout_details
-//                 ),
-//                 (
-//                     DisplayPoint::new(2, 4),
-//                     SelectionGoal::HorizontalPosition(x)
-//                 )
-//             );
-
-//             let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
-//             buffer.update(cx, |buffer, cx| {
-//                 buffer.edit([(ix..ix, "and ")], None, cx);
-//             });
-
-//             let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-//             assert_eq!(
-//                 snapshot.text_chunks(1).collect::<String>(),
-//                 "three four \nfive\nsix and \nseven eight"
-//             );
-
-//             // Re-wrap on font size changes
-//             map.update(cx, |map, cx| map.set_font_with_size(font_id, font_size + 3., cx));
-
-//             let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-//             assert_eq!(
-//                 snapshot.text_chunks(1).collect::<String>(),
-//                 "three \nfour five\nsix and \nseven \neight"
-//             )
-//         });
-//     }
-
-//     #[gpui::test]
-//     fn test_text_chunks(cx: &mut gpui::AppContext) {
-//         init_test(cx, |_| {});
-
-//         let text = sample_text(6, 6, 'a');
-//         let buffer = MultiBuffer::build_simple(&text, cx);
-//         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 map =
-//             cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, 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"),
-//                     (Point::new(2, 1)..Point::new(2, 1), "\t"),
-//                 ],
-//                 None,
-//                 cx,
-//             )
-//         });
-
-//         assert_eq!(
-//             map.update(cx, |map, cx| map.snapshot(cx))
-//                 .text_chunks(1)
-//                 .collect::<String>()
-//                 .lines()
-//                 .next(),
-//             Some("    b   bbbbb")
-//         );
-//         assert_eq!(
-//             map.update(cx, |map, cx| map.snapshot(cx))
-//                 .text_chunks(2)
-//                 .collect::<String>()
-//                 .lines()
-//                 .next(),
-//             Some("c   ccccc")
-//         );
-//     }
-
-//     #[gpui::test]
-//     async fn test_chunks(cx: &mut gpui::TestAppContext) {
-//         use unindent::Unindent as _;
-
-//         let text = r#"
-//             fn outer() {}
-
-//             mod module {
-//                 fn inner() {}
-//             }"#
-//         .unindent();
-
-//         let theme = SyntaxTheme::new(vec![
-//             ("mod.body".to_string(), Hsla::red().into()),
-//             ("fn.name".to_string(), Hsla::blue().into()),
-//         ]);
-//         let language = Arc::new(
-//             Language::new(
-//                 LanguageConfig {
-//                     name: "Test".into(),
-//                     path_suffixes: vec![".test".to_string()],
-//                     ..Default::default()
-//                 },
-//                 Some(tree_sitter_rust::language()),
-//             )
-//             .with_highlights_query(
-//                 r#"
-//                 (mod_item name: (identifier) body: _ @mod.body)
-//                 (function_item name: (identifier) @fn.name)
-//                 "#,
-//             )
-//             .unwrap(),
-//         );
-//         language.set_theme(&theme);
-
-//         cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
-
-//         let buffer = cx
-//             .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
-//         buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
-//         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-
-//         let font_cache = cx.font_cache();
-//         let family_id = font_cache
-//             .load_family(&["Helvetica"], &Default::default())
-//             .unwrap();
-//         let font_id = font_cache
-//             .select_font(family_id, &Default::default())
-//             .unwrap();
-//         let font_size = 14.0;
-
-//         let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
-//         assert_eq!(
-//             cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
-//             vec![
-//                 ("fn ".to_string(), None),
-//                 ("outer".to_string(), Some(Hsla::blue())),
-//                 ("() {}\n\nmod module ".to_string(), None),
-//                 ("{\n    fn ".to_string(), Some(Hsla::red())),
-//                 ("inner".to_string(), Some(Hsla::blue())),
-//                 ("() {}\n}".to_string(), Some(Hsla::red())),
-//             ]
-//         );
-//         assert_eq!(
-//             cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
-//             vec![
-//                 ("    fn ".to_string(), Some(Hsla::red())),
-//                 ("inner".to_string(), Some(Hsla::blue())),
-//                 ("() {}\n}".to_string(), Some(Hsla::red())),
-//             ]
-//         );
-
-//         map.update(cx, |map, cx| {
-//             map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
-//         });
-//         assert_eq!(
-//             cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
-//             vec![
-//                 ("fn ".to_string(), None),
-//                 ("out".to_string(), Some(Hsla::blue())),
-//                 ("⋯".to_string(), None),
-//                 ("  fn ".to_string(), Some(Hsla::red())),
-//                 ("inner".to_string(), Some(Hsla::blue())),
-//                 ("() {}\n}".to_string(), Some(Hsla::red())),
-//             ]
-//         );
-//     }
-
-//     #[gpui::test]
-//     async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
-//         use unindent::Unindent as _;
-
-//         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
-
-//         let text = r#"
-//             fn outer() {}
-
-//             mod module {
-//                 fn inner() {}
-//             }"#
-//         .unindent();
-
-//         let theme = SyntaxTheme::new(vec![
-//             ("mod.body".to_string(), Hsla::red().into()),
-//             ("fn.name".to_string(), Hsla::blue().into()),
-//         ]);
-//         let language = Arc::new(
-//             Language::new(
-//                 LanguageConfig {
-//                     name: "Test".into(),
-//                     path_suffixes: vec![".test".to_string()],
-//                     ..Default::default()
-//                 },
-//                 Some(tree_sitter_rust::language()),
-//             )
-//             .with_highlights_query(
-//                 r#"
-//                 (mod_item name: (identifier) body: _ @mod.body)
-//                 (function_item name: (identifier) @fn.name)
-//                 "#,
-//             )
-//             .unwrap(),
-//         );
-//         language.set_theme(&theme);
-
-//         cx.update(|cx| init_test(cx, |_| {}));
-
-//         let buffer = cx
-//             .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
-//         buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
-//         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-
-//         let font_cache = cx.font_cache();
-
-//         let family_id = font_cache
-//             .load_family(&["Courier"], &Default::default())
-//             .unwrap();
-//         let font_id = font_cache
-//             .select_font(family_id, &Default::default())
-//             .unwrap();
-//         let font_size = 16.0;
-
-//         let map =
-//             cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, Some(40.0), 1, 1, cx));
-//         assert_eq!(
-//             cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
-//             [
-//                 ("fn \n".to_string(), None),
-//                 ("oute\nr".to_string(), Some(Hsla::blue())),
-//                 ("() \n{}\n\n".to_string(), None),
-//             ]
-//         );
-//         assert_eq!(
-//             cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
-//             [("{}\n\n".to_string(), None)]
-//         );
-
-//         map.update(cx, |map, cx| {
-//             map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
-//         });
-//         assert_eq!(
-//             cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
-//             [
-//                 ("out".to_string(), Some(Hsla::blue())),
-//                 ("⋯\n".to_string(), None),
-//                 ("  \nfn ".to_string(), Some(Hsla::red())),
-//                 ("i\n".to_string(), Some(Hsla::blue()))
-//             ]
-//         );
-//     }
-
-//     #[gpui::test]
-//     async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
-//         cx.update(|cx| init_test(cx, |_| {}));
-
-//         let theme = SyntaxTheme::new(vec![
-//             ("operator".to_string(), Hsla::red().into()),
-//             ("string".to_string(), Hsla::green().into()),
-//         ]);
-//         let language = Arc::new(
-//             Language::new(
-//                 LanguageConfig {
-//                     name: "Test".into(),
-//                     path_suffixes: vec![".test".to_string()],
-//                     ..Default::default()
-//                 },
-//                 Some(tree_sitter_rust::language()),
-//             )
-//             .with_highlights_query(
-//                 r#"
-//                 ":" @operator
-//                 (string_literal) @string
-//                 "#,
-//             )
-//             .unwrap(),
-//         );
-//         language.set_theme(&theme);
-
-//         let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false);
-
-//         let buffer = cx
-//             .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
-//         buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
-
-//         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-//         let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
-
-//         let font_cache = cx.font_cache();
-//         let family_id = font_cache
-//             .load_family(&["Courier"], &Default::default())
-//             .unwrap();
-//         let font_id = font_cache
-//             .select_font(family_id, &Default::default())
-//             .unwrap();
-//         let font_size = 16.0;
-//         let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
-
-//         enum MyType {}
-
-//         let style = HighlightStyle {
-//             color: Some(Hsla::blue()),
-//             ..Default::default()
-//         };
-
-//         map.update(cx, |map, _cx| {
-//             map.highlight_text(
-//                 TypeId::of::<MyType>(),
-//                 highlighted_ranges
-//                     .into_iter()
-//                     .map(|range| {
-//                         buffer_snapshot.anchor_before(range.start)
-//                             ..buffer_snapshot.anchor_before(range.end)
-//                     })
-//                     .collect(),
-//                 style,
-//             );
-//         });
-
-//         assert_eq!(
-//             cx.update(|cx| chunks(0..10, &map, &theme, cx)),
-//             [
-//                 ("const ".to_string(), None, None),
-//                 ("a".to_string(), None, Some(Hsla::blue())),
-//                 (":".to_string(), Some(Hsla::red()), None),
-//                 (" B = ".to_string(), None, None),
-//                 ("\"c ".to_string(), Some(Hsla::green()), None),
-//                 ("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())),
-//                 ("\"".to_string(), Some(Hsla::green()), None),
-//             ]
-//         );
-//     }
-
-//     #[gpui::test]
-//     fn test_clip_point(cx: &mut gpui::AppContext) {
-//         init_test(cx, |_| {});
-
-//         fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) {
-//             let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
-
-//             match bias {
-//                 Bias::Left => {
-//                     if shift_right {
-//                         *markers[1].column_mut() += 1;
-//                     }
-
-//                     assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0])
-//                 }
-//                 Bias::Right => {
-//                     if shift_right {
-//                         *markers[0].column_mut() += 1;
-//                     }
-
-//                     assert_eq!(unmarked_snapshot.clip_point(markers[0], bias), markers[1])
-//                 }
-//             };
-//         }
-
-//         use Bias::{Left, Right};
-//         assert("ˇˇα", false, Left, cx);
-//         assert("ˇˇα", true, Left, cx);
-//         assert("ˇˇα", false, Right, cx);
-//         assert("ˇαˇ", true, Right, cx);
-//         assert("ˇˇ✋", false, Left, cx);
-//         assert("ˇˇ✋", true, Left, cx);
-//         assert("ˇˇ✋", false, Right, cx);
-//         assert("ˇ✋ˇ", true, Right, cx);
-//         assert("ˇˇ🍐", false, Left, cx);
-//         assert("ˇˇ🍐", true, Left, cx);
-//         assert("ˇˇ🍐", false, Right, cx);
-//         assert("ˇ🍐ˇ", true, Right, cx);
-//         assert("ˇˇ\t", false, Left, cx);
-//         assert("ˇˇ\t", true, Left, cx);
-//         assert("ˇˇ\t", false, Right, cx);
-//         assert("ˇ\tˇ", true, Right, cx);
-//         assert(" ˇˇ\t", false, Left, cx);
-//         assert(" ˇˇ\t", true, Left, cx);
-//         assert(" ˇˇ\t", false, Right, cx);
-//         assert(" ˇ\tˇ", true, Right, cx);
-//         assert("   ˇˇ\t", false, Left, cx);
-//         assert("   ˇˇ\t", false, Right, cx);
-//     }
-
-//     #[gpui::test]
-//     fn test_clip_at_line_ends(cx: &mut gpui::AppContext) {
-//         init_test(cx, |_| {});
-
-//         fn assert(text: &str, cx: &mut gpui::AppContext) {
-//             let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
-//             unmarked_snapshot.clip_at_line_ends = true;
-//             assert_eq!(
-//                 unmarked_snapshot.clip_point(markers[1], Bias::Left),
-//                 markers[0]
-//             );
-//         }
-
-//         assert("ˇˇ", cx);
-//         assert("ˇaˇ", cx);
-//         assert("aˇbˇ", cx);
-//         assert("aˇαˇ", cx);
-//     }
-
-//     #[gpui::test]
-//     fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
-//         init_test(cx, |_| {});
-
-//         let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
-//         let buffer = MultiBuffer::build_simple(text, cx);
-//         let font_cache = cx.font_cache();
-//         let family_id = font_cache
-//             .load_family(&["Helvetica"], &Default::default())
-//             .unwrap();
-//         let font_id = font_cache
-//             .select_font(family_id, &Default::default())
-//             .unwrap();
-//         let font_size = 14.0;
-
-//         let map =
-//             cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
-//         let map = map.update(cx, |map, cx| map.snapshot(cx));
-//         assert_eq!(map.text(), "✅       α\nβ   \n🏀β      γ");
-//         assert_eq!(
-//             map.text_chunks(0).collect::<String>(),
-//             "✅       α\nβ   \n🏀β      γ"
-//         );
-//         assert_eq!(map.text_chunks(1).collect::<String>(), "β   \n🏀β      γ");
-//         assert_eq!(map.text_chunks(2).collect::<String>(), "🏀β      γ");
-
-//         let point = Point::new(0, "✅\t\t".len() as u32);
-//         let display_point = DisplayPoint::new(0, "✅       ".len() as u32);
-//         assert_eq!(point.to_display_point(&map), display_point);
-//         assert_eq!(display_point.to_point(&map), point);
-
-//         let point = Point::new(1, "β\t".len() as u32);
-//         let display_point = DisplayPoint::new(1, "β   ".len() as u32);
-//         assert_eq!(point.to_display_point(&map), display_point);
-//         assert_eq!(display_point.to_point(&map), point,);
-
-//         let point = Point::new(2, "🏀β\t\t".len() as u32);
-//         let display_point = DisplayPoint::new(2, "🏀β      ".len() as u32);
-//         assert_eq!(point.to_display_point(&map), display_point);
-//         assert_eq!(display_point.to_point(&map), point,);
-
-//         // Display points inside of expanded tabs
-//         assert_eq!(
-//             DisplayPoint::new(0, "✅      ".len() as u32).to_point(&map),
-//             Point::new(0, "✅\t".len() as u32),
-//         );
-//         assert_eq!(
-//             DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map),
-//             Point::new(0, "✅".len() as u32),
-//         );
-
-//         // Clipping display points inside of multi-byte characters
-//         assert_eq!(
-//             map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Left),
-//             DisplayPoint::new(0, 0)
-//         );
-//         assert_eq!(
-//             map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Bias::Right),
-//             DisplayPoint::new(0, "✅".len() as u32)
-//         );
-//     }
-
-//     #[gpui::test]
-//     fn test_max_point(cx: &mut gpui::AppContext) {
-//         init_test(cx, |_| {});
-
-//         let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
-//         let font_cache = cx.font_cache();
-//         let family_id = font_cache
-//             .load_family(&["Helvetica"], &Default::default())
-//             .unwrap();
-//         let font_id = font_cache
-//             .select_font(family_id, &Default::default())
-//             .unwrap();
-//         let font_size = 14.0;
-//         let map =
-//             cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
-//         assert_eq!(
-//             map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
-//             DisplayPoint::new(1, 11)
-//         )
-//     }
-
-//     fn syntax_chunks<'a>(
-//         rows: Range<u32>,
-//         map: &Model<DisplayMap>,
-//         theme: &'a SyntaxTheme,
-//         cx: &mut AppContext,
-//     ) -> Vec<(String, Option<Hsla>)> {
-//         chunks(rows, map, theme, cx)
-//             .into_iter()
-//             .map(|(text, color, _)| (text, color))
-//             .collect()
-//     }
-
-//     fn chunks<'a>(
-//         rows: Range<u32>,
-//         map: &Model<DisplayMap>,
-//         theme: &'a SyntaxTheme,
-//         cx: &mut AppContext,
-//     ) -> Vec<(String, Option<Hsla>, Option<Hsla>)> {
-//         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-//         let mut chunks: Vec<(String, Option<Hsla>, Option<Hsla>)> = Vec::new();
-//         for chunk in snapshot.chunks(rows, true, None, None) {
-//             let syntax_color = chunk
-//                 .syntax_highlight_id
-//                 .and_then(|id| id.style(theme)?.color);
-//             let highlight_color = chunk.highlight_style.and_then(|style| style.color);
-//             if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() {
-//                 if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color {
-//                     last_chunk.push_str(chunk.text);
-//                     continue;
-//                 }
-//             }
-//             chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
-//         }
-//         chunks
-//     }
-
-//     fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
-//         cx.foreground().forbid_parking();
-//         cx.set_global(SettingsStore::test(cx));
-//         language::init(cx);
-//         crate::init(cx);
-//         Project::init_settings(cx);
-//         theme::init((), cx);
-//         cx.update_global::<SettingsStore, _, _>(|store, cx| {
-//             store.update_user_settings::<AllLanguageSettings>(cx, f);
-//         });
-//     }
-// }
+#[cfg(test)]
+pub mod tests {
+    use super::*;
+    use crate::{
+        movement,
+        test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+    };
+    use gpui::{div, font, observe, px, AppContext, Context, Element, Hsla};
+    use language::{
+        language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
+        Buffer, Language, LanguageConfig, SelectionGoal,
+    };
+    use project::Project;
+    use rand::{prelude::*, Rng};
+    use settings::SettingsStore;
+    use smol::stream::StreamExt;
+    use std::{env, sync::Arc};
+    use theme::{LoadThemes, SyntaxTheme};
+    use util::test::{marked_text_ranges, sample_text};
+    use Bias::*;
+
+    #[gpui::test(iterations = 100)]
+    async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+        cx.background_executor.set_block_on_ticks(0..=50);
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let test_platform = &cx.test_platform;
+        let mut tab_size = rng.gen_range(1..=4);
+        let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
+        let excerpt_header_height = rng.gen_range(1..=5);
+        let font_size = px(14.0);
+        let max_wrap_width = 300.0;
+        let mut wrap_width = if rng.gen_bool(0.1) {
+            None
+        } else {
+            Some(px(rng.gen_range(0.0..=max_wrap_width)))
+        };
+
+        log::info!("tab size: {}", tab_size);
+        log::info!("wrap width: {:?}", wrap_width);
+
+        cx.update(|cx| {
+            init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size));
+        });
+
+        let buffer = cx.update(|cx| {
+            if rng.gen() {
+                let len = rng.gen_range(0..10);
+                let text = util::RandomCharIter::new(&mut rng)
+                    .take(len)
+                    .collect::<String>();
+                MultiBuffer::build_simple(&text, cx)
+            } else {
+                MultiBuffer::build_random(&mut rng, cx)
+            }
+        });
+
+        let map = cx.build_model(|cx| {
+            DisplayMap::new(
+                buffer.clone(),
+                font("Helvetica"),
+                font_size,
+                wrap_width,
+                buffer_start_excerpt_header_height,
+                excerpt_header_height,
+                cx,
+            )
+        });
+        let mut notifications = observe(&map, cx);
+        let mut fold_count = 0;
+        let mut blocks = Vec::new();
+
+        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+        log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
+        log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
+        log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
+        log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
+        log::info!("block text: {:?}", snapshot.block_snapshot.text());
+        log::info!("display text: {:?}", snapshot.text());
+
+        for _i in 0..operations {
+            match rng.gen_range(0..100) {
+                0..=19 => {
+                    wrap_width = if rng.gen_bool(0.2) {
+                        None
+                    } else {
+                        Some(px(rng.gen_range(0.0..=max_wrap_width)))
+                    };
+                    log::info!("setting wrap width to {:?}", wrap_width);
+                    map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+                }
+                20..=29 => {
+                    let mut tab_sizes = vec![1, 2, 3, 4];
+                    tab_sizes.remove((tab_size - 1) as usize);
+                    tab_size = *tab_sizes.choose(&mut rng).unwrap();
+                    log::info!("setting tab size to {:?}", tab_size);
+                    cx.update(|cx| {
+                        cx.update_global::<SettingsStore, _>(|store, cx| {
+                            store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+                                s.defaults.tab_size = NonZeroU32::new(tab_size);
+                            });
+                        });
+                    });
+                }
+                30..=44 => {
+                    map.update(cx, |map, cx| {
+                        if rng.gen() || blocks.is_empty() {
+                            let buffer = map.snapshot(cx).buffer_snapshot;
+                            let block_properties = (0..rng.gen_range(1..=1))
+                                .map(|_| {
+                                    let position =
+                                        buffer.anchor_after(buffer.clip_offset(
+                                            rng.gen_range(0..=buffer.len()),
+                                            Bias::Left,
+                                        ));
+
+                                    let disposition = if rng.gen() {
+                                        BlockDisposition::Above
+                                    } else {
+                                        BlockDisposition::Below
+                                    };
+                                    let height = rng.gen_range(1..5);
+                                    log::info!(
+                                        "inserting block {:?} {:?} with height {}",
+                                        disposition,
+                                        position.to_point(&buffer),
+                                        height
+                                    );
+                                    BlockProperties {
+                                        style: BlockStyle::Fixed,
+                                        position,
+                                        height,
+                                        disposition,
+                                        render: Arc::new(|_| div().into_any()),
+                                    }
+                                })
+                                .collect::<Vec<_>>();
+                            blocks.extend(map.insert_blocks(block_properties, cx));
+                        } else {
+                            blocks.shuffle(&mut rng);
+                            let remove_count = rng.gen_range(1..=4.min(blocks.len()));
+                            let block_ids_to_remove = (0..remove_count)
+                                .map(|_| blocks.remove(rng.gen_range(0..blocks.len())))
+                                .collect();
+                            log::info!("removing block ids {:?}", block_ids_to_remove);
+                            map.remove_blocks(block_ids_to_remove, cx);
+                        }
+                    });
+                }
+                45..=79 => {
+                    let mut ranges = Vec::new();
+                    for _ in 0..rng.gen_range(1..=3) {
+                        buffer.read_with(cx, |buffer, cx| {
+                            let buffer = buffer.read(cx);
+                            let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
+                            let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
+                            ranges.push(start..end);
+                        });
+                    }
+
+                    if rng.gen() && fold_count > 0 {
+                        log::info!("unfolding ranges: {:?}", ranges);
+                        map.update(cx, |map, cx| {
+                            map.unfold(ranges, true, cx);
+                        });
+                    } else {
+                        log::info!("folding ranges: {:?}", ranges);
+                        map.update(cx, |map, cx| {
+                            map.fold(ranges, cx);
+                        });
+                    }
+                }
+                _ => {
+                    buffer.update(cx, |buffer, cx| buffer.randomly_mutate(&mut rng, 5, cx));
+                }
+            }
+
+            if map.read_with(cx, |map, cx| map.is_rewrapping(cx)) {
+                notifications.next().await.unwrap();
+            }
+
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            fold_count = snapshot.fold_count();
+            log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
+            log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
+            log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
+            log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
+            log::info!("block text: {:?}", snapshot.block_snapshot.text());
+            log::info!("display text: {:?}", snapshot.text());
+
+            // Line boundaries
+            let buffer = &snapshot.buffer_snapshot;
+            for _ in 0..5 {
+                let row = rng.gen_range(0..=buffer.max_point().row);
+                let column = rng.gen_range(0..=buffer.line_len(row));
+                let point = buffer.clip_point(Point::new(row, column), Left);
+
+                let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point);
+                let (next_buffer_bound, next_display_bound) = snapshot.next_line_boundary(point);
+
+                assert!(prev_buffer_bound <= point);
+                assert!(next_buffer_bound >= point);
+                assert_eq!(prev_buffer_bound.column, 0);
+                assert_eq!(prev_display_bound.column(), 0);
+                if next_buffer_bound < buffer.max_point() {
+                    assert_eq!(buffer.chars_at(next_buffer_bound).next(), Some('\n'));
+                }
+
+                assert_eq!(
+                    prev_display_bound,
+                    prev_buffer_bound.to_display_point(&snapshot),
+                    "row boundary before {:?}. reported buffer row boundary: {:?}",
+                    point,
+                    prev_buffer_bound
+                );
+                assert_eq!(
+                    next_display_bound,
+                    next_buffer_bound.to_display_point(&snapshot),
+                    "display row boundary after {:?}. reported buffer row boundary: {:?}",
+                    point,
+                    next_buffer_bound
+                );
+                assert_eq!(
+                    prev_buffer_bound,
+                    prev_display_bound.to_point(&snapshot),
+                    "row boundary before {:?}. reported display row boundary: {:?}",
+                    point,
+                    prev_display_bound
+                );
+                assert_eq!(
+                    next_buffer_bound,
+                    next_display_bound.to_point(&snapshot),
+                    "row boundary after {:?}. reported display row boundary: {:?}",
+                    point,
+                    next_display_bound
+                );
+            }
+
+            // Movement
+            let min_point = snapshot.clip_point(DisplayPoint::new(0, 0), Left);
+            let max_point = snapshot.clip_point(snapshot.max_point(), Right);
+            for _ in 0..5 {
+                let row = rng.gen_range(0..=snapshot.max_point().row());
+                let column = rng.gen_range(0..=snapshot.line_len(row));
+                let point = snapshot.clip_point(DisplayPoint::new(row, column), Left);
+
+                log::info!("Moving from point {:?}", point);
+
+                let moved_right = movement::right(&snapshot, point);
+                log::info!("Right {:?}", moved_right);
+                if point < max_point {
+                    assert!(moved_right > point);
+                    if point.column() == snapshot.line_len(point.row())
+                        || snapshot.soft_wrap_indent(point.row()).is_some()
+                            && point.column() == snapshot.line_len(point.row()) - 1
+                    {
+                        assert!(moved_right.row() > point.row());
+                    }
+                } else {
+                    assert_eq!(moved_right, point);
+                }
+
+                let moved_left = movement::left(&snapshot, point);
+                log::info!("Left {:?}", moved_left);
+                if point > min_point {
+                    assert!(moved_left < point);
+                    if point.column() == 0 {
+                        assert!(moved_left.row() < point.row());
+                    }
+                } else {
+                    assert_eq!(moved_left, point);
+                }
+            }
+        }
+    }
+
+    #[gpui::test(retries = 5)]
+    async fn test_soft_wraps(cx: &mut gpui::TestAppContext) {
+        cx.background_executor
+            .set_block_on_ticks(usize::MAX..=usize::MAX);
+        cx.update(|cx| {
+            init_test(cx, |_| {});
+        });
+
+        let mut cx = EditorTestContext::new(cx).await;
+        let editor = cx.editor.clone();
+        let window = cx.window.clone();
+
+        cx.update_window(window, |_, cx| {
+            let text_layout_details =
+                editor.update(cx, |editor, cx| editor.text_layout_details(cx));
+
+            let font_size = px(12.0);
+            let wrap_width = Some(px(64.));
+
+            let text = "one two three four five\nsix seven eight";
+            let buffer = MultiBuffer::build_simple(text, cx);
+            let map = cx.build_model(|cx| {
+                DisplayMap::new(
+                    buffer.clone(),
+                    font("Helvetica"),
+                    font_size,
+                    wrap_width,
+                    1,
+                    1,
+                    cx,
+                )
+            });
+
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            assert_eq!(
+                snapshot.text_chunks(0).collect::<String>(),
+                "one two \nthree four \nfive\nsix seven \neight"
+            );
+            assert_eq!(
+                snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
+                DisplayPoint::new(0, 7)
+            );
+            assert_eq!(
+                snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
+                DisplayPoint::new(1, 0)
+            );
+            assert_eq!(
+                movement::right(&snapshot, DisplayPoint::new(0, 7)),
+                DisplayPoint::new(1, 0)
+            );
+            assert_eq!(
+                movement::left(&snapshot, DisplayPoint::new(1, 0)),
+                DisplayPoint::new(0, 7)
+            );
+
+            let x = snapshot.x_for_display_point(DisplayPoint::new(1, 10), &text_layout_details);
+            assert_eq!(
+                movement::up(
+                    &snapshot,
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::None,
+                    false,
+                    &text_layout_details,
+                ),
+                (
+                    DisplayPoint::new(0, 7),
+                    SelectionGoal::HorizontalPosition(x.0)
+                )
+            );
+            assert_eq!(
+                movement::down(
+                    &snapshot,
+                    DisplayPoint::new(0, 7),
+                    SelectionGoal::HorizontalPosition(x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::HorizontalPosition(x.0)
+                )
+            );
+            assert_eq!(
+                movement::down(
+                    &snapshot,
+                    DisplayPoint::new(1, 10),
+                    SelectionGoal::HorizontalPosition(x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 4),
+                    SelectionGoal::HorizontalPosition(x.0)
+                )
+            );
+
+            let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
+            buffer.update(cx, |buffer, cx| {
+                buffer.edit([(ix..ix, "and ")], None, cx);
+            });
+
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            assert_eq!(
+                snapshot.text_chunks(1).collect::<String>(),
+                "three four \nfive\nsix and \nseven eight"
+            );
+
+            // Re-wrap on font size changes
+            map.update(cx, |map, cx| {
+                map.set_font(font("Helvetica"), px(font_size.0 + 3.), cx)
+            });
+
+            let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+            assert_eq!(
+                snapshot.text_chunks(1).collect::<String>(),
+                "three \nfour five\nsix and \nseven \neight"
+            )
+        });
+    }
+
+    #[gpui::test]
+    fn test_text_chunks(cx: &mut gpui::AppContext) {
+        init_test(cx, |_| {});
+
+        let text = sample_text(6, 6, 'a');
+        let buffer = MultiBuffer::build_simple(&text, cx);
+
+        let font_size = px(14.0);
+        let map = cx.build_model(|cx| {
+            DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, 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"),
+                    (Point::new(2, 1)..Point::new(2, 1), "\t"),
+                ],
+                None,
+                cx,
+            )
+        });
+
+        assert_eq!(
+            map.update(cx, |map, cx| map.snapshot(cx))
+                .text_chunks(1)
+                .collect::<String>()
+                .lines()
+                .next(),
+            Some("    b   bbbbb")
+        );
+        assert_eq!(
+            map.update(cx, |map, cx| map.snapshot(cx))
+                .text_chunks(2)
+                .collect::<String>()
+                .lines()
+                .next(),
+            Some("c   ccccc")
+        );
+    }
+
+    #[gpui::test]
+    async fn test_chunks(cx: &mut gpui::TestAppContext) {
+        use unindent::Unindent as _;
+
+        let text = r#"
+            fn outer() {}
+
+            mod module {
+                fn inner() {}
+            }"#
+        .unindent();
+
+        let theme = SyntaxTheme::new_test(vec![
+            ("mod.body", Hsla::red().into()),
+            ("fn.name", Hsla::blue().into()),
+        ]);
+        let language = Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Test".into(),
+                    path_suffixes: vec![".test".to_string()],
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            )
+            .with_highlights_query(
+                r#"
+                (mod_item name: (identifier) body: _ @mod.body)
+                (function_item name: (identifier) @fn.name)
+                "#,
+            )
+            .unwrap(),
+        );
+        language.set_theme(&theme);
+
+        cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
+
+        let buffer = cx.build_model(|cx| {
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+        });
+        cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
+        let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+
+        let font_size = px(14.0);
+
+        let map = cx.build_model(|cx| {
+            DisplayMap::new(buffer, font("Helvetica"), font_size, None, 1, 1, cx)
+        });
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
+            vec![
+                ("fn ".to_string(), None),
+                ("outer".to_string(), Some(Hsla::blue())),
+                ("() {}\n\nmod module ".to_string(), None),
+                ("{\n    fn ".to_string(), Some(Hsla::red())),
+                ("inner".to_string(), Some(Hsla::blue())),
+                ("() {}\n}".to_string(), Some(Hsla::red())),
+            ]
+        );
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
+            vec![
+                ("    fn ".to_string(), Some(Hsla::red())),
+                ("inner".to_string(), Some(Hsla::blue())),
+                ("() {}\n}".to_string(), Some(Hsla::red())),
+            ]
+        );
+
+        map.update(cx, |map, cx| {
+            map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
+        });
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
+            vec![
+                ("fn ".to_string(), None),
+                ("out".to_string(), Some(Hsla::blue())),
+                ("⋯".to_string(), None),
+                ("  fn ".to_string(), Some(Hsla::red())),
+                ("inner".to_string(), Some(Hsla::blue())),
+                ("() {}\n}".to_string(), Some(Hsla::red())),
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
+        use unindent::Unindent as _;
+
+        cx.background_executor
+            .set_block_on_ticks(usize::MAX..=usize::MAX);
+
+        let text = r#"
+            fn outer() {}
+
+            mod module {
+                fn inner() {}
+            }"#
+        .unindent();
+
+        let theme = SyntaxTheme::new_test(vec![
+            ("mod.body", Hsla::red().into()),
+            ("fn.name", Hsla::blue().into()),
+        ]);
+        let language = Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Test".into(),
+                    path_suffixes: vec![".test".to_string()],
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            )
+            .with_highlights_query(
+                r#"
+                (mod_item name: (identifier) body: _ @mod.body)
+                (function_item name: (identifier) @fn.name)
+                "#,
+            )
+            .unwrap(),
+        );
+        language.set_theme(&theme);
+
+        cx.update(|cx| init_test(cx, |_| {}));
+
+        let buffer = cx.build_model(|cx| {
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+        });
+        cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
+        let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+
+        let font_size = px(16.0);
+
+        let map = cx.build_model(|cx| {
+            DisplayMap::new(buffer, font("Courier"), font_size, Some(px(40.0)), 1, 1, cx)
+        });
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
+            [
+                ("fn \n".to_string(), None),
+                ("oute\nr".to_string(), Some(Hsla::blue())),
+                ("() \n{}\n\n".to_string(), None),
+            ]
+        );
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
+            [("{}\n\n".to_string(), None)]
+        );
+
+        map.update(cx, |map, cx| {
+            map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
+        });
+        assert_eq!(
+            cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
+            [
+                ("out".to_string(), Some(Hsla::blue())),
+                ("⋯\n".to_string(), None),
+                ("  \nfn ".to_string(), Some(Hsla::red())),
+                ("i\n".to_string(), Some(Hsla::blue()))
+            ]
+        );
+    }
+
+    #[gpui::test]
+    async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
+        cx.update(|cx| init_test(cx, |_| {}));
+
+        let theme = SyntaxTheme::new_test(vec![
+            ("operator", Hsla::red().into()),
+            ("string", Hsla::green().into()),
+        ]);
+        let language = Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Test".into(),
+                    path_suffixes: vec![".test".to_string()],
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            )
+            .with_highlights_query(
+                r#"
+                ":" @operator
+                (string_literal) @string
+                "#,
+            )
+            .unwrap(),
+        );
+        language.set_theme(&theme);
+
+        let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false);
+
+        let buffer = cx.build_model(|cx| {
+            Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+        });
+        cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
+
+        let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+        let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+
+        let font_size = px(16.0);
+        let map = cx
+            .build_model(|cx| DisplayMap::new(buffer, font("Courier"), font_size, None, 1, 1, cx));
+
+        enum MyType {}
+
+        let style = HighlightStyle {
+            color: Some(Hsla::blue()),
+            ..Default::default()
+        };
+
+        map.update(cx, |map, _cx| {
+            map.highlight_text(
+                TypeId::of::<MyType>(),
+                highlighted_ranges
+                    .into_iter()
+                    .map(|range| {
+                        buffer_snapshot.anchor_before(range.start)
+                            ..buffer_snapshot.anchor_before(range.end)
+                    })
+                    .collect(),
+                style,
+            );
+        });
+
+        assert_eq!(
+            cx.update(|cx| chunks(0..10, &map, &theme, cx)),
+            [
+                ("const ".to_string(), None, None),
+                ("a".to_string(), None, Some(Hsla::blue())),
+                (":".to_string(), Some(Hsla::red()), None),
+                (" B = ".to_string(), None, None),
+                ("\"c ".to_string(), Some(Hsla::green()), None),
+                ("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())),
+                ("\"".to_string(), Some(Hsla::green()), None),
+            ]
+        );
+    }
+
+    #[gpui::test]
+    fn test_clip_point(cx: &mut gpui::AppContext) {
+        init_test(cx, |_| {});
+
+        fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) {
+            let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
+
+            match bias {
+                Bias::Left => {
+                    if shift_right {
+                        *markers[1].column_mut() += 1;
+                    }
+
+                    assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0])
+                }
+                Bias::Right => {
+                    if shift_right {
+                        *markers[0].column_mut() += 1;
+                    }
+
+                    assert_eq!(unmarked_snapshot.clip_point(markers[0], bias), markers[1])
+                }
+            };
+        }
+
+        use Bias::{Left, Right};
+        assert("ˇˇα", false, Left, cx);
+        assert("ˇˇα", true, Left, cx);
+        assert("ˇˇα", false, Right, cx);
+        assert("ˇαˇ", true, Right, cx);
+        assert("ˇˇ✋", false, Left, cx);
+        assert("ˇˇ✋", true, Left, cx);
+        assert("ˇˇ✋", false, Right, cx);
+        assert("ˇ✋ˇ", true, Right, cx);
+        assert("ˇˇ🍐", false, Left, cx);
+        assert("ˇˇ🍐", true, Left, cx);
+        assert("ˇˇ🍐", false, Right, cx);
+        assert("ˇ🍐ˇ", true, Right, cx);
+        assert("ˇˇ\t", false, Left, cx);
+        assert("ˇˇ\t", true, Left, cx);
+        assert("ˇˇ\t", false, Right, cx);
+        assert("ˇ\tˇ", true, Right, cx);
+        assert(" ˇˇ\t", false, Left, cx);
+        assert(" ˇˇ\t", true, Left, cx);
+        assert(" ˇˇ\t", false, Right, cx);
+        assert(" ˇ\tˇ", true, Right, cx);
+        assert("   ˇˇ\t", false, Left, cx);
+        assert("   ˇˇ\t", false, Right, cx);
+    }
+
+    #[gpui::test]
+    fn test_clip_at_line_ends(cx: &mut gpui::AppContext) {
+        init_test(cx, |_| {});
+
+        fn assert(text: &str, cx: &mut gpui::AppContext) {
+            let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
+            unmarked_snapshot.clip_at_line_ends = true;
+            assert_eq!(
+                unmarked_snapshot.clip_point(markers[1], Bias::Left),
+                markers[0]
+            );
+        }
+
+        assert("ˇˇ", cx);
+        assert("ˇaˇ", cx);
+        assert("aˇbˇ", cx);
+        assert("aˇαˇ", cx);
+    }
+
+    #[gpui::test]
+    fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
+        init_test(cx, |_| {});
+
+        let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
+        let buffer = MultiBuffer::build_simple(text, cx);
+        let font_size = px(14.0);
+
+        let map = cx.build_model(|cx| {
+            DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
+        });
+        let map = map.update(cx, |map, cx| map.snapshot(cx));
+        assert_eq!(map.text(), "✅       α\nβ   \n🏀β      γ");
+        assert_eq!(
+            map.text_chunks(0).collect::<String>(),
+            "✅       α\nβ   \n🏀β      γ"
+        );
+        assert_eq!(map.text_chunks(1).collect::<String>(), "β   \n🏀β      γ");
+        assert_eq!(map.text_chunks(2).collect::<String>(), "🏀β      γ");
+
+        let point = Point::new(0, "✅\t\t".len() as u32);
+        let display_point = DisplayPoint::new(0, "✅       ".len() as u32);
+        assert_eq!(point.to_display_point(&map), display_point);
+        assert_eq!(display_point.to_point(&map), point);
+
+        let point = Point::new(1, "β\t".len() as u32);
+        let display_point = DisplayPoint::new(1, "β   ".len() as u32);
+        assert_eq!(point.to_display_point(&map), display_point);
+        assert_eq!(display_point.to_point(&map), point,);
+
+        let point = Point::new(2, "🏀β\t\t".len() as u32);
+        let display_point = DisplayPoint::new(2, "🏀β      ".len() as u32);
+        assert_eq!(point.to_display_point(&map), display_point);
+        assert_eq!(display_point.to_point(&map), point,);
+
+        // Display points inside of expanded tabs
+        assert_eq!(
+            DisplayPoint::new(0, "✅      ".len() as u32).to_point(&map),
+            Point::new(0, "✅\t".len() as u32),
+        );
+        assert_eq!(
+            DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map),
+            Point::new(0, "✅".len() as u32),
+        );
+
+        // Clipping display points inside of multi-byte characters
+        assert_eq!(
+            map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Left),
+            DisplayPoint::new(0, 0)
+        );
+        assert_eq!(
+            map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Bias::Right),
+            DisplayPoint::new(0, "✅".len() as u32)
+        );
+    }
+
+    #[gpui::test]
+    fn test_max_point(cx: &mut gpui::AppContext) {
+        init_test(cx, |_| {});
+
+        let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
+        let font_size = px(14.0);
+        let map = cx.build_model(|cx| {
+            DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
+        });
+        assert_eq!(
+            map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
+            DisplayPoint::new(1, 11)
+        )
+    }
+
+    fn syntax_chunks<'a>(
+        rows: Range<u32>,
+        map: &Model<DisplayMap>,
+        theme: &'a SyntaxTheme,
+        cx: &mut AppContext,
+    ) -> Vec<(String, Option<Hsla>)> {
+        chunks(rows, map, theme, cx)
+            .into_iter()
+            .map(|(text, color, _)| (text, color))
+            .collect()
+    }
+
+    fn chunks<'a>(
+        rows: Range<u32>,
+        map: &Model<DisplayMap>,
+        theme: &'a SyntaxTheme,
+        cx: &mut AppContext,
+    ) -> Vec<(String, Option<Hsla>, Option<Hsla>)> {
+        let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+        let mut chunks: Vec<(String, Option<Hsla>, Option<Hsla>)> = Vec::new();
+        for chunk in snapshot.chunks(rows, true, None, None) {
+            let syntax_color = chunk
+                .syntax_highlight_id
+                .and_then(|id| id.style(theme)?.color);
+            let highlight_color = chunk.highlight_style.and_then(|style| style.color);
+            if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() {
+                if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color {
+                    last_chunk.push_str(chunk.text);
+                    continue;
+                }
+            }
+            chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
+        }
+        chunks
+    }
+
+    fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+        let settings = SettingsStore::test(cx);
+        cx.set_global(settings);
+        language::init(cx);
+        crate::init(cx);
+        Project::init_settings(cx);
+        theme::init(LoadThemes::JustBase, cx);
+        cx.update_global::<SettingsStore, _>(|store, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, f);
+        });
+    }
+}

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

@@ -988,680 +988,664 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
     (row, offset)
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use crate::display_map::inlay_map::InlayMap;
-//     use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
-//     use gpui::Element;
-//     use multi_buffer::MultiBuffer;
-//     use rand::prelude::*;
-//     use settings::SettingsStore;
-//     use std::env;
-//     use util::RandomCharIter;
-
-//     #[gpui::test]
-//     fn test_offset_for_row() {
-//         assert_eq!(offset_for_row("", 0), (0, 0));
-//         assert_eq!(offset_for_row("", 1), (0, 0));
-//         assert_eq!(offset_for_row("abcd", 0), (0, 0));
-//         assert_eq!(offset_for_row("abcd", 1), (0, 4));
-//         assert_eq!(offset_for_row("\n", 0), (0, 0));
-//         assert_eq!(offset_for_row("\n", 1), (1, 1));
-//         assert_eq!(offset_for_row("abc\ndef\nghi", 0), (0, 0));
-//         assert_eq!(offset_for_row("abc\ndef\nghi", 1), (1, 4));
-//         assert_eq!(offset_for_row("abc\ndef\nghi", 2), (2, 8));
-//         assert_eq!(offset_for_row("abc\ndef\nghi", 3), (2, 11));
-//     }
-
-//     #[gpui::test]
-//     fn test_basic_blocks(cx: &mut gpui::AppContext) {
-//         init_test(cx);
-
-//         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 text = "aaa\nbbb\nccc\nddd";
-
-//         let buffer = MultiBuffer::build_simple(text, cx);
-//         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-//         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
-//         let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
-//         let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
-//         let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
-//         let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx);
-//         let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
-
-//         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
-//         let block_ids = writer.insert(vec![
-//             BlockProperties {
-//                 style: BlockStyle::Fixed,
-//                 position: buffer_snapshot.anchor_after(Point::new(1, 0)),
-//                 height: 1,
-//                 disposition: BlockDisposition::Above,
-//                 render: Arc::new(|_| Empty::new().into_any_named("block 1")),
-//             },
-//             BlockProperties {
-//                 style: BlockStyle::Fixed,
-//                 position: buffer_snapshot.anchor_after(Point::new(1, 2)),
-//                 height: 2,
-//                 disposition: BlockDisposition::Above,
-//                 render: Arc::new(|_| Empty::new().into_any_named("block 2")),
-//             },
-//             BlockProperties {
-//                 style: BlockStyle::Fixed,
-//                 position: buffer_snapshot.anchor_after(Point::new(3, 3)),
-//                 height: 3,
-//                 disposition: BlockDisposition::Below,
-//                 render: Arc::new(|_| Empty::new().into_any_named("block 3")),
-//             },
-//         ]);
-
-//         let snapshot = block_map.read(wraps_snapshot, Default::default());
-//         assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
-
-//         let blocks = snapshot
-//             .blocks_in_range(0..8)
-//             .map(|(start_row, block)| {
-//                 let block = block.as_custom().unwrap();
-//                 (start_row..start_row + block.height as u32, block.id)
-//             })
-//             .collect::<Vec<_>>();
-
-//         // When multiple blocks are on the same line, the newer blocks appear first.
-//         assert_eq!(
-//             blocks,
-//             &[
-//                 (1..2, block_ids[0]),
-//                 (2..4, block_ids[1]),
-//                 (7..10, block_ids[2]),
-//             ]
-//         );
-
-//         assert_eq!(
-//             snapshot.to_block_point(WrapPoint::new(0, 3)),
-//             BlockPoint::new(0, 3)
-//         );
-//         assert_eq!(
-//             snapshot.to_block_point(WrapPoint::new(1, 0)),
-//             BlockPoint::new(4, 0)
-//         );
-//         assert_eq!(
-//             snapshot.to_block_point(WrapPoint::new(3, 3)),
-//             BlockPoint::new(6, 3)
-//         );
-
-//         assert_eq!(
-//             snapshot.to_wrap_point(BlockPoint::new(0, 3)),
-//             WrapPoint::new(0, 3)
-//         );
-//         assert_eq!(
-//             snapshot.to_wrap_point(BlockPoint::new(1, 0)),
-//             WrapPoint::new(1, 0)
-//         );
-//         assert_eq!(
-//             snapshot.to_wrap_point(BlockPoint::new(3, 0)),
-//             WrapPoint::new(1, 0)
-//         );
-//         assert_eq!(
-//             snapshot.to_wrap_point(BlockPoint::new(7, 0)),
-//             WrapPoint::new(3, 3)
-//         );
-
-//         assert_eq!(
-//             snapshot.clip_point(BlockPoint::new(1, 0), Bias::Left),
-//             BlockPoint::new(0, 3)
-//         );
-//         assert_eq!(
-//             snapshot.clip_point(BlockPoint::new(1, 0), Bias::Right),
-//             BlockPoint::new(4, 0)
-//         );
-//         assert_eq!(
-//             snapshot.clip_point(BlockPoint::new(1, 1), Bias::Left),
-//             BlockPoint::new(0, 3)
-//         );
-//         assert_eq!(
-//             snapshot.clip_point(BlockPoint::new(1, 1), Bias::Right),
-//             BlockPoint::new(4, 0)
-//         );
-//         assert_eq!(
-//             snapshot.clip_point(BlockPoint::new(4, 0), Bias::Left),
-//             BlockPoint::new(4, 0)
-//         );
-//         assert_eq!(
-//             snapshot.clip_point(BlockPoint::new(4, 0), Bias::Right),
-//             BlockPoint::new(4, 0)
-//         );
-//         assert_eq!(
-//             snapshot.clip_point(BlockPoint::new(6, 3), Bias::Left),
-//             BlockPoint::new(6, 3)
-//         );
-//         assert_eq!(
-//             snapshot.clip_point(BlockPoint::new(6, 3), Bias::Right),
-//             BlockPoint::new(6, 3)
-//         );
-//         assert_eq!(
-//             snapshot.clip_point(BlockPoint::new(7, 0), Bias::Left),
-//             BlockPoint::new(6, 3)
-//         );
-//         assert_eq!(
-//             snapshot.clip_point(BlockPoint::new(7, 0), Bias::Right),
-//             BlockPoint::new(6, 3)
-//         );
-
-//         assert_eq!(
-//             snapshot.buffer_rows(0).collect::<Vec<_>>(),
-//             &[
-//                 Some(0),
-//                 None,
-//                 None,
-//                 None,
-//                 Some(1),
-//                 Some(2),
-//                 Some(3),
-//                 None,
-//                 None,
-//                 None
-//             ]
-//         );
-
-//         // Insert a line break, separating two block decorations into separate lines.
-//         let buffer_snapshot = buffer.update(cx, |buffer, cx| {
-//             buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx);
-//             buffer.snapshot(cx)
-//         });
-
-//         let (inlay_snapshot, inlay_edits) =
-//             inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
-//         let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-//         let (tab_snapshot, tab_edits) =
-//             tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
-//         let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-//             wrap_map.sync(tab_snapshot, tab_edits, cx)
-//         });
-//         let snapshot = block_map.read(wraps_snapshot, wrap_edits);
-//         assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
-//     }
-
-//     #[gpui::test]
-//     fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) {
-//         init_test(cx);
-
-//         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 text = "one two three\nfour five six\nseven eight";
-
-//         let buffer = MultiBuffer::build_simple(text, cx);
-//         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-//         let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
-//         let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
-//         let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
-//         let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
-//         let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
-
-//         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
-//         writer.insert(vec![
-//             BlockProperties {
-//                 style: BlockStyle::Fixed,
-//                 position: buffer_snapshot.anchor_after(Point::new(1, 12)),
-//                 disposition: BlockDisposition::Above,
-//                 render: Arc::new(|_| Empty::new().into_any_named("block 1")),
-//                 height: 1,
-//             },
-//             BlockProperties {
-//                 style: BlockStyle::Fixed,
-//                 position: buffer_snapshot.anchor_after(Point::new(1, 1)),
-//                 disposition: BlockDisposition::Below,
-//                 render: Arc::new(|_| Empty::new().into_any_named("block 2")),
-//                 height: 1,
-//             },
-//         ]);
-
-//         // Blocks with an 'above' disposition go above their corresponding buffer line.
-//         // Blocks with a 'below' disposition go below their corresponding buffer line.
-//         let snapshot = block_map.read(wraps_snapshot, Default::default());
-//         assert_eq!(
-//             snapshot.text(),
-//             "one two \nthree\n\nfour five \nsix\n\nseven \neight"
-//         );
-//     }
-
-//     #[gpui::test(iterations = 100)]
-//     fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) {
-//         init_test(cx);
-
-//         let operations = env::var("OPERATIONS")
-//             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-//             .unwrap_or(10);
-
-//         let wrap_width = if rng.gen_bool(0.2) {
-//             None
-//         } else {
-//             Some(rng.gen_range(0.0..=100.0))
-//         };
-//         let tab_size = 1.try_into().unwrap();
-//         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 buffer_start_header_height = rng.gen_range(1..=5);
-//         let excerpt_header_height = rng.gen_range(1..=5);
-
-//         log::info!("Wrap width: {:?}", wrap_width);
-//         log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
-
-//         let buffer = if rng.gen() {
-//             let len = rng.gen_range(0..10);
-//             let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
-//             log::info!("initial buffer text: {:?}", text);
-//             MultiBuffer::build_simple(&text, cx)
-//         } else {
-//             MultiBuffer::build_random(&mut rng, cx)
-//         };
-
-//         let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
-//         let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
-//         let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
-//         let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
-//         let (wrap_map, wraps_snapshot) =
-//             WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx);
-//         let mut block_map = BlockMap::new(
-//             wraps_snapshot,
-//             buffer_start_header_height,
-//             excerpt_header_height,
-//         );
-//         let mut custom_blocks = Vec::new();
-
-//         for _ in 0..operations {
-//             let mut buffer_edits = Vec::new();
-//             match rng.gen_range(0..=100) {
-//                 0..=19 => {
-//                     let wrap_width = if rng.gen_bool(0.2) {
-//                         None
-//                     } else {
-//                         Some(rng.gen_range(0.0..=100.0))
-//                     };
-//                     log::info!("Setting wrap width to {:?}", wrap_width);
-//                     wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
-//                 }
-//                 20..=39 => {
-//                     let block_count = rng.gen_range(1..=5);
-//                     let block_properties = (0..block_count)
-//                         .map(|_| {
-//                             let buffer = buffer.read(cx).read(cx);
-//                             let position = buffer.anchor_after(
-//                                 buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left),
-//                             );
-
-//                             let disposition = if rng.gen() {
-//                                 BlockDisposition::Above
-//                             } else {
-//                                 BlockDisposition::Below
-//                             };
-//                             let height = rng.gen_range(1..5);
-//                             log::info!(
-//                                 "inserting block {:?} {:?} with height {}",
-//                                 disposition,
-//                                 position.to_point(&buffer),
-//                                 height
-//                             );
-//                             BlockProperties {
-//                                 style: BlockStyle::Fixed,
-//                                 position,
-//                                 height,
-//                                 disposition,
-//                                 render: Arc::new(|_| Empty::new().into_any()),
-//                             }
-//                         })
-//                         .collect::<Vec<_>>();
-
-//                     let (inlay_snapshot, inlay_edits) =
-//                         inlay_map.sync(buffer_snapshot.clone(), vec![]);
-//                     let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-//                     let (tab_snapshot, tab_edits) =
-//                         tab_map.sync(fold_snapshot, fold_edits, tab_size);
-//                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-//                         wrap_map.sync(tab_snapshot, tab_edits, cx)
-//                     });
-//                     let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
-//                     let block_ids = block_map.insert(block_properties.clone());
-//                     for (block_id, props) in block_ids.into_iter().zip(block_properties) {
-//                         custom_blocks.push((block_id, props));
-//                     }
-//                 }
-//                 40..=59 if !custom_blocks.is_empty() => {
-//                     let block_count = rng.gen_range(1..=4.min(custom_blocks.len()));
-//                     let block_ids_to_remove = (0..block_count)
-//                         .map(|_| {
-//                             custom_blocks
-//                                 .remove(rng.gen_range(0..custom_blocks.len()))
-//                                 .0
-//                         })
-//                         .collect();
-
-//                     let (inlay_snapshot, inlay_edits) =
-//                         inlay_map.sync(buffer_snapshot.clone(), vec![]);
-//                     let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-//                     let (tab_snapshot, tab_edits) =
-//                         tab_map.sync(fold_snapshot, fold_edits, tab_size);
-//                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-//                         wrap_map.sync(tab_snapshot, tab_edits, cx)
-//                     });
-//                     let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
-//                     block_map.remove(block_ids_to_remove);
-//                 }
-//                 _ => {
-//                     buffer.update(cx, |buffer, cx| {
-//                         let mutation_count = rng.gen_range(1..=5);
-//                         let subscription = buffer.subscribe();
-//                         buffer.randomly_mutate(&mut rng, mutation_count, cx);
-//                         buffer_snapshot = buffer.snapshot(cx);
-//                         buffer_edits.extend(subscription.consume());
-//                         log::info!("buffer text: {:?}", buffer_snapshot.text());
-//                     });
-//                 }
-//             }
-
-//             let (inlay_snapshot, inlay_edits) =
-//                 inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
-//             let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-//             let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
-//             let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-//                 wrap_map.sync(tab_snapshot, tab_edits, cx)
-//             });
-//             let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
-//             assert_eq!(
-//                 blocks_snapshot.transforms.summary().input_rows,
-//                 wraps_snapshot.max_point().row() + 1
-//             );
-//             log::info!("blocks text: {:?}", blocks_snapshot.text());
-
-//             let mut expected_blocks = Vec::new();
-//             expected_blocks.extend(custom_blocks.iter().map(|(id, block)| {
-//                 let mut position = block.position.to_point(&buffer_snapshot);
-//                 match block.disposition {
-//                     BlockDisposition::Above => {
-//                         position.column = 0;
-//                     }
-//                     BlockDisposition::Below => {
-//                         position.column = buffer_snapshot.line_len(position.row);
-//                     }
-//                 };
-//                 let row = wraps_snapshot.make_wrap_point(position, Bias::Left).row();
-//                 (
-//                     row,
-//                     ExpectedBlock::Custom {
-//                         disposition: block.disposition,
-//                         id: *id,
-//                         height: block.height,
-//                     },
-//                 )
-//             }));
-//             expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map(
-//                 |boundary| {
-//                     let position =
-//                         wraps_snapshot.make_wrap_point(Point::new(boundary.row, 0), Bias::Left);
-//                     (
-//                         position.row(),
-//                         ExpectedBlock::ExcerptHeader {
-//                             height: if boundary.starts_new_buffer {
-//                                 buffer_start_header_height
-//                             } else {
-//                                 excerpt_header_height
-//                             },
-//                             starts_new_buffer: boundary.starts_new_buffer,
-//                         },
-//                     )
-//                 },
-//             ));
-//             expected_blocks.sort_unstable();
-//             let mut sorted_blocks_iter = expected_blocks.into_iter().peekable();
-
-//             let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::<Vec<_>>();
-//             let mut expected_buffer_rows = Vec::new();
-//             let mut expected_text = String::new();
-//             let mut expected_block_positions = Vec::new();
-//             let input_text = wraps_snapshot.text();
-//             for (row, input_line) in input_text.split('\n').enumerate() {
-//                 let row = row as u32;
-//                 if row > 0 {
-//                     expected_text.push('\n');
-//                 }
-
-//                 let buffer_row = input_buffer_rows[wraps_snapshot
-//                     .to_point(WrapPoint::new(row, 0), Bias::Left)
-//                     .row as usize];
-
-//                 while let Some((block_row, block)) = sorted_blocks_iter.peek() {
-//                     if *block_row == row && block.disposition() == BlockDisposition::Above {
-//                         let (_, block) = sorted_blocks_iter.next().unwrap();
-//                         let height = block.height() as usize;
-//                         expected_block_positions
-//                             .push((expected_text.matches('\n').count() as u32, block));
-//                         let text = "\n".repeat(height);
-//                         expected_text.push_str(&text);
-//                         for _ in 0..height {
-//                             expected_buffer_rows.push(None);
-//                         }
-//                     } else {
-//                         break;
-//                     }
-//                 }
-
-//                 let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0;
-//                 expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row });
-//                 expected_text.push_str(input_line);
-
-//                 while let Some((block_row, block)) = sorted_blocks_iter.peek() {
-//                     if *block_row == row && block.disposition() == BlockDisposition::Below {
-//                         let (_, block) = sorted_blocks_iter.next().unwrap();
-//                         let height = block.height() as usize;
-//                         expected_block_positions
-//                             .push((expected_text.matches('\n').count() as u32 + 1, block));
-//                         let text = "\n".repeat(height);
-//                         expected_text.push_str(&text);
-//                         for _ in 0..height {
-//                             expected_buffer_rows.push(None);
-//                         }
-//                     } else {
-//                         break;
-//                     }
-//                 }
-//             }
-
-//             let expected_lines = expected_text.split('\n').collect::<Vec<_>>();
-//             let expected_row_count = expected_lines.len();
-//             for start_row in 0..expected_row_count {
-//                 let expected_text = expected_lines[start_row..].join("\n");
-//                 let actual_text = blocks_snapshot
-//                     .chunks(
-//                         start_row as u32..blocks_snapshot.max_point().row + 1,
-//                         false,
-//                         Highlights::default(),
-//                     )
-//                     .map(|chunk| chunk.text)
-//                     .collect::<String>();
-//                 assert_eq!(
-//                     actual_text, expected_text,
-//                     "incorrect text starting from row {}",
-//                     start_row
-//                 );
-//                 assert_eq!(
-//                     blocks_snapshot
-//                         .buffer_rows(start_row as u32)
-//                         .collect::<Vec<_>>(),
-//                     &expected_buffer_rows[start_row..]
-//                 );
-//             }
-
-//             assert_eq!(
-//                 blocks_snapshot
-//                     .blocks_in_range(0..(expected_row_count as u32))
-//                     .map(|(row, block)| (row, block.clone().into()))
-//                     .collect::<Vec<_>>(),
-//                 expected_block_positions
-//             );
-
-//             let mut expected_longest_rows = Vec::new();
-//             let mut longest_line_len = -1_isize;
-//             for (row, line) in expected_lines.iter().enumerate() {
-//                 let row = row as u32;
-
-//                 assert_eq!(
-//                     blocks_snapshot.line_len(row),
-//                     line.len() as u32,
-//                     "invalid line len for row {}",
-//                     row
-//                 );
-
-//                 let line_char_count = line.chars().count() as isize;
-//                 match line_char_count.cmp(&longest_line_len) {
-//                     Ordering::Less => {}
-//                     Ordering::Equal => expected_longest_rows.push(row),
-//                     Ordering::Greater => {
-//                         longest_line_len = line_char_count;
-//                         expected_longest_rows.clear();
-//                         expected_longest_rows.push(row);
-//                     }
-//                 }
-//             }
-
-//             let longest_row = blocks_snapshot.longest_row();
-//             assert!(
-//                 expected_longest_rows.contains(&longest_row),
-//                 "incorrect longest row {}. expected {:?} with length {}",
-//                 longest_row,
-//                 expected_longest_rows,
-//                 longest_line_len,
-//             );
-
-//             for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
-//                 let wrap_point = WrapPoint::new(row, 0);
-//                 let block_point = blocks_snapshot.to_block_point(wrap_point);
-//                 assert_eq!(blocks_snapshot.to_wrap_point(block_point), wrap_point);
-//             }
-
-//             let mut block_point = BlockPoint::new(0, 0);
-//             for c in expected_text.chars() {
-//                 let left_point = blocks_snapshot.clip_point(block_point, Bias::Left);
-//                 let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left);
-//                 assert_eq!(
-//                     blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)),
-//                     left_point
-//                 );
-//                 assert_eq!(
-//                     left_buffer_point,
-//                     buffer_snapshot.clip_point(left_buffer_point, Bias::Right),
-//                     "{:?} is not valid in buffer coordinates",
-//                     left_point
-//                 );
-
-//                 let right_point = blocks_snapshot.clip_point(block_point, Bias::Right);
-//                 let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right);
-//                 assert_eq!(
-//                     blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)),
-//                     right_point
-//                 );
-//                 assert_eq!(
-//                     right_buffer_point,
-//                     buffer_snapshot.clip_point(right_buffer_point, Bias::Left),
-//                     "{:?} is not valid in buffer coordinates",
-//                     right_point
-//                 );
-
-//                 if c == '\n' {
-//                     block_point.0 += Point::new(1, 0);
-//                 } else {
-//                     block_point.column += c.len_utf8() as u32;
-//                 }
-//             }
-//         }
-
-//         #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
-//         enum ExpectedBlock {
-//             ExcerptHeader {
-//                 height: u8,
-//                 starts_new_buffer: bool,
-//             },
-//             Custom {
-//                 disposition: BlockDisposition,
-//                 id: BlockId,
-//                 height: u8,
-//             },
-//         }
-
-//         impl ExpectedBlock {
-//             fn height(&self) -> u8 {
-//                 match self {
-//                     ExpectedBlock::ExcerptHeader { height, .. } => *height,
-//                     ExpectedBlock::Custom { height, .. } => *height,
-//                 }
-//             }
-
-//             fn disposition(&self) -> BlockDisposition {
-//                 match self {
-//                     ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above,
-//                     ExpectedBlock::Custom { disposition, .. } => *disposition,
-//                 }
-//             }
-//         }
-
-//         impl From<TransformBlock> for ExpectedBlock {
-//             fn from(block: TransformBlock) -> Self {
-//                 match block {
-//                     TransformBlock::Custom(block) => ExpectedBlock::Custom {
-//                         id: block.id,
-//                         disposition: block.disposition,
-//                         height: block.height,
-//                     },
-//                     TransformBlock::ExcerptHeader {
-//                         height,
-//                         starts_new_buffer,
-//                         ..
-//                     } => ExpectedBlock::ExcerptHeader {
-//                         height,
-//                         starts_new_buffer,
-//                     },
-//                 }
-//             }
-//         }
-//     }
-
-//     fn init_test(cx: &mut gpui::AppContext) {
-//         cx.set_global(SettingsStore::test(cx));
-//         theme::init(cx);
-//     }
-
-//     impl TransformBlock {
-//         fn as_custom(&self) -> Option<&Block> {
-//             match self {
-//                 TransformBlock::Custom(block) => Some(block),
-//                 TransformBlock::ExcerptHeader { .. } => None,
-//             }
-//         }
-//     }
-
-//     impl BlockSnapshot {
-//         fn to_point(&self, point: BlockPoint, bias: Bias) -> Point {
-//             self.wrap_snapshot.to_point(self.to_wrap_point(point), bias)
-//         }
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::display_map::inlay_map::InlayMap;
+    use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
+    use gpui::{div, font, px, Element, Platform as _};
+    use multi_buffer::MultiBuffer;
+    use rand::prelude::*;
+    use settings::SettingsStore;
+    use std::env;
+    use util::RandomCharIter;
+
+    #[gpui::test]
+    fn test_offset_for_row() {
+        assert_eq!(offset_for_row("", 0), (0, 0));
+        assert_eq!(offset_for_row("", 1), (0, 0));
+        assert_eq!(offset_for_row("abcd", 0), (0, 0));
+        assert_eq!(offset_for_row("abcd", 1), (0, 4));
+        assert_eq!(offset_for_row("\n", 0), (0, 0));
+        assert_eq!(offset_for_row("\n", 1), (1, 1));
+        assert_eq!(offset_for_row("abc\ndef\nghi", 0), (0, 0));
+        assert_eq!(offset_for_row("abc\ndef\nghi", 1), (1, 4));
+        assert_eq!(offset_for_row("abc\ndef\nghi", 2), (2, 8));
+        assert_eq!(offset_for_row("abc\ndef\nghi", 3), (2, 11));
+    }
+
+    #[gpui::test]
+    fn test_basic_blocks(cx: &mut gpui::TestAppContext) {
+        cx.update(|cx| init_test(cx));
+
+        let text = "aaa\nbbb\nccc\nddd";
+
+        let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
+        let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
+        let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
+        let (wrap_map, wraps_snapshot) =
+            cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
+        let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
+
+        let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
+        let block_ids = writer.insert(vec![
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(1, 0)),
+                height: 1,
+                disposition: BlockDisposition::Above,
+                render: Arc::new(|_| div().into_any()),
+            },
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(1, 2)),
+                height: 2,
+                disposition: BlockDisposition::Above,
+                render: Arc::new(|_| div().into_any()),
+            },
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(3, 3)),
+                height: 3,
+                disposition: BlockDisposition::Below,
+                render: Arc::new(|_| div().into_any()),
+            },
+        ]);
+
+        let snapshot = block_map.read(wraps_snapshot, Default::default());
+        assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
+
+        let blocks = snapshot
+            .blocks_in_range(0..8)
+            .map(|(start_row, block)| {
+                let block = block.as_custom().unwrap();
+                (start_row..start_row + block.height as u32, block.id)
+            })
+            .collect::<Vec<_>>();
+
+        // When multiple blocks are on the same line, the newer blocks appear first.
+        assert_eq!(
+            blocks,
+            &[
+                (1..2, block_ids[0]),
+                (2..4, block_ids[1]),
+                (7..10, block_ids[2]),
+            ]
+        );
+
+        assert_eq!(
+            snapshot.to_block_point(WrapPoint::new(0, 3)),
+            BlockPoint::new(0, 3)
+        );
+        assert_eq!(
+            snapshot.to_block_point(WrapPoint::new(1, 0)),
+            BlockPoint::new(4, 0)
+        );
+        assert_eq!(
+            snapshot.to_block_point(WrapPoint::new(3, 3)),
+            BlockPoint::new(6, 3)
+        );
+
+        assert_eq!(
+            snapshot.to_wrap_point(BlockPoint::new(0, 3)),
+            WrapPoint::new(0, 3)
+        );
+        assert_eq!(
+            snapshot.to_wrap_point(BlockPoint::new(1, 0)),
+            WrapPoint::new(1, 0)
+        );
+        assert_eq!(
+            snapshot.to_wrap_point(BlockPoint::new(3, 0)),
+            WrapPoint::new(1, 0)
+        );
+        assert_eq!(
+            snapshot.to_wrap_point(BlockPoint::new(7, 0)),
+            WrapPoint::new(3, 3)
+        );
+
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(1, 0), Bias::Left),
+            BlockPoint::new(0, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(1, 0), Bias::Right),
+            BlockPoint::new(4, 0)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(1, 1), Bias::Left),
+            BlockPoint::new(0, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(1, 1), Bias::Right),
+            BlockPoint::new(4, 0)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(4, 0), Bias::Left),
+            BlockPoint::new(4, 0)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(4, 0), Bias::Right),
+            BlockPoint::new(4, 0)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(6, 3), Bias::Left),
+            BlockPoint::new(6, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(6, 3), Bias::Right),
+            BlockPoint::new(6, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(7, 0), Bias::Left),
+            BlockPoint::new(6, 3)
+        );
+        assert_eq!(
+            snapshot.clip_point(BlockPoint::new(7, 0), Bias::Right),
+            BlockPoint::new(6, 3)
+        );
+
+        assert_eq!(
+            snapshot.buffer_rows(0).collect::<Vec<_>>(),
+            &[
+                Some(0),
+                None,
+                None,
+                None,
+                Some(1),
+                Some(2),
+                Some(3),
+                None,
+                None,
+                None
+            ]
+        );
+
+        // Insert a line break, separating two block decorations into separate lines.
+        let buffer_snapshot = buffer.update(cx, |buffer, cx| {
+            buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx);
+            buffer.snapshot(cx)
+        });
+
+        let (inlay_snapshot, inlay_edits) =
+            inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+        let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+        let (tab_snapshot, tab_edits) =
+            tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
+        let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+            wrap_map.sync(tab_snapshot, tab_edits, cx)
+        });
+        let snapshot = block_map.read(wraps_snapshot, wrap_edits);
+        assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
+    }
+
+    #[gpui::test]
+    fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) {
+        cx.update(|cx| init_test(cx));
+
+        let font_id = cx
+            .test_platform
+            .text_system()
+            .font_id(&font("Helvetica"))
+            .unwrap();
+
+        let text = "one two three\nfour five six\nseven eight";
+
+        let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
+        let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (_, wraps_snapshot) = cx.update(|cx| {
+            WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx)
+        });
+        let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
+
+        let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
+        writer.insert(vec![
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(1, 12)),
+                disposition: BlockDisposition::Above,
+                render: Arc::new(|_| div().into_any()),
+                height: 1,
+            },
+            BlockProperties {
+                style: BlockStyle::Fixed,
+                position: buffer_snapshot.anchor_after(Point::new(1, 1)),
+                disposition: BlockDisposition::Below,
+                render: Arc::new(|_| div().into_any()),
+                height: 1,
+            },
+        ]);
+
+        // Blocks with an 'above' disposition go above their corresponding buffer line.
+        // Blocks with a 'below' disposition go below their corresponding buffer line.
+        let snapshot = block_map.read(wraps_snapshot, Default::default());
+        assert_eq!(
+            snapshot.text(),
+            "one two \nthree\n\nfour five \nsix\n\nseven \neight"
+        );
+    }
+
+    #[gpui::test(iterations = 100)]
+    fn test_random_blocks(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+        cx.update(|cx| init_test(cx));
+
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let wrap_width = if rng.gen_bool(0.2) {
+            None
+        } else {
+            Some(px(rng.gen_range(0.0..=100.0)))
+        };
+        let tab_size = 1.try_into().unwrap();
+        let font_size = px(14.0);
+        let buffer_start_header_height = rng.gen_range(1..=5);
+        let excerpt_header_height = rng.gen_range(1..=5);
+
+        log::info!("Wrap width: {:?}", wrap_width);
+        log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
+
+        let buffer = if rng.gen() {
+            let len = rng.gen_range(0..10);
+            let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+            log::info!("initial buffer text: {:?}", text);
+            cx.update(|cx| MultiBuffer::build_simple(&text, cx))
+        } else {
+            cx.update(|cx| MultiBuffer::build_random(&mut rng, cx))
+        };
+
+        let mut buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+        let (wrap_map, wraps_snapshot) = cx
+            .update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), font_size, wrap_width, cx));
+        let mut block_map = BlockMap::new(
+            wraps_snapshot,
+            buffer_start_header_height,
+            excerpt_header_height,
+        );
+        let mut custom_blocks = Vec::new();
+
+        for _ in 0..operations {
+            let mut buffer_edits = Vec::new();
+            match rng.gen_range(0..=100) {
+                0..=19 => {
+                    let wrap_width = if rng.gen_bool(0.2) {
+                        None
+                    } else {
+                        Some(px(rng.gen_range(0.0..=100.0)))
+                    };
+                    log::info!("Setting wrap width to {:?}", wrap_width);
+                    wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+                }
+                20..=39 => {
+                    let block_count = rng.gen_range(1..=5);
+                    let block_properties = (0..block_count)
+                        .map(|_| {
+                            let buffer = cx.update(|cx| buffer.read(cx).read(cx).clone());
+                            let position = buffer.anchor_after(
+                                buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left),
+                            );
+
+                            let disposition = if rng.gen() {
+                                BlockDisposition::Above
+                            } else {
+                                BlockDisposition::Below
+                            };
+                            let height = rng.gen_range(1..5);
+                            log::info!(
+                                "inserting block {:?} {:?} with height {}",
+                                disposition,
+                                position.to_point(&buffer),
+                                height
+                            );
+                            BlockProperties {
+                                style: BlockStyle::Fixed,
+                                position,
+                                height,
+                                disposition,
+                                render: Arc::new(|_| div().into_any()),
+                            }
+                        })
+                        .collect::<Vec<_>>();
+
+                    let (inlay_snapshot, inlay_edits) =
+                        inlay_map.sync(buffer_snapshot.clone(), vec![]);
+                    let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+                    let (tab_snapshot, tab_edits) =
+                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
+                    let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+                        wrap_map.sync(tab_snapshot, tab_edits, cx)
+                    });
+                    let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
+                    let block_ids = block_map.insert(block_properties.clone());
+                    for (block_id, props) in block_ids.into_iter().zip(block_properties) {
+                        custom_blocks.push((block_id, props));
+                    }
+                }
+                40..=59 if !custom_blocks.is_empty() => {
+                    let block_count = rng.gen_range(1..=4.min(custom_blocks.len()));
+                    let block_ids_to_remove = (0..block_count)
+                        .map(|_| {
+                            custom_blocks
+                                .remove(rng.gen_range(0..custom_blocks.len()))
+                                .0
+                        })
+                        .collect();
+
+                    let (inlay_snapshot, inlay_edits) =
+                        inlay_map.sync(buffer_snapshot.clone(), vec![]);
+                    let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+                    let (tab_snapshot, tab_edits) =
+                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
+                    let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+                        wrap_map.sync(tab_snapshot, tab_edits, cx)
+                    });
+                    let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
+                    block_map.remove(block_ids_to_remove);
+                }
+                _ => {
+                    buffer.update(cx, |buffer, cx| {
+                        let mutation_count = rng.gen_range(1..=5);
+                        let subscription = buffer.subscribe();
+                        buffer.randomly_mutate(&mut rng, mutation_count, cx);
+                        buffer_snapshot = buffer.snapshot(cx);
+                        buffer_edits.extend(subscription.consume());
+                        log::info!("buffer text: {:?}", buffer_snapshot.text());
+                    });
+                }
+            }
+
+            let (inlay_snapshot, inlay_edits) =
+                inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+            let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+            let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
+            let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+                wrap_map.sync(tab_snapshot, tab_edits, cx)
+            });
+            let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
+            assert_eq!(
+                blocks_snapshot.transforms.summary().input_rows,
+                wraps_snapshot.max_point().row() + 1
+            );
+            log::info!("blocks text: {:?}", blocks_snapshot.text());
+
+            let mut expected_blocks = Vec::new();
+            expected_blocks.extend(custom_blocks.iter().map(|(id, block)| {
+                let mut position = block.position.to_point(&buffer_snapshot);
+                match block.disposition {
+                    BlockDisposition::Above => {
+                        position.column = 0;
+                    }
+                    BlockDisposition::Below => {
+                        position.column = buffer_snapshot.line_len(position.row);
+                    }
+                };
+                let row = wraps_snapshot.make_wrap_point(position, Bias::Left).row();
+                (
+                    row,
+                    ExpectedBlock::Custom {
+                        disposition: block.disposition,
+                        id: *id,
+                        height: block.height,
+                    },
+                )
+            }));
+            expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map(
+                |boundary| {
+                    let position =
+                        wraps_snapshot.make_wrap_point(Point::new(boundary.row, 0), Bias::Left);
+                    (
+                        position.row(),
+                        ExpectedBlock::ExcerptHeader {
+                            height: if boundary.starts_new_buffer {
+                                buffer_start_header_height
+                            } else {
+                                excerpt_header_height
+                            },
+                            starts_new_buffer: boundary.starts_new_buffer,
+                        },
+                    )
+                },
+            ));
+            expected_blocks.sort_unstable();
+            let mut sorted_blocks_iter = expected_blocks.into_iter().peekable();
+
+            let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::<Vec<_>>();
+            let mut expected_buffer_rows = Vec::new();
+            let mut expected_text = String::new();
+            let mut expected_block_positions = Vec::new();
+            let input_text = wraps_snapshot.text();
+            for (row, input_line) in input_text.split('\n').enumerate() {
+                let row = row as u32;
+                if row > 0 {
+                    expected_text.push('\n');
+                }
+
+                let buffer_row = input_buffer_rows[wraps_snapshot
+                    .to_point(WrapPoint::new(row, 0), Bias::Left)
+                    .row as usize];
+
+                while let Some((block_row, block)) = sorted_blocks_iter.peek() {
+                    if *block_row == row && block.disposition() == BlockDisposition::Above {
+                        let (_, block) = sorted_blocks_iter.next().unwrap();
+                        let height = block.height() as usize;
+                        expected_block_positions
+                            .push((expected_text.matches('\n').count() as u32, block));
+                        let text = "\n".repeat(height);
+                        expected_text.push_str(&text);
+                        for _ in 0..height {
+                            expected_buffer_rows.push(None);
+                        }
+                    } else {
+                        break;
+                    }
+                }
+
+                let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0;
+                expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row });
+                expected_text.push_str(input_line);
+
+                while let Some((block_row, block)) = sorted_blocks_iter.peek() {
+                    if *block_row == row && block.disposition() == BlockDisposition::Below {
+                        let (_, block) = sorted_blocks_iter.next().unwrap();
+                        let height = block.height() as usize;
+                        expected_block_positions
+                            .push((expected_text.matches('\n').count() as u32 + 1, block));
+                        let text = "\n".repeat(height);
+                        expected_text.push_str(&text);
+                        for _ in 0..height {
+                            expected_buffer_rows.push(None);
+                        }
+                    } else {
+                        break;
+                    }
+                }
+            }
+
+            let expected_lines = expected_text.split('\n').collect::<Vec<_>>();
+            let expected_row_count = expected_lines.len();
+            for start_row in 0..expected_row_count {
+                let expected_text = expected_lines[start_row..].join("\n");
+                let actual_text = blocks_snapshot
+                    .chunks(
+                        start_row as u32..blocks_snapshot.max_point().row + 1,
+                        false,
+                        Highlights::default(),
+                    )
+                    .map(|chunk| chunk.text)
+                    .collect::<String>();
+                assert_eq!(
+                    actual_text, expected_text,
+                    "incorrect text starting from row {}",
+                    start_row
+                );
+                assert_eq!(
+                    blocks_snapshot
+                        .buffer_rows(start_row as u32)
+                        .collect::<Vec<_>>(),
+                    &expected_buffer_rows[start_row..]
+                );
+            }
+
+            assert_eq!(
+                blocks_snapshot
+                    .blocks_in_range(0..(expected_row_count as u32))
+                    .map(|(row, block)| (row, block.clone().into()))
+                    .collect::<Vec<_>>(),
+                expected_block_positions
+            );
+
+            let mut expected_longest_rows = Vec::new();
+            let mut longest_line_len = -1_isize;
+            for (row, line) in expected_lines.iter().enumerate() {
+                let row = row as u32;
+
+                assert_eq!(
+                    blocks_snapshot.line_len(row),
+                    line.len() as u32,
+                    "invalid line len for row {}",
+                    row
+                );
+
+                let line_char_count = line.chars().count() as isize;
+                match line_char_count.cmp(&longest_line_len) {
+                    Ordering::Less => {}
+                    Ordering::Equal => expected_longest_rows.push(row),
+                    Ordering::Greater => {
+                        longest_line_len = line_char_count;
+                        expected_longest_rows.clear();
+                        expected_longest_rows.push(row);
+                    }
+                }
+            }
+
+            let longest_row = blocks_snapshot.longest_row();
+            assert!(
+                expected_longest_rows.contains(&longest_row),
+                "incorrect longest row {}. expected {:?} with length {}",
+                longest_row,
+                expected_longest_rows,
+                longest_line_len,
+            );
+
+            for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
+                let wrap_point = WrapPoint::new(row, 0);
+                let block_point = blocks_snapshot.to_block_point(wrap_point);
+                assert_eq!(blocks_snapshot.to_wrap_point(block_point), wrap_point);
+            }
+
+            let mut block_point = BlockPoint::new(0, 0);
+            for c in expected_text.chars() {
+                let left_point = blocks_snapshot.clip_point(block_point, Bias::Left);
+                let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left);
+                assert_eq!(
+                    blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)),
+                    left_point
+                );
+                assert_eq!(
+                    left_buffer_point,
+                    buffer_snapshot.clip_point(left_buffer_point, Bias::Right),
+                    "{:?} is not valid in buffer coordinates",
+                    left_point
+                );
+
+                let right_point = blocks_snapshot.clip_point(block_point, Bias::Right);
+                let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right);
+                assert_eq!(
+                    blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)),
+                    right_point
+                );
+                assert_eq!(
+                    right_buffer_point,
+                    buffer_snapshot.clip_point(right_buffer_point, Bias::Left),
+                    "{:?} is not valid in buffer coordinates",
+                    right_point
+                );
+
+                if c == '\n' {
+                    block_point.0 += Point::new(1, 0);
+                } else {
+                    block_point.column += c.len_utf8() as u32;
+                }
+            }
+        }
+
+        #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
+        enum ExpectedBlock {
+            ExcerptHeader {
+                height: u8,
+                starts_new_buffer: bool,
+            },
+            Custom {
+                disposition: BlockDisposition,
+                id: BlockId,
+                height: u8,
+            },
+        }
+
+        impl ExpectedBlock {
+            fn height(&self) -> u8 {
+                match self {
+                    ExpectedBlock::ExcerptHeader { height, .. } => *height,
+                    ExpectedBlock::Custom { height, .. } => *height,
+                }
+            }
+
+            fn disposition(&self) -> BlockDisposition {
+                match self {
+                    ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above,
+                    ExpectedBlock::Custom { disposition, .. } => *disposition,
+                }
+            }
+        }
+
+        impl From<TransformBlock> for ExpectedBlock {
+            fn from(block: TransformBlock) -> Self {
+                match block {
+                    TransformBlock::Custom(block) => ExpectedBlock::Custom {
+                        id: block.id,
+                        disposition: block.disposition,
+                        height: block.height,
+                    },
+                    TransformBlock::ExcerptHeader {
+                        height,
+                        starts_new_buffer,
+                        ..
+                    } => ExpectedBlock::ExcerptHeader {
+                        height,
+                        starts_new_buffer,
+                    },
+                }
+            }
+        }
+    }
+
+    fn init_test(cx: &mut gpui::AppContext) {
+        let settings = SettingsStore::test(cx);
+        cx.set_global(settings);
+        theme::init(theme::LoadThemes::JustBase, cx);
+    }
+
+    impl TransformBlock {
+        fn as_custom(&self) -> Option<&Block> {
+            match self {
+                TransformBlock::Custom(block) => Some(block),
+                TransformBlock::ExcerptHeader { .. } => None,
+            }
+        }
+    }
+
+    impl BlockSnapshot {
+        fn to_point(&self, point: BlockPoint, bias: Bias) -> Point {
+            self.wrap_snapshot.to_point(self.to_wrap_point(point), bias)
+        }
+    }
+}

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

@@ -162,7 +162,7 @@ impl WrapMap {
                 {
                     let tab_snapshot = new_snapshot.tab_snapshot.clone();
                     let range = TabPoint::zero()..tab_snapshot.max_point();
-                    let edits = new_snapshot
+                    edits = new_snapshot
                         .update(
                             tab_snapshot,
                             &[TabEdit {
@@ -741,49 +741,48 @@ impl WrapSnapshot {
     }
 
     fn check_invariants(&self) {
-        // todo!()
-        // #[cfg(test)]
-        // {
-        //     assert_eq!(
-        //         TabPoint::from(self.transforms.summary().input.lines),
-        //         self.tab_snapshot.max_point()
-        //     );
-
-        //     {
-        //         let mut transforms = self.transforms.cursor::<()>().peekable();
-        //         while let Some(transform) = transforms.next() {
-        //             if let Some(next_transform) = transforms.peek() {
-        //                 assert!(transform.is_isomorphic() != next_transform.is_isomorphic());
-        //             }
-        //         }
-        //     }
-
-        //     let text = language::Rope::from(self.text().as_str());
-        //     let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
-        //     let mut expected_buffer_rows = Vec::new();
-        //     let mut prev_tab_row = 0;
-        //     for display_row in 0..=self.max_point().row() {
-        //         let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
-        //         if tab_point.row() == prev_tab_row && display_row != 0 {
-        //             expected_buffer_rows.push(None);
-        //         } else {
-        //             expected_buffer_rows.push(input_buffer_rows.next().unwrap());
-        //         }
-
-        //         prev_tab_row = tab_point.row();
-        //         assert_eq!(self.line_len(display_row), text.line_len(display_row));
-        //     }
-
-        //     for start_display_row in 0..expected_buffer_rows.len() {
-        //         assert_eq!(
-        //             self.buffer_rows(start_display_row as u32)
-        //                 .collect::<Vec<_>>(),
-        //             &expected_buffer_rows[start_display_row..],
-        //             "invalid buffer_rows({}..)",
-        //             start_display_row
-        //         );
-        //     }
-        // }
+        #[cfg(test)]
+        {
+            assert_eq!(
+                TabPoint::from(self.transforms.summary().input.lines),
+                self.tab_snapshot.max_point()
+            );
+
+            {
+                let mut transforms = self.transforms.cursor::<()>().peekable();
+                while let Some(transform) = transforms.next() {
+                    if let Some(next_transform) = transforms.peek() {
+                        assert!(transform.is_isomorphic() != next_transform.is_isomorphic());
+                    }
+                }
+            }
+
+            let text = language::Rope::from(self.text().as_str());
+            let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
+            let mut expected_buffer_rows = Vec::new();
+            let mut prev_tab_row = 0;
+            for display_row in 0..=self.max_point().row() {
+                let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
+                if tab_point.row() == prev_tab_row && display_row != 0 {
+                    expected_buffer_rows.push(None);
+                } else {
+                    expected_buffer_rows.push(input_buffer_rows.next().unwrap());
+                }
+
+                prev_tab_row = tab_point.row();
+                assert_eq!(self.line_len(display_row), text.line_len(display_row));
+            }
+
+            for start_display_row in 0..expected_buffer_rows.len() {
+                assert_eq!(
+                    self.buffer_rows(start_display_row as u32)
+                        .collect::<Vec<_>>(),
+                    &expected_buffer_rows[start_display_row..],
+                    "invalid buffer_rows({}..)",
+                    start_display_row
+                );
+            }
+        }
     }
 }
 
@@ -1026,337 +1025,334 @@ fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use crate::{
-//         display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
-//         MultiBuffer,
-//     };
-//     use gpui::test::observe;
-//     use rand::prelude::*;
-//     use settings::SettingsStore;
-//     use smol::stream::StreamExt;
-//     use std::{cmp, env, num::NonZeroU32};
-//     use text::Rope;
-
-//     #[gpui::test(iterations = 100)]
-//     async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
-//         init_test(cx);
-
-//         cx.foreground().set_block_on_ticks(0..=50);
-//         let operations = env::var("OPERATIONS")
-//             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-//             .unwrap_or(10);
-
-//         let font_cache = cx.font_cache().clone();
-//         let font_system = cx.platform().fonts();
-//         let mut wrap_width = if rng.gen_bool(0.1) {
-//             None
-//         } else {
-//             Some(rng.gen_range(0.0..=1000.0))
-//         };
-//         let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
-//         let family_id = font_cache
-//             .load_family(&["Helvetica"], &Default::default())
-//             .unwrap();
-//         let font_id = font_cache
-//             .select_font(family_id, &Default::default())
-//             .unwrap();
-//         let font_size = 14.0;
-
-//         log::info!("Tab size: {}", tab_size);
-//         log::info!("Wrap width: {:?}", wrap_width);
-
-//         let buffer = cx.update(|cx| {
-//             if rng.gen() {
-//                 MultiBuffer::build_random(&mut rng, cx)
-//             } else {
-//                 let len = rng.gen_range(0..10);
-//                 let text = util::RandomCharIter::new(&mut rng)
-//                     .take(len)
-//                     .collect::<String>();
-//                 MultiBuffer::build_simple(&text, cx)
-//             }
-//         });
-//         let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
-//         log::info!("Buffer text: {:?}", buffer_snapshot.text());
-//         let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
-//         log::info!("InlayMap text: {:?}", inlay_snapshot.text());
-//         let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
-//         log::info!("FoldMap text: {:?}", fold_snapshot.text());
-//         let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
-//         let tabs_snapshot = tab_map.set_max_expansion_column(32);
-//         log::info!("TabMap text: {:?}", tabs_snapshot.text());
-
-//         let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);
-//         let unwrapped_text = tabs_snapshot.text();
-//         let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
-
-//         let (wrap_map, _) =
-//             cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx));
-//         let mut notifications = observe(&wrap_map, cx);
-
-//         if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
-//             notifications.next().await.unwrap();
-//         }
-
-//         let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| {
-//             assert!(!map.is_rewrapping());
-//             map.sync(tabs_snapshot.clone(), Vec::new(), cx)
-//         });
-
-//         let actual_text = initial_snapshot.text();
-//         assert_eq!(
-//             actual_text, expected_text,
-//             "unwrapped text is: {:?}",
-//             unwrapped_text
-//         );
-//         log::info!("Wrapped text: {:?}", actual_text);
-
-//         let mut next_inlay_id = 0;
-//         let mut edits = Vec::new();
-//         for _i in 0..operations {
-//             log::info!("{} ==============================================", _i);
-
-//             let mut buffer_edits = Vec::new();
-//             match rng.gen_range(0..=100) {
-//                 0..=19 => {
-//                     wrap_width = if rng.gen_bool(0.2) {
-//                         None
-//                     } else {
-//                         Some(rng.gen_range(0.0..=1000.0))
-//                     };
-//                     log::info!("Setting wrap width to {:?}", wrap_width);
-//                     wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
-//                 }
-//                 20..=39 => {
-//                     for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
-//                         let (tabs_snapshot, tab_edits) =
-//                             tab_map.sync(fold_snapshot, fold_edits, tab_size);
-//                         let (mut snapshot, wrap_edits) =
-//                             wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
-//                         snapshot.check_invariants();
-//                         snapshot.verify_chunks(&mut rng);
-//                         edits.push((snapshot, wrap_edits));
-//                     }
-//                 }
-//                 40..=59 => {
-//                     let (inlay_snapshot, inlay_edits) =
-//                         inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
-//                     let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-//                     let (tabs_snapshot, tab_edits) =
-//                         tab_map.sync(fold_snapshot, fold_edits, tab_size);
-//                     let (mut snapshot, wrap_edits) =
-//                         wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
-//                     snapshot.check_invariants();
-//                     snapshot.verify_chunks(&mut rng);
-//                     edits.push((snapshot, wrap_edits));
-//                 }
-//                 _ => {
-//                     buffer.update(cx, |buffer, cx| {
-//                         let subscription = buffer.subscribe();
-//                         let edit_count = rng.gen_range(1..=5);
-//                         buffer.randomly_mutate(&mut rng, edit_count, cx);
-//                         buffer_snapshot = buffer.snapshot(cx);
-//                         buffer_edits.extend(subscription.consume());
-//                     });
-//                 }
-//             }
-
-//             log::info!("Buffer text: {:?}", buffer_snapshot.text());
-//             let (inlay_snapshot, inlay_edits) =
-//                 inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
-//             log::info!("InlayMap text: {:?}", inlay_snapshot.text());
-//             let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-//             log::info!("FoldMap text: {:?}", fold_snapshot.text());
-//             let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
-//             log::info!("TabMap text: {:?}", tabs_snapshot.text());
-
-//             let unwrapped_text = tabs_snapshot.text();
-//             let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
-//             let (mut snapshot, wrap_edits) =
-//                 wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx));
-//             snapshot.check_invariants();
-//             snapshot.verify_chunks(&mut rng);
-//             edits.push((snapshot, wrap_edits));
-
-//             if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
-//                 log::info!("Waiting for wrapping to finish");
-//                 while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
-//                     notifications.next().await.unwrap();
-//                 }
-//                 wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
-//             }
-
-//             if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
-//                 let (mut wrapped_snapshot, wrap_edits) =
-//                     wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
-//                 let actual_text = wrapped_snapshot.text();
-//                 let actual_longest_row = wrapped_snapshot.longest_row();
-//                 log::info!("Wrapping finished: {:?}", actual_text);
-//                 wrapped_snapshot.check_invariants();
-//                 wrapped_snapshot.verify_chunks(&mut rng);
-//                 edits.push((wrapped_snapshot.clone(), wrap_edits));
-//                 assert_eq!(
-//                     actual_text, expected_text,
-//                     "unwrapped text is: {:?}",
-//                     unwrapped_text
-//                 );
-
-//                 let mut summary = TextSummary::default();
-//                 for (ix, item) in wrapped_snapshot
-//                     .transforms
-//                     .items(&())
-//                     .into_iter()
-//                     .enumerate()
-//                 {
-//                     summary += &item.summary.output;
-//                     log::info!("{} summary: {:?}", ix, item.summary.output,);
-//                 }
-
-//                 if tab_size.get() == 1
-//                     || !wrapped_snapshot
-//                         .tab_snapshot
-//                         .fold_snapshot
-//                         .text()
-//                         .contains('\t')
-//                 {
-//                     let mut expected_longest_rows = Vec::new();
-//                     let mut longest_line_len = -1;
-//                     for (row, line) in expected_text.split('\n').enumerate() {
-//                         let line_char_count = line.chars().count() as isize;
-//                         if line_char_count > longest_line_len {
-//                             expected_longest_rows.clear();
-//                             longest_line_len = line_char_count;
-//                         }
-//                         if line_char_count >= longest_line_len {
-//                             expected_longest_rows.push(row as u32);
-//                         }
-//                     }
-
-//                     assert!(
-//                         expected_longest_rows.contains(&actual_longest_row),
-//                         "incorrect longest row {}. expected {:?} with length {}",
-//                         actual_longest_row,
-//                         expected_longest_rows,
-//                         longest_line_len,
-//                     )
-//                 }
-//             }
-//         }
-
-//         let mut initial_text = Rope::from(initial_snapshot.text().as_str());
-//         for (snapshot, patch) in edits {
-//             let snapshot_text = Rope::from(snapshot.text().as_str());
-//             for edit in &patch {
-//                 let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0));
-//                 let old_end = initial_text.point_to_offset(cmp::min(
-//                     Point::new(edit.new.start + edit.old.len() as u32, 0),
-//                     initial_text.max_point(),
-//                 ));
-//                 let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0));
-//                 let new_end = snapshot_text.point_to_offset(cmp::min(
-//                     Point::new(edit.new.end, 0),
-//                     snapshot_text.max_point(),
-//                 ));
-//                 let new_text = snapshot_text
-//                     .chunks_in_range(new_start..new_end)
-//                     .collect::<String>();
-
-//                 initial_text.replace(old_start..old_end, &new_text);
-//             }
-//             assert_eq!(initial_text.to_string(), snapshot_text.to_string());
-//         }
-
-//         if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
-//             log::info!("Waiting for wrapping to finish");
-//             while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
-//                 notifications.next().await.unwrap();
-//             }
-//         }
-//         wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
-//     }
-
-//     fn init_test(cx: &mut gpui::TestAppContext) {
-//         cx.foreground().forbid_parking();
-//         cx.update(|cx| {
-//             cx.set_global(SettingsStore::test(cx));
-//             theme::init((), cx);
-//         });
-//     }
-
-//     fn wrap_text(
-//         unwrapped_text: &str,
-//         wrap_width: Option<f32>,
-//         line_wrapper: &mut LineWrapper,
-//     ) -> String {
-//         if let Some(wrap_width) = wrap_width {
-//             let mut wrapped_text = String::new();
-//             for (row, line) in unwrapped_text.split('\n').enumerate() {
-//                 if row > 0 {
-//                     wrapped_text.push('\n')
-//                 }
-
-//                 let mut prev_ix = 0;
-//                 for boundary in line_wrapper.wrap_line(line, wrap_width) {
-//                     wrapped_text.push_str(&line[prev_ix..boundary.ix]);
-//                     wrapped_text.push('\n');
-//                     wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));
-//                     prev_ix = boundary.ix;
-//                 }
-//                 wrapped_text.push_str(&line[prev_ix..]);
-//             }
-//             wrapped_text
-//         } else {
-//             unwrapped_text.to_string()
-//         }
-//     }
-
-//     impl WrapSnapshot {
-//         pub fn text(&self) -> String {
-//             self.text_chunks(0).collect()
-//         }
-
-//         pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
-//             self.chunks(
-//                 wrap_row..self.max_point().row() + 1,
-//                 false,
-//                 Highlights::default(),
-//             )
-//             .map(|h| h.text)
-//         }
-
-//         fn verify_chunks(&mut self, rng: &mut impl Rng) {
-//             for _ in 0..5 {
-//                 let mut end_row = rng.gen_range(0..=self.max_point().row());
-//                 let start_row = rng.gen_range(0..=end_row);
-//                 end_row += 1;
-
-//                 let mut expected_text = self.text_chunks(start_row).collect::<String>();
-//                 if expected_text.ends_with('\n') {
-//                     expected_text.push('\n');
-//                 }
-//                 let mut expected_text = expected_text
-//                     .lines()
-//                     .take((end_row - start_row) as usize)
-//                     .collect::<Vec<_>>()
-//                     .join("\n");
-//                 if end_row <= self.max_point().row() {
-//                     expected_text.push('\n');
-//                 }
-
-//                 let actual_text = self
-//                     .chunks(start_row..end_row, true, Highlights::default())
-//                     .map(|c| c.text)
-//                     .collect::<String>();
-//                 assert_eq!(
-//                     expected_text,
-//                     actual_text,
-//                     "chunks != highlighted_chunks for rows {:?}",
-//                     start_row..end_row
-//                 );
-//             }
-//         }
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
+        MultiBuffer,
+    };
+    use gpui::{font, px, test::observe, Platform};
+    use rand::prelude::*;
+    use settings::SettingsStore;
+    use smol::stream::StreamExt;
+    use std::{cmp, env, num::NonZeroU32};
+    use text::Rope;
+    use theme::LoadThemes;
+
+    #[gpui::test(iterations = 100)]
+    async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+        // todo!() this test is flaky
+        init_test(cx);
+
+        cx.background_executor.set_block_on_ticks(0..=50);
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let text_system = cx.read(|cx| cx.text_system().clone());
+        let mut wrap_width = if rng.gen_bool(0.1) {
+            None
+        } else {
+            Some(px(rng.gen_range(0.0..=1000.0)))
+        };
+        let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
+        let font = font("Helvetica");
+        let font_id = text_system.font_id(&font).unwrap();
+        let font_size = px(14.0);
+
+        log::info!("Tab size: {}", tab_size);
+        log::info!("Wrap width: {:?}", wrap_width);
+
+        let buffer = cx.update(|cx| {
+            if rng.gen() {
+                MultiBuffer::build_random(&mut rng, cx)
+            } else {
+                let len = rng.gen_range(0..10);
+                let text = util::RandomCharIter::new(&mut rng)
+                    .take(len)
+                    .collect::<String>();
+                MultiBuffer::build_simple(&text, cx)
+            }
+        });
+        let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+        log::info!("Buffer text: {:?}", buffer_snapshot.text());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
+        log::info!("FoldMap text: {:?}", fold_snapshot.text());
+        let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
+        let tabs_snapshot = tab_map.set_max_expansion_column(32);
+        log::info!("TabMap text: {:?}", tabs_snapshot.text());
+
+        let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size).unwrap();
+        let unwrapped_text = tabs_snapshot.text();
+        let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
+
+        let (wrap_map, _) =
+            cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font, font_size, wrap_width, cx));
+        let mut notifications = observe(&wrap_map, cx);
+
+        if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+            notifications.next().await.unwrap();
+        }
+
+        let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| {
+            assert!(!map.is_rewrapping());
+            map.sync(tabs_snapshot.clone(), Vec::new(), cx)
+        });
+
+        let actual_text = initial_snapshot.text();
+        assert_eq!(
+            actual_text, expected_text,
+            "unwrapped text is: {:?}",
+            unwrapped_text
+        );
+        log::info!("Wrapped text: {:?}", actual_text);
+
+        let mut next_inlay_id = 0;
+        let mut edits = Vec::new();
+        for _i in 0..operations {
+            log::info!("{} ==============================================", _i);
+
+            let mut buffer_edits = Vec::new();
+            match rng.gen_range(0..=100) {
+                0..=19 => {
+                    wrap_width = if rng.gen_bool(0.2) {
+                        None
+                    } else {
+                        Some(px(rng.gen_range(0.0..=1000.0)))
+                    };
+                    log::info!("Setting wrap width to {:?}", wrap_width);
+                    wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+                }
+                20..=39 => {
+                    for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
+                        let (tabs_snapshot, tab_edits) =
+                            tab_map.sync(fold_snapshot, fold_edits, tab_size);
+                        let (mut snapshot, wrap_edits) =
+                            wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
+                        snapshot.check_invariants();
+                        snapshot.verify_chunks(&mut rng);
+                        edits.push((snapshot, wrap_edits));
+                    }
+                }
+                40..=59 => {
+                    let (inlay_snapshot, inlay_edits) =
+                        inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+                    let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+                    let (tabs_snapshot, tab_edits) =
+                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
+                    let (mut snapshot, wrap_edits) =
+                        wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
+                    snapshot.check_invariants();
+                    snapshot.verify_chunks(&mut rng);
+                    edits.push((snapshot, wrap_edits));
+                }
+                _ => {
+                    buffer.update(cx, |buffer, cx| {
+                        let subscription = buffer.subscribe();
+                        let edit_count = rng.gen_range(1..=5);
+                        buffer.randomly_mutate(&mut rng, edit_count, cx);
+                        buffer_snapshot = buffer.snapshot(cx);
+                        buffer_edits.extend(subscription.consume());
+                    });
+                }
+            }
+
+            log::info!("Buffer text: {:?}", buffer_snapshot.text());
+            let (inlay_snapshot, inlay_edits) =
+                inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+            log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+            let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+            log::info!("FoldMap text: {:?}", fold_snapshot.text());
+            let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
+            log::info!("TabMap text: {:?}", tabs_snapshot.text());
+
+            let unwrapped_text = tabs_snapshot.text();
+            let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
+            let (mut snapshot, wrap_edits) =
+                wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx));
+            snapshot.check_invariants();
+            snapshot.verify_chunks(&mut rng);
+            edits.push((snapshot, wrap_edits));
+
+            if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
+                log::info!("Waiting for wrapping to finish");
+                while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+                    notifications.next().await.unwrap();
+                }
+                wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
+            }
+
+            if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+                let (mut wrapped_snapshot, wrap_edits) =
+                    wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
+                let actual_text = wrapped_snapshot.text();
+                let actual_longest_row = wrapped_snapshot.longest_row();
+                log::info!("Wrapping finished: {:?}", actual_text);
+                wrapped_snapshot.check_invariants();
+                wrapped_snapshot.verify_chunks(&mut rng);
+                edits.push((wrapped_snapshot.clone(), wrap_edits));
+                assert_eq!(
+                    actual_text, expected_text,
+                    "unwrapped text is: {:?}",
+                    unwrapped_text
+                );
+
+                let mut summary = TextSummary::default();
+                for (ix, item) in wrapped_snapshot
+                    .transforms
+                    .items(&())
+                    .into_iter()
+                    .enumerate()
+                {
+                    summary += &item.summary.output;
+                    log::info!("{} summary: {:?}", ix, item.summary.output,);
+                }
+
+                if tab_size.get() == 1
+                    || !wrapped_snapshot
+                        .tab_snapshot
+                        .fold_snapshot
+                        .text()
+                        .contains('\t')
+                {
+                    let mut expected_longest_rows = Vec::new();
+                    let mut longest_line_len = -1;
+                    for (row, line) in expected_text.split('\n').enumerate() {
+                        let line_char_count = line.chars().count() as isize;
+                        if line_char_count > longest_line_len {
+                            expected_longest_rows.clear();
+                            longest_line_len = line_char_count;
+                        }
+                        if line_char_count >= longest_line_len {
+                            expected_longest_rows.push(row as u32);
+                        }
+                    }
+
+                    assert!(
+                        expected_longest_rows.contains(&actual_longest_row),
+                        "incorrect longest row {}. expected {:?} with length {}",
+                        actual_longest_row,
+                        expected_longest_rows,
+                        longest_line_len,
+                    )
+                }
+            }
+        }
+
+        let mut initial_text = Rope::from(initial_snapshot.text().as_str());
+        for (snapshot, patch) in edits {
+            let snapshot_text = Rope::from(snapshot.text().as_str());
+            for edit in &patch {
+                let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0));
+                let old_end = initial_text.point_to_offset(cmp::min(
+                    Point::new(edit.new.start + edit.old.len() as u32, 0),
+                    initial_text.max_point(),
+                ));
+                let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0));
+                let new_end = snapshot_text.point_to_offset(cmp::min(
+                    Point::new(edit.new.end, 0),
+                    snapshot_text.max_point(),
+                ));
+                let new_text = snapshot_text
+                    .chunks_in_range(new_start..new_end)
+                    .collect::<String>();
+
+                initial_text.replace(old_start..old_end, &new_text);
+            }
+            assert_eq!(initial_text.to_string(), snapshot_text.to_string());
+        }
+
+        if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+            log::info!("Waiting for wrapping to finish");
+            while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+                notifications.next().await.unwrap();
+            }
+        }
+        wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
+    }
+
+    fn init_test(cx: &mut gpui::TestAppContext) {
+        cx.update(|cx| {
+            let settings = SettingsStore::test(cx);
+            cx.set_global(settings);
+            theme::init(LoadThemes::JustBase, cx);
+        });
+    }
+
+    fn wrap_text(
+        unwrapped_text: &str,
+        wrap_width: Option<Pixels>,
+        line_wrapper: &mut LineWrapper,
+    ) -> String {
+        if let Some(wrap_width) = wrap_width {
+            let mut wrapped_text = String::new();
+            for (row, line) in unwrapped_text.split('\n').enumerate() {
+                if row > 0 {
+                    wrapped_text.push('\n')
+                }
+
+                let mut prev_ix = 0;
+                for boundary in line_wrapper.wrap_line(line, wrap_width) {
+                    wrapped_text.push_str(&line[prev_ix..boundary.ix]);
+                    wrapped_text.push('\n');
+                    wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));
+                    prev_ix = boundary.ix;
+                }
+                wrapped_text.push_str(&line[prev_ix..]);
+            }
+            wrapped_text
+        } else {
+            unwrapped_text.to_string()
+        }
+    }
+
+    impl WrapSnapshot {
+        pub fn text(&self) -> String {
+            self.text_chunks(0).collect()
+        }
+
+        pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
+            self.chunks(
+                wrap_row..self.max_point().row() + 1,
+                false,
+                Highlights::default(),
+            )
+            .map(|h| h.text)
+        }
+
+        fn verify_chunks(&mut self, rng: &mut impl Rng) {
+            for _ in 0..5 {
+                let mut end_row = rng.gen_range(0..=self.max_point().row());
+                let start_row = rng.gen_range(0..=end_row);
+                end_row += 1;
+
+                let mut expected_text = self.text_chunks(start_row).collect::<String>();
+                if expected_text.ends_with('\n') {
+                    expected_text.push('\n');
+                }
+                let mut expected_text = expected_text
+                    .lines()
+                    .take((end_row - start_row) as usize)
+                    .collect::<Vec<_>>()
+                    .join("\n");
+                if end_row <= self.max_point().row() {
+                    expected_text.push('\n');
+                }
+
+                let actual_text = self
+                    .chunks(start_row..end_row, true, Highlights::default())
+                    .map(|c| c.text)
+                    .collect::<String>();
+                assert_eq!(
+                    expected_text,
+                    actual_text,
+                    "chunks != highlighted_chunks for rows {:?}",
+                    start_row..end_row
+                );
+            }
+        }
+    }
+}

crates/editor2/src/editor.rs 🔗

@@ -63,6 +63,7 @@ use language::{
 use lazy_static::lazy_static;
 use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
 use lsp::{DiagnosticSeverity, LanguageServerId};
+use mouse_context_menu::MouseContextMenu;
 use movement::TextLayoutDetails;
 use multi_buffer::ToOffsetUtf16;
 pub use multi_buffer::{
@@ -99,8 +100,10 @@ use text::{OffsetUtf16, Rope};
 use theme::{
     ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
 };
-use ui::prelude::*;
-use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, Tooltip};
+use ui::{
+    h_stack, v_stack, ButtonSize, ButtonStyle, HighlightedLabel, Icon, IconButton, Popover, Tooltip,
+};
+use ui::{prelude::*, IconSize};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{
     item::{ItemEvent, ItemHandle},
@@ -153,7 +156,6 @@ pub fn render_parsed_markdown(
                 }
             }),
     );
-    let runs = text_runs_for_highlights(&parsed.text, &editor_style.text, highlights);
 
     let mut links = Vec::new();
     let mut link_ranges = Vec::new();
@@ -166,7 +168,7 @@ pub fn render_parsed_markdown(
 
     InteractiveText::new(
         element_id,
-        StyledText::new(parsed.text.clone()).with_runs(runs),
+        StyledText::new(parsed.text.clone()).with_highlights(&editor_style.text, highlights),
     )
     .on_click(link_ranges, move |clicked_range_ix, cx| {
         match &links[clicked_range_ix] {
@@ -407,133 +409,17 @@ pub fn init_settings(cx: &mut AppContext) {
 
 pub fn init(cx: &mut AppContext) {
     init_settings(cx);
-    // cx.register_action_type(Editor::new_file);
-    // cx.register_action_type(Editor::new_file_in_direction);
-    // cx.register_action_type(Editor::cancel);
-    // cx.register_action_type(Editor::newline);
-    // cx.register_action_type(Editor::newline_above);
-    // cx.register_action_type(Editor::newline_below);
-    // cx.register_action_type(Editor::backspace);
-    // cx.register_action_type(Editor::delete);
-    // cx.register_action_type(Editor::tab);
-    // cx.register_action_type(Editor::tab_prev);
-    // cx.register_action_type(Editor::indent);
-    // cx.register_action_type(Editor::outdent);
-    // cx.register_action_type(Editor::delete_line);
-    // cx.register_action_type(Editor::join_lines);
-    // cx.register_action_type(Editor::sort_lines_case_sensitive);
-    // cx.register_action_type(Editor::sort_lines_case_insensitive);
-    // cx.register_action_type(Editor::reverse_lines);
-    // cx.register_action_type(Editor::shuffle_lines);
-    // cx.register_action_type(Editor::convert_to_upper_case);
-    // cx.register_action_type(Editor::convert_to_lower_case);
-    // cx.register_action_type(Editor::convert_to_title_case);
-    // cx.register_action_type(Editor::convert_to_snake_case);
-    // cx.register_action_type(Editor::convert_to_kebab_case);
-    // cx.register_action_type(Editor::convert_to_upper_camel_case);
-    // cx.register_action_type(Editor::convert_to_lower_camel_case);
-    // cx.register_action_type(Editor::delete_to_previous_word_start);
-    // cx.register_action_type(Editor::delete_to_previous_subword_start);
-    // cx.register_action_type(Editor::delete_to_next_word_end);
-    // cx.register_action_type(Editor::delete_to_next_subword_end);
-    // cx.register_action_type(Editor::delete_to_beginning_of_line);
-    // cx.register_action_type(Editor::delete_to_end_of_line);
-    // cx.register_action_type(Editor::cut_to_end_of_line);
-    // cx.register_action_type(Editor::duplicate_line);
-    // cx.register_action_type(Editor::move_line_up);
-    // cx.register_action_type(Editor::move_line_down);
-    // cx.register_action_type(Editor::transpose);
-    // cx.register_action_type(Editor::cut);
-    // cx.register_action_type(Editor::copy);
-    // cx.register_action_type(Editor::paste);
-    // cx.register_action_type(Editor::undo);
-    // cx.register_action_type(Editor::redo);
-    // cx.register_action_type(Editor::move_page_up);
-    // cx.register_action_type::<MoveDown>();
-    // cx.register_action_type(Editor::move_page_down);
-    // cx.register_action_type(Editor::next_screen);
-    // cx.register_action_type::<MoveLeft>();
-    // cx.register_action_type::<MoveRight>();
-    // cx.register_action_type(Editor::move_to_previous_word_start);
-    // cx.register_action_type(Editor::move_to_previous_subword_start);
-    // cx.register_action_type(Editor::move_to_next_word_end);
-    // cx.register_action_type(Editor::move_to_next_subword_end);
-    // cx.register_action_type(Editor::move_to_beginning_of_line);
-    // cx.register_action_type(Editor::move_to_end_of_line);
-    // cx.register_action_type(Editor::move_to_start_of_paragraph);
-    // cx.register_action_type(Editor::move_to_end_of_paragraph);
-    // cx.register_action_type(Editor::move_to_beginning);
-    // cx.register_action_type(Editor::move_to_end);
-    // cx.register_action_type(Editor::select_up);
-    // cx.register_action_type(Editor::select_down);
-    // cx.register_action_type(Editor::select_left);
-    // cx.register_action_type(Editor::select_right);
-    // cx.register_action_type(Editor::select_to_previous_word_start);
-    // cx.register_action_type(Editor::select_to_previous_subword_start);
-    // cx.register_action_type(Editor::select_to_next_word_end);
-    // cx.register_action_type(Editor::select_to_next_subword_end);
-    // cx.register_action_type(Editor::select_to_beginning_of_line);
-    // cx.register_action_type(Editor::select_to_end_of_line);
-    // cx.register_action_type(Editor::select_to_start_of_paragraph);
-    // cx.register_action_type(Editor::select_to_end_of_paragraph);
-    // cx.register_action_type(Editor::select_to_beginning);
-    // cx.register_action_type(Editor::select_to_end);
-    // cx.register_action_type(Editor::select_all);
-    // cx.register_action_type(Editor::select_all_matches);
-    // cx.register_action_type(Editor::select_line);
-    // cx.register_action_type(Editor::split_selection_into_lines);
-    // cx.register_action_type(Editor::add_selection_above);
-    // cx.register_action_type(Editor::add_selection_below);
-    // cx.register_action_type(Editor::select_next);
-    // cx.register_action_type(Editor::select_previous);
-    // cx.register_action_type(Editor::toggle_comments);
-    // cx.register_action_type(Editor::select_larger_syntax_node);
-    // cx.register_action_type(Editor::select_smaller_syntax_node);
-    // cx.register_action_type(Editor::move_to_enclosing_bracket);
-    // cx.register_action_type(Editor::undo_selection);
-    // cx.register_action_type(Editor::redo_selection);
-    // cx.register_action_type(Editor::go_to_diagnostic);
-    // cx.register_action_type(Editor::go_to_prev_diagnostic);
-    // cx.register_action_type(Editor::go_to_hunk);
-    // cx.register_action_type(Editor::go_to_prev_hunk);
-    // cx.register_action_type(Editor::go_to_definition);
-    // cx.register_action_type(Editor::go_to_definition_split);
-    // cx.register_action_type(Editor::go_to_type_definition);
-    // cx.register_action_type(Editor::go_to_type_definition_split);
-    // cx.register_action_type(Editor::fold);
-    // cx.register_action_type(Editor::fold_at);
-    // cx.register_action_type(Editor::unfold_lines);
-    // cx.register_action_type(Editor::unfold_at);
-    // cx.register_action_type(Editor::gutter_hover);
-    // cx.register_action_type(Editor::fold_selected_ranges);
-    // cx.register_action_type(Editor::show_completions);
-    // cx.register_action_type(Editor::toggle_code_actions);
-    // cx.register_action_type(Editor::open_excerpts);
-    // cx.register_action_type(Editor::toggle_soft_wrap);
-    // cx.register_action_type(Editor::toggle_inlay_hints);
-    // cx.register_action_type(Editor::reveal_in_finder);
-    // cx.register_action_type(Editor::copy_path);
-    // cx.register_action_type(Editor::copy_relative_path);
-    // cx.register_action_type(Editor::copy_highlight_json);
-    // cx.add_async_action(Editor::format);
-    // cx.register_action_type(Editor::restart_language_server);
-    // cx.register_action_type(Editor::show_character_palette);
-    // cx.add_async_action(Editor::confirm_completion);
-    // cx.add_async_action(Editor::confirm_code_action);
-    // cx.add_async_action(Editor::rename);
-    // cx.add_async_action(Editor::confirm_rename);
-    // cx.add_async_action(Editor::find_all_references);
-    // cx.register_action_type(Editor::next_copilot_suggestion);
-    // cx.register_action_type(Editor::previous_copilot_suggestion);
-    // cx.register_action_type(Editor::copilot_suggest);
-    // cx.register_action_type(Editor::context_menu_first);
-    // cx.register_action_type(Editor::context_menu_prev);
-    // cx.register_action_type(Editor::context_menu_next);
-    // cx.register_action_type(Editor::context_menu_last);
 
     workspace::register_project_item::<Editor>(cx);
     workspace::register_followable_item::<Editor>(cx);
     workspace::register_deserializable_item::<Editor>(cx);
+    cx.observe_new_views(
+        |workspace: &mut Workspace, cx: &mut ViewContext<Workspace>| {
+            workspace.register_action(Editor::new_file);
+            workspace.register_action(Editor::new_file_in_direction);
+        },
+    )
+    .detach();
 }
 
 trait InvalidationRegion {
@@ -621,8 +507,6 @@ pub struct Editor {
     ime_transaction: Option<TransactionId>,
     active_diagnostics: Option<ActiveDiagnosticGroup>,
     soft_wrap_mode_override: Option<language_settings::SoftWrap>,
-    // get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
-    // override_text_style: Option<Box<OverrideTextStyle>>,
     project: Option<Model<Project>>,
     collaboration_hub: Option<Box<dyn CollaborationHub>>,
     blink_manager: Model<BlinkManager>,
@@ -636,7 +520,7 @@ pub struct Editor {
     inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
     nav_history: Option<ItemNavHistory>,
     context_menu: RwLock<Option<ContextMenu>>,
-    // mouse_context_menu: View<context_menu::ContextMenu>,
+    mouse_context_menu: Option<MouseContextMenu>,
     completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
     next_completion_id: CompletionId,
     available_code_actions: Option<(Model<Buffer>, Arc<[CodeAction]>)>,
@@ -1316,11 +1200,7 @@ impl CompletionsMenu {
                             ),
                         );
                         let completion_label = StyledText::new(completion.label.text.clone())
-                            .with_runs(text_runs_for_highlights(
-                                &completion.label.text,
-                                &style.text,
-                                highlights,
-                            ));
+                            .with_highlights(&style.text, highlights);
                         let documentation_label =
                             if let Some(Documentation::SingleLine(text)) = documentation {
                                 Some(SharedString::from(text.clone()))
@@ -1734,21 +1614,11 @@ impl Editor {
     //         Self::new(EditorMode::Full, buffer, None, field_editor_style, cx)
     //     }
 
-    //     pub fn auto_height(
-    //         max_lines: usize,
-    //         field_editor_style: Option<Arc<GetFieldEditorTheme>>,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Self {
-    //         let buffer = cx.build_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new()));
-    //         let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
-    //         Self::new(
-    //             EditorMode::AutoHeight { max_lines },
-    //             buffer,
-    //             None,
-    //             field_editor_style,
-    //             cx,
-    //         )
-    //     }
+    pub fn auto_height(max_lines: usize, cx: &mut ViewContext<Self>) -> Self {
+        let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), String::new()));
+        let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+        Self::new(EditorMode::AutoHeight { max_lines }, buffer, None, cx)
+    }
 
     pub fn for_buffer(
         buffer: Model<Buffer>,
@@ -1768,14 +1638,7 @@ impl Editor {
     }
 
     pub fn clone(&self, cx: &mut ViewContext<Self>) -> Self {
-        let mut clone = Self::new(
-            self.mode,
-            self.buffer.clone(),
-            self.project.clone(),
-            // todo!
-            // self.get_field_editor_theme.clone(),
-            cx,
-        );
+        let mut clone = Self::new(self.mode, self.buffer.clone(), self.project.clone(), cx);
         self.display_map.update(cx, |display_map, cx| {
             let snapshot = display_map.snapshot(cx);
             clone.display_map.update(cx, |display_map, cx| {
@@ -1792,17 +1655,11 @@ impl Editor {
         mode: EditorMode,
         buffer: Model<MultiBuffer>,
         project: Option<Model<Project>>,
-        // todo!()
-        // get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        // let editor_view_id = cx.view_id();
         let style = cx.text_style();
         let font_size = style.font_size.to_pixels(cx.rem_size());
         let display_map = cx.build_model(|cx| {
-            // todo!()
-            // let settings = settings::get::<ThemeSettings>(cx);
-            // let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx);
             DisplayMap::new(buffer.clone(), style.font(), font_size, None, 2, 1, cx)
         });
 
@@ -1858,7 +1715,6 @@ impl Editor {
             ime_transaction: Default::default(),
             active_diagnostics: None,
             soft_wrap_mode_override,
-            // get_field_editor_theme,
             collaboration_hub: project.clone().map(|project| Box::new(project) as _),
             project,
             blink_manager: blink_manager.clone(),
@@ -1872,8 +1728,7 @@ impl Editor {
             inlay_background_highlights: Default::default(),
             nav_history: None,
             context_menu: RwLock::new(None),
-            // mouse_context_menu: cx
-            //     .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)),
+            mouse_context_menu: None,
             completion_tasks: Default::default(),
             next_completion_id: 0,
             next_inlay_id: 0,
@@ -1882,7 +1737,6 @@ impl Editor {
             document_highlights_task: Default::default(),
             pending_rename: Default::default(),
             searchable: true,
-            // override_text_style: None,
             cursor_shape: Default::default(),
             autoindent_mode: Some(AutoindentMode::EachLine),
             collapse_matches: false,
@@ -2000,25 +1854,25 @@ impl Editor {
         }
     }
 
-    //     pub fn new_file_in_direction(
-    //         workspace: &mut Workspace,
-    //         action: &workspace::NewFileInDirection,
-    //         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.split_item(
-    //                 action.0,
-    //                 Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
-    //                 cx,
-    //             );
-    //         }
-    //     }
+    pub fn new_file_in_direction(
+        workspace: &mut Workspace,
+        action: &workspace::NewFileInDirection,
+        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.split_item(
+                action.0,
+                Box::new(cx.build_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
+                cx,
+            );
+        }
+    }
 
     pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
         self.buffer.read(cx).replica_id()
@@ -2068,14 +1922,14 @@ impl Editor {
     //         self.buffer.read(cx).read(cx).file_at(point).cloned()
     //     }
 
-    //     pub fn active_excerpt(
-    //         &self,
-    //         cx: &AppContext,
-    //     ) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
-    //         self.buffer
-    //             .read(cx)
-    //             .excerpt_containing(self.selections.newest_anchor().head(), cx)
-    //     }
+    pub fn active_excerpt(
+        &self,
+        cx: &AppContext,
+    ) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
+        self.buffer
+            .read(cx)
+            .excerpt_containing(self.selections.newest_anchor().head(), cx)
+    }
 
     //     pub fn style(&self, cx: &AppContext) -> EditorStyle {
     //         build_style(
@@ -3632,7 +3486,7 @@ impl Editor {
                         drop(context_menu);
                         this.discard_copilot_suggestion(cx);
                         cx.notify();
-                    } else if this.completion_tasks.is_empty() {
+                    } else if this.completion_tasks.len() <= 1 {
                         // If there are no more completion tasks and the last menu was
                         // empty, we should hide it. If it was already hidden, we should
                         // also show the copilot suggestion when available.
@@ -8374,6 +8228,23 @@ impl Editor {
         cx.notify();
     }
 
+    pub fn set_style(&mut self, style: EditorStyle, cx: &mut ViewContext<Self>) {
+        let rem_size = cx.rem_size();
+        self.display_map.update(cx, |map, cx| {
+            map.set_font(
+                style.text.font(),
+                style.text.font_size.to_pixels(rem_size),
+                cx,
+            )
+        });
+        self.style = Some(style);
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn style(&self) -> Option<&EditorStyle> {
+        self.style.as_ref()
+    }
+
     pub fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut AppContext) -> bool {
         self.display_map
             .update(cx, |map, cx| map.set_wrap_width(width, cx))
@@ -8796,62 +8667,56 @@ impl Editor {
     //         self.searchable
     //     }
 
-    //     fn open_excerpts(workspace: &mut Workspace, _: &OpenExcerpts, cx: &mut ViewContext<Workspace>) {
-    //         let active_item = workspace.active_item(cx);
-    //         let editor_handle = if let Some(editor) = active_item
-    //             .as_ref()
-    //             .and_then(|item| item.act_as::<Self>(cx))
-    //         {
-    //             editor
-    //         } else {
-    //             cx.propagate();
-    //             return;
-    //         };
-
-    //         let editor = editor_handle.read(cx);
-    //         let buffer = editor.buffer.read(cx);
-    //         if buffer.is_singleton() {
-    //             cx.propagate();
-    //             return;
-    //         }
+    fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
+        let buffer = self.buffer.read(cx);
+        if buffer.is_singleton() {
+            cx.propagate();
+            return;
+        }
 
-    //         let mut new_selections_by_buffer = HashMap::default();
-    //         for selection in editor.selections.all::<usize>(cx) {
-    //             for (buffer, mut range, _) in
-    //                 buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
-    //             {
-    //                 if selection.reversed {
-    //                     mem::swap(&mut range.start, &mut range.end);
-    //                 }
-    //                 new_selections_by_buffer
-    //                     .entry(buffer)
-    //                     .or_insert(Vec::new())
-    //                     .push(range)
-    //             }
-    //         }
+        let Some(workspace) = self.workspace() else {
+            cx.propagate();
+            return;
+        };
 
-    //         editor_handle.update(cx, |editor, cx| {
-    //             editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx);
-    //         });
-    //         let pane = workspace.active_pane().clone();
-    //         pane.update(cx, |pane, _| pane.disable_history());
-
-    //         // We defer the pane interaction because we ourselves are a workspace item
-    //         // and activating a new item causes the pane to call a method on us reentrantly,
-    //         // which panics if we're on the stack.
-    //         cx.defer(move |workspace, cx| {
-    //             for (buffer, ranges) in new_selections_by_buffer.into_iter() {
-    //                 let editor = workspace.open_project_item::<Self>(buffer, cx);
-    //                 editor.update(cx, |editor, cx| {
-    //                     editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
-    //                         s.select_ranges(ranges);
-    //                     });
-    //                 });
-    //             }
-
-    //             pane.update(cx, |pane, _| pane.enable_history());
-    //         });
-    //     }
+        let mut new_selections_by_buffer = HashMap::default();
+        for selection in self.selections.all::<usize>(cx) {
+            for (buffer, mut range, _) in
+                buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
+            {
+                if selection.reversed {
+                    mem::swap(&mut range.start, &mut range.end);
+                }
+                new_selections_by_buffer
+                    .entry(buffer)
+                    .or_insert(Vec::new())
+                    .push(range)
+            }
+        }
+
+        self.push_to_nav_history(self.selections.newest_anchor().head(), None, cx);
+
+        // We defer the pane interaction because we ourselves are a workspace item
+        // and activating a new item causes the pane to call a method on us reentrantly,
+        // which panics if we're on the stack.
+        cx.window_context().defer(move |cx| {
+            workspace.update(cx, |workspace, cx| {
+                let pane = workspace.active_pane().clone();
+                pane.update(cx, |pane, _| pane.disable_history());
+
+                for (buffer, ranges) in new_selections_by_buffer.into_iter() {
+                    let editor = workspace.open_project_item::<Self>(buffer, cx);
+                    editor.update(cx, |editor, cx| {
+                        editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
+                            s.select_ranges(ranges);
+                        });
+                    });
+                }
+
+                pane.update(cx, |pane, _| pane.enable_history());
+            })
+        });
+    }
 
     fn jump(
         &mut self,
@@ -9397,7 +9262,7 @@ impl Render for Editor {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let settings = ThemeSettings::get_global(cx);
         let text_style = match self.mode {
-            EditorMode::SingleLine => TextStyle {
+            EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
                 color: cx.theme().colors().text,
                 font_family: settings.ui_font.family.clone(),
                 font_features: settings.ui_font.features,
@@ -9410,8 +9275,6 @@ impl Render for Editor {
                 white_space: WhiteSpace::Normal,
             },
 
-            EditorMode::AutoHeight { max_lines } => todo!(),
-
             EditorMode::Full => TextStyle {
                 color: cx.theme().colors().text,
                 font_family: settings.buffer_font.family.clone(),
@@ -9446,106 +9309,6 @@ impl Render for Editor {
     }
 }
 
-// impl View for Editor {
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         let style = self.style(cx);
-//         let font_changed = self.display_map.update(cx, |map, cx| {
-//             map.set_fold_ellipses_color(style.folds.ellipses.text_color);
-//             map.set_font_with_size(style.text.font_id, style.text.font_size, cx)
-//         });
-
-//         if font_changed {
-//             cx.defer(move |editor, cx: &mut ViewContext<Editor>| {
-//                 hide_hover(editor, cx);
-//                 hide_link_definition(editor, cx);
-//             });
-//         }
-
-//         Stack::new()
-//             .with_child(EditorElement::new(style.clone()))
-//             .with_child(ChildView::new(&self.mouse_context_menu, cx))
-//             .into_any()
-//     }
-
-//     fn ui_name() -> &'static str {
-//         "Editor"
-//     }
-
-//     fn focus_in(&mut self, focused: AnyView, cx: &mut ViewContext<Self>) {
-//         if cx.is_self_focused() {
-//             let focused_event = EditorFocused(cx.handle());
-//             cx.emit(Event::Focused);
-//             cx.emit_global(focused_event);
-//         }
-//         if let Some(rename) = self.pending_rename.as_ref() {
-//             cx.focus(&rename.editor);
-//         } else if cx.is_self_focused() || !focused.is::<Editor>() {
-//             if !self.focused {
-//                 self.blink_manager.update(cx, BlinkManager::enable);
-//             }
-//             self.focused = true;
-//             self.buffer.update(cx, |buffer, cx| {
-//                 buffer.finalize_last_transaction(cx);
-//                 if self.leader_peer_id.is_none() {
-//                     buffer.set_active_selections(
-//                         &self.selections.disjoint_anchors(),
-//                         self.selections.line_mode,
-//                         self.cursor_shape,
-//                         cx,
-//                     );
-//                 }
-//             });
-//         }
-//     }
-
-//     fn focus_out(&mut self, _: AnyView, cx: &mut ViewContext<Self>) {
-//         let blurred_event = EditorBlurred(cx.handle());
-//         cx.emit_global(blurred_event);
-//         self.focused = false;
-//         self.blink_manager.update(cx, BlinkManager::disable);
-//         self.buffer
-//             .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
-//         self.hide_context_menu(cx);
-//         hide_hover(self, cx);
-//         cx.emit(Event::Blurred);
-//         cx.notify();
-//     }
-
-//     fn modifiers_changed(
-//         &mut self,
-//         event: &gpui::platform::ModifiersChangedEvent,
-//         cx: &mut ViewContext<Self>,
-//     ) -> bool {
-//         let pending_selection = self.has_pending_selection();
-
-//         if let Some(point) = &self.link_go_to_definition_state.last_trigger_point {
-//             if event.cmd && !pending_selection {
-//                 let point = point.clone();
-//                 let snapshot = self.snapshot(cx);
-//                 let kind = point.definition_kind(event.shift);
-
-//                 show_link_definition(kind, self, point, snapshot, cx);
-//                 return false;
-//             }
-//         }
-
-//         {
-//             if self.link_go_to_definition_state.symbol_range.is_some()
-//                 || !self.link_go_to_definition_state.definitions.is_empty()
-//             {
-//                 self.link_go_to_definition_state.symbol_range.take();
-//                 self.link_go_to_definition_state.definitions.clear();
-//                 cx.notify();
-//             }
-
-//             self.link_go_to_definition_state.task = None;
-
-//             self.clear_highlights::<LinkGoToDefinitionState>(cx);
-//         }
-
-//         false
-//     }
-
 impl InputHandler for Editor {
     fn text_for_range(
         &mut self,
@@ -9792,72 +9555,6 @@ impl InputHandler for Editor {
     }
 }
 
-// fn build_style(
-//     settings: &ThemeSettings,
-//     get_field_editor_theme: Option<&GetFieldEditorTheme>,
-//     override_text_style: Option<&OverrideTextStyle>,
-//     cx: &mut AppContext,
-// ) -> EditorStyle {
-//     let font_cache = cx.font_cache();
-//     let line_height_scalar = settings.line_height();
-//     let theme_id = settings.theme.meta.id;
-//     let mut theme = settings.theme.editor.clone();
-//     let mut style = if let Some(get_field_editor_theme) = get_field_editor_theme {
-//         let field_editor_theme = get_field_editor_theme(&settings.theme);
-//         theme.text_color = field_editor_theme.text.color;
-//         theme.selection = field_editor_theme.selection;
-//         theme.background = field_editor_theme
-//             .container
-//             .background_color
-//             .unwrap_or_default();
-//         EditorStyle {
-//             text: field_editor_theme.text,
-//             placeholder_text: field_editor_theme.placeholder_text,
-//             line_height_scalar,
-//             theme,
-//             theme_id,
-//         }
-//     } else {
-//         todo!();
-//         // let font_family_id = settings.buffer_font_family;
-//         // let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
-//         // let font_properties = Default::default();
-//         // let font_id = font_cache
-//         //     .select_font(font_family_id, &font_properties)
-//         //     .unwrap();
-//         // let font_size = settings.buffer_font_size(cx);
-//         // EditorStyle {
-//         //     text: TextStyle {
-//         //         color: settings.theme.editor.text_color,
-//         //         font_family_name,
-//         //         font_family_id,
-//         //         font_id,
-//         //         font_size,
-//         //         font_properties,
-//         //         underline: Default::default(),
-//         //         soft_wrap: false,
-//         //     },
-//         //     placeholder_text: None,
-//         //     line_height_scalar,
-//         //     theme,
-//         //     theme_id,
-//         // }
-//     };
-
-//     if let Some(highlight_style) = override_text_style.and_then(|build_style| build_style(&style)) {
-//         if let Some(highlighted) = style
-//             .text
-//             .clone()
-//             .highlight(highlight_style, font_cache)
-//             .log_err()
-//         {
-//             style.text = highlighted;
-//         }
-//     }
-
-//     style
-// }
-
 trait SelectionExt {
     fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range<usize>;
     fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point>;
@@ -9999,20 +9696,42 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
     let message = diagnostic.message;
     Arc::new(move |cx: &mut BlockContext| {
         let message = message.clone();
+        let copy_id: SharedString = format!("copy-{}", cx.block_id.clone()).to_string().into();
+        let write_to_clipboard = cx.write_to_clipboard(ClipboardItem::new(message.clone()));
+
+        // TODO: Nate: We should tint the background of the block with the severity color
+        // We need to extend the theme before we can do this
         v_stack()
             .id(cx.block_id)
+            .relative()
             .size_full()
             .bg(gpui::red())
             .children(highlighted_lines.iter().map(|(line, highlights)| {
-                div()
+                let group_id = cx.block_id.to_string();
+
+                h_stack()
+                    .group(group_id.clone())
+                    .gap_2()
+                    .absolute()
+                    .left(cx.anchor_x)
+                    .px_1p5()
                     .child(HighlightedLabel::new(line.clone(), highlights.clone()))
-                    .ml(cx.anchor_x)
-            }))
-            .cursor_pointer()
-            .on_click(cx.listener(move |_, _, cx| {
-                cx.write_to_clipboard(ClipboardItem::new(message.clone()));
+                    .child(
+                        div()
+                            .border()
+                            .border_color(gpui::red())
+                            .invisible()
+                            .group_hover(group_id, |style| style.visible())
+                            .child(
+                                IconButton::new(copy_id.clone(), Icon::Copy)
+                                    .icon_color(Color::Muted)
+                                    .size(ButtonSize::Compact)
+                                    .style(ButtonStyle::Transparent)
+                                    .on_click(cx.listener(move |_, _, cx| write_to_clipboard))
+                                    .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
+                            ),
+                    )
             }))
-            .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx))
             .into_any_element()
     })
 }
@@ -10060,31 +9779,6 @@ pub fn diagnostic_style(
     }
 }
 
-pub fn text_runs_for_highlights(
-    text: &str,
-    default_style: &TextStyle,
-    highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
-) -> Vec<TextRun> {
-    let mut runs = Vec::new();
-    let mut ix = 0;
-    for (range, highlight) in highlights {
-        if ix < range.start {
-            runs.push(default_style.clone().to_run(range.start - ix));
-        }
-        runs.push(
-            default_style
-                .clone()
-                .highlight(highlight)
-                .to_run(range.len()),
-        );
-        ix = range.end;
-    }
-    if ix < text.len() {
-        runs.push(default_style.to_run(text.len() - ix));
-    }
-    runs
-}
-
 pub fn styled_runs_for_code_label<'a>(
     label: &'a CodeLabel,
     syntax_theme: &'a theme::SyntaxTheme,

crates/editor2/src/editor_tests.rs 🔗

@@ -12,7 +12,7 @@ use futures::StreamExt;
 use gpui::{
     div,
     serde_json::{self, json},
-    Div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
+    Div, Flatten, Platform, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
 };
 use indoc::indoc;
 use language::{
@@ -36,121 +36,120 @@ use workspace::{
     NavigationEntry, ViewId,
 };
 
-// todo(finish edit tests)
-// #[gpui::test]
-// fn test_edit_events(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     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| {
-//             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.
-//     editor1.update(cx, |editor, cx| editor.insert("X", cx));
-//     assert_eq!(
-//         mem::take(&mut *events.borrow_mut()),
-//         [
-//             ("editor1", Event::Edited),
-//             ("editor1", Event::BufferEdited),
-//             ("editor2", Event::BufferEdited),
-//         ]
-//     );
-
-//     // Mutating editor 2 will emit an `Edited` event only for that editor.
-//     editor2.update(cx, |editor, cx| editor.delete(&Delete, cx));
-//     assert_eq!(
-//         mem::take(&mut *events.borrow_mut()),
-//         [
-//             ("editor2", Event::Edited),
-//             ("editor1", Event::BufferEdited),
-//             ("editor2", Event::BufferEdited),
-//         ]
-//     );
-
-//     // Undoing on editor 1 will emit an `Edited` event only for that editor.
-//     editor1.update(cx, |editor, cx| editor.undo(&Undo, cx));
-//     assert_eq!(
-//         mem::take(&mut *events.borrow_mut()),
-//         [
-//             ("editor1", Event::Edited),
-//             ("editor1", Event::BufferEdited),
-//             ("editor2", Event::BufferEdited),
-//         ]
-//     );
-
-//     // Redoing on editor 1 will emit an `Edited` event only for that editor.
-//     editor1.update(cx, |editor, cx| editor.redo(&Redo, cx));
-//     assert_eq!(
-//         mem::take(&mut *events.borrow_mut()),
-//         [
-//             ("editor1", Event::Edited),
-//             ("editor1", Event::BufferEdited),
-//             ("editor2", Event::BufferEdited),
-//         ]
-//     );
-
-//     // Undoing on editor 2 will emit an `Edited` event only for that editor.
-//     editor2.update(cx, |editor, cx| editor.undo(&Undo, cx));
-//     assert_eq!(
-//         mem::take(&mut *events.borrow_mut()),
-//         [
-//             ("editor2", Event::Edited),
-//             ("editor1", Event::BufferEdited),
-//             ("editor2", Event::BufferEdited),
-//         ]
-//     );
-
-//     // Redoing on editor 2 will emit an `Edited` event only for that editor.
-//     editor2.update(cx, |editor, cx| editor.redo(&Redo, cx));
-//     assert_eq!(
-//         mem::take(&mut *events.borrow_mut()),
-//         [
-//             ("editor2", Event::Edited),
-//             ("editor1", Event::BufferEdited),
-//             ("editor2", Event::BufferEdited),
-//         ]
-//     );
-
-//     // No event is emitted when the mutation is a no-op.
-//     editor2.update(cx, |editor, cx| {
-//         editor.change_selections(None, cx, |s| s.select_ranges([0..0]));
-
-//         editor.backspace(&Backspace, cx);
-//     });
-//     assert_eq!(mem::take(&mut *events.borrow_mut()), []);
-// }
+#[gpui::test]
+fn test_edit_events(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    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| {
+            let view = cx.view().clone();
+            cx.subscribe(&view, move |_, _, event: &EditorEvent, _| {
+                if matches!(event, EditorEvent::Edited | EditorEvent::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: &EditorEvent, _| {
+                if matches!(event, EditorEvent::Edited | EditorEvent::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.
+    editor1.update(cx, |editor, cx| editor.insert("X", cx));
+    assert_eq!(
+        mem::take(&mut *events.borrow_mut()),
+        [
+            ("editor1", EditorEvent::Edited),
+            ("editor1", EditorEvent::BufferEdited),
+            ("editor2", EditorEvent::BufferEdited),
+        ]
+    );
+
+    // Mutating editor 2 will emit an `Edited` event only for that editor.
+    editor2.update(cx, |editor, cx| editor.delete(&Delete, cx));
+    assert_eq!(
+        mem::take(&mut *events.borrow_mut()),
+        [
+            ("editor2", EditorEvent::Edited),
+            ("editor1", EditorEvent::BufferEdited),
+            ("editor2", EditorEvent::BufferEdited),
+        ]
+    );
+
+    // Undoing on editor 1 will emit an `Edited` event only for that editor.
+    editor1.update(cx, |editor, cx| editor.undo(&Undo, cx));
+    assert_eq!(
+        mem::take(&mut *events.borrow_mut()),
+        [
+            ("editor1", EditorEvent::Edited),
+            ("editor1", EditorEvent::BufferEdited),
+            ("editor2", EditorEvent::BufferEdited),
+        ]
+    );
+
+    // Redoing on editor 1 will emit an `Edited` event only for that editor.
+    editor1.update(cx, |editor, cx| editor.redo(&Redo, cx));
+    assert_eq!(
+        mem::take(&mut *events.borrow_mut()),
+        [
+            ("editor1", EditorEvent::Edited),
+            ("editor1", EditorEvent::BufferEdited),
+            ("editor2", EditorEvent::BufferEdited),
+        ]
+    );
+
+    // Undoing on editor 2 will emit an `Edited` event only for that editor.
+    editor2.update(cx, |editor, cx| editor.undo(&Undo, cx));
+    assert_eq!(
+        mem::take(&mut *events.borrow_mut()),
+        [
+            ("editor2", EditorEvent::Edited),
+            ("editor1", EditorEvent::BufferEdited),
+            ("editor2", EditorEvent::BufferEdited),
+        ]
+    );
+
+    // Redoing on editor 2 will emit an `Edited` event only for that editor.
+    editor2.update(cx, |editor, cx| editor.redo(&Redo, cx));
+    assert_eq!(
+        mem::take(&mut *events.borrow_mut()),
+        [
+            ("editor2", EditorEvent::Edited),
+            ("editor1", EditorEvent::BufferEdited),
+            ("editor2", EditorEvent::BufferEdited),
+        ]
+    );
+
+    // No event is emitted when the mutation is a no-op.
+    editor2.update(cx, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([0..0]));
+
+        editor.backspace(&Backspace, cx);
+    });
+    assert_eq!(mem::take(&mut *events.borrow_mut()), []);
+}
 
 #[gpui::test]
 fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
@@ -515,123 +514,123 @@ fn test_clone(cx: &mut TestAppContext) {
 }
 
 //todo!(editor navigate)
-// #[gpui::test]
-// async fn test_navigation_history(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     use workspace::item::Item;
-
-//     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();
-
-//     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)));
-
-//             fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option<NavigationEntry> {
-//                 editor.nav_history.as_mut().unwrap().pop_backward(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());
-
-//             // 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());
-
-//             // 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());
-
-//             // 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)
-//             );
-
-//             editor
-//         })
-//     });
-// }
+#[gpui::test]
+async fn test_navigation_history(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    use workspace::item::Item;
+
+    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();
+
+    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)));
+
+            fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option<NavigationEntry> {
+                editor.nav_history.as_mut().unwrap().pop_backward(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());
+
+            // 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());
+
+            // 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());
+
+            // 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)
+            );
+
+            editor
+        })
+    });
+}
 
 #[gpui::test]
 fn test_cancel(cx: &mut TestAppContext) {
@@ -959,55 +958,55 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
 }
 
 //todo!(finish editor tests)
-// #[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)
-//     });
-//     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())]
-//         );
-//     });
-// }
+#[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)
+    });
+    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())]
+        );
+    });
+}
 
 #[gpui::test]
 fn test_beginning_end_of_line(cx: &mut TestAppContext) {
@@ -1225,532 +1224,551 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
 }
 
 //todo!(finish editor tests)
-// #[gpui::test]
-// fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
-//     init_test(cx, |_| {});
-
-//     let view = cx.add_window(|cx| {
-//         let buffer = MultiBuffer::build_simple("use one::{\n    two::three::four::five\n};", cx);
-//         build_editor(buffer, cx)
-//     });
-
-//     view.update(cx, |view, cx| {
-//         view.set_wrap_width(Some(140.0.into()), cx);
-//         assert_eq!(
-//             view.display_text(cx),
-//             "use one::{\n    two::three::\n    four::five\n};"
-//         );
-
-//         view.change_selections(None, cx, |s| {
-//             s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]);
-//         });
-
-//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)]
-//         );
-
-//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
-//         );
-
-//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
-//         );
-
-//         view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)]
-//         );
-
-//         view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
-//         );
-
-//         view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
-//         assert_eq!(
-//             view.selections.display_ranges(cx),
-//             &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
-//         );
-//     });
-// }
-
-//todo!(simulate_resize)
-// #[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;
-
-//     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);
-
-//     cx.set_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_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!(
-//             editor.snapshot(cx).scroll_position(),
-//             gpui::Point::new(0., 0.)
-//         );
-//         editor.scroll_screen(&ScrollAmount::Page(1.), cx);
-//         assert_eq!(
-//             editor.snapshot(cx).scroll_position(),
-//             gpui::Point::new(0., 3.)
-//         );
-//         editor.scroll_screen(&ScrollAmount::Page(1.), cx);
-//         assert_eq!(
-//             editor.snapshot(cx).scroll_position(),
-//             gpui::Point::new(0., 6.)
-//         );
-//         editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
-//         assert_eq!(
-//             editor.snapshot(cx).scroll_position(),
-//             gpui::Point::new(0., 3.)
-//         );
-
-//         editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
-//         assert_eq!(
-//             editor.snapshot(cx).scroll_position(),
-//             gpui::Point::new(0., 1.)
-//         );
-//         editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
-//         assert_eq!(
-//             editor.snapshot(cx).scroll_position(),
-//             gpui::Point::new(0., 3.)
-//         );
-//     });
-// }
-
-// #[gpui::test]
-// async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
-//     let mut cx = EditorTestContext::new(cx).await;
-
-//     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(gpui::Point::new(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(),
-//             gpui::Point::new(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(),
-//             gpui::Point::new(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(),
-//             gpui::Point::new(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(),
-//             gpui::Point::new(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(gpui::Point::new(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) {
+fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
     let view = cx.add_window(|cx| {
-        let buffer = MultiBuffer::build_simple("one two three four", cx);
-        build_editor(buffer.clone(), cx)
+        let buffer = MultiBuffer::build_simple("use one::{\n    two::three::four::five\n};", cx);
+        build_editor(buffer, 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.set_wrap_width(Some(140.0.into()), cx);
+        assert_eq!(
+            view.display_text(cx),
+            "use one::{\n    two::three::\n    four::five\n};"
+        );
 
-    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),
-            ])
+            s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]);
         });
-        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, |_| {});
+        view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)]
+        );
 
-    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(1, 14)..DisplayPoint::new(1, 14)]
+        );
 
-    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_next_word_end(&MoveToNextWordEnd, 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_next_word_end(&MoveToNextWordEnd, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)]
+        );
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
+        );
+
+        view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+        assert_eq!(
+            view.selections.display_ranges(cx),
+            &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
+        );
     });
 }
 
+//todo!(simulate_resize)
 #[gpui::test]
-fn test_newline_with_old_selections(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 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),
-            ])
-        });
+    let line_height = cx.editor(|editor, cx| {
         editor
+            .style()
+            .unwrap()
+            .text
+            .line_height_in_pixels(cx.rem_size())
     });
+    cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
 
-    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()
+    cx.set_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_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()
+            .unwrap()
+            .text
+            .line_height_in_pixels(cx.rem_size())
+    });
+    let window = cx.window;
+    cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
+
+    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(),
+            gpui::Point::new(0., 0.)
+        );
+        editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+        assert_eq!(
+            editor.snapshot(cx).scroll_position(),
+            gpui::Point::new(0., 3.)
+        );
+        editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+        assert_eq!(
+            editor.snapshot(cx).scroll_position(),
+            gpui::Point::new(0., 6.)
+        );
+        editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
+        assert_eq!(
+            editor.snapshot(cx).scroll_position(),
+            gpui::Point::new(0., 3.)
+        );
+
+        editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
+        assert_eq!(
+            editor.snapshot(cx).scroll_position(),
+            gpui::Point::new(0., 1.)
+        );
+        editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
+        assert_eq!(
+            editor.snapshot(cx).scroll_position(),
+            gpui::Point::new(0., 3.)
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let line_height = cx.update_editor(|editor, cx| {
+        editor.set_vertical_scroll_margin(2, cx);
+        editor
+            .style()
+            .unwrap()
+            .text
+            .line_height_in_pixels(cx.rem_size())
+    });
+    let window = cx.window;
+    cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
+
+    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(),
+            gpui::Point::new(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(),
+            gpui::Point::new(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(),
+            gpui::Point::new(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(),
+            gpui::Point::new(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()
+            .unwrap()
+            .text
+            .line_height_in_pixels(cx.rem_size())
+    });
+    let window = cx.window;
+    cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
+    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)
+    });
+
+    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)
+    });
+
+    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
+    });
+
+    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()

crates/editor2/src/element.rs 🔗

@@ -9,9 +9,11 @@ use crate::{
         self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
     },
     link_go_to_definition::{
-        go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
-        update_inlay_link_and_hover_points, GoToDefinitionTrigger,
+        go_to_fetched_definition, go_to_fetched_type_definition, show_link_definition,
+        update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger,
+        LinkGoToDefinitionState,
     },
+    mouse_context_menu,
     scroll::scroll_amount::ScrollAmount,
     CursorShape, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
     HalfPageDown, HalfPageUp, LineDown, LineUp, MoveDown, OpenExcerpts, PageDown, PageUp, Point,
@@ -19,14 +21,15 @@ use crate::{
 };
 use anyhow::Result;
 use collections::{BTreeMap, HashMap};
+use git::diff::DiffHunkStatus;
 use gpui::{
-    div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
-    BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId,
-    ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement,
-    IntoElement, LineLayout, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
-    ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString, Size,
-    StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View,
-    ViewContext, WeakView, WindowContext, WrappedLine,
+    div, overlay, point, px, relative, size, transparent_black, Action, AnchorCorner, AnyElement,
+    AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, CursorStyle,
+    DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, EntityId, Hsla,
+    InteractiveBounds, InteractiveElement, IntoElement, LineLayout, ModifiersChangedEvent,
+    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce,
+    ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement,
+    Style, Styled, TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
@@ -48,8 +51,10 @@ use std::{
 };
 use sum_tree::Bias;
 use theme::{ActiveTheme, PlayerColor};
-use ui::prelude::*;
-use ui::{h_stack, IconButton, Tooltip};
+use ui::{
+    h_stack, ButtonLike, ButtonStyle, Disclosure, IconButton, IconElement, IconSize, Label, Tooltip,
+};
+use ui::{prelude::*, Icon};
 use util::ResultExt;
 use workspace::item::Item;
 
@@ -139,8 +144,6 @@ impl EditorElement {
         register_action(view, cx, Editor::move_right);
         register_action(view, cx, Editor::move_down);
         register_action(view, cx, Editor::move_up);
-        // on_action(cx, Editor::new_file); todo!()
-        // on_action(cx, Editor::new_file_in_direction); todo!()
         register_action(view, cx, Editor::cancel);
         register_action(view, cx, Editor::newline);
         register_action(view, cx, Editor::newline_above);
@@ -263,7 +266,7 @@ impl EditorElement {
         register_action(view, cx, Editor::fold_selected_ranges);
         register_action(view, cx, Editor::show_completions);
         register_action(view, cx, Editor::toggle_code_actions);
-        // on_action(cx, Editor::open_excerpts); todo!()
+        register_action(view, cx, Editor::open_excerpts);
         register_action(view, cx, Editor::toggle_soft_wrap);
         register_action(view, cx, Editor::toggle_inlay_hints);
         register_action(view, cx, hover_popover::hover);
@@ -312,7 +315,57 @@ impl EditorElement {
         register_action(view, cx, Editor::context_menu_last);
     }
 
-    fn mouse_down(
+    fn register_key_listeners(&self, cx: &mut WindowContext) {
+        cx.on_key_event({
+            let editor = self.editor.clone();
+            move |event: &ModifiersChangedEvent, phase, cx| {
+                if phase != DispatchPhase::Bubble {
+                    return;
+                }
+
+                if editor.update(cx, |editor, cx| Self::modifiers_changed(editor, event, cx)) {
+                    cx.stop_propagation();
+                }
+            }
+        });
+    }
+
+    pub(crate) fn modifiers_changed(
+        editor: &mut Editor,
+        event: &ModifiersChangedEvent,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
+        let pending_selection = editor.has_pending_selection();
+
+        if let Some(point) = &editor.link_go_to_definition_state.last_trigger_point {
+            if event.command && !pending_selection {
+                let point = point.clone();
+                let snapshot = editor.snapshot(cx);
+                let kind = point.definition_kind(event.shift);
+
+                show_link_definition(kind, editor, point, snapshot, cx);
+                return false;
+            }
+        }
+
+        {
+            if editor.link_go_to_definition_state.symbol_range.is_some()
+                || !editor.link_go_to_definition_state.definitions.is_empty()
+            {
+                editor.link_go_to_definition_state.symbol_range.take();
+                editor.link_go_to_definition_state.definitions.clear();
+                cx.notify();
+            }
+
+            editor.link_go_to_definition_state.task = None;
+
+            editor.clear_highlights::<LinkGoToDefinitionState>(cx);
+        }
+
+        false
+    }
+
+    fn mouse_left_down(
         editor: &mut Editor,
         event: &MouseDownEvent,
         position_map: &PositionMap,
@@ -365,25 +418,25 @@ impl EditorElement {
         true
     }
 
-    // fn mouse_right_down(
-    //     editor: &mut Editor,
-    //     position: gpui::Point<Pixels>,
-    //     position_map: &PositionMap,
-    //     text_bounds: Bounds<Pixels>,
-    //     cx: &mut EventContext<Editor>,
-    // ) -> bool {
-    //     if !text_bounds.contains_point(position) {
-    //         return false;
-    //     }
-    //     let point_for_position = position_map.point_for_position(text_bounds, position);
-    //     mouse_context_menu::deploy_context_menu(
-    //         editor,
-    //         position,
-    //         point_for_position.previous_valid,
-    //         cx,
-    //     );
-    //     true
-    // }
+    fn mouse_right_down(
+        editor: &mut Editor,
+        event: &MouseDownEvent,
+        position_map: &PositionMap,
+        text_bounds: Bounds<Pixels>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
+        if !text_bounds.contains_point(&event.position) {
+            return false;
+        }
+        let point_for_position = position_map.point_for_position(text_bounds, event.position);
+        mouse_context_menu::deploy_context_menu(
+            editor,
+            event.position,
+            point_for_position.previous_valid,
+            cx,
+        );
+        true
+    }
 
     fn mouse_up(
         editor: &mut Editor,
@@ -725,87 +778,85 @@ impl EditorElement {
     }
 
     fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut WindowContext) {
-        // todo!()
-        // let diff_style = &theme::current(cx).editor.diff.clone();
-        // let line_height = layout.position_map.line_height;
-
-        // let scroll_position = layout.position_map.snapshot.scroll_position();
-        // let scroll_top = scroll_position.y * line_height;
-
-        // for hunk in &layout.display_hunks {
-        //     let (display_row_range, status) = match hunk {
-        //         //TODO: This rendering is entirely a horrible hack
-        //         &DisplayDiffHunk::Folded { display_row: row } => {
-        //             let start_y = row as f32 * line_height - scroll_top;
-        //             let end_y = start_y + line_height;
-
-        //             let width = diff_style.removed_width_em * line_height;
-        //             let highlight_origin = bounds.origin + point(-width, start_y);
-        //             let highlight_size = point(width * 2., end_y - start_y);
-        //             let highlight_bounds = Bounds::<Pixels>::new(highlight_origin, highlight_size);
-
-        //             cx.paint_quad(Quad {
-        //                 bounds: highlight_bounds,
-        //                 background: Some(diff_style.modified),
-        //                 border: Border::new(0., Color::transparent_black()).into(),
-        //                 corner_radii: (1. * line_height).into(),
-        //             });
-
-        //             continue;
-        //         }
-
-        //         DisplayDiffHunk::Unfolded {
-        //             display_row_range,
-        //             status,
-        //         } => (display_row_range, status),
-        //     };
-
-        //     let color = match status {
-        //         DiffHunkStatus::Added => diff_style.inserted,
-        //         DiffHunkStatus::Modified => diff_style.modified,
-
-        //         //TODO: This rendering is entirely a horrible hack
-        //         DiffHunkStatus::Removed => {
-        //             let row = display_row_range.start;
-
-        //             let offset = line_height / 2.;
-        //             let start_y = row as f32 * line_height - offset - scroll_top;
-        //             let end_y = start_y + line_height;
-
-        //             let width = diff_style.removed_width_em * line_height;
-        //             let highlight_origin = bounds.origin + point(-width, start_y);
-        //             let highlight_size = point(width * 2., end_y - start_y);
-        //             let highlight_bounds = Bounds::<Pixels>::new(highlight_origin, highlight_size);
-
-        //             cx.paint_quad(Quad {
-        //                 bounds: highlight_bounds,
-        //                 background: Some(diff_style.deleted),
-        //                 border: Border::new(0., Color::transparent_black()).into(),
-        //                 corner_radii: (1. * line_height).into(),
-        //             });
-
-        //             continue;
-        //         }
-        //     };
-
-        //     let start_row = display_row_range.start;
-        //     let end_row = display_row_range.end;
-
-        //     let start_y = start_row as f32 * line_height - scroll_top;
-        //     let end_y = end_row as f32 * line_height - scroll_top;
-
-        //     let width = diff_style.width_em * line_height;
-        //     let highlight_origin = bounds.origin + point(-width, start_y);
-        //     let highlight_size = point(width * 2., end_y - start_y);
-        //     let highlight_bounds = Bounds::<Pixels>::new(highlight_origin, highlight_size);
-
-        //     cx.paint_quad(Quad {
-        //         bounds: highlight_bounds,
-        //         background: Some(color),
-        //         border: Border::new(0., Color::transparent_black()).into(),
-        //         corner_radii: (diff_style.corner_radius * line_height).into(),
-        //     });
-        // }
+        let line_height = layout.position_map.line_height;
+
+        let scroll_position = layout.position_map.snapshot.scroll_position();
+        let scroll_top = scroll_position.y * line_height;
+
+        for hunk in &layout.display_hunks {
+            let (display_row_range, status) = match hunk {
+                //TODO: This rendering is entirely a horrible hack
+                &DisplayDiffHunk::Folded { display_row: row } => {
+                    let start_y = row as f32 * line_height - scroll_top;
+                    let end_y = start_y + line_height;
+
+                    let width = 0.275 * line_height;
+                    let highlight_origin = bounds.origin + point(-width, start_y);
+                    let highlight_size = size(width * 2., end_y - start_y);
+                    let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
+                    cx.paint_quad(
+                        highlight_bounds,
+                        Corners::all(1. * line_height),
+                        gpui::yellow(), // todo!("use the right color")
+                        Edges::default(),
+                        transparent_black(),
+                    );
+
+                    continue;
+                }
+
+                DisplayDiffHunk::Unfolded {
+                    display_row_range,
+                    status,
+                } => (display_row_range, status),
+            };
+
+            let color = match status {
+                DiffHunkStatus::Added => gpui::green(), // todo!("use the appropriate color")
+                DiffHunkStatus::Modified => gpui::yellow(), // todo!("use the appropriate color")
+
+                //TODO: This rendering is entirely a horrible hack
+                DiffHunkStatus::Removed => {
+                    let row = display_row_range.start;
+
+                    let offset = line_height / 2.;
+                    let start_y = row as f32 * line_height - offset - scroll_top;
+                    let end_y = start_y + line_height;
+
+                    let width = 0.275 * line_height;
+                    let highlight_origin = bounds.origin + point(-width, start_y);
+                    let highlight_size = size(width * 2., end_y - start_y);
+                    let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
+                    cx.paint_quad(
+                        highlight_bounds,
+                        Corners::all(1. * line_height),
+                        gpui::red(), // todo!("use the right color")
+                        Edges::default(),
+                        transparent_black(),
+                    );
+
+                    continue;
+                }
+            };
+
+            let start_row = display_row_range.start;
+            let end_row = display_row_range.end;
+
+            let start_y = start_row as f32 * line_height - scroll_top;
+            let end_y = end_row as f32 * line_height - scroll_top;
+
+            let width = 0.275 * line_height;
+            let highlight_origin = bounds.origin + point(-width, start_y);
+            let highlight_size = size(width * 2., end_y - start_y);
+            let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
+            cx.paint_quad(
+                highlight_bounds,
+                Corners::all(0.05 * line_height),
+                color, // todo!("use the right color")
+                Edges::default(),
+                transparent_black(),
+            );
+        }
     }
 
     fn paint_text(
@@ -831,15 +882,19 @@ impl EditorElement {
                 bounds: text_bounds,
             }),
             |cx| {
-                // todo!("cursor region")
-                // cx.scene().push_cursor_region(CursorRegion {
-                //     bounds,
-                //     style: if !editor.link_go_to_definition_state.definitions.is_empty {
-                //         CursorStyle::PointingHand
-                //     } else {
-                //         CursorStyle::IBeam
-                //     },
-                // });
+                if text_bounds.contains_point(&cx.mouse_position()) {
+                    if self
+                        .editor
+                        .read(cx)
+                        .link_go_to_definition_state
+                        .definitions
+                        .is_empty()
+                    {
+                        cx.set_cursor_style(CursorStyle::IBeam);
+                    } else {
+                        cx.set_cursor_style(CursorStyle::PointingHand);
+                    }
+                }
 
                 let fold_corner_radius = 0.15 * layout.position_map.line_height;
                 cx.with_element_id(Some("folds"), |cx| {
@@ -1138,6 +1193,22 @@ impl EditorElement {
                             }
                         }
                     }
+
+                    if let Some(mouse_context_menu) =
+                        self.editor.read(cx).mouse_context_menu.as_ref()
+                    {
+                        let element = overlay()
+                            .position(mouse_context_menu.position)
+                            .child(mouse_context_menu.context_menu.clone())
+                            .anchor(AnchorCorner::TopLeft)
+                            .snap_to_window();
+                        element.draw(
+                            gpui::Point::default(),
+                            size(AvailableSpace::MinContent, AvailableSpace::MinContent),
+                            cx,
+                            |_, _| {},
+                        );
+                    }
                 })
             },
         )
@@ -1662,11 +1733,6 @@ impl EditorElement {
         cx: &mut WindowContext,
     ) -> LayoutState {
         self.editor.update(cx, |editor, cx| {
-            // let mut size = constraint.max;
-            // if size.x.is_infinite() {
-            //     unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
-            // }
-
             let snapshot = editor.snapshot(cx);
             let style = self.style.clone();
 
@@ -1689,7 +1755,7 @@ impl EditorElement {
             let gutter_width;
             let gutter_margin;
             if snapshot.show_gutter {
-                let descent = cx.text_system().descent(font_id, font_size).unwrap();
+                let descent = cx.text_system().descent(font_id, font_size);
 
                 let gutter_padding_factor = 3.5;
                 gutter_padding = (em_width * gutter_padding_factor).round();
@@ -1702,6 +1768,7 @@ impl EditorElement {
             };
 
             editor.gutter_width = gutter_width;
+
             let text_width = bounds.size.width - gutter_width;
             let overscroll = size(em_width, px(0.));
             let snapshot = {
@@ -1728,25 +1795,6 @@ impl EditorElement {
                 .collect::<SmallVec<[_; 2]>>();
 
             let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height;
-            // todo!("this should happen during layout")
-            let editor_mode = snapshot.mode;
-            if let EditorMode::AutoHeight { max_lines } = editor_mode {
-                todo!()
-                //     size.set_y(
-                //         scroll_height
-                //             .min(constraint.max_along(Axis::Vertical))
-                //             .max(constraint.min_along(Axis::Vertical))
-                //             .max(line_height)
-                //             .min(line_height * max_lines as f32),
-                //     )
-            } else if let EditorMode::SingleLine = editor_mode {
-                bounds.size.height = line_height.min(bounds.size.height);
-            }
-            // todo!()
-            // else if size.y.is_infinite() {
-            //     //     size.set_y(scroll_height);
-            // }
-            //
             let gutter_size = size(gutter_width, bounds.size.height);
             let text_size = size(text_width, bounds.size.height);
 
@@ -2064,7 +2112,7 @@ impl EditorElement {
                 .unwrap();
 
             LayoutState {
-                mode: editor_mode,
+                mode: snapshot.mode,
                 position_map: Arc::new(PositionMap {
                     size: bounds.size,
                     scroll_position: point(
@@ -2177,7 +2225,8 @@ impl EditorElement {
                         .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_handler = project::File::from_dyn(buffer.file()).map(|file| {
                         let jump_path = ProjectPath {
                             worktree_id: file.worktree_id(cx),
                             path: file.path.clone(),
@@ -2188,11 +2237,11 @@ impl EditorElement {
                             .map_or(range.context.start, |primary| primary.start);
                         let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
 
-                        IconButton::new(block_id, ui::Icon::ArrowUpRight)
-                            .on_click(cx.listener_for(&self.editor, move |editor, e, cx| {
-                                editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
-                            }))
-                            .tooltip(|cx| Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx))
+                        let jump_handler = cx.listener_for(&self.editor, move |editor, e, cx| {
+                            editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
+                        });
+
+                        jump_handler
                     });
 
                     let element = if *starts_new_buffer {
@@ -2207,25 +2256,108 @@ impl EditorElement {
                                 .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/"));
                         }
 
-                        h_stack()
-                            .id("path header block")
-                            .size_full()
-                            .bg(gpui::red())
-                            .child(
-                                filename
-                                    .map(SharedString::from)
-                                    .unwrap_or_else(|| "untitled".into()),
-                            )
-                            .children(parent_path)
-                            .children(jump_icon) // .p_x(gutter_padding)
+                        let is_open = true;
+
+                        div().id("path header container").size_full().p_1p5().child(
+                            h_stack()
+                                .id("path header block")
+                                .py_1p5()
+                                .pl_3()
+                                .pr_2()
+                                .rounded_lg()
+                                .shadow_md()
+                                .border()
+                                .border_color(cx.theme().colors().border)
+                                .bg(cx.theme().colors().editor_subheader_background)
+                                .justify_between()
+                                .cursor_pointer()
+                                .hover(|style| style.bg(cx.theme().colors().element_hover))
+                                .on_click(cx.listener(|_editor, _event, _cx| {
+                                    // TODO: Implement collapsing path headers
+                                    todo!("Clicking path header")
+                                }))
+                                .child(
+                                    h_stack()
+                                        .gap_3()
+                                        // TODO: Add open/close state and toggle action
+                                        .child(
+                                            div().border().border_color(gpui::red()).child(
+                                                ButtonLike::new("path-header-disclosure-control")
+                                                    .style(ButtonStyle::Subtle)
+                                                    .child(IconElement::new(match is_open {
+                                                        true => Icon::ChevronDown,
+                                                        false => Icon::ChevronRight,
+                                                    })),
+                                            ),
+                                        )
+                                        .child(
+                                            h_stack()
+                                                .gap_2()
+                                                .child(Label::new(
+                                                    filename
+                                                        .map(SharedString::from)
+                                                        .unwrap_or_else(|| "untitled".into()),
+                                                ))
+                                                .when_some(parent_path, |then, path| {
+                                                    then.child(Label::new(path).color(Color::Muted))
+                                                }),
+                                        ),
+                                )
+                                .children(jump_handler.map(|jump_handler| {
+                                    IconButton::new(block_id, Icon::ArrowUpRight)
+                                        .style(ButtonStyle::Subtle)
+                                        .on_click(jump_handler)
+                                        .tooltip(|cx| {
+                                            Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)
+                                        })
+                                })), // .p_x(gutter_padding)
+                        )
                     } else {
                         let text_style = style.text.clone();
                         h_stack()
                             .id("collapsed context")
                             .size_full()
-                            .bg(gpui::red())
-                            .child("⋯")
-                            .children(jump_icon) // .p_x(gutter_padding)
+                            .gap(gutter_padding)
+                            .child(
+                                h_stack()
+                                    .justify_end()
+                                    .flex_none()
+                                    .w(gutter_width - gutter_padding)
+                                    .h_full()
+                                    .text_buffer(cx)
+                                    .text_color(cx.theme().colors().editor_line_number)
+                                    .child("..."),
+                            )
+                            .map(|this| {
+                                if let Some(jump_handler) = jump_handler {
+                                    this.child(
+                                        ButtonLike::new("jump to collapsed context")
+                                            .style(ButtonStyle::Transparent)
+                                            .full_width()
+                                            .on_click(jump_handler)
+                                            .tooltip(|cx| {
+                                                Tooltip::for_action(
+                                                    "Jump to Buffer",
+                                                    &OpenExcerpts,
+                                                    cx,
+                                                )
+                                            })
+                                            .child(
+                                                div()
+                                                    .h_px()
+                                                    .w_full()
+                                                    .bg(cx.theme().colors().border_variant)
+                                                    .group_hover("", |style| {
+                                                        style.bg(cx.theme().colors().border)
+                                                    }),
+                                            ),
+                                    )
+                                } else {
+                                    this.child(div().size_full().bg(gpui::green()))
+                                }
+                            })
+                        // .child("⋯")
+                        // .children(jump_icon) // .p_x(gutter_padding)
                     };
                     element.into_any()
                 }
@@ -2308,10 +2440,10 @@ impl EditorElement {
                     return;
                 }
 
-                let should_cancel = editor.update(cx, |editor, cx| {
+                let handled = editor.update(cx, |editor, cx| {
                     Self::scroll(editor, event, &position_map, &interactive_bounds, cx)
                 });
-                if should_cancel {
+                if handled {
                     cx.stop_propagation();
                 }
             }
@@ -2327,19 +2459,25 @@ impl EditorElement {
                     return;
                 }
 
-                let should_cancel = editor.update(cx, |editor, cx| {
-                    Self::mouse_down(
-                        editor,
-                        event,
-                        &position_map,
-                        text_bounds,
-                        gutter_bounds,
-                        &stacking_order,
-                        cx,
-                    )
-                });
+                let handled = match event.button {
+                    MouseButton::Left => editor.update(cx, |editor, cx| {
+                        Self::mouse_left_down(
+                            editor,
+                            event,
+                            &position_map,
+                            text_bounds,
+                            gutter_bounds,
+                            &stacking_order,
+                            cx,
+                        )
+                    }),
+                    MouseButton::Right => editor.update(cx, |editor, cx| {
+                        Self::mouse_right_down(editor, event, &position_map, text_bounds, cx)
+                    }),
+                    _ => false,
+                };
 
-                if should_cancel {
+                if handled {
                     cx.stop_propagation()
                 }
             }
@@ -2351,7 +2489,7 @@ impl EditorElement {
             let stacking_order = cx.stacking_order().clone();
 
             move |event: &MouseUpEvent, phase, cx| {
-                let should_cancel = editor.update(cx, |editor, cx| {
+                let handled = editor.update(cx, |editor, cx| {
                     Self::mouse_up(
                         editor,
                         event,
@@ -2362,26 +2500,11 @@ impl EditorElement {
                     )
                 });
 
-                if should_cancel {
+                if handled {
                     cx.stop_propagation()
                 }
             }
         });
-        //todo!()
-        // on_down(MouseButton::Right, {
-        //     let position_map = layout.position_map.clone();
-        //     move |event, editor, cx| {
-        //         if !Self::mouse_right_down(
-        //             editor,
-        //             event.position,
-        //             position_map.as_ref(),
-        //             text_bounds,
-        //             cx,
-        //         ) {
-        //             cx.propagate_event();
-        //         }
-        //     }
-        // });
         cx.on_mouse_event({
             let position_map = layout.position_map.clone();
             let editor = self.editor.clone();
@@ -2617,19 +2740,44 @@ impl Element for EditorElement {
         cx: &mut gpui::WindowContext,
     ) -> (gpui::LayoutId, Self::State) {
         self.editor.update(cx, |editor, cx| {
-            editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this.
+            editor.set_style(self.style.clone(), cx);
 
-            let rem_size = cx.rem_size();
-            let mut style = Style::default();
-            style.size.width = relative(1.).into();
-            style.size.height = match editor.mode {
+            let layout_id = match editor.mode {
                 EditorMode::SingleLine => {
-                    self.style.text.line_height_in_pixels(cx.rem_size()).into()
+                    let rem_size = cx.rem_size();
+                    let mut style = Style::default();
+                    style.size.width = relative(1.).into();
+                    style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
+                    cx.request_layout(&style, None)
+                }
+                EditorMode::AutoHeight { max_lines } => {
+                    let editor_handle = cx.view().clone();
+                    let max_line_number_width =
+                        self.max_line_number_width(&editor.snapshot(cx), cx);
+                    cx.request_measured_layout(
+                        Style::default(),
+                        move |known_dimensions, available_space, cx| {
+                            editor_handle
+                                .update(cx, |editor, cx| {
+                                    compute_auto_height_layout(
+                                        editor,
+                                        max_lines,
+                                        max_line_number_width,
+                                        known_dimensions,
+                                        cx,
+                                    )
+                                })
+                                .unwrap_or_default()
+                        },
+                    )
+                }
+                EditorMode::Full => {
+                    let mut style = Style::default();
+                    style.size.width = relative(1.).into();
+                    style.size.height = relative(1.).into();
+                    cx.request_layout(&style, None)
                 }
-                EditorMode::AutoHeight { .. } => todo!(),
-                EditorMode::Full => relative(1.).into(),
             };
-            let layout_id = cx.request_layout(&style, None);
 
             (layout_id, ())
         })
@@ -2657,6 +2805,7 @@ impl Element for EditorElement {
         let dispatch_context = self.editor.read(cx).dispatch_context(cx);
         cx.with_key_dispatch(dispatch_context, Some(focus_handle.clone()), |_, cx| {
             self.register_actions(cx);
+            self.register_key_listeners(cx);
 
             // We call with_z_index to establish a new stacking context.
             cx.with_z_index(0, |cx| {
@@ -2698,604 +2847,6 @@ impl IntoElement for EditorElement {
     }
 }
 
-// impl EditorElement {
-//     type LayoutState = LayoutState;
-//     type PaintState = ();
-
-//     fn layout(
-//         &mut self,
-//         constraint: SizeConstraint,
-//         editor: &mut Editor,
-//         cx: &mut ViewContext<Editor>,
-//     ) -> (gpui::Point<Pixels>, Self::LayoutState) {
-//         let mut size = constraint.max;
-//         if size.x.is_infinite() {
-//             unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
-//         }
-
-//         let snapshot = editor.snapshot(cx);
-//         let style = self.style.clone();
-
-//         let line_height = (style.text.font_size * style.line_height_scalar).round();
-
-//         let gutter_padding;
-//         let gutter_width;
-//         let gutter_margin;
-//         if snapshot.show_gutter {
-//             let em_width = style.text.em_width(cx.font_cache());
-//             gutter_padding = (em_width * style.gutter_padding_factor).round();
-//             gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
-//             gutter_margin = -style.text.descent(cx.font_cache());
-//         } else {
-//             gutter_padding = 0.0;
-//             gutter_width = 0.0;
-//             gutter_margin = 0.0;
-//         };
-
-//         let text_width = size.x - gutter_width;
-//         let em_width = style.text.em_width(cx.font_cache());
-//         let em_advance = style.text.em_advance(cx.font_cache());
-//         let overscroll = point(em_width, 0.);
-//         let snapshot = {
-//             editor.set_visible_line_count(size.y / line_height, cx);
-
-//             let editor_width = text_width - gutter_margin - overscroll.x - em_width;
-//             let wrap_width = match editor.soft_wrap_mode(cx) {
-//                 SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
-//                 SoftWrap::EditorWidth => editor_width,
-//                 SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
-//             };
-
-//             if editor.set_wrap_width(Some(wrap_width), cx) {
-//                 editor.snapshot(cx)
-//             } else {
-//                 snapshot
-//             }
-//         };
-
-//         let wrap_guides = editor
-//             .wrap_guides(cx)
-//             .iter()
-//             .map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
-//             .collect();
-
-//         let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height;
-//         if let EditorMode::AutoHeight { max_lines } = snapshot.mode {
-//             size.set_y(
-//                 scroll_height
-//                     .min(constraint.max_along(Axis::Vertical))
-//                     .max(constraint.min_along(Axis::Vertical))
-//                     .max(line_height)
-//                     .min(line_height * max_lines as f32),
-//             )
-//         } else if let EditorMode::SingleLine = snapshot.mode {
-//             size.set_y(line_height.max(constraint.min_along(Axis::Vertical)))
-//         } else if size.y.is_infinite() {
-//             size.set_y(scroll_height);
-//         }
-//         let gutter_size = point(gutter_width, size.y);
-//         let text_size = point(text_width, size.y);
-
-//         let autoscroll_horizontally = editor.autoscroll_vertically(size.y, line_height, cx);
-//         let mut snapshot = editor.snapshot(cx);
-
-//         let scroll_position = snapshot.scroll_position();
-//         // The scroll position is a fractional point, the whole number of which represents
-//         // the top of the window in terms of display rows.
-//         let start_row = scroll_position.y as u32;
-//         let height_in_lines = size.y / line_height;
-//         let max_row = snapshot.max_point().row();
-
-//         // Add 1 to ensure selections bleed off screen
-//         let end_row = 1 + cmp::min(
-//             (scroll_position.y + height_in_lines).ceil() as u32,
-//             max_row,
-//         );
-
-//         let start_anchor = if start_row == 0 {
-//             Anchor::min()
-//         } else {
-//             snapshot
-//                 .buffer_snapshot
-//                 .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
-//         };
-//         let end_anchor = if end_row > max_row {
-//             Anchor::max
-//         } else {
-//             snapshot
-//                 .buffer_snapshot
-//                 .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
-//         };
-
-//         let mut selections: Vec<(SelectionStyle, Vec<SelectionLayout>)> = Vec::new();
-//         let mut active_rows = BTreeMap::new();
-//         let mut fold_ranges = Vec::new();
-//         let is_singleton = editor.is_singleton(cx);
-
-//         let highlighted_rows = editor.highlighted_rows();
-//         let theme = theme::current(cx);
-//         let highlighted_ranges = editor.background_highlights_in_range(
-//             start_anchor..end_anchor,
-//             &snapshot.display_snapshot,
-//             theme.as_ref(),
-//         );
-
-//         fold_ranges.extend(
-//             snapshot
-//                 .folds_in_range(start_anchor..end_anchor)
-//                 .map(|anchor| {
-//                     let start = anchor.start.to_point(&snapshot.buffer_snapshot);
-//                     (
-//                         start.row,
-//                         start.to_display_point(&snapshot.display_snapshot)
-//                             ..anchor.end.to_display_point(&snapshot),
-//                     )
-//                 }),
-//         );
-
-//         let mut newest_selection_head = None;
-
-//         if editor.show_local_selections {
-//             let mut local_selections: Vec<Selection<Point>> = editor
-//                 .selections
-//                 .disjoint_in_range(start_anchor..end_anchor, cx);
-//             local_selections.extend(editor.selections.pending(cx));
-//             let mut layouts = Vec::new();
-//             let newest = editor.selections.newest(cx);
-//             for selection in local_selections.drain(..) {
-//                 let is_empty = selection.start == selection.end;
-//                 let is_newest = selection == newest;
-
-//                 let layout = SelectionLayout::new(
-//                     selection,
-//                     editor.selections.line_mode,
-//                     editor.cursor_shape,
-//                     &snapshot.display_snapshot,
-//                     is_newest,
-//                     true,
-//                 );
-//                 if is_newest {
-//                     newest_selection_head = Some(layout.head);
-//                 }
-
-//                 for row in cmp::max(layout.active_rows.start, start_row)
-//                     ..=cmp::min(layout.active_rows.end, end_row)
-//                 {
-//                     let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
-//                     *contains_non_empty_selection |= !is_empty;
-//                 }
-//                 layouts.push(layout);
-//             }
-
-//             selections.push((style.selection, layouts));
-//         }
-
-//         if let Some(collaboration_hub) = &editor.collaboration_hub {
-//             // When following someone, render the local selections in their color.
-//             if let Some(leader_id) = editor.leader_peer_id {
-//                 if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
-//                     if let Some(participant_index) = collaboration_hub
-//                         .user_participant_indices(cx)
-//                         .get(&collaborator.user_id)
-//                     {
-//                         if let Some((local_selection_style, _)) = selections.first_mut() {
-//                             *local_selection_style =
-//                                 style.selection_style_for_room_participant(participant_index.0);
-//                         }
-//                     }
-//                 }
-//             }
-
-//             let mut remote_selections = HashMap::default();
-//             for selection in snapshot.remote_selections_in_range(
-//                 &(start_anchor..end_anchor),
-//                 collaboration_hub.as_ref(),
-//                 cx,
-//             ) {
-//                 let selection_style = if let Some(participant_index) = selection.participant_index {
-//                     style.selection_style_for_room_participant(participant_index.0)
-//                 } else {
-//                     style.absent_selection
-//                 };
-
-//                 // Don't re-render the leader's selections, since the local selections
-//                 // match theirs.
-//                 if Some(selection.peer_id) == editor.leader_peer_id {
-//                     continue;
-//                 }
-
-//                 remote_selections
-//                     .entry(selection.replica_id)
-//                     .or_insert((selection_style, Vec::new()))
-//                     .1
-//                     .push(SelectionLayout::new(
-//                         selection.selection,
-//                         selection.line_mode,
-//                         selection.cursor_shape,
-//                         &snapshot.display_snapshot,
-//                         false,
-//                         false,
-//                     ));
-//             }
-
-//             selections.extend(remote_selections.into_values());
-//         }
-
-//         let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
-//         let show_scrollbars = match scrollbar_settings.show {
-//             ShowScrollbar::Auto => {
-//                 // Git
-//                 (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
-//                 ||
-//                 // Selections
-//                 (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty)
-//                 // Scrollmanager
-//                 || editor.scroll_manager.scrollbars_visible()
-//             }
-//             ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(),
-//             ShowScrollbar::Always => true,
-//             ShowScrollbar::Never => false,
-//         };
-
-//         let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
-//             .into_iter()
-//             .map(|(id, fold)| {
-//                 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);
-//             SelectionLayout::new(
-//                 newest,
-//                 editor.selections.line_mode,
-//                 editor.cursor_shape,
-//                 &snapshot.display_snapshot,
-//                 true,
-//                 true,
-//             )
-//             .head
-//         });
-
-//         let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
-//             start_row..end_row,
-//             &active_rows,
-//             head_for_relative,
-//             is_singleton,
-//             &snapshot,
-//             cx,
-//         );
-
-//         let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
-
-//         let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines);
-
-//         let mut max_visible_line_width = 0.0;
-//         let line_layouts =
-//             self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx);
-//         for line_with_invisibles in &line_layouts {
-//             if line_with_invisibles.line.width() > max_visible_line_width {
-//                 max_visible_line_width = line_with_invisibles.line.width();
-//             }
-//         }
-
-//         let style = self.style.clone();
-//         let longest_line_width = layout_line(
-//             snapshot.longest_row(),
-//             &snapshot,
-//             &style,
-//             cx.text_layout_cache(),
-//         )
-//         .width();
-//         let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.x;
-//         let em_width = style.text.em_width(cx.font_cache());
-//         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_max = point(
-//             ((scroll_width - text_size.x) / em_width).max(0.0),
-//             max_row as f32,
-//         );
-
-//         let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
-
-//         let autoscrolled = if autoscroll_horizontally {
-//             editor.autoscroll_horizontally(
-//                 start_row,
-//                 text_size.x,
-//                 scroll_width,
-//                 em_width,
-//                 &line_layouts,
-//                 cx,
-//             )
-//         } else {
-//             false
-//         };
-
-//         if clamped || autoscrolled {
-//             snapshot = editor.snapshot(cx);
-//         }
-
-//         let style = editor.style(cx);
-
-//         let mut context_menu = None;
-//         let mut code_actions_indicator = None;
-//         if let Some(newest_selection_head) = newest_selection_head {
-//             if (start_row..end_row).contains(&newest_selection_head.row()) {
-//                 if editor.context_menu_visible() {
-//                     context_menu =
-//                         editor.render_context_menu(newest_selection_head, style.clone(), cx);
-//                 }
-
-//                 let active = matches!(
-//                     editor.context_menu.read().as_ref(),
-//                     Some(crate::ContextMenu::CodeActions(_))
-//                 );
-
-//                 code_actions_indicator = editor
-//                     .render_code_actions_indicator(&style, active, cx)
-//                     .map(|indicator| (newest_selection_head.row(), indicator));
-//             }
-//         }
-
-//         let visible_rows = start_row..start_row + line_layouts.len() as u32;
-//         let mut hover = editor.hover_state.render(
-//             &snapshot,
-//             &style,
-//             visible_rows,
-//             editor.workspace.as_ref().map(|(w, _)| w.clone()),
-//             cx,
-//         );
-//         let mode = editor.mode;
-
-//         let mut fold_indicators = editor.render_fold_indicators(
-//             fold_statuses,
-//             &style,
-//             editor.gutter_hovered,
-//             line_height,
-//             gutter_margin,
-//             cx,
-//         );
-
-//         if let Some((_, context_menu)) = context_menu.as_mut() {
-//             context_menu.layout(
-//                 SizeConstraint {
-//                     min: gpui::Point::<Pixels>::zero(),
-//                     max: point(
-//                         cx.window_size().x * 0.7,
-//                         (12. * line_height).min((size.y - line_height) / 2.),
-//                     ),
-//                 },
-//                 editor,
-//                 cx,
-//             );
-//         }
-
-//         if let Some((_, indicator)) = code_actions_indicator.as_mut() {
-//             indicator.layout(
-//                 SizeConstraint::strict_along(
-//                     Axis::Vertical,
-//                     line_height * style.code_actions.vertical_scale,
-//                 ),
-//                 editor,
-//                 cx,
-//             );
-//         }
-
-//         for fold_indicator in fold_indicators.iter_mut() {
-//             if let Some(indicator) = fold_indicator.as_mut() {
-//                 indicator.layout(
-//                     SizeConstraint::strict_along(
-//                         Axis::Vertical,
-//                         line_height * style.code_actions.vertical_scale,
-//                     ),
-//                     editor,
-//                     cx,
-//                 );
-//             }
-//         }
-
-//         if let Some((_, hover_popovers)) = hover.as_mut() {
-//             for hover_popover in hover_popovers.iter_mut() {
-//                 hover_popover.layout(
-//                     SizeConstraint {
-//                         min: gpui::Point::<Pixels>::zero(),
-//                         max: point(
-//                             (120. * em_width) // Default size
-//                                 .min(size.x / 2.) // Shrink to half of the editor width
-//                                 .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
-//                             (16. * line_height) // Default size
-//                                 .min(size.y / 2.) // Shrink to half of the editor height
-//                                 .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
-//                         ),
-//                     },
-//                     editor,
-//                     cx,
-//                 );
-//             }
-//         }
-
-//         let invisible_symbol_font_size = self.style.text.font_size / 2.0;
-//         let invisible_symbol_style = RunStyle {
-//             color: self.style.whitespace,
-//             font_id: self.style.text.font_id,
-//             underline: Default::default(),
-//         };
-
-//         (
-//             size,
-//             LayoutState {
-//                 mode,
-//                 position_map: Arc::new(PositionMap {
-//                     size,
-//                     scroll_max,
-//                     line_layouts,
-//                     line_height,
-//                     em_width,
-//                     em_advance,
-//                     snapshot,
-//                 }),
-//                 visible_display_row_range: start_row..end_row,
-//                 wrap_guides,
-//                 gutter_size,
-//                 gutter_padding,
-//                 text_size,
-//                 scrollbar_row_range,
-//                 show_scrollbars,
-//                 is_singleton,
-//                 max_row,
-//                 gutter_margin,
-//                 active_rows,
-//                 highlighted_rows,
-//                 highlighted_ranges,
-//                 fold_ranges,
-//                 line_number_layouts,
-//                 display_hunks,
-//                 blocks,
-//                 selections,
-//                 context_menu,
-//                 code_actions_indicator,
-//                 fold_indicators,
-//                 tab_invisible: cx.text_layout_cache().layout_str(
-//                     "→",
-//                     invisible_symbol_font_size,
-//                     &[("→".len(), invisible_symbol_style)],
-//                 ),
-//                 space_invisible: cx.text_layout_cache().layout_str(
-//                     "•",
-//                     invisible_symbol_font_size,
-//                     &[("•".len(), invisible_symbol_style)],
-//                 ),
-//                 hover_popovers: hover,
-//             },
-//         )
-//     }
-
-//     fn paint(
-//         &mut self,
-//         bounds: Bounds<Pixels>,
-//         visible_bounds: Bounds<Pixels>,
-//         layout: &mut Self::LayoutState,
-//         editor: &mut Editor,
-//         cx: &mut ViewContext<Editor>,
-//     ) -> Self::PaintState {
-//         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
-//         cx.scene().push_layer(Some(visible_bounds));
-
-//         let gutter_bounds = Bounds::<Pixels>::new(bounds.origin, layout.gutter_size);
-//         let text_bounds = Bounds::<Pixels>::new(
-//             bounds.origin + point(layout.gutter_size.x, 0.0),
-//             layout.text_size,
-//         );
-
-//         Self::attach_mouse_handlers(
-//             &layout.position_map,
-//             layout.hover_popovers.is_some(),
-//             visible_bounds,
-//             text_bounds,
-//             gutter_bounds,
-//             bounds,
-//             cx,
-//         );
-
-//         self.paint_background(gutter_bounds, text_bounds, layout, cx);
-//         if layout.gutter_size.x > 0. {
-//             self.paint_gutter(gutter_bounds, visible_bounds, layout, editor, cx);
-//         }
-//         self.paint_text(text_bounds, visible_bounds, layout, editor, cx);
-
-//         cx.scene().push_layer(Some(bounds));
-//         if !layout.blocks.is_empty {
-//             self.paint_blocks(bounds, visible_bounds, layout, editor, cx);
-//         }
-//         self.paint_scrollbar(bounds, layout, &editor, cx);
-//         cx.scene().pop_layer();
-//         cx.scene().pop_layer();
-//     }
-
-//     fn rect_for_text_range(
-//         &self,
-//         range_utf16: Range<usize>,
-//         bounds: Bounds<Pixels>,
-//         _: Bounds<Pixels>,
-//         layout: &Self::LayoutState,
-//         _: &Self::PaintState,
-//         _: &Editor,
-//         _: &ViewContext<Editor>,
-//     ) -> Option<Bounds<Pixels>> {
-//         let text_bounds = Bounds::<Pixels>::new(
-//             bounds.origin + point(layout.gutter_size.x, 0.0),
-//             layout.text_size,
-//         );
-//         let content_origin = text_bounds.origin + point(layout.gutter_margin, 0.);
-//         let scroll_position = layout.position_map.snapshot.scroll_position();
-//         let start_row = scroll_position.y as u32;
-//         let scroll_top = scroll_position.y * layout.position_map.line_height;
-//         let scroll_left = scroll_position.x * layout.position_map.em_width;
-
-//         let range_start = OffsetUtf16(range_utf16.start)
-//             .to_display_point(&layout.position_map.snapshot.display_snapshot);
-//         if range_start.row() < start_row {
-//             return None;
-//         }
-
-//         let line = &layout
-//             .position_map
-//             .line_layouts
-//             .get((range_start.row() - start_row) as usize)?
-//             .line;
-//         let range_start_x = line.x_for_index(range_start.column() as usize);
-//         let range_start_y = range_start.row() as f32 * layout.position_map.line_height;
-//         Some(Bounds::<Pixels>::new(
-//             content_origin
-//                 + point(
-//                     range_start_x,
-//                     range_start_y + layout.position_map.line_height,
-//                 )
-//                 - point(scroll_left, scroll_top),
-//             point(
-//                 layout.position_map.em_width,
-//                 layout.position_map.line_height,
-//             ),
-//         ))
-//     }
-
-//     fn debug(
-//         &self,
-//         bounds: Bounds<Pixels>,
-//         _: &Self::LayoutState,
-//         _: &Self::PaintState,
-//         _: &Editor,
-//         _: &ViewContext<Editor>,
-//     ) -> json::Value {
-//         json!({
-//             "type": "BufferElement",
-//             "bounds": bounds.to_json()
-//         })
-//     }
-// }
-
 type BufferRow = u32;
 
 pub struct LayoutState {

crates/editor2/src/git.rs 🔗

@@ -88,195 +88,195 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
     }
 }
 
-// #[cfg(any(test, feature = "test_support"))]
-// mod tests {
-//     // use crate::editor_tests::init_test;
-//     use crate::Point;
-//     use gpui::TestAppContext;
-//     use multi_buffer::{ExcerptRange, MultiBuffer};
-//     use project::{FakeFs, Project};
-//     use unindent::Unindent;
-//     #[gpui::test]
-//     async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
-//         use git::diff::DiffHunkStatus;
-//         init_test(cx, |_| {});
+#[cfg(test)]
+mod tests {
+    use crate::editor_tests::init_test;
+    use crate::Point;
+    use gpui::{Context, TestAppContext};
+    use multi_buffer::{ExcerptRange, MultiBuffer};
+    use project::{FakeFs, Project};
+    use unindent::Unindent;
+    #[gpui::test]
+    async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
+        use git::diff::DiffHunkStatus;
+        init_test(cx, |_| {});
 
-//         let fs = FakeFs::new(cx.background());
-//         let project = Project::test(fs, [], cx).await;
+        let fs = FakeFs::new(cx.background_executor.clone());
+        let project = Project::test(fs, [], cx).await;
 
-//         // buffer has two modified hunks with two rows each
-//         let buffer_1 = project
-//             .update(cx, |project, cx| {
-//                 project.create_buffer(
-//                     "
-//                         1.zero
-//                         1.ONE
-//                         1.TWO
-//                         1.three
-//                         1.FOUR
-//                         1.FIVE
-//                         1.six
-//                     "
-//                     .unindent()
-//                     .as_str(),
-//                     None,
-//                     cx,
-//                 )
-//             })
-//             .unwrap();
-//         buffer_1.update(cx, |buffer, cx| {
-//             buffer.set_diff_base(
-//                 Some(
-//                     "
-//                         1.zero
-//                         1.one
-//                         1.two
-//                         1.three
-//                         1.four
-//                         1.five
-//                         1.six
-//                     "
-//                     .unindent(),
-//                 ),
-//                 cx,
-//             );
-//         });
+        // buffer has two modified hunks with two rows each
+        let buffer_1 = project
+            .update(cx, |project, cx| {
+                project.create_buffer(
+                    "
+                        1.zero
+                        1.ONE
+                        1.TWO
+                        1.three
+                        1.FOUR
+                        1.FIVE
+                        1.six
+                    "
+                    .unindent()
+                    .as_str(),
+                    None,
+                    cx,
+                )
+            })
+            .unwrap();
+        buffer_1.update(cx, |buffer, cx| {
+            buffer.set_diff_base(
+                Some(
+                    "
+                        1.zero
+                        1.one
+                        1.two
+                        1.three
+                        1.four
+                        1.five
+                        1.six
+                    "
+                    .unindent(),
+                ),
+                cx,
+            );
+        });
 
-//         // buffer has a deletion hunk and an insertion hunk
-//         let buffer_2 = project
-//             .update(cx, |project, cx| {
-//                 project.create_buffer(
-//                     "
-//                         2.zero
-//                         2.one
-//                         2.two
-//                         2.three
-//                         2.four
-//                         2.five
-//                         2.six
-//                     "
-//                     .unindent()
-//                     .as_str(),
-//                     None,
-//                     cx,
-//                 )
-//             })
-//             .unwrap();
-//         buffer_2.update(cx, |buffer, cx| {
-//             buffer.set_diff_base(
-//                 Some(
-//                     "
-//                         2.zero
-//                         2.one
-//                         2.one-and-a-half
-//                         2.two
-//                         2.three
-//                         2.four
-//                         2.six
-//                     "
-//                     .unindent(),
-//                 ),
-//                 cx,
-//             );
-//         });
+        // buffer has a deletion hunk and an insertion hunk
+        let buffer_2 = project
+            .update(cx, |project, cx| {
+                project.create_buffer(
+                    "
+                        2.zero
+                        2.one
+                        2.two
+                        2.three
+                        2.four
+                        2.five
+                        2.six
+                    "
+                    .unindent()
+                    .as_str(),
+                    None,
+                    cx,
+                )
+            })
+            .unwrap();
+        buffer_2.update(cx, |buffer, cx| {
+            buffer.set_diff_base(
+                Some(
+                    "
+                        2.zero
+                        2.one
+                        2.one-and-a-half
+                        2.two
+                        2.three
+                        2.four
+                        2.six
+                    "
+                    .unindent(),
+                ),
+                cx,
+            );
+        });
 
-//         cx.foreground().run_until_parked();
+        cx.background_executor.run_until_parked();
 
-//         let multibuffer = cx.add_model(|cx| {
-//             let mut multibuffer = MultiBuffer::new(0);
-//             multibuffer.push_excerpts(
-//                 buffer_1.clone(),
-//                 [
-//                     // excerpt ends in the middle of a modified hunk
-//                     ExcerptRange {
-//                         context: Point::new(0, 0)..Point::new(1, 5),
-//                         primary: Default::default(),
-//                     },
-//                     // excerpt begins in the middle of a modified hunk
-//                     ExcerptRange {
-//                         context: Point::new(5, 0)..Point::new(6, 5),
-//                         primary: Default::default(),
-//                     },
-//                 ],
-//                 cx,
-//             );
-//             multibuffer.push_excerpts(
-//                 buffer_2.clone(),
-//                 [
-//                     // excerpt ends at a deletion
-//                     ExcerptRange {
-//                         context: Point::new(0, 0)..Point::new(1, 5),
-//                         primary: Default::default(),
-//                     },
-//                     // excerpt starts at a deletion
-//                     ExcerptRange {
-//                         context: Point::new(2, 0)..Point::new(2, 5),
-//                         primary: Default::default(),
-//                     },
-//                     // excerpt fully contains a deletion hunk
-//                     ExcerptRange {
-//                         context: Point::new(1, 0)..Point::new(2, 5),
-//                         primary: Default::default(),
-//                     },
-//                     // excerpt fully contains an insertion hunk
-//                     ExcerptRange {
-//                         context: Point::new(4, 0)..Point::new(6, 5),
-//                         primary: Default::default(),
-//                     },
-//                 ],
-//                 cx,
-//             );
-//             multibuffer
-//         });
+        let multibuffer = cx.build_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(0);
+            multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [
+                    // excerpt ends in the middle of a modified hunk
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(1, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt begins in the middle of a modified hunk
+                    ExcerptRange {
+                        context: Point::new(5, 0)..Point::new(6, 5),
+                        primary: Default::default(),
+                    },
+                ],
+                cx,
+            );
+            multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [
+                    // excerpt ends at a deletion
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(1, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt starts at a deletion
+                    ExcerptRange {
+                        context: Point::new(2, 0)..Point::new(2, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt fully contains a deletion hunk
+                    ExcerptRange {
+                        context: Point::new(1, 0)..Point::new(2, 5),
+                        primary: Default::default(),
+                    },
+                    // excerpt fully contains an insertion hunk
+                    ExcerptRange {
+                        context: Point::new(4, 0)..Point::new(6, 5),
+                        primary: Default::default(),
+                    },
+                ],
+                cx,
+            );
+            multibuffer
+        });
 
-//         let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
+        let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
 
-//         assert_eq!(
-//             snapshot.text(),
-//             "
-//                 1.zero
-//                 1.ONE
-//                 1.FIVE
-//                 1.six
-//                 2.zero
-//                 2.one
-//                 2.two
-//                 2.one
-//                 2.two
-//                 2.four
-//                 2.five
-//                 2.six"
-//                 .unindent()
-//         );
+        assert_eq!(
+            snapshot.text(),
+            "
+                1.zero
+                1.ONE
+                1.FIVE
+                1.six
+                2.zero
+                2.one
+                2.two
+                2.one
+                2.two
+                2.four
+                2.five
+                2.six"
+                .unindent()
+        );
 
-//         let expected = [
-//             (DiffHunkStatus::Modified, 1..2),
-//             (DiffHunkStatus::Modified, 2..3),
-//             //TODO: Define better when and where removed hunks show up at range extremities
-//             (DiffHunkStatus::Removed, 6..6),
-//             (DiffHunkStatus::Removed, 8..8),
-//             (DiffHunkStatus::Added, 10..11),
-//         ];
+        let expected = [
+            (DiffHunkStatus::Modified, 1..2),
+            (DiffHunkStatus::Modified, 2..3),
+            //TODO: Define better when and where removed hunks show up at range extremities
+            (DiffHunkStatus::Removed, 6..6),
+            (DiffHunkStatus::Removed, 8..8),
+            (DiffHunkStatus::Added, 10..11),
+        ];
 
-//         assert_eq!(
-//             snapshot
-//                 .git_diff_hunks_in_range(0..12)
-//                 .map(|hunk| (hunk.status(), hunk.buffer_range))
-//                 .collect::<Vec<_>>(),
-//             &expected,
-//         );
+        assert_eq!(
+            snapshot
+                .git_diff_hunks_in_range(0..12)
+                .map(|hunk| (hunk.status(), hunk.buffer_range))
+                .collect::<Vec<_>>(),
+            &expected,
+        );
 
-//         assert_eq!(
-//             snapshot
-//                 .git_diff_hunks_in_range_rev(0..12)
-//                 .map(|hunk| (hunk.status(), hunk.buffer_range))
-//                 .collect::<Vec<_>>(),
-//             expected
-//                 .iter()
-//                 .rev()
-//                 .cloned()
-//                 .collect::<Vec<_>>()
-//                 .as_slice(),
-//         );
-//     }
-// }
+        assert_eq!(
+            snapshot
+                .git_diff_hunks_in_range_rev(0..12)
+                .map(|hunk| (hunk.status(), hunk.buffer_range))
+                .collect::<Vec<_>>(),
+            expected
+                .iter()
+                .rev()
+                .cloned()
+                .collect::<Vec<_>>()
+                .as_slice(),
+        );
+    }
+}

crates/editor2/src/highlight_matching_bracket.rs 🔗

@@ -5,7 +5,7 @@ use crate::{Editor, RangeToAnchorExt};
 enum MatchingBracketHighlight {}
 
 pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
-    // editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
+    editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
 
     let newest_selection = editor.selections.newest::<usize>(cx);
     // Don't highlight brackets if the selection isn't empty
@@ -30,109 +30,109 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
-//     use indoc::indoc;
-//     use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
+    use indoc::indoc;
+    use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
 
-//     #[gpui::test]
-//     async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
+    #[gpui::test]
+    async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
 
-//         let mut cx = EditorLspTestContext::new(
-//             Language::new(
-//                 LanguageConfig {
-//                     name: "Rust".into(),
-//                     path_suffixes: vec!["rs".to_string()],
-//                     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_brackets_query(indoc! {r#"
-//                 ("{" @open "}" @close)
-//                 ("(" @open ")" @close)
-//                 "#})
-//             .unwrap(),
-//             Default::default(),
-//             cx,
-//         )
-//         .await;
+        let mut cx = EditorLspTestContext::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Rust".into(),
+                    path_suffixes: vec!["rs".to_string()],
+                    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_brackets_query(indoc! {r#"
+                ("{" @open "}" @close)
+                ("(" @open ")" @close)
+                "#})
+            .unwrap(),
+            Default::default(),
+            cx,
+        )
+        .await;
 
-//         // positioning cursor inside bracket highlights both
-//         cx.set_state(indoc! {r#"
-//             pub fn test("Test ˇargument") {
-//                 another_test(1, 2, 3);
-//             }
-//         "#});
-//         cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
-//             pub fn test«(»"Test argument"«)» {
-//                 another_test(1, 2, 3);
-//             }
-//         "#});
+        // positioning cursor inside bracket highlights both
+        cx.set_state(indoc! {r#"
+            pub fn test("Test ˇargument") {
+                another_test(1, 2, 3);
+            }
+        "#});
+        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+            pub fn test«(»"Test argument"«)» {
+                another_test(1, 2, 3);
+            }
+        "#});
 
-//         cx.set_state(indoc! {r#"
-//             pub fn test("Test argument") {
-//                 another_test(1, ˇ2, 3);
-//             }
-//         "#});
-//         cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
-//             pub fn test("Test argument") {
-//                 another_test«(»1, 2, 3«)»;
-//             }
-//         "#});
+        cx.set_state(indoc! {r#"
+            pub fn test("Test argument") {
+                another_test(1, ˇ2, 3);
+            }
+        "#});
+        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+            pub fn test("Test argument") {
+                another_test«(»1, 2, 3«)»;
+            }
+        "#});
 
-//         cx.set_state(indoc! {r#"
-//             pub fn test("Test argument") {
-//                 anotherˇ_test(1, 2, 3);
-//             }
-//         "#});
-//         cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
-//             pub fn test("Test argument") «{»
-//                 another_test(1, 2, 3);
-//             «}»
-//         "#});
+        cx.set_state(indoc! {r#"
+            pub fn test("Test argument") {
+                anotherˇ_test(1, 2, 3);
+            }
+        "#});
+        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+            pub fn test("Test argument") «{»
+                another_test(1, 2, 3);
+            «}»
+        "#});
 
-//         // positioning outside of brackets removes highlight
-//         cx.set_state(indoc! {r#"
-//             pub fˇn test("Test argument") {
-//                 another_test(1, 2, 3);
-//             }
-//         "#});
-//         cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
-//             pub fn test("Test argument") {
-//                 another_test(1, 2, 3);
-//             }
-//         "#});
+        // positioning outside of brackets removes highlight
+        cx.set_state(indoc! {r#"
+            pub fˇn test("Test argument") {
+                another_test(1, 2, 3);
+            }
+        "#});
+        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+            pub fn test("Test argument") {
+                another_test(1, 2, 3);
+            }
+        "#});
 
-//         // non empty selection dismisses highlight
-//         cx.set_state(indoc! {r#"
-//             pub fn test("Te«st argˇ»ument") {
-//                 another_test(1, 2, 3);
-//             }
-//         "#});
-//         cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
-//             pub fn test("Test argument") {
-//                 another_test(1, 2, 3);
-//             }
-//         "#});
-//     }
-// }
+        // non empty selection dismisses highlight
+        cx.set_state(indoc! {r#"
+            pub fn test("Te«st argˇ»ument") {
+                another_test(1, 2, 3);
+            }
+        "#});
+        cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+            pub fn test("Test argument") {
+                another_test(1, 2, 3);
+            }
+        "#});
+    }
+}

crates/editor2/src/inlay_hint_cache.rs 🔗

@@ -2432,13 +2432,13 @@ pub mod tests {
         let language = Arc::new(language);
         let fs = FakeFs::new(cx.background_executor.clone());
         fs.insert_tree(
-            "/a",
-            json!({
-                "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
-                "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
-            }),
-        )
-        .await;
+                "/a",
+                json!({
+                    "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+                    "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
+                }),
+            )
+            .await;
         let project = Project::test(fs, ["/a".as_ref()], cx).await;
         project.update(cx, |project, _| {
             project.languages().add(Arc::clone(&language))
@@ -2598,24 +2598,22 @@ pub mod tests {
         cx.executor().run_until_parked();
 
         editor.update(cx, |editor, cx| {
-            let expected_hints = vec![
-                "main hint #0".to_string(),
-                "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,
-                cached_hint_labels(editor),
-                "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
-            );
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison");
-        });
+                let expected_hints = vec![
+                    "main hint #0".to_string(),
+                    "main hint #1".to_string(),
+                    "main hint #2".to_string(),
+                    "main hint #3".to_string(),
+                    "main hint #4".to_string(),
+                    "main hint #5".to_string(),
+                ];
+                assert_eq!(
+                    expected_hints,
+                    cached_hint_labels(editor),
+                    "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
+                );
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison");
+            });
 
         editor.update(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::Next), cx, |s| {
@@ -2630,23 +2628,23 @@ pub mod tests {
         });
         cx.executor().run_until_parked();
         editor.update(cx, |editor, cx| {
-            let expected_hints = vec![
-                "main hint #0".to_string(),
-                "main hint #1".to_string(),
-                "main hint #2".to_string(),
-                "main hint #3".to_string(),
-                "main hint #4".to_string(),
-                "main hint #5".to_string(),
-                "other hint #0".to_string(),
-                "other hint #1".to_string(),
-                "other hint #2".to_string(),
-            ];
-            assert_eq!(expected_hints, cached_hint_labels(editor),
-                "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(),
-                "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
-        });
+                let expected_hints = vec![
+                    "main hint #0".to_string(),
+                    "main hint #1".to_string(),
+                    "main hint #2".to_string(),
+                    "main hint #3".to_string(),
+                    "main hint #4".to_string(),
+                    "main hint #5".to_string(),
+                    "other hint #0".to_string(),
+                    "other hint #1".to_string(),
+                    "other hint #2".to_string(),
+                ];
+                assert_eq!(expected_hints, cached_hint_labels(editor),
+                    "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(),
+                    "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
+            });
 
         editor.update(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::Next), cx, |s| {
@@ -2658,26 +2656,26 @@ pub mod tests {
         ));
         cx.executor().run_until_parked();
         let last_scroll_update_version = editor.update(cx, |editor, cx| {
-            let expected_hints = vec![
-                "main hint #0".to_string(),
-                "main hint #1".to_string(),
-                "main hint #2".to_string(),
-                "main hint #3".to_string(),
-                "main hint #4".to_string(),
-                "main hint #5".to_string(),
-                "other hint #0".to_string(),
-                "other hint #1".to_string(),
-                "other hint #2".to_string(),
-                "other hint #3".to_string(),
-                "other hint #4".to_string(),
-                "other hint #5".to_string(),
-            ];
-            assert_eq!(expected_hints, cached_hint_labels(editor),
-                "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(editor.inlay_hint_cache().version, expected_hints.len());
-            expected_hints.len()
-        }).unwrap();
+                let expected_hints = vec![
+                    "main hint #0".to_string(),
+                    "main hint #1".to_string(),
+                    "main hint #2".to_string(),
+                    "main hint #3".to_string(),
+                    "main hint #4".to_string(),
+                    "main hint #5".to_string(),
+                    "other hint #0".to_string(),
+                    "other hint #1".to_string(),
+                    "other hint #2".to_string(),
+                    "other hint #3".to_string(),
+                    "other hint #4".to_string(),
+                    "other hint #5".to_string(),
+                ];
+                assert_eq!(expected_hints, cached_hint_labels(editor),
+                    "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(editor.inlay_hint_cache().version, expected_hints.len());
+                expected_hints.len()
+            }).unwrap();
 
         editor.update(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::Next), cx, |s| {
@@ -2686,30 +2684,31 @@ pub mod tests {
         });
         cx.executor().run_until_parked();
         editor.update(cx, |editor, cx| {
-            let expected_hints = vec![
-                "main hint #0".to_string(),
-                "main hint #1".to_string(),
-                "main hint #2".to_string(),
-                "main hint #3".to_string(),
-                "main hint #4".to_string(),
-                "main hint #5".to_string(),
-                "other hint #0".to_string(),
-                "other hint #1".to_string(),
-                "other hint #2".to_string(),
-                "other hint #3".to_string(),
-                "other hint #4".to_string(),
-                "other hint #5".to_string(),
-            ];
-            assert_eq!(expected_hints, cached_hint_labels(editor),
-                "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
-            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
-            assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
-        });
+                let expected_hints = vec![
+                    "main hint #0".to_string(),
+                    "main hint #1".to_string(),
+                    "main hint #2".to_string(),
+                    "main hint #3".to_string(),
+                    "main hint #4".to_string(),
+                    "main hint #5".to_string(),
+                    "other hint #0".to_string(),
+                    "other hint #1".to_string(),
+                    "other hint #2".to_string(),
+                    "other hint #3".to_string(),
+                    "other hint #4".to_string(),
+                    "other hint #5".to_string(),
+                ];
+                assert_eq!(expected_hints, cached_hint_labels(editor),
+                    "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
+                assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+                assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
+            });
 
         editor_edited.store(true, Ordering::Release);
         editor.update(cx, |editor, cx| {
             editor.change_selections(None, cx, |s| {
-                s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
+                // TODO if this gets set to hint boundary (e.g. 56) we sometimes get an extra cache version bump, why?
+                s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
             });
             editor.handle_input("++++more text++++", cx);
         });
@@ -2729,15 +2728,15 @@ pub mod tests {
                 expected_hints,
                 cached_hint_labels(editor),
                 "After multibuffer edit, editor gets scolled back to the last selection; \
-all hints should be invalidated and requeried for all of its visible excerpts"
+    all hints should be invalidated and requeried for all of its visible excerpts"
             );
             assert_eq!(expected_hints, visible_hint_labels(editor, cx));
 
             let current_cache_version = editor.inlay_hint_cache().version;
-            let minimum_expected_version = last_scroll_update_version + expected_hints.len();
-            assert!(
-                current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1,
-                "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update"
+            assert_eq!(
+                current_cache_version,
+                last_scroll_update_version + expected_hints.len(),
+                "We should have updated cache N times == N of new hints arrived (separately from each excerpt)"
             );
         });
     }

crates/editor2/src/items.rs 🔗

@@ -4,13 +4,14 @@ use crate::{
     EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
     NavigationData, ToPoint as _,
 };
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Context as _, Result};
 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,
-    ViewContext, VisualContext, WeakView, WindowContext,
+    div, point, AnyElement, AppContext, AsyncAppContext, AsyncWindowContext, Context, Div, Entity,
+    EntityId, EventEmitter, FocusHandle, IntoElement, Model, ParentElement, Pixels, Render,
+    SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+    WindowContext,
 };
 use language::{
     proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
@@ -20,6 +21,7 @@ use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPat
 use rpc::proto::{self, update_view, PeerId};
 use settings::Settings;
 use smallvec::SmallVec;
+use std::fmt::Write;
 use std::{
     borrow::Cow,
     cmp::{self, Ordering},
@@ -31,8 +33,11 @@ use std::{
 use text::Selection;
 use theme::{ActiveTheme, Theme};
 use ui::{Color, Label};
-use util::{paths::PathExt, ResultExt, TryFutureExt};
-use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
+use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
+use workspace::{
+    item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle},
+    StatusItemView,
+};
 use workspace::{
     item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
     searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
@@ -71,110 +76,108 @@ impl FollowableItem for Editor {
         workspace: View<Workspace>,
         remote_id: ViewId,
         state: &mut Option<proto::view::Variant>,
-        cx: &mut AppContext,
+        cx: &mut WindowContext,
     ) -> Option<Task<Result<View<Self>>>> {
-        todo!()
+        let project = workspace.read(cx).project().to_owned();
+        let Some(proto::view::Variant::Editor(_)) = state else {
+            return None;
+        };
+        let Some(proto::view::Variant::Editor(state)) = state.take() else {
+            unreachable!()
+        };
+
+        let client = project.read(cx).client();
+        let replica_id = project.read(cx).replica_id();
+        let buffer_ids = state
+            .excerpts
+            .iter()
+            .map(|excerpt| excerpt.buffer_id)
+            .collect::<HashSet<_>>();
+        let buffers = project.update(cx, |project, cx| {
+            buffer_ids
+                .iter()
+                .map(|id| project.open_buffer_by_id(*id, cx))
+                .collect::<Vec<_>>()
+        });
+
+        let pane = pane.downgrade();
+        Some(cx.spawn(|mut cx| async move {
+            let mut buffers = futures::future::try_join_all(buffers).await?;
+            let editor = pane.update(&mut cx, |pane, cx| {
+                let mut editors = pane.items_of_type::<Self>();
+                editors.find(|editor| {
+                    let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
+                    let singleton_buffer_matches = state.singleton
+                        && buffers.first()
+                            == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
+                    ids_match || singleton_buffer_matches
+                })
+            })?;
+
+            let editor = if let Some(editor) = editor {
+                editor
+            } else {
+                pane.update(&mut cx, |_, cx| {
+                    let multibuffer = cx.build_model(|cx| {
+                        let mut multibuffer;
+                        if state.singleton && buffers.len() == 1 {
+                            multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
+                        } else {
+                            multibuffer = MultiBuffer::new(replica_id);
+                            let mut excerpts = state.excerpts.into_iter().peekable();
+                            while let Some(excerpt) = excerpts.peek() {
+                                let buffer_id = excerpt.buffer_id;
+                                let buffer_excerpts = iter::from_fn(|| {
+                                    let excerpt = excerpts.peek()?;
+                                    (excerpt.buffer_id == buffer_id)
+                                        .then(|| excerpts.next().unwrap())
+                                });
+                                let buffer =
+                                    buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
+                                if let Some(buffer) = buffer {
+                                    multibuffer.push_excerpts(
+                                        buffer.clone(),
+                                        buffer_excerpts.filter_map(deserialize_excerpt_range),
+                                        cx,
+                                    );
+                                }
+                            }
+                        };
+
+                        if let Some(title) = &state.title {
+                            multibuffer = multibuffer.with_title(title.clone())
+                        }
+
+                        multibuffer
+                    });
+
+                    cx.build_view(|cx| {
+                        let mut editor =
+                            Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
+                        editor.remote_id = Some(remote_id);
+                        editor
+                    })
+                })?
+            };
+
+            update_editor_from_message(
+                editor.downgrade(),
+                project,
+                proto::update_view::Editor {
+                    selections: state.selections,
+                    pending_selection: state.pending_selection,
+                    scroll_top_anchor: state.scroll_top_anchor,
+                    scroll_x: state.scroll_x,
+                    scroll_y: state.scroll_y,
+                    ..Default::default()
+                },
+                &mut cx,
+            )
+            .await?;
+
+            Ok(editor)
+        }))
     }
-    //     let project = workspace.read(cx).project().to_owned();
-    //     let Some(proto::view::Variant::Editor(_)) = state else {
-    //         return None;
-    //     };
-    //     let Some(proto::view::Variant::Editor(state)) = state.take() else {
-    //         unreachable!()
-    //     };
-
-    //     let client = project.read(cx).client();
-    //     let replica_id = project.read(cx).replica_id();
-    //     let buffer_ids = state
-    //         .excerpts
-    //         .iter()
-    //         .map(|excerpt| excerpt.buffer_id)
-    //         .collect::<HashSet<_>>();
-    //     let buffers = project.update(cx, |project, cx| {
-    //         buffer_ids
-    //             .iter()
-    //             .map(|id| project.open_buffer_by_id(*id, cx))
-    //             .collect::<Vec<_>>()
-    //     });
-
-    //     let pane = pane.downgrade();
-    //     Some(cx.spawn(|mut cx| async move {
-    //         let mut buffers = futures::future::try_join_all(buffers).await?;
-    //         let editor = pane.read_with(&cx, |pane, cx| {
-    //             let mut editors = pane.items_of_type::<Self>();
-    //             editors.find(|editor| {
-    //                 let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
-    //                 let singleton_buffer_matches = state.singleton
-    //                     && buffers.first()
-    //                         == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
-    //                 ids_match || singleton_buffer_matches
-    //             })
-    //         })?;
-
-    //         let editor = if let Some(editor) = editor {
-    //             editor
-    //         } else {
-    //             pane.update(&mut cx, |_, cx| {
-    //                 let multibuffer = cx.add_model(|cx| {
-    //                     let mut multibuffer;
-    //                     if state.singleton && buffers.len() == 1 {
-    //                         multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
-    //                     } else {
-    //                         multibuffer = MultiBuffer::new(replica_id);
-    //                         let mut excerpts = state.excerpts.into_iter().peekable();
-    //                         while let Some(excerpt) = excerpts.peek() {
-    //                             let buffer_id = excerpt.buffer_id;
-    //                             let buffer_excerpts = iter::from_fn(|| {
-    //                                 let excerpt = excerpts.peek()?;
-    //                                 (excerpt.buffer_id == buffer_id)
-    //                                     .then(|| excerpts.next().unwrap())
-    //                             });
-    //                             let buffer =
-    //                                 buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
-    //                             if let Some(buffer) = buffer {
-    //                                 multibuffer.push_excerpts(
-    //                                     buffer.clone(),
-    //                                     buffer_excerpts.filter_map(deserialize_excerpt_range),
-    //                                     cx,
-    //                                 );
-    //                             }
-    //                         }
-    //                     };
-
-    //                     if let Some(title) = &state.title {
-    //                         multibuffer = multibuffer.with_title(title.clone())
-    //                     }
-
-    //                     multibuffer
-    //                 });
-
-    //                 cx.add_view(|cx| {
-    //                     let mut editor =
-    //                         Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
-    //                     editor.remote_id = Some(remote_id);
-    //                     editor
-    //                 })
-    //             })?
-    //         };
-
-    //         update_editor_from_message(
-    //             editor.downgrade(),
-    //             project,
-    //             proto::update_view::Editor {
-    //                 selections: state.selections,
-    //                 pending_selection: state.pending_selection,
-    //                 scroll_top_anchor: state.scroll_top_anchor,
-    //                 scroll_x: state.scroll_x,
-    //                 scroll_y: state.scroll_y,
-    //                 ..Default::default()
-    //             },
-    //             &mut cx,
-    //         )
-    //         .await?;
-
-    //         Ok(editor)
-    //     }))
-    // }
 
     fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
         self.leader_peer_id = leader_peer_id;
@@ -195,7 +198,7 @@ impl FollowableItem for Editor {
         cx.notify();
     }
 
-    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+    fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
         let buffer = self.buffer.read(cx);
         let scroll_anchor = self.scroll_manager.anchor();
         let excerpts = buffer
@@ -242,7 +245,7 @@ impl FollowableItem for Editor {
         &self,
         event: &Self::FollowableEvent,
         update: &mut Option<proto::update_view::Variant>,
-        cx: &AppContext,
+        cx: &WindowContext,
     ) -> bool {
         let update =
             update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
@@ -315,7 +318,7 @@ impl FollowableItem for Editor {
         })
     }
 
-    fn is_project_item(&self, _cx: &AppContext) -> bool {
+    fn is_project_item(&self, _cx: &WindowContext) -> bool {
         true
     }
 }
@@ -324,132 +327,129 @@ async fn update_editor_from_message(
     this: WeakView<Editor>,
     project: Model<Project>,
     message: proto::update_view::Editor,
-    cx: &mut AsyncAppContext,
+    cx: &mut AsyncWindowContext,
 ) -> Result<()> {
-    todo!()
+    // Open all of the buffers of which excerpts were added to the editor.
+    let inserted_excerpt_buffer_ids = message
+        .inserted_excerpts
+        .iter()
+        .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
+        .collect::<HashSet<_>>();
+    let inserted_excerpt_buffers = project.update(cx, |project, cx| {
+        inserted_excerpt_buffer_ids
+            .into_iter()
+            .map(|id| project.open_buffer_by_id(id, cx))
+            .collect::<Vec<_>>()
+    })?;
+    let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
+
+    // Update the editor's excerpts.
+    this.update(cx, |editor, cx| {
+        editor.buffer.update(cx, |multibuffer, cx| {
+            let mut removed_excerpt_ids = message
+                .deleted_excerpts
+                .into_iter()
+                .map(ExcerptId::from_proto)
+                .collect::<Vec<_>>();
+            removed_excerpt_ids.sort_by({
+                let multibuffer = multibuffer.read(cx);
+                move |a, b| a.cmp(&b, &multibuffer)
+            });
+
+            let mut insertions = message.inserted_excerpts.into_iter().peekable();
+            while let Some(insertion) = insertions.next() {
+                let Some(excerpt) = insertion.excerpt else {
+                    continue;
+                };
+                let Some(previous_excerpt_id) = insertion.previous_excerpt_id else {
+                    continue;
+                };
+                let buffer_id = excerpt.buffer_id;
+                let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else {
+                    continue;
+                };
+
+                let adjacent_excerpts = iter::from_fn(|| {
+                    let insertion = insertions.peek()?;
+                    if insertion.previous_excerpt_id.is_none()
+                        && insertion.excerpt.as_ref()?.buffer_id == buffer_id
+                    {
+                        insertions.next()?.excerpt
+                    } else {
+                        None
+                    }
+                });
+
+                multibuffer.insert_excerpts_with_ids_after(
+                    ExcerptId::from_proto(previous_excerpt_id),
+                    buffer,
+                    [excerpt]
+                        .into_iter()
+                        .chain(adjacent_excerpts)
+                        .filter_map(|excerpt| {
+                            Some((
+                                ExcerptId::from_proto(excerpt.id),
+                                deserialize_excerpt_range(excerpt)?,
+                            ))
+                        }),
+                    cx,
+                );
+            }
+
+            multibuffer.remove_excerpts(removed_excerpt_ids, cx);
+        });
+    })?;
+
+    // Deserialize the editor state.
+    let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
+        let buffer = editor.buffer.read(cx).read(cx);
+        let selections = message
+            .selections
+            .into_iter()
+            .filter_map(|selection| deserialize_selection(&buffer, selection))
+            .collect::<Vec<_>>();
+        let pending_selection = message
+            .pending_selection
+            .and_then(|selection| deserialize_selection(&buffer, selection));
+        let scroll_top_anchor = message
+            .scroll_top_anchor
+            .and_then(|anchor| deserialize_anchor(&buffer, anchor));
+        anyhow::Ok((selections, pending_selection, scroll_top_anchor))
+    })??;
+
+    // Wait until the buffer has received all of the operations referenced by
+    // the editor's new state.
+    this.update(cx, |editor, cx| {
+        editor.buffer.update(cx, |buffer, cx| {
+            buffer.wait_for_anchors(
+                selections
+                    .iter()
+                    .chain(pending_selection.as_ref())
+                    .flat_map(|selection| [selection.start, selection.end])
+                    .chain(scroll_top_anchor),
+                cx,
+            )
+        })
+    })?
+    .await?;
+
+    // Update the editor's state.
+    this.update(cx, |editor, cx| {
+        if !selections.is_empty() || pending_selection.is_some() {
+            editor.set_selections_from_remote(selections, pending_selection, cx);
+            editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
+        } else if let Some(scroll_top_anchor) = scroll_top_anchor {
+            editor.set_scroll_anchor_remote(
+                ScrollAnchor {
+                    anchor: scroll_top_anchor,
+                    offset: point(message.scroll_x, message.scroll_y),
+                },
+                cx,
+            );
+        }
+    })?;
+    Ok(())
 }
-// Previous implementation of the above
-//     // Open all of the buffers of which excerpts were added to the editor.
-//     let inserted_excerpt_buffer_ids = message
-//         .inserted_excerpts
-//         .iter()
-//         .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
-//         .collect::<HashSet<_>>();
-//     let inserted_excerpt_buffers = project.update(cx, |project, cx| {
-//         inserted_excerpt_buffer_ids
-//             .into_iter()
-//             .map(|id| project.open_buffer_by_id(id, cx))
-//             .collect::<Vec<_>>()
-//     })?;
-//     let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
-
-//     // Update the editor's excerpts.
-//     this.update(cx, |editor, cx| {
-//         editor.buffer.update(cx, |multibuffer, cx| {
-//             let mut removed_excerpt_ids = message
-//                 .deleted_excerpts
-//                 .into_iter()
-//                 .map(ExcerptId::from_proto)
-//                 .collect::<Vec<_>>();
-//             removed_excerpt_ids.sort_by({
-//                 let multibuffer = multibuffer.read(cx);
-//                 move |a, b| a.cmp(&b, &multibuffer)
-//             });
-
-//             let mut insertions = message.inserted_excerpts.into_iter().peekable();
-//             while let Some(insertion) = insertions.next() {
-//                 let Some(excerpt) = insertion.excerpt else {
-//                     continue;
-//                 };
-//                 let Some(previous_excerpt_id) = insertion.previous_excerpt_id else {
-//                     continue;
-//                 };
-//                 let buffer_id = excerpt.buffer_id;
-//                 let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else {
-//                     continue;
-//                 };
-
-//                 let adjacent_excerpts = iter::from_fn(|| {
-//                     let insertion = insertions.peek()?;
-//                     if insertion.previous_excerpt_id.is_none()
-//                         && insertion.excerpt.as_ref()?.buffer_id == buffer_id
-//                     {
-//                         insertions.next()?.excerpt
-//                     } else {
-//                         None
-//                     }
-//                 });
-
-//                 multibuffer.insert_excerpts_with_ids_after(
-//                     ExcerptId::from_proto(previous_excerpt_id),
-//                     buffer,
-//                     [excerpt]
-//                         .into_iter()
-//                         .chain(adjacent_excerpts)
-//                         .filter_map(|excerpt| {
-//                             Some((
-//                                 ExcerptId::from_proto(excerpt.id),
-//                                 deserialize_excerpt_range(excerpt)?,
-//                             ))
-//                         }),
-//                     cx,
-//                 );
-//             }
-
-//             multibuffer.remove_excerpts(removed_excerpt_ids, cx);
-//         });
-//     })?;
-
-//     // Deserialize the editor state.
-//     let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
-//         let buffer = editor.buffer.read(cx).read(cx);
-//         let selections = message
-//             .selections
-//             .into_iter()
-//             .filter_map(|selection| deserialize_selection(&buffer, selection))
-//             .collect::<Vec<_>>();
-//         let pending_selection = message
-//             .pending_selection
-//             .and_then(|selection| deserialize_selection(&buffer, selection));
-//         let scroll_top_anchor = message
-//             .scroll_top_anchor
-//             .and_then(|anchor| deserialize_anchor(&buffer, anchor));
-//         anyhow::Ok((selections, pending_selection, scroll_top_anchor))
-//     })??;
-
-//     // Wait until the buffer has received all of the operations referenced by
-//     // the editor's new state.
-//     this.update(cx, |editor, cx| {
-//         editor.buffer.update(cx, |buffer, cx| {
-//             buffer.wait_for_anchors(
-//                 selections
-//                     .iter()
-//                     .chain(pending_selection.as_ref())
-//                     .flat_map(|selection| [selection.start, selection.end])
-//                     .chain(scroll_top_anchor),
-//                 cx,
-//             )
-//         })
-//     })?
-//     .await?;
-
-//     // Update the editor's state.
-//     this.update(cx, |editor, cx| {
-//         if !selections.is_empty() || pending_selection.is_some() {
-//             editor.set_selections_from_remote(selections, pending_selection, cx);
-//             editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
-//         } else if let Some(scroll_top_anchor) = scroll_top_anchor {
-//             editor.set_scroll_anchor_remote(
-//                 ScrollAnchor {
-//                     anchor: scroll_top_anchor,
-//                     offset: point(message.scroll_x, message.scroll_y),
-//                 },
-//                 cx,
-//             );
-//         }
-//     })?;
-//     Ok(())
-// }
 
 fn serialize_excerpt(
     buffer_id: u64,
@@ -529,39 +529,38 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
 
 impl Item for Editor {
     fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
-        todo!();
-        // if let Ok(data) = data.downcast::<NavigationData>() {
-        //     let newest_selection = self.selections.newest::<Point>(cx);
-        //     let buffer = self.buffer.read(cx).read(cx);
-        //     let offset = if buffer.can_resolve(&data.cursor_anchor) {
-        //         data.cursor_anchor.to_point(&buffer)
-        //     } else {
-        //         buffer.clip_point(data.cursor_position, Bias::Left)
-        //     };
-
-        //     let mut scroll_anchor = data.scroll_anchor;
-        //     if !buffer.can_resolve(&scroll_anchor.anchor) {
-        //         scroll_anchor.anchor = buffer.anchor_before(
-        //             buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
-        //         );
-        //     }
-
-        //     drop(buffer);
-
-        //     if newest_selection.head() == offset {
-        //         false
-        //     } else {
-        //         let nav_history = self.nav_history.take();
-        //         self.set_scroll_anchor(scroll_anchor, cx);
-        //         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
-        //             s.select_ranges([offset..offset])
-        //         });
-        //         self.nav_history = nav_history;
-        //         true
-        //     }
-        // } else {
-        //     false
-        // }
+        if let Ok(data) = data.downcast::<NavigationData>() {
+            let newest_selection = self.selections.newest::<Point>(cx);
+            let buffer = self.buffer.read(cx).read(cx);
+            let offset = if buffer.can_resolve(&data.cursor_anchor) {
+                data.cursor_anchor.to_point(&buffer)
+            } else {
+                buffer.clip_point(data.cursor_position, Bias::Left)
+            };
+
+            let mut scroll_anchor = data.scroll_anchor;
+            if !buffer.can_resolve(&scroll_anchor.anchor) {
+                scroll_anchor.anchor = buffer.anchor_before(
+                    buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
+                );
+            }
+
+            drop(buffer);
+
+            if newest_selection.head() == offset {
+                false
+            } else {
+                let nav_history = self.nav_history.take();
+                self.set_scroll_anchor(scroll_anchor, cx);
+                self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.select_ranges([offset..offset])
+                });
+                self.nav_history = nav_history;
+                true
+            }
+        } else {
+            false
+        }
     }
 
     fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
@@ -765,35 +764,34 @@ impl Item for Editor {
     }
 
     fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
-        todo!();
-        // let cursor = self.selections.newest_anchor().head();
-        // let multibuffer = &self.buffer().read(cx);
-        // let (buffer_id, symbols) =
-        //     multibuffer.symbols_containing(cursor, Some(&theme.editor.syntax), cx)?;
-        // let buffer = multibuffer.buffer(buffer_id)?;
-
-        // let buffer = buffer.read(cx);
-        // let filename = buffer
-        //     .snapshot()
-        //     .resolve_file_path(
-        //         cx,
-        //         self.project
-        //             .as_ref()
-        //             .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
-        //             .unwrap_or_default(),
-        //     )
-        //     .map(|path| path.to_string_lossy().to_string())
-        //     .unwrap_or_else(|| "untitled".to_string());
-
-        // let mut breadcrumbs = vec![BreadcrumbText {
-        //     text: filename,
-        //     highlights: None,
-        // }];
-        // breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
-        //     text: symbol.text,
-        //     highlights: Some(symbol.highlight_ranges),
-        // }));
-        // Some(breadcrumbs)
+        let cursor = self.selections.newest_anchor().head();
+        let multibuffer = &self.buffer().read(cx);
+        let (buffer_id, symbols) =
+            multibuffer.symbols_containing(cursor, Some(&variant.syntax()), cx)?;
+        let buffer = multibuffer.buffer(buffer_id)?;
+
+        let buffer = buffer.read(cx);
+        let filename = buffer
+            .snapshot()
+            .resolve_file_path(
+                cx,
+                self.project
+                    .as_ref()
+                    .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
+                    .unwrap_or_default(),
+            )
+            .map(|path| path.to_string_lossy().to_string())
+            .unwrap_or_else(|| "untitled".to_string());
+
+        let mut breadcrumbs = vec![BreadcrumbText {
+            text: filename,
+            highlights: None,
+        }];
+        breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
+            text: symbol.text,
+            highlights: Some(symbol.highlight_ranges),
+        }));
+        Some(breadcrumbs)
     }
 
     fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
@@ -1120,86 +1118,78 @@ pub struct CursorPosition {
     _observe_active_editor: Option<Subscription>,
 }
 
-// impl Default for CursorPosition {
-//     fn default() -> Self {
-//         Self::new()
-//     }
-// }
-
-// impl CursorPosition {
-//     pub fn new() -> Self {
-//         Self {
-//             position: None,
-//             selected_count: 0,
-//             _observe_active_editor: None,
-//         }
-//     }
-
-//     fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
-//         let editor = editor.read(cx);
-//         let buffer = editor.buffer().read(cx).snapshot(cx);
-
-//         self.selected_count = 0;
-//         let mut last_selection: Option<Selection<usize>> = None;
-//         for selection in editor.selections.all::<usize>(cx) {
-//             self.selected_count += selection.end - selection.start;
-//             if last_selection
-//                 .as_ref()
-//                 .map_or(true, |last_selection| selection.id > last_selection.id)
-//             {
-//                 last_selection = Some(selection);
-//             }
-//         }
-//         self.position = last_selection.map(|s| s.head().to_point(&buffer));
-
-//         cx.notify();
-//     }
-// }
-
-// impl Entity for CursorPosition {
-//     type Event = ();
-// }
-
-// impl View for CursorPosition {
-//     fn ui_name() -> &'static str {
-//         "CursorPosition"
-//     }
-
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         if let Some(position) = self.position {
-//             let theme = &theme::current(cx).workspace.status_bar;
-//             let mut text = format!(
-//                 "{}{FILE_ROW_COLUMN_DELIMITER}{}",
-//                 position.row + 1,
-//                 position.column + 1
-//             );
-//             if self.selected_count > 0 {
-//                 write!(text, " ({} selected)", self.selected_count).unwrap();
-//             }
-//             Label::new(text, theme.cursor_position.clone()).into_any()
-//         } else {
-//             Empty::new().into_any()
-//         }
-//     }
-// }
-
-// impl StatusItemView for CursorPosition {
-//     fn set_active_pane_item(
-//         &mut self,
-//         active_pane_item: Option<&dyn ItemHandle>,
-//         cx: &mut ViewContext<Self>,
-//     ) {
-//         if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
-//             self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
-//             self.update_position(editor, cx);
-//         } else {
-//             self.position = None;
-//             self._observe_active_editor = None;
-//         }
-
-//         cx.notify();
-//     }
-// }
+impl Default for CursorPosition {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl CursorPosition {
+    pub fn new() -> Self {
+        Self {
+            position: None,
+            selected_count: 0,
+            _observe_active_editor: None,
+        }
+    }
+
+    fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
+        let editor = editor.read(cx);
+        let buffer = editor.buffer().read(cx).snapshot(cx);
+
+        self.selected_count = 0;
+        let mut last_selection: Option<Selection<usize>> = None;
+        for selection in editor.selections.all::<usize>(cx) {
+            self.selected_count += selection.end - selection.start;
+            if last_selection
+                .as_ref()
+                .map_or(true, |last_selection| selection.id > last_selection.id)
+            {
+                last_selection = Some(selection);
+            }
+        }
+        self.position = last_selection.map(|s| s.head().to_point(&buffer));
+
+        cx.notify();
+    }
+}
+
+impl Render for CursorPosition {
+    type Element = Div;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div().when_some(self.position, |el, position| {
+            let mut text = format!(
+                "{}{FILE_ROW_COLUMN_DELIMITER}{}",
+                position.row + 1,
+                position.column + 1
+            );
+            if self.selected_count > 0 {
+                write!(text, " ({} selected)", self.selected_count).unwrap();
+            }
+
+            el.child(Label::new(text))
+        })
+    }
+}
+
+impl StatusItemView for CursorPosition {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
+            self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
+            self.update_position(editor, cx);
+        } else {
+            self.position = None;
+            self._observe_active_editor = None;
+        }
+
+        cx.notify();
+    }
+}
 
 fn path_for_buffer<'a>(
     buffer: &Model<MultiBuffer>,
@@ -608,671 +608,672 @@ fn go_to_fetched_definition_of_kind(
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use crate::{
-//         display_map::ToDisplayPoint,
-//         editor_tests::init_test,
-//         inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
-//         test::editor_lsp_test_context::EditorLspTestContext,
-//     };
-//     use futures::StreamExt;
-//     use gpui::{
-//         platform::{self, Modifiers, ModifiersChangedEvent},
-//         View,
-//     };
-//     use indoc::indoc;
-//     use language::language_settings::InlayHintSettings;
-//     use lsp::request::{GotoDefinition, GotoTypeDefinition};
-//     use util::assert_set_eq;
-
-//     #[gpui::test]
-//     async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         cx.set_state(indoc! {"
-//             struct A;
-//             let vˇariable = A;
-//         "});
-
-//         // Basic hold cmd+shift, expect highlight in region if response contains type definition
-//         let hover_point = cx.display_point(indoc! {"
-//             struct A;
-//             let vˇariable = A;
-//         "});
-//         let symbol_range = cx.lsp_range(indoc! {"
-//             struct A;
-//             let «variable» = A;
-//         "});
-//         let target_range = cx.lsp_range(indoc! {"
-//             struct «A»;
-//             let variable = A;
-//         "});
-
-//         let mut requests =
-//             cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
-//                 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
-//                     lsp::LocationLink {
-//                         origin_selection_range: Some(symbol_range),
-//                         target_uri: url.clone(),
-//                         target_range,
-//                         target_selection_range: target_range,
-//                     },
-//                 ])))
-//             });
-
-//         // Press cmd+shift to trigger highlight
-//         cx.update_editor(|editor, cx| {
-//             update_go_to_definition_link(
-//                 editor,
-//                 Some(GoToDefinitionTrigger::Text(hover_point)),
-//                 true,
-//                 true,
-//                 cx,
-//             );
-//         });
-//         requests.next().await;
-//         cx.foreground().run_until_parked();
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             struct A;
-//             let «variable» = A;
-//         "});
-
-//         // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
-//         cx.update_editor(|editor, cx| {
-//             editor.modifiers_changed(
-//                 &platform::ModifiersChangedEvent {
-//                     modifiers: Modifiers {
-//                         cmd: true,
-//                         ..Default::default()
-//                     },
-//                     ..Default::default()
-//                 },
-//                 cx,
-//             );
-//         });
-//         // Assert no link highlights
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             struct A;
-//             let variable = A;
-//         "});
-
-//         // Cmd+shift click without existing definition requests and jumps
-//         let hover_point = cx.display_point(indoc! {"
-//             struct A;
-//             let vˇariable = A;
-//         "});
-//         let target_range = cx.lsp_range(indoc! {"
-//             struct «A»;
-//             let variable = A;
-//         "});
-
-//         let mut requests =
-//             cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
-//                 Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
-//                     lsp::LocationLink {
-//                         origin_selection_range: None,
-//                         target_uri: url,
-//                         target_range,
-//                         target_selection_range: target_range,
-//                     },
-//                 ])))
-//             });
-
-//         cx.update_editor(|editor, cx| {
-//             go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
-//         });
-//         requests.next().await;
-//         cx.foreground().run_until_parked();
-
-//         cx.assert_editor_state(indoc! {"
-//             struct «Aˇ»;
-//             let variable = A;
-//         "});
-//     }
-
-//     #[gpui::test]
-//     async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         cx.set_state(indoc! {"
-//             fn ˇtest() { do_work(); }
-//             fn do_work() { test(); }
-//         "});
-
-//         // Basic hold cmd, expect highlight in region if response contains definition
-//         let hover_point = cx.display_point(indoc! {"
-//             fn test() { do_wˇork(); }
-//             fn do_work() { test(); }
-//         "});
-//         let symbol_range = cx.lsp_range(indoc! {"
-//             fn test() { «do_work»(); }
-//             fn do_work() { test(); }
-//         "});
-//         let target_range = cx.lsp_range(indoc! {"
-//             fn test() { do_work(); }
-//             fn «do_work»() { test(); }
-//         "});
-
-//         let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
-//             Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-//                 lsp::LocationLink {
-//                     origin_selection_range: Some(symbol_range),
-//                     target_uri: url.clone(),
-//                     target_range,
-//                     target_selection_range: target_range,
-//                 },
-//             ])))
-//         });
-
-//         cx.update_editor(|editor, cx| {
-//             update_go_to_definition_link(
-//                 editor,
-//                 Some(GoToDefinitionTrigger::Text(hover_point)),
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         requests.next().await;
-//         cx.foreground().run_until_parked();
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             fn test() { «do_work»(); }
-//             fn do_work() { test(); }
-//         "});
-
-//         // Unpress cmd causes highlight to go away
-//         cx.update_editor(|editor, cx| {
-//             editor.modifiers_changed(&Default::default(), cx);
-//         });
-
-//         // Assert no link highlights
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             fn test() { do_work(); }
-//             fn do_work() { test(); }
-//         "});
-
-//         // Response without source range still highlights word
-//         cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None);
-//         let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
-//             Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-//                 lsp::LocationLink {
-//                     // No origin range
-//                     origin_selection_range: None,
-//                     target_uri: url.clone(),
-//                     target_range,
-//                     target_selection_range: target_range,
-//                 },
-//             ])))
-//         });
-//         cx.update_editor(|editor, cx| {
-//             update_go_to_definition_link(
-//                 editor,
-//                 Some(GoToDefinitionTrigger::Text(hover_point)),
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         requests.next().await;
-//         cx.foreground().run_until_parked();
-
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             fn test() { «do_work»(); }
-//             fn do_work() { test(); }
-//         "});
-
-//         // Moving mouse to location with no response dismisses highlight
-//         let hover_point = cx.display_point(indoc! {"
-//             fˇn test() { do_work(); }
-//             fn do_work() { test(); }
-//         "});
-//         let mut requests = cx
-//             .lsp
-//             .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
-//                 // No definitions returned
-//                 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
-//             });
-//         cx.update_editor(|editor, cx| {
-//             update_go_to_definition_link(
-//                 editor,
-//                 Some(GoToDefinitionTrigger::Text(hover_point)),
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         requests.next().await;
-//         cx.foreground().run_until_parked();
-
-//         // Assert no link highlights
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             fn test() { do_work(); }
-//             fn do_work() { test(); }
-//         "});
-
-//         // Move mouse without cmd and then pressing cmd triggers highlight
-//         let hover_point = cx.display_point(indoc! {"
-//             fn test() { do_work(); }
-//             fn do_work() { teˇst(); }
-//         "});
-//         cx.update_editor(|editor, cx| {
-//             update_go_to_definition_link(
-//                 editor,
-//                 Some(GoToDefinitionTrigger::Text(hover_point)),
-//                 false,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         cx.foreground().run_until_parked();
-
-//         // Assert no link highlights
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             fn test() { do_work(); }
-//             fn do_work() { test(); }
-//         "});
-
-//         let symbol_range = cx.lsp_range(indoc! {"
-//             fn test() { do_work(); }
-//             fn do_work() { «test»(); }
-//         "});
-//         let target_range = cx.lsp_range(indoc! {"
-//             fn «test»() { do_work(); }
-//             fn do_work() { test(); }
-//         "});
-
-//         let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
-//             Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-//                 lsp::LocationLink {
-//                     origin_selection_range: Some(symbol_range),
-//                     target_uri: url,
-//                     target_range,
-//                     target_selection_range: target_range,
-//                 },
-//             ])))
-//         });
-//         cx.update_editor(|editor, cx| {
-//             editor.modifiers_changed(
-//                 &ModifiersChangedEvent {
-//                     modifiers: Modifiers {
-//                         cmd: true,
-//                         ..Default::default()
-//                     },
-//                 },
-//                 cx,
-//             );
-//         });
-//         requests.next().await;
-//         cx.foreground().run_until_parked();
-
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             fn test() { do_work(); }
-//             fn do_work() { «test»(); }
-//         "});
-
-//         // Deactivating the window dismisses the highlight
-//         cx.update_workspace(|workspace, cx| {
-//             workspace.on_window_activation_changed(false, cx);
-//         });
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             fn test() { do_work(); }
-//             fn do_work() { test(); }
-//         "});
-
-//         // Moving the mouse restores the highlights.
-//         cx.update_editor(|editor, cx| {
-//             update_go_to_definition_link(
-//                 editor,
-//                 Some(GoToDefinitionTrigger::Text(hover_point)),
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         cx.foreground().run_until_parked();
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             fn test() { do_work(); }
-//             fn do_work() { «test»(); }
-//         "});
-
-//         // Moving again within the same symbol range doesn't re-request
-//         let hover_point = cx.display_point(indoc! {"
-//             fn test() { do_work(); }
-//             fn do_work() { tesˇt(); }
-//         "});
-//         cx.update_editor(|editor, cx| {
-//             update_go_to_definition_link(
-//                 editor,
-//                 Some(GoToDefinitionTrigger::Text(hover_point)),
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         cx.foreground().run_until_parked();
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             fn test() { do_work(); }
-//             fn do_work() { «test»(); }
-//         "});
-
-//         // Cmd click with existing definition doesn't re-request and dismisses highlight
-//         cx.update_editor(|editor, cx| {
-//             go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
-//         });
-//         // Assert selection moved to to definition
-//         cx.lsp
-//             .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
-//                 // Empty definition response to make sure we aren't hitting the lsp and using
-//                 // the cached location instead
-//                 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
-//             });
-//         cx.foreground().run_until_parked();
-//         cx.assert_editor_state(indoc! {"
-//             fn «testˇ»() { do_work(); }
-//             fn do_work() { test(); }
-//         "});
-
-//         // Assert no link highlights after jump
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             fn test() { do_work(); }
-//             fn do_work() { test(); }
-//         "});
-
-//         // Cmd click without existing definition requests and jumps
-//         let hover_point = cx.display_point(indoc! {"
-//             fn test() { do_wˇork(); }
-//             fn do_work() { test(); }
-//         "});
-//         let target_range = cx.lsp_range(indoc! {"
-//             fn test() { do_work(); }
-//             fn «do_work»() { test(); }
-//         "});
-
-//         let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
-//             Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-//                 lsp::LocationLink {
-//                     origin_selection_range: None,
-//                     target_uri: url,
-//                     target_range,
-//                     target_selection_range: target_range,
-//                 },
-//             ])))
-//         });
-//         cx.update_editor(|editor, cx| {
-//             go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
-//         });
-//         requests.next().await;
-//         cx.foreground().run_until_parked();
-//         cx.assert_editor_state(indoc! {"
-//             fn test() { do_work(); }
-//             fn «do_workˇ»() { test(); }
-//         "});
-
-//         // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
-//         // 2. Selection is completed, hovering
-//         let hover_point = cx.display_point(indoc! {"
-//             fn test() { do_wˇork(); }
-//             fn do_work() { test(); }
-//         "});
-//         let target_range = cx.lsp_range(indoc! {"
-//             fn test() { do_work(); }
-//             fn «do_work»() { test(); }
-//         "});
-//         let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
-//             Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-//                 lsp::LocationLink {
-//                     origin_selection_range: None,
-//                     target_uri: url,
-//                     target_range,
-//                     target_selection_range: target_range,
-//                 },
-//             ])))
-//         });
-
-//         // create a pending selection
-//         let selection_range = cx.ranges(indoc! {"
-//             fn «test() { do_w»ork(); }
-//             fn do_work() { test(); }
-//         "})[0]
-//             .clone();
-//         cx.update_editor(|editor, cx| {
-//             let snapshot = editor.buffer().read(cx).snapshot(cx);
-//             let anchor_range = snapshot.anchor_before(selection_range.start)
-//                 ..snapshot.anchor_after(selection_range.end);
-//             editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
-//                 s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
-//             });
-//         });
-//         cx.update_editor(|editor, cx| {
-//             update_go_to_definition_link(
-//                 editor,
-//                 Some(GoToDefinitionTrigger::Text(hover_point)),
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         cx.foreground().run_until_parked();
-//         assert!(requests.try_next().is_err());
-//         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-//             fn test() { do_work(); }
-//             fn do_work() { test(); }
-//         "});
-//         cx.foreground().run_until_parked();
-//     }
-
-//     #[gpui::test]
-//     async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |settings| {
-//             settings.defaults.inlay_hints = Some(InlayHintSettings {
-//                 enabled: true,
-//                 show_type_hints: true,
-//                 show_parameter_hints: true,
-//                 show_other_hints: true,
-//             })
-//         });
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-//         cx.set_state(indoc! {"
-//             struct TestStruct;
-
-//             fn main() {
-//                 let variableˇ = TestStruct;
-//             }
-//         "});
-//         let hint_start_offset = cx.ranges(indoc! {"
-//             struct TestStruct;
-
-//             fn main() {
-//                 let variableˇ = TestStruct;
-//             }
-//         "})[0]
-//             .start;
-//         let hint_position = cx.to_lsp(hint_start_offset);
-//         let target_range = cx.lsp_range(indoc! {"
-//             struct «TestStruct»;
-
-//             fn main() {
-//                 let variable = TestStruct;
-//             }
-//         "});
-
-//         let expected_uri = cx.buffer_lsp_url.clone();
-//         let hint_label = ": TestStruct";
-//         cx.lsp
-//             .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
-//                 let expected_uri = expected_uri.clone();
-//                 async move {
-//                     assert_eq!(params.text_document.uri, expected_uri);
-//                     Ok(Some(vec![lsp::InlayHint {
-//                         position: hint_position,
-//                         label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
-//                             value: hint_label.to_string(),
-//                             location: Some(lsp::Location {
-//                                 uri: params.text_document.uri,
-//                                 range: target_range,
-//                             }),
-//                             ..Default::default()
-//                         }]),
-//                         kind: Some(lsp::InlayHintKind::TYPE),
-//                         text_edits: None,
-//                         tooltip: None,
-//                         padding_left: Some(false),
-//                         padding_right: Some(false),
-//                         data: None,
-//                     }]))
-//                 }
-//             })
-//             .next()
-//             .await;
-//         cx.foreground().run_until_parked();
-//         cx.update_editor(|editor, cx| {
-//             let expected_layers = vec![hint_label.to_string()];
-//             assert_eq!(expected_layers, cached_hint_labels(editor));
-//             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-//         });
-
-//         let inlay_range = cx
-//             .ranges(indoc! {"
-//             struct TestStruct;
-
-//             fn main() {
-//                 let variable« »= TestStruct;
-//             }
-//         "})
-//             .get(0)
-//             .cloned()
-//             .unwrap();
-//         let hint_hover_position = cx.update_editor(|editor, cx| {
-//             let snapshot = editor.snapshot(cx);
-//             let previous_valid = inlay_range.start.to_display_point(&snapshot);
-//             let next_valid = inlay_range.end.to_display_point(&snapshot);
-//             assert_eq!(previous_valid.row(), next_valid.row());
-//             assert!(previous_valid.column() < next_valid.column());
-//             let exact_unclipped = DisplayPoint::new(
-//                 previous_valid.row(),
-//                 previous_valid.column() + (hint_label.len() / 2) as u32,
-//             );
-//             PointForPosition {
-//                 previous_valid,
-//                 next_valid,
-//                 exact_unclipped,
-//                 column_overshoot_after_line_end: 0,
-//             }
-//         });
-//         // Press cmd to trigger highlight
-//         cx.update_editor(|editor, cx| {
-//             update_inlay_link_and_hover_points(
-//                 &editor.snapshot(cx),
-//                 hint_hover_position,
-//                 editor,
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         cx.foreground().run_until_parked();
-//         cx.update_editor(|editor, cx| {
-//             let snapshot = editor.snapshot(cx);
-//             let actual_highlights = snapshot
-//                 .inlay_highlights::<LinkGoToDefinitionState>()
-//                 .into_iter()
-//                 .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
-//                 .collect::<Vec<_>>();
-
-//             let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-//             let expected_highlight = InlayHighlight {
-//                 inlay: InlayId::Hint(0),
-//                 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
-//                 range: 0..hint_label.len(),
-//             };
-//             assert_set_eq!(actual_highlights, vec![&expected_highlight]);
-//         });
-
-//         // Unpress cmd causes highlight to go away
-//         cx.update_editor(|editor, cx| {
-//             editor.modifiers_changed(
-//                 &platform::ModifiersChangedEvent {
-//                     modifiers: Modifiers {
-//                         cmd: false,
-//                         ..Default::default()
-//                     },
-//                     ..Default::default()
-//                 },
-//                 cx,
-//             );
-//         });
-//         // Assert no link highlights
-//         cx.update_editor(|editor, cx| {
-//             let snapshot = editor.snapshot(cx);
-//             let actual_ranges = snapshot
-//                 .text_highlight_ranges::<LinkGoToDefinitionState>()
-//                 .map(|ranges| ranges.as_ref().clone().1)
-//                 .unwrap_or_default();
-
-//             assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
-//         });
-
-//         // Cmd+click without existing definition requests and jumps
-//         cx.update_editor(|editor, cx| {
-//             editor.modifiers_changed(
-//                 &platform::ModifiersChangedEvent {
-//                     modifiers: Modifiers {
-//                         cmd: true,
-//                         ..Default::default()
-//                     },
-//                     ..Default::default()
-//                 },
-//                 cx,
-//             );
-//             update_inlay_link_and_hover_points(
-//                 &editor.snapshot(cx),
-//                 hint_hover_position,
-//                 editor,
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         cx.foreground().run_until_parked();
-//         cx.update_editor(|editor, cx| {
-//             go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
-//         });
-//         cx.foreground().run_until_parked();
-//         cx.assert_editor_state(indoc! {"
-//             struct «TestStructˇ»;
-
-//             fn main() {
-//                 let variable = TestStruct;
-//             }
-//         "});
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        display_map::ToDisplayPoint,
+        editor_tests::init_test,
+        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+        test::editor_lsp_test_context::EditorLspTestContext,
+    };
+    use futures::StreamExt;
+    use gpui::{Modifiers, ModifiersChangedEvent, View};
+    use indoc::indoc;
+    use language::language_settings::InlayHintSettings;
+    use lsp::request::{GotoDefinition, GotoTypeDefinition};
+    use util::assert_set_eq;
+
+    #[gpui::test]
+    async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        cx.set_state(indoc! {"
+            struct A;
+            let vˇariable = A;
+        "});
+
+        // Basic hold cmd+shift, expect highlight in region if response contains type definition
+        let hover_point = cx.display_point(indoc! {"
+            struct A;
+            let vˇariable = A;
+        "});
+        let symbol_range = cx.lsp_range(indoc! {"
+            struct A;
+            let «variable» = A;
+        "});
+        let target_range = cx.lsp_range(indoc! {"
+            struct «A»;
+            let variable = A;
+        "});
+
+        let mut requests =
+            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
+                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
+                    lsp::LocationLink {
+                        origin_selection_range: Some(symbol_range),
+                        target_uri: url.clone(),
+                        target_range,
+                        target_selection_range: target_range,
+                    },
+                ])))
+            });
+
+        // Press cmd+shift to trigger highlight
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                true,
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.background_executor.run_until_parked();
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            struct A;
+            let «variable» = A;
+        "});
+
+        // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
+        cx.update_editor(|editor, cx| {
+            crate::element::EditorElement::modifiers_changed(
+                editor,
+                &ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        command: true,
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                },
+                cx,
+            );
+        });
+        // Assert no link highlights
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            struct A;
+            let variable = A;
+        "});
+
+        // Cmd+shift click without existing definition requests and jumps
+        let hover_point = cx.display_point(indoc! {"
+            struct A;
+            let vˇariable = A;
+        "});
+        let target_range = cx.lsp_range(indoc! {"
+            struct «A»;
+            let variable = A;
+        "});
+
+        let mut requests =
+            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
+                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
+                    lsp::LocationLink {
+                        origin_selection_range: None,
+                        target_uri: url,
+                        target_range,
+                        target_selection_range: target_range,
+                    },
+                ])))
+            });
+
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
+        });
+        requests.next().await;
+        cx.background_executor.run_until_parked();
+
+        cx.assert_editor_state(indoc! {"
+            struct «Aˇ»;
+            let variable = A;
+        "});
+    }
+
+    #[gpui::test]
+    async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        cx.set_state(indoc! {"
+                fn ˇtest() { do_work(); }
+                fn do_work() { test(); }
+            "});
+
+        // Basic hold cmd, expect highlight in region if response contains definition
+        let hover_point = cx.display_point(indoc! {"
+                fn test() { do_wˇork(); }
+                fn do_work() { test(); }
+            "});
+        let symbol_range = cx.lsp_range(indoc! {"
+                fn test() { «do_work»(); }
+                fn do_work() { test(); }
+            "});
+        let target_range = cx.lsp_range(indoc! {"
+                fn test() { do_work(); }
+                fn «do_work»() { test(); }
+            "});
+
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    origin_selection_range: Some(symbol_range),
+                    target_uri: url.clone(),
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
+
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.background_executor.run_until_parked();
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+                fn test() { «do_work»(); }
+                fn do_work() { test(); }
+            "});
+
+        // Unpress cmd causes highlight to go away
+        cx.update_editor(|editor, cx| {
+            crate::element::EditorElement::modifiers_changed(editor, &Default::default(), cx);
+        });
+
+        // Assert no link highlights
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+                fn test() { do_work(); }
+                fn do_work() { test(); }
+            "});
+
+        // Response without source range still highlights word
+        cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None);
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    // No origin range
+                    origin_selection_range: None,
+                    target_uri: url.clone(),
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.background_executor.run_until_parked();
+
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+                fn test() { «do_work»(); }
+                fn do_work() { test(); }
+            "});
+
+        // Moving mouse to location with no response dismisses highlight
+        let hover_point = cx.display_point(indoc! {"
+                fˇn test() { do_work(); }
+                fn do_work() { test(); }
+            "});
+        let mut requests = cx
+            .lsp
+            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
+                // No definitions returned
+                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
+            });
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.background_executor.run_until_parked();
+
+        // Assert no link highlights
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+                fn test() { do_work(); }
+                fn do_work() { test(); }
+            "});
+
+        // Move mouse without cmd and then pressing cmd triggers highlight
+        let hover_point = cx.display_point(indoc! {"
+                fn test() { do_work(); }
+                fn do_work() { teˇst(); }
+            "});
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                false,
+                false,
+                cx,
+            );
+        });
+        cx.background_executor.run_until_parked();
+
+        // Assert no link highlights
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+                fn test() { do_work(); }
+                fn do_work() { test(); }
+            "});
+
+        let symbol_range = cx.lsp_range(indoc! {"
+                fn test() { do_work(); }
+                fn do_work() { «test»(); }
+            "});
+        let target_range = cx.lsp_range(indoc! {"
+                fn «test»() { do_work(); }
+                fn do_work() { test(); }
+            "});
+
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    origin_selection_range: Some(symbol_range),
+                    target_uri: url,
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
+        cx.update_editor(|editor, cx| {
+            crate::element::EditorElement::modifiers_changed(
+                editor,
+                &ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        command: true,
+                        ..Default::default()
+                    },
+                },
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.background_executor.run_until_parked();
+
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+                fn test() { do_work(); }
+                fn do_work() { «test»(); }
+            "});
+
+        // Deactivating the window dismisses the highlight
+        cx.update_workspace(|workspace, cx| {
+            workspace.on_window_activation_changed(cx);
+        });
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+                fn test() { do_work(); }
+                fn do_work() { test(); }
+            "});
+
+        // Moving the mouse restores the highlights.
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.background_executor.run_until_parked();
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+                fn test() { do_work(); }
+                fn do_work() { «test»(); }
+            "});
+
+        // Moving again within the same symbol range doesn't re-request
+        let hover_point = cx.display_point(indoc! {"
+                fn test() { do_work(); }
+                fn do_work() { tesˇt(); }
+            "});
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.background_executor.run_until_parked();
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+                fn test() { do_work(); }
+                fn do_work() { «test»(); }
+            "});
+
+        // Cmd click with existing definition doesn't re-request and dismisses highlight
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
+        });
+        // Assert selection moved to to definition
+        cx.lsp
+            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
+                // Empty definition response to make sure we aren't hitting the lsp and using
+                // the cached location instead
+                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
+            });
+        cx.background_executor.run_until_parked();
+        cx.assert_editor_state(indoc! {"
+                fn «testˇ»() { do_work(); }
+                fn do_work() { test(); }
+            "});
+
+        // Assert no link highlights after jump
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+                fn test() { do_work(); }
+                fn do_work() { test(); }
+            "});
+
+        // Cmd click without existing definition requests and jumps
+        let hover_point = cx.display_point(indoc! {"
+                fn test() { do_wˇork(); }
+                fn do_work() { test(); }
+            "});
+        let target_range = cx.lsp_range(indoc! {"
+                fn test() { do_work(); }
+                fn «do_work»() { test(); }
+            "});
+
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    origin_selection_range: None,
+                    target_uri: url,
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
+        });
+        requests.next().await;
+        cx.background_executor.run_until_parked();
+        cx.assert_editor_state(indoc! {"
+                fn test() { do_work(); }
+                fn «do_workˇ»() { test(); }
+            "});
+
+        // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
+        // 2. Selection is completed, hovering
+        let hover_point = cx.display_point(indoc! {"
+                fn test() { do_wˇork(); }
+                fn do_work() { test(); }
+            "});
+        let target_range = cx.lsp_range(indoc! {"
+                fn test() { do_work(); }
+                fn «do_work»() { test(); }
+            "});
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    origin_selection_range: None,
+                    target_uri: url,
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
+
+        // create a pending selection
+        let selection_range = cx.ranges(indoc! {"
+                fn «test() { do_w»ork(); }
+                fn do_work() { test(); }
+            "})[0]
+            .clone();
+        cx.update_editor(|editor, cx| {
+            let snapshot = editor.buffer().read(cx).snapshot(cx);
+            let anchor_range = snapshot.anchor_before(selection_range.start)
+                ..snapshot.anchor_after(selection_range.end);
+            editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
+                s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
+            });
+        });
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                Some(GoToDefinitionTrigger::Text(hover_point)),
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.background_executor.run_until_parked();
+        assert!(requests.try_next().is_err());
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+                fn test() { do_work(); }
+                fn do_work() { test(); }
+            "});
+        cx.background_executor.run_until_parked();
+    }
+
+    #[gpui::test]
+    async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+        cx.set_state(indoc! {"
+                struct TestStruct;
+
+                fn main() {
+                    let variableˇ = TestStruct;
+                }
+            "});
+        let hint_start_offset = cx.ranges(indoc! {"
+                struct TestStruct;
+
+                fn main() {
+                    let variableˇ = TestStruct;
+                }
+            "})[0]
+            .start;
+        let hint_position = cx.to_lsp(hint_start_offset);
+        let target_range = cx.lsp_range(indoc! {"
+                struct «TestStruct»;
+
+                fn main() {
+                    let variable = TestStruct;
+                }
+            "});
+
+        let expected_uri = cx.buffer_lsp_url.clone();
+        let hint_label = ": TestStruct";
+        cx.lsp
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let expected_uri = expected_uri.clone();
+                async move {
+                    assert_eq!(params.text_document.uri, expected_uri);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: hint_position,
+                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
+                            value: hint_label.to_string(),
+                            location: Some(lsp::Location {
+                                uri: params.text_document.uri,
+                                range: target_range,
+                            }),
+                            ..Default::default()
+                        }]),
+                        kind: Some(lsp::InlayHintKind::TYPE),
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: Some(false),
+                        padding_right: Some(false),
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.background_executor.run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let expected_layers = vec![hint_label.to_string()];
+            assert_eq!(expected_layers, cached_hint_labels(editor));
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+        });
+
+        let inlay_range = cx
+            .ranges(indoc! {"
+                struct TestStruct;
+
+                fn main() {
+                    let variable« »= TestStruct;
+                }
+            "})
+            .get(0)
+            .cloned()
+            .unwrap();
+        let hint_hover_position = cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let previous_valid = inlay_range.start.to_display_point(&snapshot);
+            let next_valid = inlay_range.end.to_display_point(&snapshot);
+            assert_eq!(previous_valid.row(), next_valid.row());
+            assert!(previous_valid.column() < next_valid.column());
+            let exact_unclipped = DisplayPoint::new(
+                previous_valid.row(),
+                previous_valid.column() + (hint_label.len() / 2) as u32,
+            );
+            PointForPosition {
+                previous_valid,
+                next_valid,
+                exact_unclipped,
+                column_overshoot_after_line_end: 0,
+            }
+        });
+        // Press cmd to trigger highlight
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                hint_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.background_executor.run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let actual_highlights = snapshot
+                .inlay_highlights::<LinkGoToDefinitionState>()
+                .into_iter()
+                .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
+                .collect::<Vec<_>>();
+
+            let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+            let expected_highlight = InlayHighlight {
+                inlay: InlayId::Hint(0),
+                inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+                range: 0..hint_label.len(),
+            };
+            assert_set_eq!(actual_highlights, vec![&expected_highlight]);
+        });
+
+        // Unpress cmd causes highlight to go away
+        cx.update_editor(|editor, cx| {
+            crate::element::EditorElement::modifiers_changed(
+                editor,
+                &ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        command: false,
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                },
+                cx,
+            );
+        });
+        // Assert no link highlights
+        cx.update_editor(|editor, cx| {
+                let snapshot = editor.snapshot(cx);
+                let actual_ranges = snapshot
+                    .text_highlight_ranges::<LinkGoToDefinitionState>()
+                    .map(|ranges| ranges.as_ref().clone().1)
+                    .unwrap_or_default();
+
+                assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
+            });
+
+        // Cmd+click without existing definition requests and jumps
+        cx.update_editor(|editor, cx| {
+            crate::element::EditorElement::modifiers_changed(
+                editor,
+                &ModifiersChangedEvent {
+                    modifiers: Modifiers {
+                        command: true,
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                },
+                cx,
+            );
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                hint_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.background_executor.run_until_parked();
+        cx.update_editor(|editor, cx| {
+            go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
+        });
+        cx.background_executor.run_until_parked();
+        cx.assert_editor_state(indoc! {"
+                struct «TestStructˇ»;
+
+                fn main() {
+                    let variable = TestStruct;
+                }
+            "});
+    }
+}

crates/editor2/src/mouse_context_menu.rs 🔗

@@ -1,5 +1,14 @@
-use crate::{DisplayPoint, Editor, EditorMode, SelectMode};
-use gpui::{Pixels, Point, ViewContext};
+use crate::{
+    DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
+    Rename, RevealInFinder, SelectMode, ToggleCodeActions,
+};
+use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
+
+pub struct MouseContextMenu {
+    pub(crate) position: Point<Pixels>,
+    pub(crate) context_menu: View<ui::ContextMenu>,
+    _subscription: Subscription,
+}
 
 pub fn deploy_context_menu(
     editor: &mut Editor,
@@ -7,88 +16,95 @@ pub fn deploy_context_menu(
     point: DisplayPoint,
     cx: &mut ViewContext<Editor>,
 ) {
-    todo!();
+    if !editor.is_focused(cx) {
+        editor.focus(cx);
+    }
 
-    // if !editor.focused {
-    //     cx.focus_self();
-    // }
+    // Don't show context menu for inline editors
+    if editor.mode() != EditorMode::Full {
+        return;
+    }
 
-    // // Don't show context menu for inline editors
-    // if editor.mode() != EditorMode::Full {
-    //     return;
-    // }
+    // Don't show the context menu if there isn't a project associated with this editor
+    if editor.project.is_none() {
+        return;
+    }
 
-    // // Don't show the context menu if there isn't a project associated with this editor
-    // if editor.project.is_none() {
-    //     return;
-    // }
+    // Move the cursor to the clicked location so that dispatched actions make sense
+    editor.change_selections(None, cx, |s| {
+        s.clear_disjoint();
+        s.set_pending_display_range(point..point, SelectMode::Character);
+    });
 
-    // // Move the cursor to the clicked location so that dispatched actions make sense
-    // editor.change_selections(None, cx, |s| {
-    //     s.clear_disjoint();
-    //     s.set_pending_display_range(point..point, SelectMode::Character);
-    // });
+    let context_menu = ui::ContextMenu::build(cx, |menu, cx| {
+        menu.action("Rename Symbol", Box::new(Rename))
+            .action("Go to Definition", Box::new(GoToDefinition))
+            .action("Go to Type Definition", Box::new(GoToTypeDefinition))
+            .action("Find All References", Box::new(FindAllReferences))
+            .action(
+                "Code Actions",
+                Box::new(ToggleCodeActions {
+                    deployed_from_indicator: false,
+                }),
+            )
+            .separator()
+            .action("Reveal in Finder", Box::new(RevealInFinder))
+    });
+    let context_menu_focus = context_menu.focus_handle(cx);
+    cx.focus(&context_menu_focus);
 
-    // editor.mouse_context_menu.update(cx, |menu, cx| {
-    //     menu.show(
-    //         position,
-    //         AnchorCorner::TopLeft,
-    //         vec![
-    //             ContextMenuItem::action("Rename Symbol", Rename),
-    //             ContextMenuItem::action("Go to Definition", GoToDefinition),
-    //             ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
-    //             ContextMenuItem::action("Find All References", FindAllReferences),
-    //             ContextMenuItem::action(
-    //                 "Code Actions",
-    //                 ToggleCodeActions {
-    //                     deployed_from_indicator: false,
-    //                 },
-    //             ),
-    //             ContextMenuItem::Separator,
-    //             ContextMenuItem::action("Reveal in Finder", RevealInFinder),
-    //         ],
-    //         cx,
-    //     );
-    // });
-    // cx.notify();
+    let _subscription = cx.subscribe(&context_menu, move |this, _, event: &DismissEvent, cx| {
+        this.mouse_context_menu.take();
+        if context_menu_focus.contains_focused(cx) {
+            this.focus(cx);
+        }
+    });
+
+    editor.mouse_context_menu = Some(MouseContextMenu {
+        position,
+        context_menu,
+        _subscription,
+    });
+    cx.notify();
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
-//     use indoc::indoc;
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
+    use indoc::indoc;
 
-//     #[gpui::test]
-//     async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
+    #[gpui::test]
+    async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
 
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
 
-//         cx.set_state(indoc! {"
-//             fn teˇst() {
-//                 do_work();
-//             }
-//         "});
-//         let point = cx.display_point(indoc! {"
-//             fn test() {
-//                 do_wˇork();
-//             }
-//         "});
-//         cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
+        cx.set_state(indoc! {"
+            fn teˇst() {
+                do_work();
+            }
+        "});
+        let point = cx.display_point(indoc! {"
+            fn test() {
+                do_wˇork();
+            }
+        "});
+        cx.editor(|editor, app| assert!(editor.mouse_context_menu.is_none()));
+        cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
 
-//         cx.assert_editor_state(indoc! {"
-//             fn test() {
-//                 do_wˇork();
-//             }
-//         "});
-//         cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible()));
-//     }
-// }
+        cx.assert_editor_state(indoc! {"
+            fn test() {
+                do_wˇork();
+            }
+        "});
+        cx.editor(|editor, app| assert!(editor.mouse_context_menu.is_some()));
+    }
+}

crates/editor2/src/movement.rs 🔗

@@ -452,483 +452,475 @@ pub fn split_display_range_by_lines(
     result
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use crate::{
-//         display_map::Inlay,
-//         test::{},
-//         Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
-//     };
-//     use project::Project;
-//     use settings::SettingsStore;
-//     use util::post_inc;
-
-//     #[gpui::test]
-//     fn test_previous_word_start(cx: &mut gpui::AppContext) {
-//         init_test(cx);
-
-//         fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
-//             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-//             assert_eq!(
-//                 previous_word_start(&snapshot, display_points[1]),
-//                 display_points[0]
-//             );
-//         }
-
-//         assert("\nˇ   ˇlorem", cx);
-//         assert("ˇ\nˇ   lorem", cx);
-//         assert("    ˇloremˇ", cx);
-//         assert("ˇ    ˇlorem", cx);
-//         assert("    ˇlorˇem", cx);
-//         assert("\nlorem\nˇ   ˇipsum", cx);
-//         assert("\n\nˇ\nˇ", cx);
-//         assert("    ˇlorem  ˇipsum", cx);
-//         assert("loremˇ-ˇipsum", cx);
-//         assert("loremˇ-#$@ˇipsum", cx);
-//         assert("ˇlorem_ˇipsum", cx);
-//         assert(" ˇdefγˇ", cx);
-//         assert(" ˇbcΔˇ", cx);
-//         assert(" abˇ——ˇcd", cx);
-//     }
-
-//     #[gpui::test]
-//     fn test_previous_subword_start(cx: &mut gpui::AppContext) {
-//         init_test(cx);
-
-//         fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
-//             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-//             assert_eq!(
-//                 previous_subword_start(&snapshot, display_points[1]),
-//                 display_points[0]
-//             );
-//         }
-
-//         // Subword boundaries are respected
-//         assert("lorem_ˇipˇsum", cx);
-//         assert("lorem_ˇipsumˇ", cx);
-//         assert("ˇlorem_ˇipsum", cx);
-//         assert("lorem_ˇipsum_ˇdolor", cx);
-//         assert("loremˇIpˇsum", cx);
-//         assert("loremˇIpsumˇ", cx);
-
-//         // Word boundaries are still respected
-//         assert("\nˇ   ˇlorem", cx);
-//         assert("    ˇloremˇ", cx);
-//         assert("    ˇlorˇem", cx);
-//         assert("\nlorem\nˇ   ˇipsum", cx);
-//         assert("\n\nˇ\nˇ", cx);
-//         assert("    ˇlorem  ˇipsum", cx);
-//         assert("loremˇ-ˇipsum", cx);
-//         assert("loremˇ-#$@ˇipsum", cx);
-//         assert(" ˇdefγˇ", cx);
-//         assert(" bcˇΔˇ", cx);
-//         assert(" ˇbcδˇ", cx);
-//         assert(" abˇ——ˇcd", cx);
-//     }
-
-//     #[gpui::test]
-//     fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
-//         init_test(cx);
-
-//         fn assert(
-//             marked_text: &str,
-//             cx: &mut gpui::AppContext,
-//             is_boundary: impl FnMut(char, char) -> bool,
-//         ) {
-//             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-//             assert_eq!(
-//                 find_preceding_boundary(
-//                     &snapshot,
-//                     display_points[1],
-//                     FindRange::MultiLine,
-//                     is_boundary
-//                 ),
-//                 display_points[0]
-//             );
-//         }
-
-//         assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
-//             left == 'c' && right == 'd'
-//         });
-//         assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
-//             left == '\n' && right == 'g'
-//         });
-//         let mut line_count = 0;
-//         assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
-//             if left == '\n' {
-//                 line_count += 1;
-//                 line_count == 2
-//             } else {
-//                 false
-//             }
-//         });
-//     }
-
-//     #[gpui::test]
-//     fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
-//         init_test(cx);
-
-//         let input_text = "abcdefghijklmnopqrstuvwxys";
-//         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 buffer = MultiBuffer::build_simple(input_text, cx);
-//         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-//         let display_map =
-//             cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
-
-//         // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
-//         let mut id = 0;
-//         let inlays = (0..buffer_snapshot.len())
-//             .map(|offset| {
-//                 [
-//                     Inlay {
-//                         id: InlayId::Suggestion(post_inc(&mut id)),
-//                         position: buffer_snapshot.anchor_at(offset, Bias::Left),
-//                         text: format!("test").into(),
-//                     },
-//                     Inlay {
-//                         id: InlayId::Suggestion(post_inc(&mut id)),
-//                         position: buffer_snapshot.anchor_at(offset, Bias::Right),
-//                         text: format!("test").into(),
-//                     },
-//                     Inlay {
-//                         id: InlayId::Hint(post_inc(&mut id)),
-//                         position: buffer_snapshot.anchor_at(offset, Bias::Left),
-//                         text: format!("test").into(),
-//                     },
-//                     Inlay {
-//                         id: InlayId::Hint(post_inc(&mut id)),
-//                         position: buffer_snapshot.anchor_at(offset, Bias::Right),
-//                         text: format!("test").into(),
-//                     },
-//                 ]
-//             })
-//             .flatten()
-//             .collect();
-//         let snapshot = display_map.update(cx, |map, cx| {
-//             map.splice_inlays(Vec::new(), inlays, cx);
-//             map.snapshot(cx)
-//         });
-
-//         assert_eq!(
-//             find_preceding_boundary(
-//                 &snapshot,
-//                 buffer_snapshot.len().to_display_point(&snapshot),
-//                 FindRange::MultiLine,
-//                 |left, _| left == 'e',
-//             ),
-//             snapshot
-//                 .buffer_snapshot
-//                 .offset_to_point(5)
-//                 .to_display_point(&snapshot),
-//             "Should not stop at inlays when looking for boundaries"
-//         );
-//     }
-
-//     #[gpui::test]
-//     fn test_next_word_end(cx: &mut gpui::AppContext) {
-//         init_test(cx);
-
-//         fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
-//             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-//             assert_eq!(
-//                 next_word_end(&snapshot, display_points[0]),
-//                 display_points[1]
-//             );
-//         }
-
-//         assert("\nˇ   loremˇ", cx);
-//         assert("    ˇloremˇ", cx);
-//         assert("    lorˇemˇ", cx);
-//         assert("    loremˇ    ˇ\nipsum\n", cx);
-//         assert("\nˇ\nˇ\n\n", cx);
-//         assert("loremˇ    ipsumˇ   ", cx);
-//         assert("loremˇ-ˇipsum", cx);
-//         assert("loremˇ#$@-ˇipsum", cx);
-//         assert("loremˇ_ipsumˇ", cx);
-//         assert(" ˇbcΔˇ", cx);
-//         assert(" abˇ——ˇcd", cx);
-//     }
-
-//     #[gpui::test]
-//     fn test_next_subword_end(cx: &mut gpui::AppContext) {
-//         init_test(cx);
-
-//         fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
-//             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-//             assert_eq!(
-//                 next_subword_end(&snapshot, display_points[0]),
-//                 display_points[1]
-//             );
-//         }
-
-//         // Subword boundaries are respected
-//         assert("loˇremˇ_ipsum", cx);
-//         assert("ˇloremˇ_ipsum", cx);
-//         assert("loremˇ_ipsumˇ", cx);
-//         assert("loremˇ_ipsumˇ_dolor", cx);
-//         assert("loˇremˇIpsum", cx);
-//         assert("loremˇIpsumˇDolor", cx);
-
-//         // Word boundaries are still respected
-//         assert("\nˇ   loremˇ", cx);
-//         assert("    ˇloremˇ", cx);
-//         assert("    lorˇemˇ", cx);
-//         assert("    loremˇ    ˇ\nipsum\n", cx);
-//         assert("\nˇ\nˇ\n\n", cx);
-//         assert("loremˇ    ipsumˇ   ", cx);
-//         assert("loremˇ-ˇipsum", cx);
-//         assert("loremˇ#$@-ˇipsum", cx);
-//         assert("loremˇ_ipsumˇ", cx);
-//         assert(" ˇbcˇΔ", cx);
-//         assert(" abˇ——ˇcd", cx);
-//     }
-
-//     #[gpui::test]
-//     fn test_find_boundary(cx: &mut gpui::AppContext) {
-//         init_test(cx);
-
-//         fn assert(
-//             marked_text: &str,
-//             cx: &mut gpui::AppContext,
-//             is_boundary: impl FnMut(char, char) -> bool,
-//         ) {
-//             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-//             assert_eq!(
-//                 find_boundary(
-//                     &snapshot,
-//                     display_points[0],
-//                     FindRange::MultiLine,
-//                     is_boundary
-//                 ),
-//                 display_points[1]
-//             );
-//         }
-
-//         assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
-//             left == 'j' && right == 'k'
-//         });
-//         assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
-//             left == '\n' && right == 'i'
-//         });
-//         let mut line_count = 0;
-//         assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
-//             if left == '\n' {
-//                 line_count += 1;
-//                 line_count == 2
-//             } else {
-//                 false
-//             }
-//         });
-//     }
-
-//     #[gpui::test]
-//     fn test_surrounding_word(cx: &mut gpui::AppContext) {
-//         init_test(cx);
-
-//         fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
-//             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-//             assert_eq!(
-//                 surrounding_word(&snapshot, display_points[1]),
-//                 display_points[0]..display_points[2],
-//                 "{}",
-//                 marked_text.to_string()
-//             );
-//         }
-
-//         assert("ˇˇloremˇ  ipsum", cx);
-//         assert("ˇloˇremˇ  ipsum", cx);
-//         assert("ˇloremˇˇ  ipsum", cx);
-//         assert("loremˇ ˇ  ˇipsum", cx);
-//         assert("lorem\nˇˇˇ\nipsum", cx);
-//         assert("lorem\nˇˇipsumˇ", cx);
-//         assert("loremˇ,ˇˇ ipsum", cx);
-//         assert("ˇloremˇˇ, ipsum", cx);
-//     }
-
-//     #[gpui::test]
-//     async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
-//         cx.update(|cx| {
-//             init_test(cx);
-//         });
-
-//         let mut cx = EditorTestContext::new(cx).await;
-//         let editor = cx.editor.clone();
-//         let window = cx.window.clone();
-//         cx.update_window(window, |cx| {
-//             let text_layout_details =
-//                 editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
-
-//             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 buffer =
-//                 cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
-//             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(1, 4),
-//                             primary: None,
-//                         },
-//                         ExcerptRange {
-//                             context: Point::new(2, 0)..Point::new(3, 2),
-//                             primary: None,
-//                         },
-//                     ],
-//                     cx,
-//                 );
-//                 multibuffer
-//             });
-//             let display_map =
-//                 cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
-//             let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
-
-//             assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
-
-//             let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details);
-
-//             // Can't move up into the first excerpt's header
-//             assert_eq!(
-//                 up(
-//                     &snapshot,
-//                     DisplayPoint::new(2, 2),
-//                     SelectionGoal::HorizontalPosition(col_2_x),
-//                     false,
-//                     &text_layout_details
-//                 ),
-//                 (
-//                     DisplayPoint::new(2, 0),
-//                     SelectionGoal::HorizontalPosition(0.0)
-//                 ),
-//             );
-//             assert_eq!(
-//                 up(
-//                     &snapshot,
-//                     DisplayPoint::new(2, 0),
-//                     SelectionGoal::None,
-//                     false,
-//                     &text_layout_details
-//                 ),
-//                 (
-//                     DisplayPoint::new(2, 0),
-//                     SelectionGoal::HorizontalPosition(0.0)
-//                 ),
-//             );
-
-//             let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details);
-
-//             // Move up and down within first excerpt
-//             assert_eq!(
-//                 up(
-//                     &snapshot,
-//                     DisplayPoint::new(3, 4),
-//                     SelectionGoal::HorizontalPosition(col_4_x),
-//                     false,
-//                     &text_layout_details
-//                 ),
-//                 (
-//                     DisplayPoint::new(2, 3),
-//                     SelectionGoal::HorizontalPosition(col_4_x)
-//                 ),
-//             );
-//             assert_eq!(
-//                 down(
-//                     &snapshot,
-//                     DisplayPoint::new(2, 3),
-//                     SelectionGoal::HorizontalPosition(col_4_x),
-//                     false,
-//                     &text_layout_details
-//                 ),
-//                 (
-//                     DisplayPoint::new(3, 4),
-//                     SelectionGoal::HorizontalPosition(col_4_x)
-//                 ),
-//             );
-
-//             let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details);
-
-//             // Move up and down across second excerpt's header
-//             assert_eq!(
-//                 up(
-//                     &snapshot,
-//                     DisplayPoint::new(6, 5),
-//                     SelectionGoal::HorizontalPosition(col_5_x),
-//                     false,
-//                     &text_layout_details
-//                 ),
-//                 (
-//                     DisplayPoint::new(3, 4),
-//                     SelectionGoal::HorizontalPosition(col_5_x)
-//                 ),
-//             );
-//             assert_eq!(
-//                 down(
-//                     &snapshot,
-//                     DisplayPoint::new(3, 4),
-//                     SelectionGoal::HorizontalPosition(col_5_x),
-//                     false,
-//                     &text_layout_details
-//                 ),
-//                 (
-//                     DisplayPoint::new(6, 5),
-//                     SelectionGoal::HorizontalPosition(col_5_x)
-//                 ),
-//             );
-
-//             let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details);
-
-//             // Can't move down off the end
-//             assert_eq!(
-//                 down(
-//                     &snapshot,
-//                     DisplayPoint::new(7, 0),
-//                     SelectionGoal::HorizontalPosition(0.0),
-//                     false,
-//                     &text_layout_details
-//                 ),
-//                 (
-//                     DisplayPoint::new(7, 2),
-//                     SelectionGoal::HorizontalPosition(max_point_x)
-//                 ),
-//             );
-//             assert_eq!(
-//                 down(
-//                     &snapshot,
-//                     DisplayPoint::new(7, 2),
-//                     SelectionGoal::HorizontalPosition(max_point_x),
-//                     false,
-//                     &text_layout_details
-//                 ),
-//                 (
-//                     DisplayPoint::new(7, 2),
-//                     SelectionGoal::HorizontalPosition(max_point_x)
-//                 ),
-//             );
-//         });
-//     }
-
-//     fn init_test(cx: &mut gpui::AppContext) {
-//         cx.set_global(SettingsStore::test(cx));
-//         theme::init(cx);
-//         language::init(cx);
-//         crate::init(cx);
-//         Project::init_settings(cx);
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        display_map::Inlay,
+        test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+        Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
+    };
+    use gpui::{font, Context as _};
+    use project::Project;
+    use settings::SettingsStore;
+    use util::post_inc;
+
+    #[gpui::test]
+    fn test_previous_word_start(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                previous_word_start(&snapshot, display_points[1]),
+                display_points[0]
+            );
+        }
+
+        assert("\nˇ   ˇlorem", cx);
+        assert("ˇ\nˇ   lorem", cx);
+        assert("    ˇloremˇ", cx);
+        assert("ˇ    ˇlorem", cx);
+        assert("    ˇlorˇem", cx);
+        assert("\nlorem\nˇ   ˇipsum", cx);
+        assert("\n\nˇ\nˇ", cx);
+        assert("    ˇlorem  ˇipsum", cx);
+        assert("loremˇ-ˇipsum", cx);
+        assert("loremˇ-#$@ˇipsum", cx);
+        assert("ˇlorem_ˇipsum", cx);
+        assert(" ˇdefγˇ", cx);
+        assert(" ˇbcΔˇ", cx);
+        assert(" abˇ——ˇcd", cx);
+    }
+
+    #[gpui::test]
+    fn test_previous_subword_start(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                previous_subword_start(&snapshot, display_points[1]),
+                display_points[0]
+            );
+        }
+
+        // Subword boundaries are respected
+        assert("lorem_ˇipˇsum", cx);
+        assert("lorem_ˇipsumˇ", cx);
+        assert("ˇlorem_ˇipsum", cx);
+        assert("lorem_ˇipsum_ˇdolor", cx);
+        assert("loremˇIpˇsum", cx);
+        assert("loremˇIpsumˇ", cx);
+
+        // Word boundaries are still respected
+        assert("\nˇ   ˇlorem", cx);
+        assert("    ˇloremˇ", cx);
+        assert("    ˇlorˇem", cx);
+        assert("\nlorem\nˇ   ˇipsum", cx);
+        assert("\n\nˇ\nˇ", cx);
+        assert("    ˇlorem  ˇipsum", cx);
+        assert("loremˇ-ˇipsum", cx);
+        assert("loremˇ-#$@ˇipsum", cx);
+        assert(" ˇdefγˇ", cx);
+        assert(" bcˇΔˇ", cx);
+        assert(" ˇbcδˇ", cx);
+        assert(" abˇ——ˇcd", cx);
+    }
+
+    #[gpui::test]
+    fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(
+            marked_text: &str,
+            cx: &mut gpui::AppContext,
+            is_boundary: impl FnMut(char, char) -> bool,
+        ) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                find_preceding_boundary(
+                    &snapshot,
+                    display_points[1],
+                    FindRange::MultiLine,
+                    is_boundary
+                ),
+                display_points[0]
+            );
+        }
+
+        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
+            left == 'c' && right == 'd'
+        });
+        assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
+            left == '\n' && right == 'g'
+        });
+        let mut line_count = 0;
+        assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
+            if left == '\n' {
+                line_count += 1;
+                line_count == 2
+            } else {
+                false
+            }
+        });
+    }
+
+    #[gpui::test]
+    fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        let input_text = "abcdefghijklmnopqrstuvwxys";
+        let font = font("Helvetica");
+        let font_size = px(14.0);
+        let buffer = MultiBuffer::build_simple(input_text, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let display_map =
+            cx.build_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx));
+
+        // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
+        let mut id = 0;
+        let inlays = (0..buffer_snapshot.len())
+            .map(|offset| {
+                [
+                    Inlay {
+                        id: InlayId::Suggestion(post_inc(&mut id)),
+                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
+                        text: format!("test").into(),
+                    },
+                    Inlay {
+                        id: InlayId::Suggestion(post_inc(&mut id)),
+                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
+                        text: format!("test").into(),
+                    },
+                    Inlay {
+                        id: InlayId::Hint(post_inc(&mut id)),
+                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
+                        text: format!("test").into(),
+                    },
+                    Inlay {
+                        id: InlayId::Hint(post_inc(&mut id)),
+                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
+                        text: format!("test").into(),
+                    },
+                ]
+            })
+            .flatten()
+            .collect();
+        let snapshot = display_map.update(cx, |map, cx| {
+            map.splice_inlays(Vec::new(), inlays, cx);
+            map.snapshot(cx)
+        });
+
+        assert_eq!(
+            find_preceding_boundary(
+                &snapshot,
+                buffer_snapshot.len().to_display_point(&snapshot),
+                FindRange::MultiLine,
+                |left, _| left == 'e',
+            ),
+            snapshot
+                .buffer_snapshot
+                .offset_to_point(5)
+                .to_display_point(&snapshot),
+            "Should not stop at inlays when looking for boundaries"
+        );
+    }
+
+    #[gpui::test]
+    fn test_next_word_end(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                next_word_end(&snapshot, display_points[0]),
+                display_points[1]
+            );
+        }
+
+        assert("\nˇ   loremˇ", cx);
+        assert("    ˇloremˇ", cx);
+        assert("    lorˇemˇ", cx);
+        assert("    loremˇ    ˇ\nipsum\n", cx);
+        assert("\nˇ\nˇ\n\n", cx);
+        assert("loremˇ    ipsumˇ   ", cx);
+        assert("loremˇ-ˇipsum", cx);
+        assert("loremˇ#$@-ˇipsum", cx);
+        assert("loremˇ_ipsumˇ", cx);
+        assert(" ˇbcΔˇ", cx);
+        assert(" abˇ——ˇcd", cx);
+    }
+
+    #[gpui::test]
+    fn test_next_subword_end(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                next_subword_end(&snapshot, display_points[0]),
+                display_points[1]
+            );
+        }
+
+        // Subword boundaries are respected
+        assert("loˇremˇ_ipsum", cx);
+        assert("ˇloremˇ_ipsum", cx);
+        assert("loremˇ_ipsumˇ", cx);
+        assert("loremˇ_ipsumˇ_dolor", cx);
+        assert("loˇremˇIpsum", cx);
+        assert("loremˇIpsumˇDolor", cx);
+
+        // Word boundaries are still respected
+        assert("\nˇ   loremˇ", cx);
+        assert("    ˇloremˇ", cx);
+        assert("    lorˇemˇ", cx);
+        assert("    loremˇ    ˇ\nipsum\n", cx);
+        assert("\nˇ\nˇ\n\n", cx);
+        assert("loremˇ    ipsumˇ   ", cx);
+        assert("loremˇ-ˇipsum", cx);
+        assert("loremˇ#$@-ˇipsum", cx);
+        assert("loremˇ_ipsumˇ", cx);
+        assert(" ˇbcˇΔ", cx);
+        assert(" abˇ——ˇcd", cx);
+    }
+
+    #[gpui::test]
+    fn test_find_boundary(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(
+            marked_text: &str,
+            cx: &mut gpui::AppContext,
+            is_boundary: impl FnMut(char, char) -> bool,
+        ) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                find_boundary(
+                    &snapshot,
+                    display_points[0],
+                    FindRange::MultiLine,
+                    is_boundary
+                ),
+                display_points[1]
+            );
+        }
+
+        assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
+            left == 'j' && right == 'k'
+        });
+        assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
+            left == '\n' && right == 'i'
+        });
+        let mut line_count = 0;
+        assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
+            if left == '\n' {
+                line_count += 1;
+                line_count == 2
+            } else {
+                false
+            }
+        });
+    }
+
+    #[gpui::test]
+    fn test_surrounding_word(cx: &mut gpui::AppContext) {
+        init_test(cx);
+
+        fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+            let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+            assert_eq!(
+                surrounding_word(&snapshot, display_points[1]),
+                display_points[0]..display_points[2],
+                "{}",
+                marked_text.to_string()
+            );
+        }
+
+        assert("ˇˇloremˇ  ipsum", cx);
+        assert("ˇloˇremˇ  ipsum", cx);
+        assert("ˇloremˇˇ  ipsum", cx);
+        assert("loremˇ ˇ  ˇipsum", cx);
+        assert("lorem\nˇˇˇ\nipsum", cx);
+        assert("lorem\nˇˇipsumˇ", cx);
+        assert("loremˇ,ˇˇ ipsum", cx);
+        assert("ˇloremˇˇ, ipsum", cx);
+    }
+
+    #[gpui::test]
+    async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
+        cx.update(|cx| {
+            init_test(cx);
+        });
+
+        let mut cx = EditorTestContext::new(cx).await;
+        let editor = cx.editor.clone();
+        let window = cx.window.clone();
+        cx.update_window(window, |_, cx| {
+            let text_layout_details =
+                editor.update(cx, |editor, cx| editor.text_layout_details(cx));
+
+            let font = font("Helvetica");
+
+            let buffer = cx
+                .build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abc\ndefg\nhijkl\nmn"));
+            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(1, 4),
+                            primary: None,
+                        },
+                        ExcerptRange {
+                            context: Point::new(2, 0)..Point::new(3, 2),
+                            primary: None,
+                        },
+                    ],
+                    cx,
+                );
+                multibuffer
+            });
+            let display_map =
+                cx.build_model(|cx| DisplayMap::new(multibuffer, font, px(14.0), None, 2, 2, cx));
+            let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+
+            assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
+
+            let col_2_x =
+                snapshot.x_for_display_point(DisplayPoint::new(2, 2), &text_layout_details);
+
+            // Can't move up into the first excerpt's header
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(2, 2),
+                    SelectionGoal::HorizontalPosition(col_2_x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 0),
+                    SelectionGoal::HorizontalPosition(0.0)
+                ),
+            );
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(2, 0),
+                    SelectionGoal::None,
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 0),
+                    SelectionGoal::HorizontalPosition(0.0)
+                ),
+            );
+
+            let col_4_x =
+                snapshot.x_for_display_point(DisplayPoint::new(3, 4), &text_layout_details);
+
+            // Move up and down within first excerpt
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_4_x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(2, 3),
+                    SelectionGoal::HorizontalPosition(col_4_x.0)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(2, 3),
+                    SelectionGoal::HorizontalPosition(col_4_x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_4_x.0)
+                ),
+            );
+
+            let col_5_x =
+                snapshot.x_for_display_point(DisplayPoint::new(6, 5), &text_layout_details);
+
+            // Move up and down across second excerpt's header
+            assert_eq!(
+                up(
+                    &snapshot,
+                    DisplayPoint::new(6, 5),
+                    SelectionGoal::HorizontalPosition(col_5_x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_5_x.0)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(3, 4),
+                    SelectionGoal::HorizontalPosition(col_5_x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(6, 5),
+                    SelectionGoal::HorizontalPosition(col_5_x.0)
+                ),
+            );
+
+            let max_point_x =
+                snapshot.x_for_display_point(DisplayPoint::new(7, 2), &text_layout_details);
+
+            // Can't move down off the end
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(7, 0),
+                    SelectionGoal::HorizontalPosition(0.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x.0)
+                ),
+            );
+            assert_eq!(
+                down(
+                    &snapshot,
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x.0),
+                    false,
+                    &text_layout_details
+                ),
+                (
+                    DisplayPoint::new(7, 2),
+                    SelectionGoal::HorizontalPosition(max_point_x.0)
+                ),
+            );
+        });
+    }
+
+    fn init_test(cx: &mut gpui::AppContext) {
+        let settings_store = SettingsStore::test(cx);
+        cx.set_global(settings_store);
+        theme::init(theme::LoadThemes::JustBase, cx);
+        language::init(cx);
+        crate::init(cx);
+        Project::init_settings(cx);
+    }
+}

crates/editor2/src/selections_collection.rs 🔗

@@ -315,14 +315,11 @@ impl SelectionsCollection {
 
         let line = display_map.layout_row(row, &text_layout_details);
 
-        dbg!("****START COL****");
         let start_col = line.closest_index_for_x(positions.start) as u32;
         if start_col < line_len || (is_empty && positions.start == line.width) {
             let start = DisplayPoint::new(row, start_col);
-            dbg!("****END COL****");
             let end_col = 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/file_finder2/src/file_finder.rs 🔗

@@ -1256,7 +1256,7 @@ mod tests {
         //
         // 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.
-        workspace.update(cx, |_, cx| dbg!(cx.focused()));
+        workspace.update(cx, |_, cx| cx.focused());
 
         let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
         assert!(

crates/gpui2/src/app.rs 🔗

@@ -358,7 +358,7 @@ impl AppContext {
     {
         let entity_id = entity.entity_id();
         let handle = entity.downgrade();
-        self.observers.insert(
+        let (subscription, activate) = self.observers.insert(
             entity_id,
             Box::new(move |cx| {
                 if let Some(handle) = E::upgrade_from(&handle) {
@@ -367,7 +367,9 @@ impl AppContext {
                     false
                 }
             }),
-        )
+        );
+        self.defer(move |_| activate());
+        subscription
     }
 
     pub fn subscribe<T, E, Evt>(
@@ -398,8 +400,7 @@ impl AppContext {
     {
         let entity_id = entity.entity_id();
         let entity = entity.downgrade();
-
-        self.event_listeners.insert(
+        let (subscription, activate) = self.event_listeners.insert(
             entity_id,
             (
                 TypeId::of::<Evt>(),
@@ -412,7 +413,9 @@ impl AppContext {
                     }
                 }),
             ),
-        )
+        );
+        self.defer(move |_| activate());
+        subscription
     }
 
     pub fn windows(&self) -> Vec<AnyWindowHandle> {
@@ -873,13 +876,15 @@ impl AppContext {
         &mut self,
         mut f: impl FnMut(&mut Self) + 'static,
     ) -> Subscription {
-        self.global_observers.insert(
+        let (subscription, activate) = self.global_observers.insert(
             TypeId::of::<G>(),
             Box::new(move |cx| {
                 f(cx);
                 true
             }),
-        )
+        );
+        self.defer(move |_| activate());
+        subscription
     }
 
     /// Move the global of the given type to the stack.
@@ -903,7 +908,7 @@ impl AppContext {
         &mut self,
         on_new: impl 'static + Fn(&mut V, &mut ViewContext<V>),
     ) -> Subscription {
-        self.new_view_observers.insert(
+        let (subscription, activate) = self.new_view_observers.insert(
             TypeId::of::<V>(),
             Box::new(move |any_view: AnyView, cx: &mut WindowContext| {
                 any_view
@@ -913,7 +918,9 @@ impl AppContext {
                         on_new(view_state, cx);
                     })
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn observe_release<E, T>(
@@ -925,13 +932,15 @@ impl AppContext {
         E: Entity<T>,
         T: 'static,
     {
-        self.release_listeners.insert(
+        let (subscription, activate) = self.release_listeners.insert(
             handle.entity_id(),
             Box::new(move |entity, cx| {
                 let entity = entity.downcast_mut().expect("invalid entity type");
                 on_release(entity, cx)
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) {
@@ -996,13 +1005,15 @@ impl AppContext {
     where
         Fut: 'static + Future<Output = ()>,
     {
-        self.quit_observers.insert(
+        let (subscription, activate) = self.quit_observers.insert(
             (),
             Box::new(move |cx| {
                 let future = on_quit(cx);
                 async move { future.await }.boxed_local()
             }),
-        )
+        );
+        activate();
+        subscription
     }
 }
 

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

@@ -482,10 +482,6 @@ impl<T: 'static> WeakModel<T> {
     /// Update the entity referenced by this model with the given function if
     /// the referenced entity still exists. Returns an error if the entity has
     /// been released.
-    ///
-    /// The update function receives a context appropriate for its environment.
-    /// When updating in an `AppContext`, it receives a `ModelContext`.
-    /// When updating an a `WindowContext`, it receives a `ViewContext`.
     pub fn update<C, R>(
         &self,
         cx: &mut C,
@@ -501,6 +497,21 @@ impl<T: 'static> WeakModel<T> {
                 .map(|this| cx.update_model(&this, update)),
         )
     }
+
+    /// Reads the entity referenced by this model with the given function if
+    /// the referenced entity still exists. Returns an error if the entity has
+    /// been released.
+    pub fn read_with<C, R>(&self, cx: &C, read: impl FnOnce(&T, &AppContext) -> R) -> Result<R>
+    where
+        C: Context,
+        Result<C::Result<R>>: crate::Flatten<R>,
+    {
+        crate::Flatten::flatten(
+            self.upgrade()
+                .ok_or_else(|| anyhow!("entity release"))
+                .map(|this| cx.read_model(&this, read)),
+        )
+    }
 }
 
 impl<T> Hash for WeakModel<T> {

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

@@ -88,13 +88,15 @@ impl<'a, T: 'static> ModelContext<'a, T> {
     where
         T: 'static,
     {
-        self.app.release_listeners.insert(
+        let (subscription, activate) = self.app.release_listeners.insert(
             self.model_state.entity_id,
             Box::new(move |this, cx| {
                 let this = this.downcast_mut().expect("invalid entity type");
                 on_release(this, cx);
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn observe_release<T2, E>(
@@ -109,7 +111,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
     {
         let entity_id = entity.entity_id();
         let this = self.weak_model();
-        self.app.release_listeners.insert(
+        let (subscription, activate) = self.app.release_listeners.insert(
             entity_id,
             Box::new(move |entity, cx| {
                 let entity = entity.downcast_mut().expect("invalid entity type");
@@ -117,7 +119,9 @@ impl<'a, T: 'static> ModelContext<'a, T> {
                     this.update(cx, |this, cx| on_release(this, entity, cx));
                 }
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn observe_global<G: 'static>(
@@ -128,10 +132,12 @@ impl<'a, T: 'static> ModelContext<'a, T> {
         T: 'static,
     {
         let handle = self.weak_model();
-        self.global_observers.insert(
+        let (subscription, activate) = self.global_observers.insert(
             TypeId::of::<G>(),
             Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()),
-        )
+        );
+        self.defer(move |_| activate());
+        subscription
     }
 
     pub fn on_app_quit<Fut>(
@@ -143,7 +149,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
         T: 'static,
     {
         let handle = self.weak_model();
-        self.app.quit_observers.insert(
+        let (subscription, activate) = self.app.quit_observers.insert(
             (),
             Box::new(move |cx| {
                 let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
@@ -154,7 +160,9 @@ impl<'a, T: 'static> ModelContext<'a, T> {
                 }
                 .boxed_local()
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn notify(&mut self) {

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

@@ -1,13 +1,13 @@
 use crate::{
     div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
-    BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
-    KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher,
-    TestPlatform, TestWindow, View, ViewContext, VisualContext, WindowContext, WindowHandle,
-    WindowOptions,
+    BackgroundExecutor, Bounds, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
+    KeyDownEvent, Keystroke, Model, ModelContext, Pixels, PlatformWindow, Point, Render, Result,
+    Size, Task, TestDispatcher, TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext,
+    VisualContext, WindowBounds, WindowContext, WindowHandle, WindowOptions,
 };
 use anyhow::{anyhow, bail};
 use futures::{Stream, StreamExt};
-use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
+use std::{future::Future, mem, ops::Deref, rc::Rc, sync::Arc, time::Duration};
 
 #[derive(Clone)]
 pub struct TestAppContext {
@@ -170,6 +170,45 @@ impl TestAppContext {
         self.test_platform.has_pending_prompt()
     }
 
+    pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
+        let (mut handlers, scale_factor) = self
+            .app
+            .borrow_mut()
+            .update_window(window_handle, |_, cx| {
+                let platform_window = cx.window.platform_window.as_test().unwrap();
+                let scale_factor = platform_window.scale_factor();
+                match &mut platform_window.bounds {
+                    WindowBounds::Fullscreen | WindowBounds::Maximized => {
+                        platform_window.bounds = WindowBounds::Fixed(Bounds {
+                            origin: Point::default(),
+                            size: size.map(|pixels| f64::from(pixels).into()),
+                        });
+                    }
+                    WindowBounds::Fixed(bounds) => {
+                        bounds.size = size.map(|pixels| f64::from(pixels).into());
+                    }
+                }
+
+                (
+                    mem::take(&mut platform_window.handlers.lock().resize),
+                    scale_factor,
+                )
+            })
+            .unwrap();
+
+        for handler in &mut handlers {
+            handler(size, scale_factor);
+        }
+
+        self.app
+            .borrow_mut()
+            .update_window(window_handle, |_, cx| {
+                let platform_window = cx.window.platform_window.as_test().unwrap();
+                platform_window.handlers.lock().resize = handlers;
+            })
+            .unwrap();
+    }
+
     pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
     where
         Fut: Future<Output = R> + 'static,
@@ -343,12 +382,15 @@ impl TestAppContext {
         use smol::future::FutureExt as _;
 
         async {
-            while notifications.next().await.is_some() {
+            loop {
                 if model.update(self, &mut predicate) {
                     return Ok(());
                 }
+
+                if notifications.next().await.is_none() {
+                    bail!("model dropped")
+                }
             }
-            bail!("model dropped")
         }
         .race(timer.map(|_| Err(anyhow!("condition timed out"))))
         .await
@@ -502,6 +544,19 @@ impl<'a> VisualTestContext<'a> {
         self.cx.dispatch_action(self.window, action)
     }
 
+    pub fn window_title(&mut self) -> Option<String> {
+        self.cx
+            .update_window(self.window, |_, cx| {
+                cx.window
+                    .platform_window
+                    .as_test()
+                    .unwrap()
+                    .window_title
+                    .clone()
+            })
+            .unwrap()
+    }
+
     pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
         self.cx.simulate_keystrokes(self.window, keystrokes)
     }
@@ -509,6 +564,39 @@ impl<'a> VisualTestContext<'a> {
     pub fn simulate_input(&mut self, input: &str) {
         self.cx.simulate_input(self.window, input)
     }
+
+    pub fn simulate_activation(&mut self) {
+        self.simulate_window_events(&mut |handlers| {
+            handlers
+                .active_status_change
+                .iter_mut()
+                .for_each(|f| f(true));
+        })
+    }
+
+    pub fn simulate_deactivation(&mut self) {
+        self.simulate_window_events(&mut |handlers| {
+            handlers
+                .active_status_change
+                .iter_mut()
+                .for_each(|f| f(false));
+        })
+    }
+
+    fn simulate_window_events(&mut self, f: &mut dyn FnMut(&mut TestWindowHandlers)) {
+        let handlers = self
+            .cx
+            .update_window(self.window, |_, cx| {
+                cx.window
+                    .platform_window
+                    .as_test()
+                    .unwrap()
+                    .handlers
+                    .clone()
+            })
+            .unwrap();
+        f(&mut *handlers.lock());
+    }
 }
 
 impl<'a> Context for VisualTestContext<'a> {

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

@@ -0,0 +1,48 @@
+use crate::{Bounds, Element, IntoElement, Pixels, StyleRefinement, Styled, WindowContext};
+
+pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext)) -> Canvas {
+    Canvas {
+        paint_callback: Box::new(callback),
+        style: Default::default(),
+    }
+}
+
+pub struct Canvas {
+    paint_callback: Box<dyn FnOnce(Bounds<Pixels>, &mut WindowContext)>,
+    style: StyleRefinement,
+}
+
+impl IntoElement for Canvas {
+    type Element = Self;
+
+    fn element_id(&self) -> Option<crate::ElementId> {
+        None
+    }
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}
+
+impl Element for Canvas {
+    type State = ();
+
+    fn layout(
+        &mut self,
+        _: Option<Self::State>,
+        cx: &mut WindowContext,
+    ) -> (crate::LayoutId, Self::State) {
+        let layout_id = cx.request_layout(&self.style.clone().into(), []);
+        (layout_id, ())
+    }
+
+    fn paint(self, bounds: Bounds<Pixels>, _: &mut (), cx: &mut WindowContext) {
+        (self.paint_callback)(bounds, cx)
+    }
+}
+
+impl Styled for Canvas {
+    fn style(&mut self) -> &mut crate::StyleRefinement {
+        &mut self.style
+    }
+}

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

@@ -221,20 +221,6 @@ pub trait InteractiveElement: Sized + Element {
 
     /// Add a listener for the given action, fires during the bubble event phase
     fn on_action<A: Action>(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self {
-        // NOTE: this debug assert has the side-effect of working around
-        // a bug where a crate consisting only of action definitions does
-        // not register the actions in debug builds:
-        //
-        // https://github.com/rust-lang/rust/issues/47384
-        // https://github.com/mmastrac/rust-ctor/issues/280
-        //
-        // if we are relying on this side-effect still, removing the debug_assert!
-        // likely breaks the command_palette tests.
-        // debug_assert!(
-        //     A::is_registered(),
-        //     "{:?} is not registered as an action",
-        //     A::qualified_name()
-        // );
         self.interactivity().action_listeners.push((
             TypeId::of::<A>(),
             Box::new(move |action, phase, cx| {
@@ -247,6 +233,23 @@ pub trait InteractiveElement: Sized + Element {
         self
     }
 
+    fn on_boxed_action(
+        mut self,
+        action: &Box<dyn Action>,
+        listener: impl Fn(&Box<dyn Action>, &mut WindowContext) + 'static,
+    ) -> Self {
+        let action = action.boxed_clone();
+        self.interactivity().action_listeners.push((
+            (*action).type_id(),
+            Box::new(move |_, phase, cx| {
+                if phase == DispatchPhase::Bubble {
+                    (listener)(&action, cx)
+                }
+            }),
+        ));
+        self
+    }
+
     fn on_key_down(
         mut self,
         listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,
@@ -866,6 +869,7 @@ impl Interactivity {
         }
 
         if self.hover_style.is_some()
+            || self.base_style.mouse_cursor.is_some()
             || cx.active_drag.is_some() && !self.drag_over_styles.is_empty()
         {
             let bounds = bounds.intersect(&cx.content_mask().bounds);
@@ -992,10 +996,6 @@ impl Interactivity {
             let interactive_bounds = interactive_bounds.clone();
 
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
-                if phase != DispatchPhase::Bubble {
-                    return;
-                }
-
                 let is_hovered = interactive_bounds.visibly_contains(&event.position, cx)
                     && pending_mouse_down.borrow().is_none();
                 if !is_hovered {
@@ -1003,6 +1003,10 @@ impl Interactivity {
                     return;
                 }
 
+                if phase != DispatchPhase::Bubble {
+                    return;
+                }
+
                 if active_tooltip.borrow().is_none() {
                     let task = cx.spawn({
                         let active_tooltip = active_tooltip.clone();

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

@@ -1,3 +1,4 @@
+mod canvas;
 mod div;
 mod img;
 mod overlay;
@@ -5,6 +6,7 @@ mod svg;
 mod text;
 mod uniform_list;
 
+pub use canvas::*;
 pub use div::*;
 pub use img::*;
 pub use overlay::*;

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

@@ -1,6 +1,7 @@
 use crate::{
-    Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent,
-    Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine,
+    Bounds, DispatchPhase, Element, ElementId, HighlightStyle, IntoElement, LayoutId,
+    MouseDownEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextRun, TextStyle,
+    WhiteSpace, WindowContext, WrappedLine,
 };
 use anyhow::anyhow;
 use parking_lot::{Mutex, MutexGuard};
@@ -87,7 +88,28 @@ impl StyledText {
         }
     }
 
-    pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
+    pub fn with_highlights(
+        mut self,
+        default_style: &TextStyle,
+        highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+    ) -> Self {
+        let mut runs = Vec::new();
+        let mut ix = 0;
+        for (range, highlight) in highlights {
+            if ix < range.start {
+                runs.push(default_style.clone().to_run(range.start - ix));
+            }
+            runs.push(
+                default_style
+                    .clone()
+                    .highlight(highlight)
+                    .to_run(range.len()),
+            );
+            ix = range.end;
+        }
+        if ix < self.text.len() {
+            runs.push(default_style.to_run(self.text.len() - ix));
+        }
         self.runs = Some(runs);
         self
     }
@@ -144,7 +166,6 @@ impl TextState {
         runs: Option<Vec<TextRun>>,
         cx: &mut WindowContext,
     ) -> LayoutId {
-        let text_system = cx.text_system().clone();
         let text_style = cx.text_style();
         let font_size = text_style.font_size.to_pixels(cx.rem_size());
         let line_height = text_style
@@ -152,18 +173,16 @@ impl TextState {
             .to_pixels(font_size.into(), cx.rem_size());
         let text = SharedString::from(text);
 
-        let rem_size = cx.rem_size();
-
         let runs = if let Some(runs) = runs {
             runs
         } else {
             vec![text_style.to_run(text.len())]
         };
 
-        let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
+        let layout_id = cx.request_measured_layout(Default::default(), {
             let element_state = self.clone();
 
-            move |known_dimensions, available_space| {
+            move |known_dimensions, available_space, cx| {
                 let wrap_width = if text_style.white_space == WhiteSpace::Normal {
                     known_dimensions.width.or(match available_space.width {
                         crate::AvailableSpace::Definite(x) => Some(x),
@@ -181,7 +200,8 @@ impl TextState {
                     }
                 }
 
-                let Some(lines) = text_system
+                let Some(lines) = cx
+                    .text_system()
                     .shape_text(
                         &text, font_size, &runs, wrap_width, // Wrap if we know the width.
                     )

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

@@ -109,7 +109,6 @@ impl Element for UniformList {
         cx: &mut WindowContext,
     ) -> (LayoutId, Self::State) {
         let max_items = self.item_count;
-        let rem_size = cx.rem_size();
         let item_size = state
             .as_ref()
             .map(|s| s.item_size)
@@ -120,9 +119,7 @@ impl Element for UniformList {
                 .layout(state.map(|s| s.interactive), cx, |style, cx| {
                     cx.request_measured_layout(
                         style,
-                        rem_size,
-                        move |known_dimensions: Size<Option<Pixels>>,
-                              available_space: Size<AvailableSpace>| {
+                        move |known_dimensions, available_space, _cx| {
                             let desired_height = item_size.height * max_items;
                             let width =
                                 known_dimensions

crates/gpui2/src/executor.rs 🔗

@@ -128,11 +128,19 @@ impl BackgroundExecutor {
     #[cfg(any(test, feature = "test-support"))]
     #[track_caller]
     pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
-        self.block_internal(false, future)
+        if let Ok(value) = self.block_internal(false, future, usize::MAX) {
+            value
+        } else {
+            unreachable!()
+        }
     }
 
     pub fn block<R>(&self, future: impl Future<Output = R>) -> R {
-        self.block_internal(true, future)
+        if let Ok(value) = self.block_internal(true, future, usize::MAX) {
+            value
+        } else {
+            unreachable!()
+        }
     }
 
     #[track_caller]
@@ -140,7 +148,8 @@ impl BackgroundExecutor {
         &self,
         background_only: bool,
         future: impl Future<Output = R>,
-    ) -> R {
+        mut max_ticks: usize,
+    ) -> Result<R, ()> {
         pin_mut!(future);
         let unparker = self.dispatcher.unparker();
         let awoken = Arc::new(AtomicBool::new(false));
@@ -156,8 +165,13 @@ impl BackgroundExecutor {
 
         loop {
             match future.as_mut().poll(&mut cx) {
-                Poll::Ready(result) => return result,
+                Poll::Ready(result) => return Ok(result),
                 Poll::Pending => {
+                    if max_ticks == 0 {
+                        return Err(());
+                    }
+                    max_ticks -= 1;
+
                     if !self.dispatcher.tick(background_only) {
                         if awoken.swap(false, SeqCst) {
                             continue;
@@ -192,16 +206,25 @@ impl BackgroundExecutor {
             return Err(future);
         }
 
+        #[cfg(any(test, feature = "test-support"))]
+        let max_ticks = self
+            .dispatcher
+            .as_test()
+            .map_or(usize::MAX, |dispatcher| dispatcher.gen_block_on_ticks());
+        #[cfg(not(any(test, feature = "test-support")))]
+        let max_ticks = usize::MAX;
+
         let mut timer = self.timer(duration).fuse();
+
         let timeout = async {
             futures::select_biased! {
                 value = future => Ok(value),
                 _ = timer => Err(()),
             }
         };
-        match self.block(timeout) {
-            Ok(value) => Ok(value),
-            Err(_) => Err(future),
+        match self.block_internal(true, timeout, max_ticks) {
+            Ok(Ok(value)) => Ok(value),
+            _ => Err(future),
         }
     }
 
@@ -281,6 +304,11 @@ impl BackgroundExecutor {
     pub fn is_main_thread(&self) -> bool {
         self.dispatcher.is_main_thread()
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
+        self.dispatcher.as_test().unwrap().set_block_on_ticks(range);
+    }
 }
 
 impl ForegroundExecutor {

crates/gpui2/src/geometry.rs 🔗

@@ -655,6 +655,20 @@ pub struct Corners<T: Clone + Default + Debug> {
     pub bottom_left: T,
 }
 
+impl<T> Corners<T>
+where
+    T: Clone + Default + Debug,
+{
+    pub fn all(value: T) -> Self {
+        Self {
+            top_left: value.clone(),
+            top_right: value.clone(),
+            bottom_right: value.clone(),
+            bottom_left: value,
+        }
+    }
+}
+
 impl Corners<AbsoluteLength> {
     pub fn to_pixels(&self, size: Size<Pixels>, rem_size: Pixels) -> Corners<Pixels> {
         let max = size.width.max(size.height) / 2.;

crates/gpui2/src/gpui2.rs 🔗

@@ -21,7 +21,7 @@ mod subscription;
 mod svg_renderer;
 mod taffy;
 #[cfg(any(test, feature = "test-support"))]
-mod test;
+pub mod test;
 mod text_system;
 mod util;
 mod view;

crates/gpui2/src/key_dispatch.rs 🔗

@@ -16,7 +16,7 @@ pub struct DispatchNodeId(usize);
 
 pub(crate) struct DispatchTree {
     node_stack: Vec<DispatchNodeId>,
-    context_stack: Vec<KeyContext>,
+    pub(crate) context_stack: Vec<KeyContext>,
     nodes: Vec<DispatchNode>,
     focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
     keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
@@ -163,11 +163,25 @@ impl DispatchTree {
         actions
     }
 
-    pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
+    pub fn bindings_for_action(
+        &self,
+        action: &dyn Action,
+        context_stack: &Vec<KeyContext>,
+    ) -> Vec<KeyBinding> {
         self.keymap
             .lock()
             .bindings_for_action(action.type_id())
-            .filter(|candidate| candidate.action.partial_eq(action))
+            .filter(|candidate| {
+                if !candidate.action.partial_eq(action) {
+                    return false;
+                }
+                for i in 1..context_stack.len() {
+                    if candidate.matches_context(&context_stack[0..=i]) {
+                        return true;
+                    }
+                }
+                return false;
+            })
             .cloned()
             .collect()
     }

crates/gpui2/src/platform.rs 🔗

@@ -44,7 +44,7 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
     Rc::new(MacPlatform::new())
 }
 
-pub(crate) trait Platform: 'static {
+pub trait Platform: 'static {
     fn background_executor(&self) -> BackgroundExecutor;
     fn foreground_executor(&self) -> ForegroundExecutor;
     fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
@@ -128,7 +128,7 @@ impl Debug for DisplayId {
 
 unsafe impl Send for DisplayId {}
 
-pub(crate) trait PlatformWindow {
+pub trait PlatformWindow {
     fn bounds(&self) -> WindowBounds;
     fn content_size(&self) -> Size<Pixels>;
     fn scale_factor(&self) -> f32;
@@ -158,6 +158,11 @@ pub(crate) trait PlatformWindow {
     fn draw(&self, scene: Scene);
 
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
+
+    #[cfg(any(test, feature = "test-support"))]
+    fn as_test(&mut self) -> Option<&mut TestWindow> {
+        None
+    }
 }
 
 pub trait PlatformDispatcher: Send + Sync {
@@ -467,13 +472,27 @@ pub enum PromptLevel {
     Critical,
 }
 
+/// The style of the cursor (pointer)
 #[derive(Copy, Clone, Debug)]
 pub enum CursorStyle {
     Arrow,
+    IBeam,
+    Crosshair,
+    ClosedHand,
+    OpenHand,
+    PointingHand,
+    ResizeLeft,
+    ResizeRight,
     ResizeLeftRight,
+    ResizeUp,
+    ResizeDown,
     ResizeUpDown,
-    PointingHand,
-    IBeam,
+    DisappearingItem,
+    IBeamCursorForVerticalLayout,
+    OperationNotAllowed,
+    DragLink,
+    DragCopy,
+    ContextualMenu,
 }
 
 impl Default for CursorStyle {

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

@@ -724,16 +724,35 @@ impl Platform for MacPlatform {
         }
     }
 
+    /// Match cursor style to one of the styles available
+    /// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor).
     fn set_cursor_style(&self, style: CursorStyle) {
         unsafe {
             let new_cursor: id = match style {
                 CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
-                CursorStyle::ResizeLeftRight => {
-                    msg_send![class!(NSCursor), resizeLeftRightCursor]
-                }
-                CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
-                CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
                 CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
+                CursorStyle::Crosshair => msg_send![class!(NSCursor), crosshairCursor],
+                CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
+                CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor],
+                CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
+                CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor],
+                CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
+                CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
+                CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor],
+                CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
+                CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
+                CursorStyle::DisappearingItem => {
+                    msg_send![class!(NSCursor), disappearingItemCursor]
+                }
+                CursorStyle::IBeamCursorForVerticalLayout => {
+                    msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
+                }
+                CursorStyle::OperationNotAllowed => {
+                    msg_send![class!(NSCursor), operationNotAllowedCursor]
+                }
+                CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor],
+                CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor],
+                CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor],
             };
 
             let old_cursor: id = msg_send![class!(NSCursor), currentCursor];

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

@@ -7,6 +7,7 @@ use parking_lot::Mutex;
 use rand::prelude::*;
 use std::{
     future::Future,
+    ops::RangeInclusive,
     pin::Pin,
     sync::Arc,
     task::{Context, Poll},
@@ -36,6 +37,7 @@ struct TestDispatcherState {
     allow_parking: bool,
     waiting_backtrace: Option<Backtrace>,
     deprioritized_task_labels: HashSet<TaskLabel>,
+    block_on_ticks: RangeInclusive<usize>,
 }
 
 impl TestDispatcher {
@@ -53,6 +55,7 @@ impl TestDispatcher {
             allow_parking: false,
             waiting_backtrace: None,
             deprioritized_task_labels: Default::default(),
+            block_on_ticks: 0..=1000,
         };
 
         TestDispatcher {
@@ -82,8 +85,8 @@ impl TestDispatcher {
     }
 
     pub fn simulate_random_delay(&self) -> impl 'static + Send + Future<Output = ()> {
-        pub struct YieldNow {
-            count: usize,
+        struct YieldNow {
+            pub(crate) count: usize,
         }
 
         impl Future for YieldNow {
@@ -142,6 +145,16 @@ impl TestDispatcher {
     pub fn rng(&self) -> StdRng {
         self.state.lock().random.clone()
     }
+
+    pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
+        self.state.lock().block_on_ticks = range;
+    }
+
+    pub fn gen_block_on_ticks(&self) -> usize {
+        let mut lock = self.state.lock();
+        let block_on_ticks = lock.block_on_ticks.clone();
+        lock.random.gen_range(block_on_ticks)
+    }
 }
 
 impl Clone for TestDispatcher {

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

@@ -1,6 +1,6 @@
 use crate::{
-    AnyWindowHandle, BackgroundExecutor, CursorStyle, DisplayId, ForegroundExecutor, Platform,
-    PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
+    AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
+    Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
 };
 use anyhow::{anyhow, Result};
 use collections::VecDeque;
@@ -20,6 +20,7 @@ pub struct TestPlatform {
     active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
     active_display: Rc<dyn PlatformDisplay>,
     active_cursor: Mutex<CursorStyle>,
+    current_clipboard_item: Mutex<Option<ClipboardItem>>,
     pub(crate) prompts: RefCell<TestPrompts>,
     weak: Weak<Self>,
 }
@@ -39,6 +40,7 @@ impl TestPlatform {
             active_cursor: Default::default(),
             active_display: Rc::new(TestDisplay::new()),
             active_window: Default::default(),
+            current_clipboard_item: Mutex::new(None),
             weak: weak.clone(),
         })
     }
@@ -189,13 +191,9 @@ impl Platform for TestPlatform {
         unimplemented!()
     }
 
-    fn on_become_active(&self, _callback: Box<dyn FnMut()>) {
-        unimplemented!()
-    }
+    fn on_become_active(&self, _callback: Box<dyn FnMut()>) {}
 
-    fn on_resign_active(&self, _callback: Box<dyn FnMut()>) {
-        unimplemented!()
-    }
+    fn on_resign_active(&self, _callback: Box<dyn FnMut()>) {}
 
     fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
 
@@ -240,12 +238,12 @@ impl Platform for TestPlatform {
         true
     }
 
-    fn write_to_clipboard(&self, _item: crate::ClipboardItem) {
-        unimplemented!()
+    fn write_to_clipboard(&self, item: ClipboardItem) {
+        *self.current_clipboard_item.lock() = Some(item);
     }
 
-    fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
-        unimplemented!()
+    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
+        self.current_clipboard_item.lock().clone()
     }
 
     fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Result<()> {

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

@@ -11,19 +11,20 @@ use std::{
 };
 
 #[derive(Default)]
-struct Handlers {
-    active_status_change: Vec<Box<dyn FnMut(bool)>>,
-    input: Vec<Box<dyn FnMut(crate::InputEvent) -> bool>>,
-    moved: Vec<Box<dyn FnMut()>>,
-    resize: Vec<Box<dyn FnMut(Size<Pixels>, f32)>>,
+pub(crate) struct TestWindowHandlers {
+    pub(crate) active_status_change: Vec<Box<dyn FnMut(bool)>>,
+    pub(crate) input: Vec<Box<dyn FnMut(crate::InputEvent) -> bool>>,
+    pub(crate) moved: Vec<Box<dyn FnMut()>>,
+    pub(crate) resize: Vec<Box<dyn FnMut(Size<Pixels>, f32)>>,
 }
 
 pub struct TestWindow {
-    bounds: WindowBounds,
+    pub(crate) bounds: WindowBounds,
     current_scene: Mutex<Option<Scene>>,
     display: Rc<dyn PlatformDisplay>,
+    pub(crate) window_title: Option<String>,
     pub(crate) input_handler: Option<Arc<Mutex<Box<dyn PlatformInputHandler>>>>,
-    handlers: Mutex<Handlers>,
+    pub(crate) handlers: Arc<Mutex<TestWindowHandlers>>,
     platform: Weak<TestPlatform>,
     sprite_atlas: Arc<dyn PlatformAtlas>,
 }
@@ -42,6 +43,7 @@ impl TestWindow {
             input_handler: None,
             sprite_atlas: Arc::new(TestAtlas::new()),
             handlers: Default::default(),
+            window_title: Default::default(),
         }
     }
 }
@@ -100,8 +102,8 @@ impl PlatformWindow for TestWindow {
         todo!()
     }
 
-    fn set_title(&mut self, _title: &str) {
-        todo!()
+    fn set_title(&mut self, title: &str) {
+        self.window_title = Some(title.to_owned());
     }
 
     fn set_edited(&mut self, _edited: bool) {
@@ -167,6 +169,10 @@ impl PlatformWindow for TestWindow {
     fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
         self.sprite_atlas.clone()
     }
+
+    fn as_test(&mut self) -> Option<&mut TestWindow> {
+        Some(self)
+    }
 }
 
 pub struct TestAtlasState {

crates/gpui2/src/scene.rs 🔗

@@ -198,7 +198,7 @@ impl SceneBuilder {
     }
 }
 
-pub(crate) struct Scene {
+pub struct Scene {
     pub shadows: Vec<Shadow>,
     pub quads: Vec<Quad>,
     pub paths: Vec<Path<ScaledPixels>>,
@@ -214,7 +214,7 @@ impl Scene {
         &self.paths
     }
 
-    pub fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
+    pub(crate) fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
         BatchIterator {
             shadows: &self.shadows,
             shadows_start: 0,

crates/gpui2/src/style.rs 🔗

@@ -208,8 +208,9 @@ impl TextStyle {
         }
     }
 
+    /// Returns the rounded line height in pixels.
     pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels {
-        self.line_height.to_pixels(self.font_size, rem_size)
+        self.line_height.to_pixels(self.font_size, rem_size).round()
     }
 
     pub fn to_run(&self, len: usize) -> TextRun {

crates/gpui2/src/styled.rs 🔗

@@ -101,6 +101,125 @@ pub trait Styled: Sized {
         self
     }
 
+    /// Sets cursor style when hovering over an element to `text`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_text(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::IBeam);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `move`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_move(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ClosedHand);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `not-allowed`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_not_allowed(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::OperationNotAllowed);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `context-menu`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_context_menu(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ContextualMenu);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `crosshair`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_crosshair(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::Crosshair);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `vertical-text`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_vertical_text(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::IBeamCursorForVerticalLayout);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `alias`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_alias(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::DragLink);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `copy`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_copy(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::DragCopy);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `no-drop`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_no_drop(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::OperationNotAllowed);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `grab`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_grab(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::OpenHand);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `grabbing`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_grabbing(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ClosedHand);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `col-resize`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_col_resize(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ResizeLeftRight);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `row-resize`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_row_resize(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ResizeUpDown);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `n-resize`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_n_resize(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ResizeUp);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `e-resize`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_e_resize(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ResizeRight);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `s-resize`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_s_resize(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ResizeDown);
+        self
+    }
+
+    /// Sets cursor style when hovering over an element to `w-resize`.
+    /// [Docs](https://tailwindcss.com/docs/cursor)
+    fn cursor_w_resize(mut self) -> Self {
+        self.style().mouse_cursor = Some(CursorStyle::ResizeLeft);
+        self
+    }
+
     /// Sets the whitespace of the element to `normal`.
     /// [Docs](https://tailwindcss.com/docs/whitespace#normal)
     fn whitespace_normal(mut self) -> Self {

crates/gpui2/src/subscription.rs 🔗

@@ -1,6 +1,6 @@
 use collections::{BTreeMap, BTreeSet};
 use parking_lot::Mutex;
-use std::{fmt::Debug, mem, sync::Arc};
+use std::{cell::Cell, fmt::Debug, mem, rc::Rc, sync::Arc};
 use util::post_inc;
 
 pub(crate) struct SubscriberSet<EmitterKey, Callback>(
@@ -14,11 +14,16 @@ impl<EmitterKey, Callback> Clone for SubscriberSet<EmitterKey, Callback> {
 }
 
 struct SubscriberSetState<EmitterKey, Callback> {
-    subscribers: BTreeMap<EmitterKey, Option<BTreeMap<usize, Callback>>>,
+    subscribers: BTreeMap<EmitterKey, Option<BTreeMap<usize, Subscriber<Callback>>>>,
     dropped_subscribers: BTreeSet<(EmitterKey, usize)>,
     next_subscriber_id: usize,
 }
 
+struct Subscriber<Callback> {
+    active: Rc<Cell<bool>>,
+    callback: Callback,
+}
+
 impl<EmitterKey, Callback> SubscriberSet<EmitterKey, Callback>
 where
     EmitterKey: 'static + Ord + Clone + Debug,
@@ -32,16 +37,33 @@ where
         })))
     }
 
-    pub fn insert(&self, emitter_key: EmitterKey, callback: Callback) -> Subscription {
+    /// Inserts a new `[Subscription]` for the given `emitter_key`. By default, subscriptions
+    /// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`.
+    /// This method returns a tuple of a `[Subscription]` and an `impl FnOnce`, and you can use the latter
+    /// to activate the `[Subscription]`.
+    #[must_use]
+    pub fn insert(
+        &self,
+        emitter_key: EmitterKey,
+        callback: Callback,
+    ) -> (Subscription, impl FnOnce()) {
+        let active = Rc::new(Cell::new(false));
         let mut lock = self.0.lock();
         let subscriber_id = post_inc(&mut lock.next_subscriber_id);
         lock.subscribers
             .entry(emitter_key.clone())
             .or_default()
             .get_or_insert_with(|| Default::default())
-            .insert(subscriber_id, callback);
+            .insert(
+                subscriber_id,
+                Subscriber {
+                    active: active.clone(),
+                    callback,
+                },
+            );
         let this = self.0.clone();
-        Subscription {
+
+        let subscription = Subscription {
             unsubscribe: Some(Box::new(move || {
                 let mut lock = this.lock();
                 let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) else {
@@ -63,7 +85,8 @@ where
                 lock.dropped_subscribers
                     .insert((emitter_key, subscriber_id));
             })),
-        }
+        };
+        (subscription, move || active.set(true))
     }
 
     pub fn remove(&self, emitter: &EmitterKey) -> impl IntoIterator<Item = Callback> {
@@ -73,6 +96,13 @@ where
             .map(|s| s.into_values())
             .into_iter()
             .flatten()
+            .filter_map(|subscriber| {
+                if subscriber.active.get() {
+                    Some(subscriber.callback)
+                } else {
+                    None
+                }
+            })
     }
 
     /// Call the given callback for each subscriber to the given emitter.
@@ -91,7 +121,13 @@ where
             return;
         };
 
-        subscribers.retain(|_, callback| f(callback));
+        subscribers.retain(|_, subscriber| {
+            if subscriber.active.get() {
+                f(&mut subscriber.callback)
+            } else {
+                true
+            }
+        });
         let mut lock = self.0.lock();
 
         // Add any new subscribers that were added while invoking the callback.

crates/gpui2/src/taffy.rs 🔗

@@ -1,4 +1,7 @@
-use super::{AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style};
+use crate::{
+    AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style,
+    WindowContext,
+};
 use collections::{HashMap, HashSet};
 use smallvec::SmallVec;
 use std::fmt::Debug;
@@ -9,13 +12,21 @@ use taffy::{
     Taffy,
 };
 
-type Measureable = dyn Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync;
-
 pub struct TaffyLayoutEngine {
-    taffy: Taffy<Box<Measureable>>,
+    taffy: Taffy,
     children_to_parents: HashMap<LayoutId, LayoutId>,
     absolute_layout_bounds: HashMap<LayoutId, Bounds<Pixels>>,
     computed_layouts: HashSet<LayoutId>,
+    nodes_to_measure: HashMap<
+        LayoutId,
+        Box<
+            dyn FnMut(
+                Size<Option<Pixels>>,
+                Size<AvailableSpace>,
+                &mut WindowContext,
+            ) -> Size<Pixels>,
+        >,
+    >,
 }
 
 static EXPECT_MESSAGE: &'static str =
@@ -28,6 +39,7 @@ impl TaffyLayoutEngine {
             children_to_parents: HashMap::default(),
             absolute_layout_bounds: HashMap::default(),
             computed_layouts: HashSet::default(),
+            nodes_to_measure: HashMap::default(),
         }
     }
 
@@ -36,6 +48,7 @@ impl TaffyLayoutEngine {
         self.children_to_parents.clear();
         self.absolute_layout_bounds.clear();
         self.computed_layouts.clear();
+        self.nodes_to_measure.clear();
     }
 
     pub fn request_layout(
@@ -65,18 +78,18 @@ impl TaffyLayoutEngine {
         &mut self,
         style: Style,
         rem_size: Pixels,
-        measure: impl Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels>
-            + Send
-            + Sync
+        measure: impl FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut WindowContext) -> Size<Pixels>
             + 'static,
     ) -> LayoutId {
         let style = style.to_taffy(rem_size);
 
-        let measurable = Box::new(measure);
-        self.taffy
-            .new_leaf_with_context(style, measurable)
+        let layout_id = self
+            .taffy
+            .new_leaf_with_context(style, ())
             .expect(EXPECT_MESSAGE)
-            .into()
+            .into();
+        self.nodes_to_measure.insert(layout_id, Box::new(measure));
+        layout_id
     }
 
     // Used to understand performance
@@ -126,7 +139,12 @@ impl TaffyLayoutEngine {
         Ok(edges)
     }
 
-    pub fn compute_layout(&mut self, id: LayoutId, available_space: Size<AvailableSpace>) {
+    pub fn compute_layout(
+        &mut self,
+        id: LayoutId,
+        available_space: Size<AvailableSpace>,
+        cx: &mut WindowContext,
+    ) {
         // Leaving this here until we have a better instrumentation approach.
         // println!("Laying out {} children", self.count_all_children(id)?);
         // println!("Max layout depth: {}", self.max_depth(0, id)?);
@@ -159,8 +177,8 @@ impl TaffyLayoutEngine {
             .compute_layout_with_measure(
                 id.into(),
                 available_space.into(),
-                |known_dimensions, available_space, _node_id, context| {
-                    let Some(measure) = context else {
+                |known_dimensions, available_space, node_id, _context| {
+                    let Some(measure) = self.nodes_to_measure.get_mut(&node_id.into()) else {
                         return taffy::geometry::Size::default();
                     };
 
@@ -169,10 +187,11 @@ impl TaffyLayoutEngine {
                         height: known_dimensions.height.map(Pixels),
                     };
 
-                    measure(known_dimensions, available_space.into()).into()
+                    measure(known_dimensions, available_space.into(), cx).into()
                 },
             )
             .expect(EXPECT_MESSAGE);
+
         // println!("compute_layout took {:?}", started_at.elapsed());
     }
 

crates/gpui2/src/test.rs 🔗

@@ -1,5 +1,7 @@
-use crate::TestDispatcher;
+use crate::{Entity, Subscription, TestAppContext, TestDispatcher};
+use futures::StreamExt as _;
 use rand::prelude::*;
+use smol::channel;
 use std::{
     env,
     panic::{self, RefUnwindSafe},
@@ -49,3 +51,30 @@ pub fn run_test(
         }
     }
 }
+
+pub struct Observation<T> {
+    rx: channel::Receiver<T>,
+    _subscription: Subscription,
+}
+
+impl<T: 'static> futures::Stream for Observation<T> {
+    type Item = T;
+
+    fn poll_next(
+        mut self: std::pin::Pin<&mut Self>,
+        cx: &mut std::task::Context<'_>,
+    ) -> std::task::Poll<Option<Self::Item>> {
+        self.rx.poll_next_unpin(cx)
+    }
+}
+
+pub fn observe<T: 'static>(entity: &impl Entity<T>, cx: &mut TestAppContext) -> Observation<()> {
+    let (tx, rx) = smol::channel::unbounded();
+    let _subscription = cx.update(|cx| {
+        cx.observe(entity, move |_, _| {
+            let _ = smol::block_on(tx.send(()));
+        })
+    });
+
+    Observation { rx, _subscription }
+}

crates/gpui2/src/text_system.rs 🔗

@@ -72,7 +72,7 @@ impl TextSystem {
         }
     }
 
-    pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Result<Bounds<Pixels>> {
+    pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds<Pixels> {
         self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size))
     }
 
@@ -89,9 +89,9 @@ impl TextSystem {
         let bounds = self
             .platform_text_system
             .typographic_bounds(font_id, glyph_id)?;
-        self.read_metrics(font_id, |metrics| {
+        Ok(self.read_metrics(font_id, |metrics| {
             (bounds / metrics.units_per_em as f32 * font_size.0).map(px)
-        })
+        }))
     }
 
     pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
@@ -100,28 +100,28 @@ impl TextSystem {
             .glyph_for_char(font_id, ch)
             .ok_or_else(|| anyhow!("glyph not found for character '{}'", ch))?;
         let result = self.platform_text_system.advance(font_id, glyph_id)?
-            / self.units_per_em(font_id)? as f32;
+            / self.units_per_em(font_id) as f32;
 
         Ok(result * font_size)
     }
 
-    pub fn units_per_em(&self, font_id: FontId) -> Result<u32> {
+    pub fn units_per_em(&self, font_id: FontId) -> u32 {
         self.read_metrics(font_id, |metrics| metrics.units_per_em as u32)
     }
 
-    pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+    pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
         self.read_metrics(font_id, |metrics| metrics.cap_height(font_size))
     }
 
-    pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+    pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
         self.read_metrics(font_id, |metrics| metrics.x_height(font_size))
     }
 
-    pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+    pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
         self.read_metrics(font_id, |metrics| metrics.ascent(font_size))
     }
 
-    pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+    pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
         self.read_metrics(font_id, |metrics| metrics.descent(font_size))
     }
 
@@ -130,24 +130,24 @@ impl TextSystem {
         font_id: FontId,
         font_size: Pixels,
         line_height: Pixels,
-    ) -> Result<Pixels> {
-        let ascent = self.ascent(font_id, font_size)?;
-        let descent = self.descent(font_id, font_size)?;
+    ) -> Pixels {
+        let ascent = self.ascent(font_id, font_size);
+        let descent = self.descent(font_id, font_size);
         let padding_top = (line_height - ascent - descent) / 2.;
-        Ok(padding_top + ascent)
+        padding_top + ascent
     }
 
-    fn read_metrics<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> Result<T> {
+    fn read_metrics<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T {
         let lock = self.font_metrics.upgradable_read();
 
         if let Some(metrics) = lock.get(&font_id) {
-            Ok(read(metrics))
+            read(metrics)
         } else {
             let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
             let metrics = lock
                 .entry(font_id)
                 .or_insert_with(|| self.platform_text_system.font_metrics(font_id));
-            Ok(read(metrics))
+            read(metrics)
         }
     }
 

crates/gpui2/src/text_system/line.rs 🔗

@@ -101,9 +101,7 @@ fn paint_line(
     let mut glyph_origin = origin;
     let mut prev_glyph_position = Point::default();
     for (run_ix, run) in layout.runs.iter().enumerate() {
-        let max_glyph_size = text_system
-            .bounding_box(run.font_id, layout.font_size)?
-            .size;
+        let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
 
         for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
             glyph_origin.x += glyph.position.x - prev_glyph_position.x;

crates/gpui2/src/view.rs 🔗

@@ -209,9 +209,7 @@ impl AnyView {
     ) {
         cx.with_absolute_element_offset(origin, |cx| {
             let (layout_id, rendered_element) = (self.layout)(self, cx);
-            cx.window
-                .layout_engine
-                .compute_layout(layout_id, available_space);
+            cx.compute_layout(layout_id, available_space);
             (self.paint)(self, rendered_element, cx);
         })
     }
@@ -240,6 +238,10 @@ impl Element for AnyView {
     }
 
     fn paint(self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+        debug_assert!(
+            state.is_some(),
+            "state is None. Did you include an AnyView twice in the tree?"
+        );
         (self.paint)(&self, state.take().unwrap(), cx)
     }
 }

crates/gpui2/src/window.rs 🔗

@@ -209,7 +209,7 @@ pub struct Window {
     sprite_atlas: Arc<dyn PlatformAtlas>,
     rem_size: Pixels,
     viewport_size: Size<Pixels>,
-    pub(crate) layout_engine: TaffyLayoutEngine,
+    layout_engine: Option<TaffyLayoutEngine>,
     pub(crate) root_view: Option<AnyView>,
     pub(crate) element_id_stack: GlobalElementId,
     pub(crate) previous_frame: Frame,
@@ -327,7 +327,7 @@ impl Window {
             sprite_atlas,
             rem_size: px(16.),
             viewport_size: content_size,
-            layout_engine: TaffyLayoutEngine::new(),
+            layout_engine: Some(TaffyLayoutEngine::new()),
             root_view: None,
             element_id_stack: GlobalElementId::default(),
             previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
@@ -490,7 +490,7 @@ impl<'a> WindowContext<'a> {
         let entity_id = entity.entity_id();
         let entity = entity.downgrade();
         let window_handle = self.window.handle;
-        self.app.event_listeners.insert(
+        let (subscription, activate) = self.app.event_listeners.insert(
             entity_id,
             (
                 TypeId::of::<Evt>(),
@@ -508,7 +508,9 @@ impl<'a> WindowContext<'a> {
                         .unwrap_or(false)
                 }),
             ),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     /// Create an `AsyncWindowContext`, which has a static lifetime and can be held across
@@ -606,9 +608,11 @@ impl<'a> WindowContext<'a> {
         self.app.layout_id_buffer.extend(children.into_iter());
         let rem_size = self.rem_size();
 
-        self.window
-            .layout_engine
-            .request_layout(style, rem_size, &self.app.layout_id_buffer)
+        self.window.layout_engine.as_mut().unwrap().request_layout(
+            style,
+            rem_size,
+            &self.app.layout_id_buffer,
+        )
     }
 
     /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children,
@@ -618,22 +622,25 @@ impl<'a> WindowContext<'a> {
     /// The given closure is invoked at layout time with the known dimensions and available space and
     /// returns a `Size`.
     pub fn request_measured_layout<
-        F: Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync + 'static,
+        F: FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut WindowContext) -> Size<Pixels>
+            + 'static,
     >(
         &mut self,
         style: Style,
-        rem_size: Pixels,
         measure: F,
     ) -> LayoutId {
+        let rem_size = self.rem_size();
         self.window
             .layout_engine
+            .as_mut()
+            .unwrap()
             .request_measured_layout(style, rem_size, measure)
     }
 
     pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size<AvailableSpace>) {
-        self.window
-            .layout_engine
-            .compute_layout(layout_id, available_space)
+        let mut layout_engine = self.window.layout_engine.take().unwrap();
+        layout_engine.compute_layout(layout_id, available_space, self);
+        self.window.layout_engine = Some(layout_engine);
     }
 
     /// Obtain the bounds computed for the given LayoutId relative to the window. This method should not
@@ -643,6 +650,8 @@ impl<'a> WindowContext<'a> {
         let mut bounds = self
             .window
             .layout_engine
+            .as_mut()
+            .unwrap()
             .layout_bounds(layout_id)
             .map(Into::into);
         bounds.origin += self.element_offset();
@@ -678,6 +687,10 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.zoom();
     }
 
+    pub fn set_window_title(&mut self, title: &str) {
+        self.window.platform_window.set_title(title);
+    }
+
     pub fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
         self.platform
             .displays()
@@ -1189,7 +1202,7 @@ impl<'a> WindowContext<'a> {
         self.text_system().start_frame();
 
         let window = &mut *self.window;
-        window.layout_engine.clear();
+        window.layout_engine.as_mut().unwrap().clear();
 
         mem::swap(&mut window.previous_frame, &mut window.current_frame);
         let frame = &mut window.current_frame;
@@ -1337,6 +1350,8 @@ impl<'a> WindowContext<'a> {
                 .dispatch_tree
                 .dispatch_path(node_id);
 
+            let mut actions: Vec<Box<dyn Action>> = Vec::new();
+
             // Capture phase
             let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new();
             self.propagate_event = true;
@@ -1371,22 +1386,26 @@ impl<'a> WindowContext<'a> {
                 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
+                        if let Some(found) = 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;
-                            }
+                            actions.push(found.boxed_clone())
                         }
                     }
 
                     context_stack.pop();
                 }
             }
+
+            for action in actions {
+                self.dispatch_action_on_node(node_id, action);
+                if !self.propagate_event {
+                    return;
+                }
+            }
         }
     }
 
@@ -1414,7 +1433,6 @@ impl<'a> WindowContext<'a> {
                 }
             }
         }
-
         // Bubble phase
         for node_id in dispatch_path.iter().rev() {
             let node = self.window.current_frame.dispatch_tree.node(*node_id);
@@ -1442,10 +1460,12 @@ impl<'a> WindowContext<'a> {
         f: impl Fn(&mut WindowContext<'_>) + 'static,
     ) -> Subscription {
         let window_handle = self.window.handle;
-        self.global_observers.insert(
+        let (subscription, activate) = self.global_observers.insert(
             TypeId::of::<G>(),
             Box::new(move |cx| window_handle.update(cx, |_, cx| f(cx)).is_ok()),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     pub fn activate_window(&self) {
@@ -1482,9 +1502,30 @@ impl<'a> WindowContext<'a> {
 
     pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
         self.window
-            .current_frame
+            .previous_frame
             .dispatch_tree
-            .bindings_for_action(action)
+            .bindings_for_action(
+                action,
+                &self.window.previous_frame.dispatch_tree.context_stack,
+            )
+    }
+
+    pub fn bindings_for_action_in(
+        &self,
+        action: &dyn Action,
+        focus_handle: &FocusHandle,
+    ) -> Vec<KeyBinding> {
+        let dispatch_tree = &self.window.previous_frame.dispatch_tree;
+
+        let Some(node_id) = dispatch_tree.focusable_node_id(focus_handle.id) else {
+            return vec![];
+        };
+        let context_stack = dispatch_tree
+            .dispatch_path(node_id)
+            .into_iter()
+            .map(|node_id| dispatch_tree.node(node_id).context.clone())
+            .collect();
+        dispatch_tree.bindings_for_action(action, &context_stack)
     }
 
     pub fn listener_for<V: Render, E>(
@@ -2085,7 +2126,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         let entity_id = entity.entity_id();
         let entity = entity.downgrade();
         let window_handle = self.window.handle;
-        self.app.observers.insert(
+        let (subscription, activate) = self.app.observers.insert(
             entity_id,
             Box::new(move |cx| {
                 window_handle
@@ -2099,7 +2140,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                     })
                     .unwrap_or(false)
             }),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     pub fn subscribe<V2, E, Evt>(
@@ -2116,7 +2159,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         let entity_id = entity.entity_id();
         let handle = entity.downgrade();
         let window_handle = self.window.handle;
-        self.app.event_listeners.insert(
+        let (subscription, activate) = self.app.event_listeners.insert(
             entity_id,
             (
                 TypeId::of::<Evt>(),
@@ -2134,7 +2177,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                         .unwrap_or(false)
                 }),
             ),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     pub fn on_release(
@@ -2142,13 +2187,15 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         on_release: impl FnOnce(&mut V, &mut WindowContext) + 'static,
     ) -> Subscription {
         let window_handle = self.window.handle;
-        self.app.release_listeners.insert(
+        let (subscription, activate) = self.app.release_listeners.insert(
             self.view.model.entity_id,
             Box::new(move |this, cx| {
                 let this = this.downcast_mut().expect("invalid entity type");
                 let _ = window_handle.update(cx, |_, cx| on_release(this, cx));
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn observe_release<V2, E>(
@@ -2164,7 +2211,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         let view = self.view().downgrade();
         let entity_id = entity.entity_id();
         let window_handle = self.window.handle;
-        self.app.release_listeners.insert(
+        let (subscription, activate) = self.app.release_listeners.insert(
             entity_id,
             Box::new(move |entity, cx| {
                 let entity = entity.downcast_mut().expect("invalid entity type");
@@ -2172,7 +2219,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                     view.update(cx, |this, cx| on_release(this, entity, cx))
                 });
             }),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn notify(&mut self) {
@@ -2187,10 +2236,12 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
     ) -> Subscription {
         let view = self.view.downgrade();
-        self.window.bounds_observers.insert(
+        let (subscription, activate) = self.window.bounds_observers.insert(
             (),
             Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
-        )
+        );
+        activate();
+        subscription
     }
 
     pub fn observe_window_activation(
@@ -2198,10 +2249,12 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
     ) -> Subscription {
         let view = self.view.downgrade();
-        self.window.activation_observers.insert(
+        let (subscription, activate) = self.window.activation_observers.insert(
             (),
             Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
-        )
+        );
+        activate();
+        subscription
     }
 
     /// Register a listener to be called when the given focus handle receives focus.
@@ -2214,7 +2267,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let view = self.view.downgrade();
         let focus_id = handle.id;
-        self.window.focus_listeners.insert(
+        let (subscription, activate) = self.window.focus_listeners.insert(
             (),
             Box::new(move |event, cx| {
                 view.update(cx, |view, cx| {
@@ -2224,7 +2277,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                 })
                 .is_ok()
             }),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     /// Register a listener to be called when the given focus handle or one of its descendants receives focus.
@@ -2237,7 +2292,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let view = self.view.downgrade();
         let focus_id = handle.id;
-        self.window.focus_listeners.insert(
+        let (subscription, activate) = self.window.focus_listeners.insert(
             (),
             Box::new(move |event, cx| {
                 view.update(cx, |view, cx| {
@@ -2251,7 +2306,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                 })
                 .is_ok()
             }),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     /// Register a listener to be called when the given focus handle loses focus.
@@ -2264,7 +2321,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let view = self.view.downgrade();
         let focus_id = handle.id;
-        self.window.focus_listeners.insert(
+        let (subscription, activate) = self.window.focus_listeners.insert(
             (),
             Box::new(move |event, cx| {
                 view.update(cx, |view, cx| {
@@ -2274,7 +2331,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                 })
                 .is_ok()
             }),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     /// Register a listener to be called when the given focus handle or one of its descendants loses focus.
@@ -2287,7 +2346,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let view = self.view.downgrade();
         let focus_id = handle.id;
-        self.window.focus_listeners.insert(
+        let (subscription, activate) = self.window.focus_listeners.insert(
             (),
             Box::new(move |event, cx| {
                 view.update(cx, |view, cx| {
@@ -2301,7 +2360,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
                 })
                 .is_ok()
             }),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     pub fn spawn<Fut, R>(
@@ -2332,14 +2393,16 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     ) -> Subscription {
         let window_handle = self.window.handle;
         let view = self.view().downgrade();
-        self.global_observers.insert(
+        let (subscription, activate) = self.global_observers.insert(
             TypeId::of::<G>(),
             Box::new(move |cx| {
                 window_handle
                     .update(cx, |_, cx| view.update(cx, |view, cx| f(view, cx)).is_ok())
                     .unwrap_or(false)
             }),
-        )
+        );
+        self.app.defer(move |_| activate());
+        subscription
     }
 
     pub fn on_mouse_event<Event: 'static>(
@@ -2697,6 +2760,7 @@ pub enum ElementId {
     Integer(usize),
     Name(SharedString),
     FocusHandle(FocusId),
+    NamedInteger(SharedString, usize),
 }
 
 impl ElementId {
@@ -2746,3 +2810,9 @@ impl<'a> From<&'a FocusHandle> for ElementId {
         ElementId::FocusHandle(handle.id)
     }
 }
+
+impl From<(&'static str, EntityId)> for ElementId {
+    fn from((name, id): (&'static str, EntityId)) -> Self {
+        ElementId::NamedInteger(name.into(), id.as_u64() as usize)
+    }
+}

crates/language2/src/outline.rs 🔗

@@ -81,6 +81,7 @@ impl<T> Outline<T> {
         let mut prev_item_ix = 0;
         for mut string_match in matches {
             let outline_match = &self.items[string_match.candidate_id];
+            string_match.string = outline_match.text.clone();
 
             if is_path_query {
                 let prefix_len = self.path_candidate_prefixes[string_match.candidate_id];

crates/language_selector2/Cargo.toml 🔗

@@ -0,0 +1,26 @@
+[package]
+name = "language_selector2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/language_selector.rs"
+doctest = false
+
+[dependencies]
+editor = { package = "editor2", path = "../editor2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+language = { package = "language2", path = "../language2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+picker = { package = "picker2", path = "../picker2" }
+project = { package = "project2", path = "../project2" }
+theme = { package = "theme2", path = "../theme2" }
+ui = { package = "ui2", path = "../ui2" }
+settings = { package = "settings2", path = "../settings2" }
+util = { path = "../util" }
+workspace = { package = "workspace2", path = "../workspace2" }
+anyhow.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/language_selector2/src/active_buffer_language.rs 🔗

@@ -0,0 +1,82 @@
+use editor::Editor;
+use gpui::{
+    div, Div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView,
+};
+use std::sync::Arc;
+use ui::{Button, ButtonCommon, Clickable, Tooltip};
+use workspace::{item::ItemHandle, StatusItemView, Workspace};
+
+use crate::LanguageSelector;
+
+pub struct ActiveBufferLanguage {
+    active_language: Option<Option<Arc<str>>>,
+    workspace: WeakView<Workspace>,
+    _observe_active_editor: Option<Subscription>,
+}
+
+impl ActiveBufferLanguage {
+    pub fn new(workspace: &Workspace) -> Self {
+        Self {
+            active_language: None,
+            workspace: workspace.weak_handle(),
+            _observe_active_editor: None,
+        }
+    }
+
+    fn update_language(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
+        self.active_language = Some(None);
+
+        let editor = editor.read(cx);
+        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
+            if let Some(language) = buffer.read(cx).language() {
+                self.active_language = Some(Some(language.name()));
+            }
+        }
+
+        cx.notify();
+    }
+}
+
+impl Render for ActiveBufferLanguage {
+    type Element = Div;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Div {
+        div().when_some(self.active_language.as_ref(), |el, active_language| {
+            let active_language_text = if let Some(active_language_text) = active_language {
+                active_language_text.to_string()
+            } else {
+                "Unknown".to_string()
+            };
+
+            el.child(
+                Button::new("change-language", active_language_text)
+                    .on_click(cx.listener(|this, _, cx| {
+                        if let Some(workspace) = this.workspace.upgrade() {
+                            workspace.update(cx, |workspace, cx| {
+                                LanguageSelector::toggle(workspace, cx)
+                            });
+                        }
+                    }))
+                    .tooltip(|cx| Tooltip::text("Select Language", cx)),
+            )
+        })
+    }
+}
+
+impl StatusItemView for ActiveBufferLanguage {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
+            self._observe_active_editor = Some(cx.observe(&editor, Self::update_language));
+            self.update_language(editor, cx);
+        } else {
+            self.active_language = None;
+            self._observe_active_editor = None;
+        }
+
+        cx.notify();
+    }
+}

crates/language_selector2/src/language_selector.rs 🔗

@@ -0,0 +1,232 @@
+mod active_buffer_language;
+
+pub use active_buffer_language::ActiveBufferLanguage;
+use anyhow::anyhow;
+use editor::Editor;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+    actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model,
+    ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
+};
+use language::{Buffer, LanguageRegistry};
+use picker::{Picker, PickerDelegate};
+use project::Project;
+use std::sync::Arc;
+use ui::{v_stack, HighlightedLabel, ListItem, Selectable};
+use util::ResultExt;
+use workspace::Workspace;
+
+actions!(Toggle);
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(LanguageSelector::register).detach();
+}
+
+pub struct LanguageSelector {
+    picker: View<Picker<LanguageSelectorDelegate>>,
+}
+
+impl LanguageSelector {
+    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+        workspace.register_action(move |workspace, _: &Toggle, cx| {
+            Self::toggle(workspace, cx);
+        });
+    }
+
+    fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
+        let registry = workspace.app_state().languages.clone();
+        let (_, buffer, _) = workspace
+            .active_item(cx)?
+            .act_as::<Editor>(cx)?
+            .read(cx)
+            .active_excerpt(cx)?;
+        let project = workspace.project().clone();
+
+        workspace.toggle_modal(cx, move |cx| {
+            LanguageSelector::new(buffer, project, registry, cx)
+        });
+        Some(())
+    }
+
+    fn new(
+        buffer: Model<Buffer>,
+        project: Model<Project>,
+        language_registry: Arc<LanguageRegistry>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let delegate = LanguageSelectorDelegate::new(
+            cx.view().downgrade(),
+            buffer,
+            project,
+            language_registry,
+        );
+
+        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+        Self { picker }
+    }
+}
+
+impl Render for LanguageSelector {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        v_stack().min_w_96().child(self.picker.clone())
+    }
+}
+
+impl FocusableView for LanguageSelector {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
+    }
+}
+
+impl EventEmitter<DismissEvent> for LanguageSelector {}
+
+pub struct LanguageSelectorDelegate {
+    language_selector: WeakView<LanguageSelector>,
+    buffer: Model<Buffer>,
+    project: Model<Project>,
+    language_registry: Arc<LanguageRegistry>,
+    candidates: Vec<StringMatchCandidate>,
+    matches: Vec<StringMatch>,
+    selected_index: usize,
+}
+
+impl LanguageSelectorDelegate {
+    fn new(
+        language_selector: WeakView<LanguageSelector>,
+        buffer: Model<Buffer>,
+        project: Model<Project>,
+        language_registry: Arc<LanguageRegistry>,
+    ) -> Self {
+        let candidates = language_registry
+            .language_names()
+            .into_iter()
+            .enumerate()
+            .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
+            .collect::<Vec<_>>();
+
+        Self {
+            language_selector,
+            buffer,
+            project,
+            language_registry,
+            candidates,
+            matches: vec![],
+            selected_index: 0,
+        }
+    }
+}
+
+impl PickerDelegate for LanguageSelectorDelegate {
+    type ListItem = ListItem;
+
+    fn placeholder_text(&self) -> Arc<str> {
+        "Select a language...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
+        if let Some(mat) = self.matches.get(self.selected_index) {
+            let language_name = &self.candidates[mat.candidate_id].string;
+            let language = self.language_registry.language_for_name(language_name);
+            let project = self.project.downgrade();
+            let buffer = self.buffer.downgrade();
+            cx.spawn(|_, mut cx| async move {
+                let language = language.await?;
+                let project = project
+                    .upgrade()
+                    .ok_or_else(|| anyhow!("project was dropped"))?;
+                let buffer = buffer
+                    .upgrade()
+                    .ok_or_else(|| anyhow!("buffer was dropped"))?;
+                project.update(&mut cx, |project, cx| {
+                    project.set_language_for_buffer(&buffer, language, cx);
+                })
+            })
+            .detach_and_log_err(cx);
+        }
+        self.dismissed(cx);
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+        self.language_selector
+            .update(cx, |_, cx| cx.emit(DismissEvent))
+            .log_err();
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> gpui::Task<()> {
+        let background = cx.background_executor().clone();
+        let candidates = self.candidates.clone();
+        cx.spawn(|this, mut cx| async move {
+            let matches = if query.is_empty() {
+                candidates
+                    .into_iter()
+                    .enumerate()
+                    .map(|(index, candidate)| StringMatch {
+                        candidate_id: index,
+                        string: candidate.string,
+                        positions: Vec::new(),
+                        score: 0.0,
+                    })
+                    .collect()
+            } else {
+                match_strings(
+                    &candidates,
+                    &query,
+                    false,
+                    100,
+                    &Default::default(),
+                    background,
+                )
+                .await
+            };
+
+            this.update(&mut cx, |this, cx| {
+                let delegate = &mut this.delegate;
+                delegate.matches = matches;
+                delegate.selected_index = delegate
+                    .selected_index
+                    .min(delegate.matches.len().saturating_sub(1));
+                cx.notify();
+            })
+            .log_err();
+        })
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let mat = &self.matches[ix];
+        let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
+        let mut label = mat.string.clone();
+        if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
+            label.push_str(" (current)");
+        }
+
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .selected(selected)
+                .child(HighlightedLabel::new(label, mat.positions.clone())),
+        )
+    }
+}

crates/outline2/Cargo.toml 🔗

@@ -0,0 +1,29 @@
+[package]
+name = "outline2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/outline.rs"
+doctest = false
+
+[dependencies]
+editor = { package = "editor2", path = "../editor2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
+language = { package = "language2", path = "../language2" }
+picker = { package = "picker2", path = "../picker2" }
+settings = { package = "settings2", path = "../settings2" }
+text = { package = "text2", path = "../text2" }
+theme = { package = "theme2", path = "../theme2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+util = { path = "../util" }
+
+ordered-float.workspace = true
+postage.workspace = true
+smol.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/outline2/src/outline.rs 🔗

@@ -0,0 +1,276 @@
+use editor::{
+    display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt,
+    DisplayPoint, Editor, ToPoint,
+};
+use fuzzy::StringMatch;
+use gpui::{
+    actions, div, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
+    FontWeight, ParentElement, Point, Render, Styled, StyledText, Task, TextStyle, View,
+    ViewContext, VisualContext, WeakView, WindowContext,
+};
+use language::Outline;
+use ordered_float::OrderedFloat;
+use picker::{Picker, PickerDelegate};
+use std::{
+    cmp::{self, Reverse},
+    sync::Arc,
+};
+use theme::ActiveTheme;
+use ui::{v_stack, ListItem, Selectable};
+use util::ResultExt;
+use workspace::Workspace;
+
+actions!(Toggle);
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(OutlineView::register).detach();
+}
+
+pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+    if let Some(editor) = workspace
+        .active_item(cx)
+        .and_then(|item| item.downcast::<Editor>())
+    {
+        let outline = editor
+            .read(cx)
+            .buffer()
+            .read(cx)
+            .snapshot(cx)
+            .outline(Some(&cx.theme().syntax()));
+
+        if let Some(outline) = outline {
+            workspace.toggle_modal(cx, |cx| OutlineView::new(outline, editor, cx));
+        }
+    }
+}
+
+pub struct OutlineView {
+    picker: View<Picker<OutlineViewDelegate>>,
+}
+
+impl FocusableView for OutlineView {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
+    }
+}
+
+impl EventEmitter<DismissEvent> for OutlineView {}
+
+impl Render for OutlineView {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        v_stack().min_w_96().child(self.picker.clone())
+    }
+}
+
+impl OutlineView {
+    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+        workspace.register_action(toggle);
+    }
+
+    fn new(
+        outline: Outline<Anchor>,
+        editor: View<Editor>,
+        cx: &mut ViewContext<Self>,
+    ) -> OutlineView {
+        let delegate = OutlineViewDelegate::new(cx.view().downgrade(), outline, editor, cx);
+        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+        OutlineView { picker }
+    }
+}
+
+struct OutlineViewDelegate {
+    outline_view: WeakView<OutlineView>,
+    active_editor: View<Editor>,
+    outline: Outline<Anchor>,
+    selected_match_index: usize,
+    prev_scroll_position: Option<Point<f32>>,
+    matches: Vec<StringMatch>,
+    last_query: String,
+}
+
+impl OutlineViewDelegate {
+    fn new(
+        outline_view: WeakView<OutlineView>,
+        outline: Outline<Anchor>,
+        editor: View<Editor>,
+        cx: &mut ViewContext<OutlineView>,
+    ) -> Self {
+        Self {
+            outline_view,
+            last_query: Default::default(),
+            matches: Default::default(),
+            selected_match_index: 0,
+            prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))),
+            active_editor: editor,
+            outline,
+        }
+    }
+
+    fn restore_active_editor(&mut self, cx: &mut WindowContext) {
+        self.active_editor.update(cx, |editor, cx| {
+            editor.highlight_rows(None);
+            if let Some(scroll_position) = self.prev_scroll_position {
+                editor.set_scroll_position(scroll_position, cx);
+            }
+        })
+    }
+
+    fn set_selected_index(
+        &mut self,
+        ix: usize,
+        navigate: bool,
+        cx: &mut ViewContext<Picker<OutlineViewDelegate>>,
+    ) {
+        self.selected_match_index = ix;
+
+        if navigate && !self.matches.is_empty() {
+            let selected_match = &self.matches[self.selected_match_index];
+            let outline_item = &self.outline.items[selected_match.candidate_id];
+
+            self.active_editor.update(cx, |active_editor, cx| {
+                let snapshot = active_editor.snapshot(cx).display_snapshot;
+                let buffer_snapshot = &snapshot.buffer_snapshot;
+                let start = outline_item.range.start.to_point(buffer_snapshot);
+                let end = outline_item.range.end.to_point(buffer_snapshot);
+                let display_rows = start.to_display_point(&snapshot).row()
+                    ..end.to_display_point(&snapshot).row() + 1;
+                active_editor.highlight_rows(Some(display_rows));
+                active_editor.request_autoscroll(Autoscroll::center(), cx);
+            });
+        }
+    }
+}
+
+impl PickerDelegate for OutlineViewDelegate {
+    type ListItem = ListItem;
+
+    fn placeholder_text(&self) -> Arc<str> {
+        "Search buffer symbols...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_match_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
+        self.set_selected_index(ix, true, cx);
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<Picker<OutlineViewDelegate>>,
+    ) -> Task<()> {
+        let selected_index;
+        if query.is_empty() {
+            self.restore_active_editor(cx);
+            self.matches = self
+                .outline
+                .items
+                .iter()
+                .enumerate()
+                .map(|(index, _)| StringMatch {
+                    candidate_id: index,
+                    score: Default::default(),
+                    positions: Default::default(),
+                    string: Default::default(),
+                })
+                .collect();
+
+            let editor = self.active_editor.read(cx);
+            let cursor_offset = editor.selections.newest::<usize>(cx).head();
+            let buffer = editor.buffer().read(cx).snapshot(cx);
+            selected_index = self
+                .outline
+                .items
+                .iter()
+                .enumerate()
+                .map(|(ix, item)| {
+                    let range = item.range.to_offset(&buffer);
+                    let distance_to_closest_endpoint = cmp::min(
+                        (range.start as isize - cursor_offset as isize).abs(),
+                        (range.end as isize - cursor_offset as isize).abs(),
+                    );
+                    let depth = if range.contains(&cursor_offset) {
+                        Some(item.depth)
+                    } else {
+                        None
+                    };
+                    (ix, depth, distance_to_closest_endpoint)
+                })
+                .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
+                .map(|(ix, _, _)| ix)
+                .unwrap_or(0);
+        } else {
+            self.matches = smol::block_on(
+                self.outline
+                    .search(&query, cx.background_executor().clone()),
+            );
+            selected_index = self
+                .matches
+                .iter()
+                .enumerate()
+                .max_by_key(|(_, m)| OrderedFloat(m.score))
+                .map(|(ix, _)| ix)
+                .unwrap_or(0);
+        }
+        self.last_query = query;
+        self.set_selected_index(selected_index, !self.last_query.is_empty(), cx);
+        Task::ready(())
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
+        self.prev_scroll_position.take();
+
+        self.active_editor.update(cx, |active_editor, cx| {
+            if let Some(rows) = active_editor.highlighted_rows() {
+                let snapshot = active_editor.snapshot(cx).display_snapshot;
+                let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
+                active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
+                    s.select_ranges([position..position])
+                });
+                active_editor.highlight_rows(None);
+            }
+        });
+
+        self.dismissed(cx);
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
+        self.outline_view
+            .update(cx, |_, cx| cx.emit(DismissEvent))
+            .log_err();
+        self.restore_active_editor(cx);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let mat = &self.matches[ix];
+        let outline_item = &self.outline.items[mat.candidate_id];
+
+        let highlights = gpui::combine_highlights(
+            mat.ranges().map(|range| (range, FontWeight::BOLD.into())),
+            outline_item.highlight_ranges.iter().cloned(),
+        );
+
+        let styled_text = StyledText::new(outline_item.text.clone())
+            .with_highlights(&TextStyle::default(), highlights);
+
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .selected(selected)
+                .child(div().pl(rems(outline_item.depth as f32)).child(styled_text)),
+        )
+    }
+}

crates/picker2/src/picker2.rs 🔗

@@ -178,6 +178,15 @@ impl<D: PickerDelegate> Picker<D> {
         }
         cx.notify();
     }
+
+    pub fn query(&self, cx: &AppContext) -> String {
+        self.editor.read(cx).text(cx)
+    }
+
+    pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
+        self.editor
+            .update(cx, |editor, cx| editor.set_text(query, cx));
+    }
 }
 
 impl<D: PickerDelegate> Render for Picker<D> {

crates/project/src/project.rs 🔗

@@ -1121,20 +1121,22 @@ impl Project {
         project_path: impl Into<ProjectPath>,
         is_directory: bool,
         cx: &mut ModelContext<Self>,
-    ) -> Option<Task<Result<Entry>>> {
+    ) -> Task<Result<Option<Entry>>> {
         let project_path = project_path.into();
-        let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
+        let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
+            return Task::ready(Ok(None));
+        };
         if self.is_local() {
-            Some(worktree.update(cx, |worktree, cx| {
+            worktree.update(cx, |worktree, cx| {
                 worktree
                     .as_local_mut()
                     .unwrap()
                     .create_entry(project_path.path, is_directory, cx)
-            }))
+            })
         } else {
             let client = self.client.clone();
             let project_id = self.remote_id().unwrap();
-            Some(cx.spawn_weak(|_, mut cx| async move {
+            cx.spawn_weak(|_, mut cx| async move {
                 let response = client
                     .request(proto::CreateProjectEntry {
                         worktree_id: project_path.worktree_id.to_proto(),
@@ -1143,19 +1145,20 @@ impl Project {
                         is_directory,
                     })
                     .await?;
-                let entry = response
-                    .entry
-                    .ok_or_else(|| anyhow!("missing entry in response"))?;
-                worktree
-                    .update(&mut cx, |worktree, cx| {
-                        worktree.as_remote_mut().unwrap().insert_entry(
-                            entry,
-                            response.worktree_scan_id as usize,
-                            cx,
-                        )
-                    })
-                    .await
-            }))
+                match response.entry {
+                    Some(entry) => worktree
+                        .update(&mut cx, |worktree, cx| {
+                            worktree.as_remote_mut().unwrap().insert_entry(
+                                entry,
+                                response.worktree_scan_id as usize,
+                                cx,
+                            )
+                        })
+                        .await
+                        .map(Some),
+                    None => Ok(None),
+                }
+            })
         }
     }
 
@@ -1164,8 +1167,10 @@ impl Project {
         entry_id: ProjectEntryId,
         new_path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Self>,
-    ) -> Option<Task<Result<Entry>>> {
-        let worktree = self.worktree_for_entry(entry_id, cx)?;
+    ) -> Task<Result<Option<Entry>>> {
+        let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
+            return Task::ready(Ok(None));
+        };
         let new_path = new_path.into();
         if self.is_local() {
             worktree.update(cx, |worktree, cx| {
@@ -1178,7 +1183,7 @@ impl Project {
             let client = self.client.clone();
             let project_id = self.remote_id().unwrap();
 
-            Some(cx.spawn_weak(|_, mut cx| async move {
+            cx.spawn_weak(|_, mut cx| async move {
                 let response = client
                     .request(proto::CopyProjectEntry {
                         project_id,
@@ -1186,19 +1191,20 @@ impl Project {
                         new_path: new_path.to_string_lossy().into(),
                     })
                     .await?;
-                let entry = response
-                    .entry
-                    .ok_or_else(|| anyhow!("missing entry in response"))?;
-                worktree
-                    .update(&mut cx, |worktree, cx| {
-                        worktree.as_remote_mut().unwrap().insert_entry(
-                            entry,
-                            response.worktree_scan_id as usize,
-                            cx,
-                        )
-                    })
-                    .await
-            }))
+                match response.entry {
+                    Some(entry) => worktree
+                        .update(&mut cx, |worktree, cx| {
+                            worktree.as_remote_mut().unwrap().insert_entry(
+                                entry,
+                                response.worktree_scan_id as usize,
+                                cx,
+                            )
+                        })
+                        .await
+                        .map(Some),
+                    None => Ok(None),
+                }
+            })
         }
     }
 
@@ -1207,8 +1213,10 @@ impl Project {
         entry_id: ProjectEntryId,
         new_path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Self>,
-    ) -> Option<Task<Result<Entry>>> {
-        let worktree = self.worktree_for_entry(entry_id, cx)?;
+    ) -> Task<Result<Option<Entry>>> {
+        let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
+            return Task::ready(Ok(None));
+        };
         let new_path = new_path.into();
         if self.is_local() {
             worktree.update(cx, |worktree, cx| {
@@ -1221,7 +1229,7 @@ impl Project {
             let client = self.client.clone();
             let project_id = self.remote_id().unwrap();
 
-            Some(cx.spawn_weak(|_, mut cx| async move {
+            cx.spawn_weak(|_, mut cx| async move {
                 let response = client
                     .request(proto::RenameProjectEntry {
                         project_id,
@@ -1229,19 +1237,20 @@ impl Project {
                         new_path: new_path.to_string_lossy().into(),
                     })
                     .await?;
-                let entry = response
-                    .entry
-                    .ok_or_else(|| anyhow!("missing entry in response"))?;
-                worktree
-                    .update(&mut cx, |worktree, cx| {
-                        worktree.as_remote_mut().unwrap().insert_entry(
-                            entry,
-                            response.worktree_scan_id as usize,
-                            cx,
-                        )
-                    })
-                    .await
-            }))
+                match response.entry {
+                    Some(entry) => worktree
+                        .update(&mut cx, |worktree, cx| {
+                            worktree.as_remote_mut().unwrap().insert_entry(
+                                entry,
+                                response.worktree_scan_id as usize,
+                                cx,
+                            )
+                        })
+                        .await
+                        .map(Some),
+                    None => Ok(None),
+                }
+            })
         }
     }
 
@@ -1658,18 +1667,15 @@ impl Project {
 
     pub fn open_path(
         &mut self,
-        path: impl Into<ProjectPath>,
+        path: ProjectPath,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<(ProjectEntryId, AnyModelHandle)>> {
-        let task = self.open_buffer(path, cx);
+    ) -> Task<Result<(Option<ProjectEntryId>, AnyModelHandle)>> {
+        let task = self.open_buffer(path.clone(), cx);
         cx.spawn_weak(|_, cx| async move {
             let buffer = task.await?;
-            let project_entry_id = buffer
-                .read_with(&cx, |buffer, cx| {
-                    File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
-                })
-                .ok_or_else(|| anyhow!("no project entry"))?;
-
+            let project_entry_id = buffer.read_with(&cx, |buffer, cx| {
+                File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
+            });
             let buffer: &AnyModelHandle = &buffer;
             Ok((project_entry_id, buffer.clone()))
         })
@@ -1984,8 +1990,10 @@ impl Project {
                     remote_id,
                 );
 
-                self.local_buffer_ids_by_entry_id
-                    .insert(file.entry_id, remote_id);
+                if let Some(entry_id) = file.entry_id {
+                    self.local_buffer_ids_by_entry_id
+                        .insert(entry_id, remote_id);
+                }
             }
         }
 
@@ -2440,24 +2448,25 @@ impl Project {
                     return None;
                 };
 
-                match self.local_buffer_ids_by_entry_id.get(&file.entry_id) {
-                    Some(_) => {
-                        return None;
-                    }
-                    None => {
-                        let remote_id = buffer.read(cx).remote_id();
-                        self.local_buffer_ids_by_entry_id
-                            .insert(file.entry_id, remote_id);
-
-                        self.local_buffer_ids_by_path.insert(
-                            ProjectPath {
-                                worktree_id: file.worktree_id(cx),
-                                path: file.path.clone(),
-                            },
-                            remote_id,
-                        );
+                let remote_id = buffer.read(cx).remote_id();
+                if let Some(entry_id) = file.entry_id {
+                    match self.local_buffer_ids_by_entry_id.get(&entry_id) {
+                        Some(_) => {
+                            return None;
+                        }
+                        None => {
+                            self.local_buffer_ids_by_entry_id
+                                .insert(entry_id, remote_id);
+                        }
                     }
-                }
+                };
+                self.local_buffer_ids_by_path.insert(
+                    ProjectPath {
+                        worktree_id: file.worktree_id(cx),
+                        path: file.path.clone(),
+                    },
+                    remote_id,
+                );
             }
             _ => {}
         }
@@ -5775,11 +5784,6 @@ impl Project {
                                 while let Some(ignored_abs_path) =
                                     ignored_paths_to_process.pop_front()
                                 {
-                                    if !query.file_matches(Some(&ignored_abs_path))
-                                        || snapshot.is_path_excluded(&ignored_abs_path)
-                                    {
-                                        continue;
-                                    }
                                     if let Some(fs_metadata) = fs
                                         .metadata(&ignored_abs_path)
                                         .await
@@ -5807,6 +5811,13 @@ impl Project {
                                                 }
                                             }
                                         } else if !fs_metadata.is_symlink {
+                                            if !query.file_matches(Some(&ignored_abs_path))
+                                                || snapshot.is_path_excluded(
+                                                    ignored_entry.path.to_path_buf(),
+                                                )
+                                            {
+                                                continue;
+                                            }
                                             let matches = if let Some(file) = fs
                                                 .open_sync(&ignored_abs_path)
                                                 .await
@@ -6207,10 +6218,13 @@ impl Project {
                         return;
                     }
 
-                    let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) {
+                    let new_file = if let Some(entry) = old_file
+                        .entry_id
+                        .and_then(|entry_id| snapshot.entry_for_id(entry_id))
+                    {
                         File {
                             is_local: true,
-                            entry_id: entry.id,
+                            entry_id: Some(entry.id),
                             mtime: entry.mtime,
                             path: entry.path.clone(),
                             worktree: worktree_handle.clone(),
@@ -6219,7 +6233,7 @@ impl Project {
                     } else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) {
                         File {
                             is_local: true,
-                            entry_id: entry.id,
+                            entry_id: Some(entry.id),
                             mtime: entry.mtime,
                             path: entry.path.clone(),
                             worktree: worktree_handle.clone(),
@@ -6249,10 +6263,12 @@ impl Project {
                         );
                     }
 
-                    if new_file.entry_id != *entry_id {
+                    if new_file.entry_id != Some(*entry_id) {
                         self.local_buffer_ids_by_entry_id.remove(entry_id);
-                        self.local_buffer_ids_by_entry_id
-                            .insert(new_file.entry_id, buffer_id);
+                        if let Some(entry_id) = new_file.entry_id {
+                            self.local_buffer_ids_by_entry_id
+                                .insert(entry_id, buffer_id);
+                        }
                     }
 
                     if new_file != *old_file {
@@ -6815,7 +6831,7 @@ impl Project {
             })
             .await?;
         Ok(proto::ProjectEntryResponse {
-            entry: Some((&entry).into()),
+            entry: entry.as_ref().map(|e| e.into()),
             worktree_scan_id: worktree_scan_id as u64,
         })
     }
@@ -6839,11 +6855,10 @@ impl Project {
                     .as_local_mut()
                     .unwrap()
                     .rename_entry(entry_id, new_path, cx)
-                    .ok_or_else(|| anyhow!("invalid entry"))
-            })?
+            })
             .await?;
         Ok(proto::ProjectEntryResponse {
-            entry: Some((&entry).into()),
+            entry: entry.as_ref().map(|e| e.into()),
             worktree_scan_id: worktree_scan_id as u64,
         })
     }
@@ -6867,11 +6882,10 @@ impl Project {
                     .as_local_mut()
                     .unwrap()
                     .copy_entry(entry_id, new_path, cx)
-                    .ok_or_else(|| anyhow!("invalid entry"))
-            })?
+            })
             .await?;
         Ok(proto::ProjectEntryResponse {
-            entry: Some((&entry).into()),
+            entry: entry.as_ref().map(|e| e.into()),
             worktree_scan_id: worktree_scan_id as u64,
         })
     }

crates/project/src/project_tests.rs 🔗

@@ -4050,6 +4050,94 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
     );
 }
 
+#[gpui::test]
+async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            ".git": {},
+            ".gitignore": "**/target\n/node_modules\n",
+            "target": {
+                "index.txt": "index_key:index_value"
+            },
+            "node_modules": {
+                "eslint": {
+                    "index.ts": "const eslint_key = 'eslint value'",
+                    "package.json": r#"{ "some_key": "some value" }"#,
+                },
+                "prettier": {
+                    "index.ts": "const prettier_key = 'prettier value'",
+                    "package.json": r#"{ "other_key": "other value" }"#,
+                },
+            },
+            "package.json": r#"{ "main_key": "main value" }"#,
+        }),
+    )
+    .await;
+    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+    let query = "key";
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([("package.json".to_string(), vec![8..11])]),
+        "Only one non-ignored file should have the query"
+    );
+
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([
+            ("package.json".to_string(), vec![8..11]),
+            ("target/index.txt".to_string(), vec![6..9]),
+            (
+                "node_modules/prettier/package.json".to_string(),
+                vec![9..12]
+            ),
+            ("node_modules/prettier/index.ts".to_string(), vec![15..18]),
+            ("node_modules/eslint/index.ts".to_string(), vec![13..16]),
+            ("node_modules/eslint/package.json".to_string(), vec![8..11]),
+        ]),
+        "Unrestricted search with ignored directories should find every file with the query"
+    );
+
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(
+                query,
+                false,
+                false,
+                true,
+                vec![PathMatcher::new("node_modules/prettier/**").unwrap()],
+                vec![PathMatcher::new("*.ts").unwrap()],
+            )
+            .unwrap(),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([(
+            "node_modules/prettier/package.json".to_string(),
+            vec![9..12]
+        )]),
+        "With search including ignored prettier directory and excluding TS files, only one file should be found"
+    );
+}
+
 #[test]
 fn test_glob_literal_prefix() {
     assert_eq!(glob_literal_prefix("**/*.js"), "");

crates/project/src/search.rs 🔗

@@ -371,15 +371,25 @@ impl SearchQuery {
     pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
         match file_path {
             Some(file_path) => {
-                !self
-                    .files_to_exclude()
-                    .iter()
-                    .any(|exclude_glob| exclude_glob.is_match(file_path))
-                    && (self.files_to_include().is_empty()
+                let mut path = file_path.to_path_buf();
+                loop {
+                    if self
+                        .files_to_exclude()
+                        .iter()
+                        .any(|exclude_glob| exclude_glob.is_match(&path))
+                    {
+                        return false;
+                    } else if self.files_to_include().is_empty()
                         || self
                             .files_to_include()
                             .iter()
-                            .any(|include_glob| include_glob.is_match(file_path)))
+                            .any(|include_glob| include_glob.is_match(&path))
+                    {
+                        return true;
+                    } else if !path.pop() {
+                        return false;
+                    }
+                }
             }
             None => self.files_to_include().is_empty(),
         }

crates/project/src/worktree.rs 🔗

@@ -960,8 +960,6 @@ impl LocalWorktree {
 
         cx.spawn(|this, cx| async move {
             let text = fs.load(&abs_path).await?;
-            let entry = entry.await?;
-
             let mut index_task = None;
             let snapshot = this.read_with(&cx, |this, _| this.as_local().unwrap().snapshot());
             if let Some(repo) = snapshot.repository_for_path(&path) {
@@ -981,18 +979,43 @@ impl LocalWorktree {
                 None
             };
 
-            Ok((
-                File {
-                    entry_id: entry.id,
-                    worktree: this,
-                    path: entry.path,
-                    mtime: entry.mtime,
-                    is_local: true,
-                    is_deleted: false,
-                },
-                text,
-                diff_base,
-            ))
+            match entry.await? {
+                Some(entry) => Ok((
+                    File {
+                        entry_id: Some(entry.id),
+                        worktree: this,
+                        path: entry.path,
+                        mtime: entry.mtime,
+                        is_local: true,
+                        is_deleted: false,
+                    },
+                    text,
+                    diff_base,
+                )),
+                None => {
+                    let metadata = fs
+                        .metadata(&abs_path)
+                        .await
+                        .with_context(|| {
+                            format!("Loading metadata for excluded file {abs_path:?}")
+                        })?
+                        .with_context(|| {
+                            format!("Excluded file {abs_path:?} got removed during loading")
+                        })?;
+                    Ok((
+                        File {
+                            entry_id: None,
+                            worktree: this,
+                            path,
+                            mtime: metadata.mtime,
+                            is_local: true,
+                            is_deleted: false,
+                        },
+                        text,
+                        diff_base,
+                    ))
+                }
+            }
         })
     }
 
@@ -1013,17 +1036,37 @@ impl LocalWorktree {
         let text = buffer.as_rope().clone();
         let fingerprint = text.fingerprint();
         let version = buffer.version();
-        let save = self.write_file(path, text, buffer.line_ending(), cx);
+        let save = self.write_file(path.as_ref(), text, buffer.line_ending(), cx);
+        let fs = Arc::clone(&self.fs);
+        let abs_path = self.absolutize(&path);
 
         cx.as_mut().spawn(|mut cx| async move {
             let entry = save.await?;
 
+            let (entry_id, mtime, path) = match entry {
+                Some(entry) => (Some(entry.id), entry.mtime, entry.path),
+                None => {
+                    let metadata = fs
+                        .metadata(&abs_path)
+                        .await
+                        .with_context(|| {
+                            format!(
+                                "Fetching metadata after saving the excluded buffer {abs_path:?}"
+                            )
+                        })?
+                        .with_context(|| {
+                            format!("Excluded buffer {path:?} got removed during saving")
+                        })?;
+                    (None, metadata.mtime, path)
+                }
+            };
+
             if has_changed_file {
                 let new_file = Arc::new(File {
-                    entry_id: entry.id,
+                    entry_id,
                     worktree: handle,
-                    path: entry.path,
-                    mtime: entry.mtime,
+                    path,
+                    mtime,
                     is_local: true,
                     is_deleted: false,
                 });
@@ -1049,13 +1092,13 @@ impl LocalWorktree {
                     project_id,
                     buffer_id,
                     version: serialize_version(&version),
-                    mtime: Some(entry.mtime.into()),
+                    mtime: Some(mtime.into()),
                     fingerprint: serialize_fingerprint(fingerprint),
                 })?;
             }
 
             buffer_handle.update(&mut cx, |buffer, cx| {
-                buffer.did_save(version.clone(), fingerprint, entry.mtime, cx);
+                buffer.did_save(version.clone(), fingerprint, mtime, cx);
             });
 
             Ok(())
@@ -1080,7 +1123,7 @@ impl LocalWorktree {
         path: impl Into<Arc<Path>>,
         is_dir: bool,
         cx: &mut ModelContext<Worktree>,
-    ) -> Task<Result<Entry>> {
+    ) -> Task<Result<Option<Entry>>> {
         let path = path.into();
         let lowest_ancestor = self.lowest_ancestor(&path);
         let abs_path = self.absolutize(&path);
@@ -1097,7 +1140,7 @@ impl LocalWorktree {
         cx.spawn(|this, mut cx| async move {
             write.await?;
             let (result, refreshes) = this.update(&mut cx, |this, cx| {
-                let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
+                let mut refreshes = Vec::new();
                 let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
                 for refresh_path in refresh_paths.ancestors() {
                     if refresh_path == Path::new("") {
@@ -1124,14 +1167,14 @@ impl LocalWorktree {
         })
     }
 
-    pub fn write_file(
+    pub(crate) fn write_file(
         &self,
         path: impl Into<Arc<Path>>,
         text: Rope,
         line_ending: LineEnding,
         cx: &mut ModelContext<Worktree>,
-    ) -> Task<Result<Entry>> {
-        let path = path.into();
+    ) -> Task<Result<Option<Entry>>> {
+        let path: Arc<Path> = path.into();
         let abs_path = self.absolutize(&path);
         let fs = self.fs.clone();
         let write = cx
@@ -1190,8 +1233,11 @@ impl LocalWorktree {
         entry_id: ProjectEntryId,
         new_path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Worktree>,
-    ) -> Option<Task<Result<Entry>>> {
-        let old_path = self.entry_for_id(entry_id)?.path.clone();
+    ) -> Task<Result<Option<Entry>>> {
+        let old_path = match self.entry_for_id(entry_id) {
+            Some(entry) => entry.path.clone(),
+            None => return Task::ready(Ok(None)),
+        };
         let new_path = new_path.into();
         let abs_old_path = self.absolutize(&old_path);
         let abs_new_path = self.absolutize(&new_path);
@@ -1201,7 +1247,7 @@ impl LocalWorktree {
                 .await
         });
 
-        Some(cx.spawn(|this, mut cx| async move {
+        cx.spawn(|this, mut cx| async move {
             rename.await?;
             this.update(&mut cx, |this, cx| {
                 this.as_local_mut()
@@ -1209,7 +1255,7 @@ impl LocalWorktree {
                     .refresh_entry(new_path.clone(), Some(old_path), cx)
             })
             .await
-        }))
+        })
     }
 
     pub fn copy_entry(
@@ -1217,8 +1263,11 @@ impl LocalWorktree {
         entry_id: ProjectEntryId,
         new_path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Worktree>,
-    ) -> Option<Task<Result<Entry>>> {
-        let old_path = self.entry_for_id(entry_id)?.path.clone();
+    ) -> Task<Result<Option<Entry>>> {
+        let old_path = match self.entry_for_id(entry_id) {
+            Some(entry) => entry.path.clone(),
+            None => return Task::ready(Ok(None)),
+        };
         let new_path = new_path.into();
         let abs_old_path = self.absolutize(&old_path);
         let abs_new_path = self.absolutize(&new_path);
@@ -1233,7 +1282,7 @@ impl LocalWorktree {
             .await
         });
 
-        Some(cx.spawn(|this, mut cx| async move {
+        cx.spawn(|this, mut cx| async move {
             copy.await?;
             this.update(&mut cx, |this, cx| {
                 this.as_local_mut()
@@ -1241,7 +1290,7 @@ impl LocalWorktree {
                     .refresh_entry(new_path.clone(), None, cx)
             })
             .await
-        }))
+        })
     }
 
     pub fn expand_entry(
@@ -1277,7 +1326,10 @@ impl LocalWorktree {
         path: Arc<Path>,
         old_path: Option<Arc<Path>>,
         cx: &mut ModelContext<Worktree>,
-    ) -> Task<Result<Entry>> {
+    ) -> Task<Result<Option<Entry>>> {
+        if self.is_path_excluded(path.to_path_buf()) {
+            return Task::ready(Ok(None));
+        }
         let paths = if let Some(old_path) = old_path.as_ref() {
             vec![old_path.clone(), path.clone()]
         } else {
@@ -1286,13 +1338,15 @@ impl LocalWorktree {
         let mut refresh = self.refresh_entries_for_paths(paths);
         cx.spawn_weak(move |this, mut cx| async move {
             refresh.recv().await;
-            this.upgrade(&cx)
+            let new_entry = this
+                .upgrade(&cx)
                 .ok_or_else(|| anyhow!("worktree was dropped"))?
                 .update(&mut cx, |this, _| {
                     this.entry_for_path(path)
                         .cloned()
                         .ok_or_else(|| anyhow!("failed to read path after update"))
-                })
+                })?;
+            Ok(Some(new_entry))
         })
     }
 
@@ -2226,10 +2280,19 @@ impl LocalSnapshot {
         paths
     }
 
-    pub fn is_path_excluded(&self, abs_path: &Path) -> bool {
-        self.file_scan_exclusions
-            .iter()
-            .any(|exclude_matcher| exclude_matcher.is_match(abs_path))
+    pub fn is_path_excluded(&self, mut path: PathBuf) -> bool {
+        loop {
+            if self
+                .file_scan_exclusions
+                .iter()
+                .any(|exclude_matcher| exclude_matcher.is_match(&path))
+            {
+                return true;
+            }
+            if !path.pop() {
+                return false;
+            }
+        }
     }
 }
 
@@ -2458,8 +2521,7 @@ impl BackgroundScannerState {
                 ids_to_preserve.insert(work_directory_id);
             } else {
                 let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
-                let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path)
-                    || snapshot.is_path_excluded(&git_dir_abs_path);
+                let git_dir_excluded = snapshot.is_path_excluded(entry.git_dir_path.to_path_buf());
                 if git_dir_excluded
                     && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
                 {
@@ -2666,7 +2728,7 @@ pub struct File {
     pub worktree: ModelHandle<Worktree>,
     pub path: Arc<Path>,
     pub mtime: SystemTime,
-    pub(crate) entry_id: ProjectEntryId,
+    pub(crate) entry_id: Option<ProjectEntryId>,
     pub(crate) is_local: bool,
     pub(crate) is_deleted: bool,
 }
@@ -2735,7 +2797,7 @@ impl language::File for File {
     fn to_proto(&self) -> rpc::proto::File {
         rpc::proto::File {
             worktree_id: self.worktree.id() as u64,
-            entry_id: self.entry_id.to_proto(),
+            entry_id: self.entry_id.map(|id| id.to_proto()),
             path: self.path.to_string_lossy().into(),
             mtime: Some(self.mtime.into()),
             is_deleted: self.is_deleted,
@@ -2793,7 +2855,7 @@ impl File {
             worktree,
             path: entry.path.clone(),
             mtime: entry.mtime,
-            entry_id: entry.id,
+            entry_id: Some(entry.id),
             is_local: true,
             is_deleted: false,
         })
@@ -2818,7 +2880,7 @@ impl File {
             worktree,
             path: Path::new(&proto.path).into(),
             mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(),
-            entry_id: ProjectEntryId::from_proto(proto.entry_id),
+            entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
             is_local: false,
             is_deleted: proto.is_deleted,
         })
@@ -2836,7 +2898,7 @@ impl File {
         if self.is_deleted {
             None
         } else {
-            Some(self.entry_id)
+            self.entry_id
         }
     }
 }
@@ -3338,16 +3400,7 @@ impl BackgroundScanner {
                     return false;
                 }
 
-                // FS events may come for files which parent directory is excluded, need to check ignore those.
-                let mut path_to_test = abs_path.clone();
-                let mut excluded_file_event = snapshot.is_path_excluded(abs_path)
-                    || snapshot.is_path_excluded(&relative_path);
-                while !excluded_file_event && path_to_test.pop() {
-                    if snapshot.is_path_excluded(&path_to_test) {
-                        excluded_file_event = true;
-                    }
-                }
-                if excluded_file_event {
+                if snapshot.is_path_excluded(relative_path.to_path_buf()) {
                     if !is_git_related {
                         log::debug!("ignoring FS event for excluded path {relative_path:?}");
                     }
@@ -3531,7 +3584,7 @@ impl BackgroundScanner {
             let state = self.state.lock();
             let snapshot = &state.snapshot;
             root_abs_path = snapshot.abs_path().clone();
-            if snapshot.is_path_excluded(&job.abs_path) {
+            if snapshot.is_path_excluded(job.path.to_path_buf()) {
                 log::error!("skipping excluded directory {:?}", job.path);
                 return Ok(());
             }
@@ -3603,8 +3656,8 @@ impl BackgroundScanner {
 
             {
                 let mut state = self.state.lock();
-                if state.snapshot.is_path_excluded(&child_abs_path) {
-                    let relative_path = job.path.join(child_name);
+                let relative_path = job.path.join(child_name);
+                if state.snapshot.is_path_excluded(relative_path.clone()) {
                     log::debug!("skipping excluded child entry {relative_path:?}");
                     state.remove_path(&relative_path);
                     continue;

crates/project/src/worktree_tests.rs 🔗

@@ -1052,11 +1052,12 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
             &[
                 ".git/HEAD",
                 ".git/foo",
+                "node_modules",
                 "node_modules/.DS_Store",
                 "node_modules/prettier",
                 "node_modules/prettier/package.json",
             ],
-            &["target", "node_modules"],
+            &["target"],
             &[
                 ".DS_Store",
                 "src/.DS_Store",
@@ -1106,6 +1107,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
                 ".git/HEAD",
                 ".git/foo",
                 ".git/new_file",
+                "node_modules",
                 "node_modules/.DS_Store",
                 "node_modules/prettier",
                 "node_modules/prettier/package.json",
@@ -1114,7 +1116,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
                 "build_output/new_file",
                 "test_output/new_file",
             ],
-            &["target", "node_modules", "test_output"],
+            &["target", "test_output"],
             &[
                 ".DS_Store",
                 "src/.DS_Store",
@@ -1174,6 +1176,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
                 .create_entry("a/e".as_ref(), true, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     assert!(entry.is_dir());
 
@@ -1222,6 +1225,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
                 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     assert!(entry.is_file());
 
@@ -1257,6 +1261,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
                 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     assert!(entry.is_file());
 
@@ -1275,6 +1280,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
                 .create_entry("a/b/c/e.txt".as_ref(), false, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     assert!(entry.is_file());
 
@@ -1291,6 +1297,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
                 .create_entry("d/e/f/g.txt".as_ref(), false, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     assert!(entry.is_file());
 
@@ -1616,14 +1623,14 @@ fn randomly_mutate_worktree(
                 entry.id.0,
                 new_path
             );
-            let task = worktree.rename_entry(entry.id, new_path, cx).unwrap();
+            let task = worktree.rename_entry(entry.id, new_path, cx);
             cx.foreground().spawn(async move {
-                task.await?;
+                task.await?.unwrap();
                 Ok(())
             })
         }
         _ => {
-            let task = if entry.is_dir() {
+            if entry.is_dir() {
                 let child_path = entry.path.join(random_filename(rng));
                 let is_dir = rng.gen_bool(0.3);
                 log::info!(
@@ -1631,15 +1638,20 @@ fn randomly_mutate_worktree(
                     if is_dir { "dir" } else { "file" },
                     child_path,
                 );
-                worktree.create_entry(child_path, is_dir, cx)
+                let task = worktree.create_entry(child_path, is_dir, cx);
+                cx.foreground().spawn(async move {
+                    task.await?;
+                    Ok(())
+                })
             } else {
                 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
-                worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx)
-            };
-            cx.foreground().spawn(async move {
-                task.await?;
-                Ok(())
-            })
+                let task =
+                    worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
+                cx.foreground().spawn(async move {
+                    task.await?;
+                    Ok(())
+                })
+            }
         }
     }
 }

crates/project2/src/project2.rs 🔗

@@ -1151,20 +1151,22 @@ impl Project {
         project_path: impl Into<ProjectPath>,
         is_directory: bool,
         cx: &mut ModelContext<Self>,
-    ) -> Option<Task<Result<Entry>>> {
+    ) -> Task<Result<Option<Entry>>> {
         let project_path = project_path.into();
-        let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
+        let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
+            return Task::ready(Ok(None));
+        };
         if self.is_local() {
-            Some(worktree.update(cx, |worktree, cx| {
+            worktree.update(cx, |worktree, cx| {
                 worktree
                     .as_local_mut()
                     .unwrap()
                     .create_entry(project_path.path, is_directory, cx)
-            }))
+            })
         } else {
             let client = self.client.clone();
             let project_id = self.remote_id().unwrap();
-            Some(cx.spawn(move |_, mut cx| async move {
+            cx.spawn(move |_, mut cx| async move {
                 let response = client
                     .request(proto::CreateProjectEntry {
                         worktree_id: project_path.worktree_id.to_proto(),
@@ -1173,19 +1175,20 @@ impl Project {
                         is_directory,
                     })
                     .await?;
-                let entry = response
-                    .entry
-                    .ok_or_else(|| anyhow!("missing entry in response"))?;
-                worktree
-                    .update(&mut cx, |worktree, cx| {
-                        worktree.as_remote_mut().unwrap().insert_entry(
-                            entry,
-                            response.worktree_scan_id as usize,
-                            cx,
-                        )
-                    })?
-                    .await
-            }))
+                match response.entry {
+                    Some(entry) => worktree
+                        .update(&mut cx, |worktree, cx| {
+                            worktree.as_remote_mut().unwrap().insert_entry(
+                                entry,
+                                response.worktree_scan_id as usize,
+                                cx,
+                            )
+                        })?
+                        .await
+                        .map(Some),
+                    None => Ok(None),
+                }
+            })
         }
     }
 
@@ -1194,8 +1197,10 @@ impl Project {
         entry_id: ProjectEntryId,
         new_path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Self>,
-    ) -> Option<Task<Result<Entry>>> {
-        let worktree = self.worktree_for_entry(entry_id, cx)?;
+    ) -> Task<Result<Option<Entry>>> {
+        let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
+            return Task::ready(Ok(None));
+        };
         let new_path = new_path.into();
         if self.is_local() {
             worktree.update(cx, |worktree, cx| {
@@ -1208,7 +1213,7 @@ impl Project {
             let client = self.client.clone();
             let project_id = self.remote_id().unwrap();
 
-            Some(cx.spawn(move |_, mut cx| async move {
+            cx.spawn(move |_, mut cx| async move {
                 let response = client
                     .request(proto::CopyProjectEntry {
                         project_id,
@@ -1216,19 +1221,20 @@ impl Project {
                         new_path: new_path.to_string_lossy().into(),
                     })
                     .await?;
-                let entry = response
-                    .entry
-                    .ok_or_else(|| anyhow!("missing entry in response"))?;
-                worktree
-                    .update(&mut cx, |worktree, cx| {
-                        worktree.as_remote_mut().unwrap().insert_entry(
-                            entry,
-                            response.worktree_scan_id as usize,
-                            cx,
-                        )
-                    })?
-                    .await
-            }))
+                match response.entry {
+                    Some(entry) => worktree
+                        .update(&mut cx, |worktree, cx| {
+                            worktree.as_remote_mut().unwrap().insert_entry(
+                                entry,
+                                response.worktree_scan_id as usize,
+                                cx,
+                            )
+                        })?
+                        .await
+                        .map(Some),
+                    None => Ok(None),
+                }
+            })
         }
     }
 
@@ -1237,8 +1243,10 @@ impl Project {
         entry_id: ProjectEntryId,
         new_path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Self>,
-    ) -> Option<Task<Result<Entry>>> {
-        let worktree = self.worktree_for_entry(entry_id, cx)?;
+    ) -> Task<Result<Option<Entry>>> {
+        let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
+            return Task::ready(Ok(None));
+        };
         let new_path = new_path.into();
         if self.is_local() {
             worktree.update(cx, |worktree, cx| {
@@ -1251,7 +1259,7 @@ impl Project {
             let client = self.client.clone();
             let project_id = self.remote_id().unwrap();
 
-            Some(cx.spawn(move |_, mut cx| async move {
+            cx.spawn(move |_, mut cx| async move {
                 let response = client
                     .request(proto::RenameProjectEntry {
                         project_id,
@@ -1259,19 +1267,20 @@ impl Project {
                         new_path: new_path.to_string_lossy().into(),
                     })
                     .await?;
-                let entry = response
-                    .entry
-                    .ok_or_else(|| anyhow!("missing entry in response"))?;
-                worktree
-                    .update(&mut cx, |worktree, cx| {
-                        worktree.as_remote_mut().unwrap().insert_entry(
-                            entry,
-                            response.worktree_scan_id as usize,
-                            cx,
-                        )
-                    })?
-                    .await
-            }))
+                match response.entry {
+                    Some(entry) => worktree
+                        .update(&mut cx, |worktree, cx| {
+                            worktree.as_remote_mut().unwrap().insert_entry(
+                                entry,
+                                response.worktree_scan_id as usize,
+                                cx,
+                            )
+                        })?
+                        .await
+                        .map(Some),
+                    None => Ok(None),
+                }
+            })
         }
     }
 
@@ -1688,17 +1697,15 @@ impl Project {
 
     pub fn open_path(
         &mut self,
-        path: impl Into<ProjectPath>,
+        path: ProjectPath,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<(ProjectEntryId, AnyModel)>> {
-        let task = self.open_buffer(path, cx);
-        cx.spawn(move |_, mut cx| async move {
+    ) -> Task<Result<(Option<ProjectEntryId>, AnyModel)>> {
+        let task = self.open_buffer(path.clone(), cx);
+        cx.spawn(move |_, cx| async move {
             let buffer = task.await?;
-            let project_entry_id = buffer
-                .update(&mut cx, |buffer, cx| {
-                    File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
-                })?
-                .ok_or_else(|| anyhow!("no project entry"))?;
+            let project_entry_id = buffer.read_with(&cx, |buffer, cx| {
+                File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
+            })?;
 
             let buffer: &AnyModel = &buffer;
             Ok((project_entry_id, buffer.clone()))
@@ -2017,8 +2024,10 @@ impl Project {
                     remote_id,
                 );
 
-                self.local_buffer_ids_by_entry_id
-                    .insert(file.entry_id, remote_id);
+                if let Some(entry_id) = file.entry_id {
+                    self.local_buffer_ids_by_entry_id
+                        .insert(entry_id, remote_id);
+                }
             }
         }
 
@@ -2473,24 +2482,25 @@ impl Project {
                     return None;
                 };
 
-                match self.local_buffer_ids_by_entry_id.get(&file.entry_id) {
-                    Some(_) => {
-                        return None;
-                    }
-                    None => {
-                        let remote_id = buffer.read(cx).remote_id();
-                        self.local_buffer_ids_by_entry_id
-                            .insert(file.entry_id, remote_id);
-
-                        self.local_buffer_ids_by_path.insert(
-                            ProjectPath {
-                                worktree_id: file.worktree_id(cx),
-                                path: file.path.clone(),
-                            },
-                            remote_id,
-                        );
+                let remote_id = buffer.read(cx).remote_id();
+                if let Some(entry_id) = file.entry_id {
+                    match self.local_buffer_ids_by_entry_id.get(&entry_id) {
+                        Some(_) => {
+                            return None;
+                        }
+                        None => {
+                            self.local_buffer_ids_by_entry_id
+                                .insert(entry_id, remote_id);
+                        }
                     }
-                }
+                };
+                self.local_buffer_ids_by_path.insert(
+                    ProjectPath {
+                        worktree_id: file.worktree_id(cx),
+                        path: file.path.clone(),
+                    },
+                    remote_id,
+                );
             }
             _ => {}
         }
@@ -5844,11 +5854,6 @@ impl Project {
                                 while let Some(ignored_abs_path) =
                                     ignored_paths_to_process.pop_front()
                                 {
-                                    if !query.file_matches(Some(&ignored_abs_path))
-                                        || snapshot.is_path_excluded(&ignored_abs_path)
-                                    {
-                                        continue;
-                                    }
                                     if let Some(fs_metadata) = fs
                                         .metadata(&ignored_abs_path)
                                         .await
@@ -5876,6 +5881,13 @@ impl Project {
                                                 }
                                             }
                                         } else if !fs_metadata.is_symlink {
+                                            if !query.file_matches(Some(&ignored_abs_path))
+                                                || snapshot.is_path_excluded(
+                                                    ignored_entry.path.to_path_buf(),
+                                                )
+                                            {
+                                                continue;
+                                            }
                                             let matches = if let Some(file) = fs
                                                 .open_sync(&ignored_abs_path)
                                                 .await
@@ -6277,10 +6289,13 @@ impl Project {
                         return;
                     }
 
-                    let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) {
+                    let new_file = if let Some(entry) = old_file
+                        .entry_id
+                        .and_then(|entry_id| snapshot.entry_for_id(entry_id))
+                    {
                         File {
                             is_local: true,
-                            entry_id: entry.id,
+                            entry_id: Some(entry.id),
                             mtime: entry.mtime,
                             path: entry.path.clone(),
                             worktree: worktree_handle.clone(),
@@ -6289,7 +6304,7 @@ impl Project {
                     } else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) {
                         File {
                             is_local: true,
-                            entry_id: entry.id,
+                            entry_id: Some(entry.id),
                             mtime: entry.mtime,
                             path: entry.path.clone(),
                             worktree: worktree_handle.clone(),
@@ -6319,10 +6334,12 @@ impl Project {
                         );
                     }
 
-                    if new_file.entry_id != *entry_id {
+                    if new_file.entry_id != Some(*entry_id) {
                         self.local_buffer_ids_by_entry_id.remove(entry_id);
-                        self.local_buffer_ids_by_entry_id
-                            .insert(new_file.entry_id, buffer_id);
+                        if let Some(entry_id) = new_file.entry_id {
+                            self.local_buffer_ids_by_entry_id
+                                .insert(entry_id, buffer_id);
+                        }
                     }
 
                     if new_file != *old_file {
@@ -6889,7 +6906,7 @@ impl Project {
             })?
             .await?;
         Ok(proto::ProjectEntryResponse {
-            entry: Some((&entry).into()),
+            entry: entry.as_ref().map(|e| e.into()),
             worktree_scan_id: worktree_scan_id as u64,
         })
     }
@@ -6913,11 +6930,10 @@ impl Project {
                     .as_local_mut()
                     .unwrap()
                     .rename_entry(entry_id, new_path, cx)
-                    .ok_or_else(|| anyhow!("invalid entry"))
-            })??
+            })?
             .await?;
         Ok(proto::ProjectEntryResponse {
-            entry: Some((&entry).into()),
+            entry: entry.as_ref().map(|e| e.into()),
             worktree_scan_id: worktree_scan_id as u64,
         })
     }
@@ -6941,11 +6957,10 @@ impl Project {
                     .as_local_mut()
                     .unwrap()
                     .copy_entry(entry_id, new_path, cx)
-                    .ok_or_else(|| anyhow!("invalid entry"))
-            })??
+            })?
             .await?;
         Ok(proto::ProjectEntryResponse {
-            entry: Some((&entry).into()),
+            entry: entry.as_ref().map(|e| e.into()),
             worktree_scan_id: worktree_scan_id as u64,
         })
     }

crates/project2/src/project_tests.rs 🔗

@@ -4182,6 +4182,94 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
     );
 }
 
+#[gpui::test]
+async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.background_executor.clone());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            ".git": {},
+            ".gitignore": "**/target\n/node_modules\n",
+            "target": {
+                "index.txt": "index_key:index_value"
+            },
+            "node_modules": {
+                "eslint": {
+                    "index.ts": "const eslint_key = 'eslint value'",
+                    "package.json": r#"{ "some_key": "some value" }"#,
+                },
+                "prettier": {
+                    "index.ts": "const prettier_key = 'prettier value'",
+                    "package.json": r#"{ "other_key": "other value" }"#,
+                },
+            },
+            "package.json": r#"{ "main_key": "main value" }"#,
+        }),
+    )
+    .await;
+    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+    let query = "key";
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([("package.json".to_string(), vec![8..11])]),
+        "Only one non-ignored file should have the query"
+    );
+
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([
+            ("package.json".to_string(), vec![8..11]),
+            ("target/index.txt".to_string(), vec![6..9]),
+            (
+                "node_modules/prettier/package.json".to_string(),
+                vec![9..12]
+            ),
+            ("node_modules/prettier/index.ts".to_string(), vec![15..18]),
+            ("node_modules/eslint/index.ts".to_string(), vec![13..16]),
+            ("node_modules/eslint/package.json".to_string(), vec![8..11]),
+        ]),
+        "Unrestricted search with ignored directories should find every file with the query"
+    );
+
+    assert_eq!(
+        search(
+            &project,
+            SearchQuery::text(
+                query,
+                false,
+                false,
+                true,
+                vec![PathMatcher::new("node_modules/prettier/**").unwrap()],
+                vec![PathMatcher::new("*.ts").unwrap()],
+            )
+            .unwrap(),
+            cx
+        )
+        .await
+        .unwrap(),
+        HashMap::from_iter([(
+            "node_modules/prettier/package.json".to_string(),
+            vec![9..12]
+        )]),
+        "With search including ignored prettier directory and excluding TS files, only one file should be found"
+    );
+}
+
 #[test]
 fn test_glob_literal_prefix() {
     assert_eq!(glob_literal_prefix("**/*.js"), "");

crates/project2/src/search.rs 🔗

@@ -371,15 +371,25 @@ impl SearchQuery {
     pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
         match file_path {
             Some(file_path) => {
-                !self
-                    .files_to_exclude()
-                    .iter()
-                    .any(|exclude_glob| exclude_glob.is_match(file_path))
-                    && (self.files_to_include().is_empty()
+                let mut path = file_path.to_path_buf();
+                loop {
+                    if self
+                        .files_to_exclude()
+                        .iter()
+                        .any(|exclude_glob| exclude_glob.is_match(&path))
+                    {
+                        return false;
+                    } else if self.files_to_include().is_empty()
                         || self
                             .files_to_include()
                             .iter()
-                            .any(|include_glob| include_glob.is_match(file_path)))
+                            .any(|include_glob| include_glob.is_match(&path))
+                    {
+                        return true;
+                    } else if !path.pop() {
+                        return false;
+                    }
+                }
             }
             None => self.files_to_include().is_empty(),
         }

crates/project2/src/worktree.rs 🔗

@@ -958,8 +958,6 @@ impl LocalWorktree {
 
         cx.spawn(|this, mut cx| async move {
             let text = fs.load(&abs_path).await?;
-            let entry = entry.await?;
-
             let mut index_task = None;
             let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?;
             if let Some(repo) = snapshot.repository_for_path(&path) {
@@ -982,18 +980,43 @@ impl LocalWorktree {
             let worktree = this
                 .upgrade()
                 .ok_or_else(|| anyhow!("worktree was dropped"))?;
-            Ok((
-                File {
-                    entry_id: entry.id,
-                    worktree,
-                    path: entry.path,
-                    mtime: entry.mtime,
-                    is_local: true,
-                    is_deleted: false,
-                },
-                text,
-                diff_base,
-            ))
+            match entry.await? {
+                Some(entry) => Ok((
+                    File {
+                        entry_id: Some(entry.id),
+                        worktree,
+                        path: entry.path,
+                        mtime: entry.mtime,
+                        is_local: true,
+                        is_deleted: false,
+                    },
+                    text,
+                    diff_base,
+                )),
+                None => {
+                    let metadata = fs
+                        .metadata(&abs_path)
+                        .await
+                        .with_context(|| {
+                            format!("Loading metadata for excluded file {abs_path:?}")
+                        })?
+                        .with_context(|| {
+                            format!("Excluded file {abs_path:?} got removed during loading")
+                        })?;
+                    Ok((
+                        File {
+                            entry_id: None,
+                            worktree,
+                            path,
+                            mtime: metadata.mtime,
+                            is_local: true,
+                            is_deleted: false,
+                        },
+                        text,
+                        diff_base,
+                    ))
+                }
+            }
         })
     }
 
@@ -1013,18 +1036,38 @@ impl LocalWorktree {
         let text = buffer.as_rope().clone();
         let fingerprint = text.fingerprint();
         let version = buffer.version();
-        let save = self.write_file(path, text, buffer.line_ending(), cx);
+        let save = self.write_file(path.as_ref(), text, buffer.line_ending(), cx);
+        let fs = Arc::clone(&self.fs);
+        let abs_path = self.absolutize(&path);
 
         cx.spawn(move |this, mut cx| async move {
             let entry = save.await?;
             let this = this.upgrade().context("worktree dropped")?;
 
+            let (entry_id, mtime, path) = match entry {
+                Some(entry) => (Some(entry.id), entry.mtime, entry.path),
+                None => {
+                    let metadata = fs
+                        .metadata(&abs_path)
+                        .await
+                        .with_context(|| {
+                            format!(
+                                "Fetching metadata after saving the excluded buffer {abs_path:?}"
+                            )
+                        })?
+                        .with_context(|| {
+                            format!("Excluded buffer {path:?} got removed during saving")
+                        })?;
+                    (None, metadata.mtime, path)
+                }
+            };
+
             if has_changed_file {
                 let new_file = Arc::new(File {
-                    entry_id: entry.id,
+                    entry_id,
                     worktree: this,
-                    path: entry.path,
-                    mtime: entry.mtime,
+                    path,
+                    mtime,
                     is_local: true,
                     is_deleted: false,
                 });
@@ -1050,13 +1093,13 @@ impl LocalWorktree {
                     project_id,
                     buffer_id,
                     version: serialize_version(&version),
-                    mtime: Some(entry.mtime.into()),
+                    mtime: Some(mtime.into()),
                     fingerprint: serialize_fingerprint(fingerprint),
                 })?;
             }
 
             buffer_handle.update(&mut cx, |buffer, cx| {
-                buffer.did_save(version.clone(), fingerprint, entry.mtime, cx);
+                buffer.did_save(version.clone(), fingerprint, mtime, cx);
             })?;
 
             Ok(())
@@ -1081,7 +1124,7 @@ impl LocalWorktree {
         path: impl Into<Arc<Path>>,
         is_dir: bool,
         cx: &mut ModelContext<Worktree>,
-    ) -> Task<Result<Entry>> {
+    ) -> Task<Result<Option<Entry>>> {
         let path = path.into();
         let lowest_ancestor = self.lowest_ancestor(&path);
         let abs_path = self.absolutize(&path);
@@ -1098,7 +1141,7 @@ impl LocalWorktree {
         cx.spawn(|this, mut cx| async move {
             write.await?;
             let (result, refreshes) = this.update(&mut cx, |this, cx| {
-                let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
+                let mut refreshes = Vec::new();
                 let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
                 for refresh_path in refresh_paths.ancestors() {
                     if refresh_path == Path::new("") {
@@ -1125,14 +1168,14 @@ impl LocalWorktree {
         })
     }
 
-    pub fn write_file(
+    pub(crate) fn write_file(
         &self,
         path: impl Into<Arc<Path>>,
         text: Rope,
         line_ending: LineEnding,
         cx: &mut ModelContext<Worktree>,
-    ) -> Task<Result<Entry>> {
-        let path = path.into();
+    ) -> Task<Result<Option<Entry>>> {
+        let path: Arc<Path> = path.into();
         let abs_path = self.absolutize(&path);
         let fs = self.fs.clone();
         let write = cx
@@ -1191,8 +1234,11 @@ impl LocalWorktree {
         entry_id: ProjectEntryId,
         new_path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Worktree>,
-    ) -> Option<Task<Result<Entry>>> {
-        let old_path = self.entry_for_id(entry_id)?.path.clone();
+    ) -> Task<Result<Option<Entry>>> {
+        let old_path = match self.entry_for_id(entry_id) {
+            Some(entry) => entry.path.clone(),
+            None => return Task::ready(Ok(None)),
+        };
         let new_path = new_path.into();
         let abs_old_path = self.absolutize(&old_path);
         let abs_new_path = self.absolutize(&new_path);
@@ -1202,7 +1248,7 @@ impl LocalWorktree {
                 .await
         });
 
-        Some(cx.spawn(|this, mut cx| async move {
+        cx.spawn(|this, mut cx| async move {
             rename.await?;
             this.update(&mut cx, |this, cx| {
                 this.as_local_mut()
@@ -1210,7 +1256,7 @@ impl LocalWorktree {
                     .refresh_entry(new_path.clone(), Some(old_path), cx)
             })?
             .await
-        }))
+        })
     }
 
     pub fn copy_entry(
@@ -1218,8 +1264,11 @@ impl LocalWorktree {
         entry_id: ProjectEntryId,
         new_path: impl Into<Arc<Path>>,
         cx: &mut ModelContext<Worktree>,
-    ) -> Option<Task<Result<Entry>>> {
-        let old_path = self.entry_for_id(entry_id)?.path.clone();
+    ) -> Task<Result<Option<Entry>>> {
+        let old_path = match self.entry_for_id(entry_id) {
+            Some(entry) => entry.path.clone(),
+            None => return Task::ready(Ok(None)),
+        };
         let new_path = new_path.into();
         let abs_old_path = self.absolutize(&old_path);
         let abs_new_path = self.absolutize(&new_path);
@@ -1234,7 +1283,7 @@ impl LocalWorktree {
             .await
         });
 
-        Some(cx.spawn(|this, mut cx| async move {
+        cx.spawn(|this, mut cx| async move {
             copy.await?;
             this.update(&mut cx, |this, cx| {
                 this.as_local_mut()
@@ -1242,7 +1291,7 @@ impl LocalWorktree {
                     .refresh_entry(new_path.clone(), None, cx)
             })?
             .await
-        }))
+        })
     }
 
     pub fn expand_entry(
@@ -1278,7 +1327,10 @@ impl LocalWorktree {
         path: Arc<Path>,
         old_path: Option<Arc<Path>>,
         cx: &mut ModelContext<Worktree>,
-    ) -> Task<Result<Entry>> {
+    ) -> Task<Result<Option<Entry>>> {
+        if self.is_path_excluded(path.to_path_buf()) {
+            return Task::ready(Ok(None));
+        }
         let paths = if let Some(old_path) = old_path.as_ref() {
             vec![old_path.clone(), path.clone()]
         } else {
@@ -1287,11 +1339,12 @@ impl LocalWorktree {
         let mut refresh = self.refresh_entries_for_paths(paths);
         cx.spawn(move |this, mut cx| async move {
             refresh.recv().await;
-            this.update(&mut cx, |this, _| {
+            let new_entry = this.update(&mut cx, |this, _| {
                 this.entry_for_path(path)
                     .cloned()
                     .ok_or_else(|| anyhow!("failed to read path after update"))
-            })?
+            })??;
+            Ok(Some(new_entry))
         })
     }
 
@@ -2222,10 +2275,19 @@ impl LocalSnapshot {
         paths
     }
 
-    pub fn is_path_excluded(&self, abs_path: &Path) -> bool {
-        self.file_scan_exclusions
-            .iter()
-            .any(|exclude_matcher| exclude_matcher.is_match(abs_path))
+    pub fn is_path_excluded(&self, mut path: PathBuf) -> bool {
+        loop {
+            if self
+                .file_scan_exclusions
+                .iter()
+                .any(|exclude_matcher| exclude_matcher.is_match(&path))
+            {
+                return true;
+            }
+            if !path.pop() {
+                return false;
+            }
+        }
     }
 }
 
@@ -2455,8 +2517,7 @@ impl BackgroundScannerState {
                 ids_to_preserve.insert(work_directory_id);
             } else {
                 let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
-                let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path)
-                    || snapshot.is_path_excluded(&git_dir_abs_path);
+                let git_dir_excluded = snapshot.is_path_excluded(entry.git_dir_path.to_path_buf());
                 if git_dir_excluded
                     && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
                 {
@@ -2663,7 +2724,7 @@ pub struct File {
     pub worktree: Model<Worktree>,
     pub path: Arc<Path>,
     pub mtime: SystemTime,
-    pub(crate) entry_id: ProjectEntryId,
+    pub(crate) entry_id: Option<ProjectEntryId>,
     pub(crate) is_local: bool,
     pub(crate) is_deleted: bool,
 }
@@ -2732,7 +2793,7 @@ impl language::File for File {
     fn to_proto(&self) -> rpc::proto::File {
         rpc::proto::File {
             worktree_id: self.worktree.entity_id().as_u64(),
-            entry_id: self.entry_id.to_proto(),
+            entry_id: self.entry_id.map(|id| id.to_proto()),
             path: self.path.to_string_lossy().into(),
             mtime: Some(self.mtime.into()),
             is_deleted: self.is_deleted,
@@ -2790,7 +2851,7 @@ impl File {
             worktree,
             path: entry.path.clone(),
             mtime: entry.mtime,
-            entry_id: entry.id,
+            entry_id: Some(entry.id),
             is_local: true,
             is_deleted: false,
         })
@@ -2815,7 +2876,7 @@ impl File {
             worktree,
             path: Path::new(&proto.path).into(),
             mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(),
-            entry_id: ProjectEntryId::from_proto(proto.entry_id),
+            entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
             is_local: false,
             is_deleted: proto.is_deleted,
         })
@@ -2833,7 +2894,7 @@ impl File {
         if self.is_deleted {
             None
         } else {
-            Some(self.entry_id)
+            self.entry_id
         }
     }
 }
@@ -3329,16 +3390,7 @@ impl BackgroundScanner {
                     return false;
                 }
 
-                // FS events may come for files which parent directory is excluded, need to check ignore those.
-                let mut path_to_test = abs_path.clone();
-                let mut excluded_file_event = snapshot.is_path_excluded(abs_path)
-                    || snapshot.is_path_excluded(&relative_path);
-                while !excluded_file_event && path_to_test.pop() {
-                    if snapshot.is_path_excluded(&path_to_test) {
-                        excluded_file_event = true;
-                    }
-                }
-                if excluded_file_event {
+                if snapshot.is_path_excluded(relative_path.to_path_buf()) {
                     if !is_git_related {
                         log::debug!("ignoring FS event for excluded path {relative_path:?}");
                     }
@@ -3522,7 +3574,7 @@ impl BackgroundScanner {
             let state = self.state.lock();
             let snapshot = &state.snapshot;
             root_abs_path = snapshot.abs_path().clone();
-            if snapshot.is_path_excluded(&job.abs_path) {
+            if snapshot.is_path_excluded(job.path.to_path_buf()) {
                 log::error!("skipping excluded directory {:?}", job.path);
                 return Ok(());
             }
@@ -3593,9 +3645,9 @@ impl BackgroundScanner {
             }
 
             {
+                let relative_path = job.path.join(child_name);
                 let mut state = self.state.lock();
-                if state.snapshot.is_path_excluded(&child_abs_path) {
-                    let relative_path = job.path.join(child_name);
+                if state.snapshot.is_path_excluded(relative_path.clone()) {
                     log::debug!("skipping excluded child entry {relative_path:?}");
                     state.remove_path(&relative_path);
                     continue;

crates/project2/src/worktree_tests.rs 🔗

@@ -1055,11 +1055,12 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
             &[
                 ".git/HEAD",
                 ".git/foo",
+                "node_modules",
                 "node_modules/.DS_Store",
                 "node_modules/prettier",
                 "node_modules/prettier/package.json",
             ],
-            &["target", "node_modules"],
+            &["target"],
             &[
                 ".DS_Store",
                 "src/.DS_Store",
@@ -1109,6 +1110,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
                 ".git/HEAD",
                 ".git/foo",
                 ".git/new_file",
+                "node_modules",
                 "node_modules/.DS_Store",
                 "node_modules/prettier",
                 "node_modules/prettier/package.json",
@@ -1117,7 +1119,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
                 "build_output/new_file",
                 "test_output/new_file",
             ],
-            &["target", "node_modules", "test_output"],
+            &["target", "test_output"],
             &[
                 ".DS_Store",
                 "src/.DS_Store",
@@ -1177,6 +1179,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
                 .create_entry("a/e".as_ref(), true, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     assert!(entry.is_dir());
 
@@ -1226,6 +1229,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
                 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     assert!(entry.is_file());
 
@@ -1261,6 +1265,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
                 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     assert!(entry.is_file());
 
@@ -1279,6 +1284,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
                 .create_entry("a/b/c/e.txt".as_ref(), false, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     assert!(entry.is_file());
 
@@ -1295,6 +1301,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
                 .create_entry("d/e/f/g.txt".as_ref(), false, cx)
         })
         .await
+        .unwrap()
         .unwrap();
     assert!(entry.is_file());
 
@@ -1620,14 +1627,14 @@ fn randomly_mutate_worktree(
                 entry.id.0,
                 new_path
             );
-            let task = worktree.rename_entry(entry.id, new_path, cx).unwrap();
+            let task = worktree.rename_entry(entry.id, new_path, cx);
             cx.background_executor().spawn(async move {
-                task.await?;
+                task.await?.unwrap();
                 Ok(())
             })
         }
         _ => {
-            let task = if entry.is_dir() {
+            if entry.is_dir() {
                 let child_path = entry.path.join(random_filename(rng));
                 let is_dir = rng.gen_bool(0.3);
                 log::info!(
@@ -1635,15 +1642,20 @@ fn randomly_mutate_worktree(
                     if is_dir { "dir" } else { "file" },
                     child_path,
                 );
-                worktree.create_entry(child_path, is_dir, cx)
+                let task = worktree.create_entry(child_path, is_dir, cx);
+                cx.background_executor().spawn(async move {
+                    task.await?;
+                    Ok(())
+                })
             } else {
                 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
-                worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx)
-            };
-            cx.background_executor().spawn(async move {
-                task.await?;
-                Ok(())
-            })
+                let task =
+                    worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
+                cx.background_executor().spawn(async move {
+                    task.await?;
+                    Ok(())
+                })
+            }
         }
     }
 }

crates/project_panel/src/project_panel.rs 🔗

@@ -621,7 +621,7 @@ impl ProjectPanel {
             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)
@@ -635,7 +635,7 @@ impl ProjectPanel {
             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);
@@ -648,21 +648,22 @@ impl ProjectPanel {
                 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);
+            if let Some(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();
-            })?;
+                    this.update_visible_entries(None, cx);
+                    if is_new_entry && !is_dir {
+                        this.open_entry(new_entry.id, true, cx);
+                    }
+                    cx.notify();
+                })?;
+            }
             Ok(())
         }))
     }
@@ -935,15 +936,17 @@ impl ProjectPanel {
             }
 
             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)
+                self.project
+                    .update(cx, |project, cx| {
+                        project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
+                    })
+                    .detach_and_log_err(cx)
+            } else {
+                self.project
+                    .update(cx, |project, cx| {
+                        project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
+                    })
+                    .detach_and_log_err(cx)
             }
         }
         None
@@ -1026,7 +1029,7 @@ impl ProjectPanel {
             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)?;
+                let task = project.rename_entry(entry_to_move, new_path, cx);
                 cx.foreground().spawn(task).detach_and_log_err(cx);
             }
 
@@ -1627,9 +1630,21 @@ impl View for ProjectPanel {
         }
     }
 
-    fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
+    fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) {
         Self::reset_to_default_keymap_context(keymap);
         keymap.add_identifier("menu");
+
+        if let Some(window) = cx.active_window() {
+            window.read_with(cx, |cx| {
+                let identifier = if self.filename_editor.is_focused(cx) {
+                    "editing"
+                } else {
+                    "not_editing"
+                };
+
+                keymap.add_identifier(identifier);
+            });
+        }
     }
 
     fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {

crates/project_panel2/src/project_panel.rs 🔗

@@ -10,9 +10,9 @@ use anyhow::{anyhow, Result};
 use gpui::{
     actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
     ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
-    InteractiveElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
-    PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View,
-    ViewContext, VisualContext as _, WeakView, WindowContext,
+    InteractiveElement, KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels,
+    Point, PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle,
+    View, ViewContext, VisualContext as _, WeakView, WindowContext,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{
@@ -397,7 +397,6 @@ impl ProjectPanel {
                     menu = menu.action(
                         "Add Folder to Project",
                         Box::new(workspace::AddFolderToProject),
-                        cx,
                     );
                     if is_root {
                         menu = menu.entry(
@@ -412,35 +411,35 @@ impl ProjectPanel {
                 }
 
                 menu = menu
-                    .action("New File", Box::new(NewFile), cx)
-                    .action("New Folder", Box::new(NewDirectory), cx)
+                    .action("New File", Box::new(NewFile))
+                    .action("New Folder", Box::new(NewDirectory))
                     .separator()
-                    .action("Cut", Box::new(Cut), cx)
-                    .action("Copy", Box::new(Copy), cx);
+                    .action("Cut", Box::new(Cut))
+                    .action("Copy", Box::new(Copy));
 
                 if let Some(clipboard_entry) = self.clipboard_entry {
                     if clipboard_entry.worktree_id() == worktree_id {
-                        menu = menu.action("Paste", Box::new(Paste), cx);
+                        menu = menu.action("Paste", Box::new(Paste));
                     }
                 }
 
                 menu = menu
                     .separator()
-                    .action("Copy Path", Box::new(CopyPath), cx)
-                    .action("Copy Relative Path", Box::new(CopyRelativePath), cx)
+                    .action("Copy Path", Box::new(CopyPath))
+                    .action("Copy Relative Path", Box::new(CopyRelativePath))
                     .separator()
-                    .action("Reveal in Finder", Box::new(RevealInFinder), cx);
+                    .action("Reveal in Finder", Box::new(RevealInFinder));
 
                 if is_dir {
                     menu = menu
-                        .action("Open in Terminal", Box::new(OpenInTerminal), cx)
-                        .action("Search Inside", Box::new(NewSearchInDirectory), cx)
+                        .action("Open in Terminal", Box::new(OpenInTerminal))
+                        .action("Search Inside", Box::new(NewSearchInDirectory))
                 }
 
-                menu = menu.separator().action("Rename", Box::new(Rename), cx);
+                menu = menu.separator().action("Rename", Box::new(Rename));
 
                 if !is_root {
-                    menu = menu.action("Delete", Box::new(Delete), cx);
+                    menu = menu.action("Delete", Box::new(Delete));
                 }
 
                 menu
@@ -611,7 +610,7 @@ impl ProjectPanel {
             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)
@@ -625,7 +624,7 @@ impl ProjectPanel {
             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);
@@ -638,21 +637,22 @@ impl ProjectPanel {
                 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);
+            if let Some(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();
-            })?;
+                    this.update_visible_entries(None, cx);
+                    if is_new_entry && !is_dir {
+                        this.open_entry(new_entry.id, true, cx);
+                    }
+                    cx.notify();
+                })?;
+            }
             Ok(())
         }))
     }
@@ -932,15 +932,17 @@ impl ProjectPanel {
             }
 
             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);
+                self.project
+                    .update(cx, |project, cx| {
+                        project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
+                    })
+                    .detach_and_log_err(cx)
+            } else {
+                self.project
+                    .update(cx, |project, cx| {
+                        project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
+                    })
+                    .detach_and_log_err(cx)
             }
 
             Some(())
@@ -1026,7 +1028,7 @@ impl ProjectPanel {
     //         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)?;
+    //             let task = project.rename_entry(entry_to_move, new_path, cx);
     //             cx.foreground_executor().spawn(task).detach_and_log_err(cx);
     //         }
 
@@ -1420,6 +1422,22 @@ impl ProjectPanel {
         //     );
         // })
     }
+
+    fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
+        let mut dispatch_context = KeyContext::default();
+        dispatch_context.add("ProjectPanel");
+        dispatch_context.add("menu");
+
+        let identifier = if self.filename_editor.focus_handle(cx).is_focused(cx) {
+            "editing"
+        } else {
+            "not_editing"
+        };
+
+        dispatch_context.add(identifier);
+
+        dispatch_context
+    }
 }
 
 impl Render for ProjectPanel {
@@ -1433,7 +1451,7 @@ impl Render for ProjectPanel {
                 .id("project-panel")
                 .size_full()
                 .relative()
-                .key_context("ProjectPanel")
+                .key_context(self.dispatch_context(cx))
                 .on_action(cx.listener(Self::select_next))
                 .on_action(cx.listener(Self::select_prev))
                 .on_action(cx.listener(Self::expand_selected_entry))

crates/rpc/proto/zed.proto 🔗

@@ -430,7 +430,7 @@ message ExpandProjectEntryResponse {
 }
 
 message ProjectEntryResponse {
-    Entry entry = 1;
+    optional Entry entry = 1;
     uint64 worktree_scan_id = 2;
 }
 
@@ -1357,7 +1357,7 @@ message User {
 
 message File {
     uint64 worktree_id = 1;
-    uint64 entry_id = 2;
+    optional uint64 entry_id = 2;
     string path = 3;
     Timestamp mtime = 4;
     bool is_deleted = 5;

crates/rpc/src/rpc.rs 🔗

@@ -9,4 +9,4 @@ pub use notification::*;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 66;
+pub const PROTOCOL_VERSION: u32 = 67;

crates/rpc2/proto/zed.proto 🔗

@@ -430,7 +430,7 @@ message ExpandProjectEntryResponse {
 }
 
 message ProjectEntryResponse {
-    Entry entry = 1;
+    optional Entry entry = 1;
     uint64 worktree_scan_id = 2;
 }
 
@@ -1357,7 +1357,7 @@ message User {
 
 message File {
     uint64 worktree_id = 1;
-    uint64 entry_id = 2;
+    optional uint64 entry_id = 2;
     string path = 3;
     Timestamp mtime = 4;
     bool is_deleted = 5;

crates/rpc2/src/rpc.rs 🔗

@@ -9,4 +9,4 @@ pub use notification::*;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 64;
+pub const PROTOCOL_VERSION: u32 = 67;

crates/semantic_index2/Cargo.toml 🔗

@@ -0,0 +1,69 @@
+[package]
+name = "semantic_index2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/semantic_index.rs"
+doctest = false
+
+[dependencies]
+ai = { package = "ai2", path = "../ai2" }
+collections = { path = "../collections" }
+gpui = { package = "gpui2", path = "../gpui2" }
+language = { package = "language2", path = "../language2" }
+project = { package = "project2", path = "../project2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+util = { path = "../util" }
+rpc = { package = "rpc2", path = "../rpc2" }
+settings = { package = "settings2", path = "../settings2" }
+anyhow.workspace = true
+postage.workspace = true
+futures.workspace = true
+ordered-float.workspace = true
+smol.workspace = true
+rusqlite.workspace = true
+log.workspace = true
+tree-sitter.workspace = true
+lazy_static.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+async-trait.workspace = true
+tiktoken-rs.workspace = true
+parking_lot.workspace = true
+rand.workspace = true
+schemars.workspace = true
+globset.workspace = true
+sha1 = "0.10.5"
+ndarray = { version = "0.15.0" }
+
+[dev-dependencies]
+ai = { package = "ai2", path = "../ai2", features = ["test-support"] }
+collections = { path = "../collections", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+language = { package = "language2", path = "../language2", features = ["test-support"] }
+project = { package = "project2", path = "../project2", features = ["test-support"] }
+rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+settings = { package = "settings2", path = "../settings2", features = ["test-support"]}
+rust-embed = { version = "8.0", features = ["include-exclude"] }
+client = { package = "client2", path = "../client2" }
+node_runtime = { path = "../node_runtime"}
+
+pretty_assertions.workspace = true
+rand.workspace = true
+unindent.workspace = true
+tempdir.workspace = true
+ctor.workspace = true
+env_logger.workspace = true
+
+tree-sitter-typescript.workspace = true
+tree-sitter-json.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-toml.workspace = true
+tree-sitter-cpp.workspace = true
+tree-sitter-elixir.workspace = true
+tree-sitter-lua.workspace = true
+tree-sitter-ruby.workspace = true
+tree-sitter-php.workspace = true

crates/semantic_index2/README.md 🔗

@@ -0,0 +1,20 @@
+
+# Semantic Index
+
+## Evaluation
+
+### Metrics
+
+nDCG@k:
+- "The value of NDCG is determined by comparing the relevance of the items returned by the search engine to the relevance of the item that a hypothetical "ideal" search engine would return.
+- "The relevance of result is represented by a score (also known as a 'grade') that is assigned to the search query. The scores of these results are then discounted based on their position in the search results -- did they get recommended first or last?"
+
+MRR@k:
+- "Mean reciprocal rank quantifies the rank of the first relevant item found in teh recommendation list."
+
+MAP@k:
+- "Mean average precision averages the precision@k metric at each relevant item position in the recommendation list.
+
+Resources:
+- [Evaluating recommendation metrics](https://www.shaped.ai/blog/evaluating-recommendation-systems-map-mmr-ndcg)
+- [Math Walkthrough](https://towardsdatascience.com/demystifying-ndcg-bee3be58cfe0)

crates/semantic_index2/eval/gpt-engineer.json 🔗

@@ -0,0 +1,114 @@
+{
+  "repo": "https://github.com/AntonOsika/gpt-engineer.git",
+  "commit": "7735a6445bae3611c62f521e6464c67c957f87c2",
+  "assertions": [
+    {
+      "query": "How do I contribute to this project?",
+      "matches": [
+        ".github/CONTRIBUTING.md:1",
+        "ROADMAP.md:48"
+      ]
+    },
+    {
+      "query": "What version of the openai package is active?",
+      "matches": [
+        "pyproject.toml:14"
+      ]
+    },
+    {
+      "query": "Ask user for clarification",
+      "matches": [
+        "gpt_engineer/steps.py:69"
+      ]
+    },
+    {
+      "query": "generate tests for python code",
+      "matches": [
+        "gpt_engineer/steps.py:153"
+      ]
+    },
+    {
+      "query": "get item from database based on key",
+      "matches": [
+        "gpt_engineer/db.py:42",
+        "gpt_engineer/db.py:68"
+      ]
+    },
+    {
+      "query": "prompt user to select files",
+      "matches": [
+        "gpt_engineer/file_selector.py:171",
+        "gpt_engineer/file_selector.py:306",
+        "gpt_engineer/file_selector.py:289",
+        "gpt_engineer/file_selector.py:234"
+      ]
+    },
+    {
+      "query": "send to rudderstack",
+      "matches": [
+        "gpt_engineer/collect.py:11",
+        "gpt_engineer/collect.py:38"
+      ]
+    },
+    {
+      "query": "parse code blocks from chat messages",
+      "matches": [
+        "gpt_engineer/chat_to_files.py:10",
+        "docs/intro/chat_parsing.md:1"
+      ]
+    },
+    {
+      "query": "how do I use the docker cli?",
+      "matches": [
+        "docker/README.md:1"
+      ]
+    },
+    {
+      "query": "ask the user if the code ran successfully?",
+      "matches": [
+        "gpt_engineer/learning.py:54"
+      ]
+    },
+    {
+      "query": "how is consent granted by the user?",
+      "matches": [
+        "gpt_engineer/learning.py:107",
+        "gpt_engineer/learning.py:130",
+        "gpt_engineer/learning.py:152"
+      ]
+    },
+    {
+      "query": "what are all the different steps the agent can take?",
+      "matches": [
+        "docs/intro/steps_module.md:1",
+        "gpt_engineer/steps.py:391"
+      ]
+    },
+    {
+      "query": "ask the user for clarification?",
+      "matches": [
+        "gpt_engineer/steps.py:69"
+      ]
+    },
+    {
+      "query": "what models are available?",
+      "matches": [
+        "gpt_engineer/ai.py:315",
+        "gpt_engineer/ai.py:341",
+        "docs/open-models.md:1"
+      ]
+    },
+    {
+      "query": "what is the current focus of the project?",
+      "matches": [
+        "ROADMAP.md:11"
+      ]
+    },
+    {
+      "query": "does the agent know how to fix code?",
+      "matches": [
+        "gpt_engineer/steps.py:367"
+      ]
+    }
+  ]
+}

crates/semantic_index2/eval/tree-sitter.json 🔗

@@ -0,0 +1,104 @@
+{
+  "repo": "https://github.com/tree-sitter/tree-sitter.git",
+  "commit": "46af27796a76c72d8466627d499f2bca4af958ee",
+  "assertions": [
+    {
+      "query": "What attributes are available for the tags configuration struct?",
+      "matches": [
+        "tags/src/lib.rs:24"
+      ]
+    },
+    {
+      "query": "create a new tag configuration",
+      "matches": [
+        "tags/src/lib.rs:119"
+      ]
+    },
+    {
+      "query": "generate tags based on config",
+      "matches": [
+        "tags/src/lib.rs:261"
+      ]
+    },
+    {
+      "query": "match on ts quantifier in rust",
+      "matches": [
+        "lib/binding_rust/lib.rs:139"
+      ]
+    },
+    {
+      "query": "cli command to generate tags",
+      "matches": [
+        "cli/src/tags.rs:10"
+      ]
+    },
+    {
+      "query": "what version of the tree-sitter-tags package is active?",
+      "matches": [
+        "tags/Cargo.toml:4"
+      ]
+    },
+    {
+      "query": "Insert a new parse state",
+      "matches": [
+        "cli/src/generate/build_tables/build_parse_table.rs:153"
+      ]
+    },
+    {
+      "query": "Handle conflict when numerous actions occur on the same symbol",
+      "matches": [
+        "cli/src/generate/build_tables/build_parse_table.rs:363",
+        "cli/src/generate/build_tables/build_parse_table.rs:442"
+      ]
+    },
+    {
+      "query": "Match based on associativity of actions",
+      "matches": [
+        "cri/src/generate/build_tables/build_parse_table.rs:542"
+      ]
+    },
+    {
+      "query": "Format token set display",
+      "matches": [
+        "cli/src/generate/build_tables/item.rs:246"
+      ]
+    },
+    {
+      "query": "extract choices from rule",
+      "matches": [
+        "cli/src/generate/prepare_grammar/flatten_grammar.rs:124"
+      ]
+    },
+    {
+      "query": "How do we identify if a symbol is being used?",
+      "matches": [
+        "cli/src/generate/prepare_grammar/flatten_grammar.rs:175"
+      ]
+    },
+    {
+      "query": "How do we launch the playground?",
+      "matches": [
+        "cli/src/playground.rs:46"
+      ]
+    },
+    {
+      "query": "How do we test treesitter query matches in rust?",
+      "matches": [
+        "cli/src/query_testing.rs:152",
+        "cli/src/tests/query_test.rs:781",
+        "cli/src/tests/query_test.rs:2163",
+        "cli/src/tests/query_test.rs:3781",
+        "cli/src/tests/query_test.rs:887"
+      ]
+    },
+    {
+      "query": "What does the CLI do?",
+      "matches": [
+        "cli/README.md:10",
+        "cli/loader/README.md:3",
+        "docs/section-5-implementation.md:14",
+        "docs/section-5-implementation.md:18"
+      ]
+    }
+  ]
+}

crates/semantic_index2/src/db.rs 🔗

@@ -0,0 +1,603 @@
+use crate::{
+    parsing::{Span, SpanDigest},
+    SEMANTIC_INDEX_VERSION,
+};
+use ai::embedding::Embedding;
+use anyhow::{anyhow, Context, Result};
+use collections::HashMap;
+use futures::channel::oneshot;
+use gpui::BackgroundExecutor;
+use ndarray::{Array1, Array2};
+use ordered_float::OrderedFloat;
+use project::Fs;
+use rpc::proto::Timestamp;
+use rusqlite::params;
+use rusqlite::types::Value;
+use std::{
+    future::Future,
+    ops::Range,
+    path::{Path, PathBuf},
+    rc::Rc,
+    sync::Arc,
+    time::SystemTime,
+};
+use util::{paths::PathMatcher, TryFutureExt};
+
+pub fn argsort<T: Ord>(data: &[T]) -> Vec<usize> {
+    let mut indices = (0..data.len()).collect::<Vec<_>>();
+    indices.sort_by_key(|&i| &data[i]);
+    indices.reverse();
+    indices
+}
+
+#[derive(Debug)]
+pub struct FileRecord {
+    pub id: usize,
+    pub relative_path: String,
+    pub mtime: Timestamp,
+}
+
+#[derive(Clone)]
+pub struct VectorDatabase {
+    path: Arc<Path>,
+    transactions:
+        smol::channel::Sender<Box<dyn 'static + Send + FnOnce(&mut rusqlite::Connection)>>,
+}
+
+impl VectorDatabase {
+    pub async fn new(
+        fs: Arc<dyn Fs>,
+        path: Arc<Path>,
+        executor: BackgroundExecutor,
+    ) -> Result<Self> {
+        if let Some(db_directory) = path.parent() {
+            fs.create_dir(db_directory).await?;
+        }
+
+        let (transactions_tx, transactions_rx) = smol::channel::unbounded::<
+            Box<dyn 'static + Send + FnOnce(&mut rusqlite::Connection)>,
+        >();
+        executor
+            .spawn({
+                let path = path.clone();
+                async move {
+                    let mut connection = rusqlite::Connection::open(&path)?;
+
+                    connection.pragma_update(None, "journal_mode", "wal")?;
+                    connection.pragma_update(None, "synchronous", "normal")?;
+                    connection.pragma_update(None, "cache_size", 1000000)?;
+                    connection.pragma_update(None, "temp_store", "MEMORY")?;
+
+                    while let Ok(transaction) = transactions_rx.recv().await {
+                        transaction(&mut connection);
+                    }
+
+                    anyhow::Ok(())
+                }
+                .log_err()
+            })
+            .detach();
+        let this = Self {
+            transactions: transactions_tx,
+            path,
+        };
+        this.initialize_database().await?;
+        Ok(this)
+    }
+
+    pub fn path(&self) -> &Arc<Path> {
+        &self.path
+    }
+
+    fn transact<F, T>(&self, f: F) -> impl Future<Output = Result<T>>
+    where
+        F: 'static + Send + FnOnce(&rusqlite::Transaction) -> Result<T>,
+        T: 'static + Send,
+    {
+        let (tx, rx) = oneshot::channel();
+        let transactions = self.transactions.clone();
+        async move {
+            if transactions
+                .send(Box::new(|connection| {
+                    let result = connection
+                        .transaction()
+                        .map_err(|err| anyhow!(err))
+                        .and_then(|transaction| {
+                            let result = f(&transaction)?;
+                            transaction.commit()?;
+                            Ok(result)
+                        });
+                    let _ = tx.send(result);
+                }))
+                .await
+                .is_err()
+            {
+                return Err(anyhow!("connection was dropped"))?;
+            }
+            rx.await?
+        }
+    }
+
+    fn initialize_database(&self) -> impl Future<Output = Result<()>> {
+        self.transact(|db| {
+            rusqlite::vtab::array::load_module(&db)?;
+
+            // Delete existing tables, if SEMANTIC_INDEX_VERSION is bumped
+            let version_query = db.prepare("SELECT version from semantic_index_config");
+            let version = version_query
+                .and_then(|mut query| query.query_row([], |row| Ok(row.get::<_, i64>(0)?)));
+            if version.map_or(false, |version| version == SEMANTIC_INDEX_VERSION as i64) {
+                log::trace!("vector database schema up to date");
+                return Ok(());
+            }
+
+            log::trace!("vector database schema out of date. updating...");
+            // We renamed the `documents` table to `spans`, so we want to drop
+            // `documents` without recreating it if it exists.
+            db.execute("DROP TABLE IF EXISTS documents", [])
+                .context("failed to drop 'documents' table")?;
+            db.execute("DROP TABLE IF EXISTS spans", [])
+                .context("failed to drop 'spans' table")?;
+            db.execute("DROP TABLE IF EXISTS files", [])
+                .context("failed to drop 'files' table")?;
+            db.execute("DROP TABLE IF EXISTS worktrees", [])
+                .context("failed to drop 'worktrees' table")?;
+            db.execute("DROP TABLE IF EXISTS semantic_index_config", [])
+                .context("failed to drop 'semantic_index_config' table")?;
+
+            // Initialize Vector Databasing Tables
+            db.execute(
+                "CREATE TABLE semantic_index_config (
+                    version INTEGER NOT NULL
+                )",
+                [],
+            )?;
+
+            db.execute(
+                "INSERT INTO semantic_index_config (version) VALUES (?1)",
+                params![SEMANTIC_INDEX_VERSION],
+            )?;
+
+            db.execute(
+                "CREATE TABLE worktrees (
+                    id INTEGER PRIMARY KEY AUTOINCREMENT,
+                    absolute_path VARCHAR NOT NULL
+                );
+                CREATE UNIQUE INDEX worktrees_absolute_path ON worktrees (absolute_path);
+                ",
+                [],
+            )?;
+
+            db.execute(
+                "CREATE TABLE files (
+                    id INTEGER PRIMARY KEY AUTOINCREMENT,
+                    worktree_id INTEGER NOT NULL,
+                    relative_path VARCHAR NOT NULL,
+                    mtime_seconds INTEGER NOT NULL,
+                    mtime_nanos INTEGER NOT NULL,
+                    FOREIGN KEY(worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE
+                )",
+                [],
+            )?;
+
+            db.execute(
+                "CREATE UNIQUE INDEX files_worktree_id_and_relative_path ON files (worktree_id, relative_path)",
+                [],
+            )?;
+
+            db.execute(
+                "CREATE TABLE spans (
+                    id INTEGER PRIMARY KEY AUTOINCREMENT,
+                    file_id INTEGER NOT NULL,
+                    start_byte INTEGER NOT NULL,
+                    end_byte INTEGER NOT NULL,
+                    name VARCHAR NOT NULL,
+                    embedding BLOB NOT NULL,
+                    digest BLOB NOT NULL,
+                    FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
+                )",
+                [],
+            )?;
+            db.execute(
+                "CREATE INDEX spans_digest ON spans (digest)",
+                [],
+            )?;
+
+            log::trace!("vector database initialized with updated schema.");
+            Ok(())
+        })
+    }
+
+    pub fn delete_file(
+        &self,
+        worktree_id: i64,
+        delete_path: Arc<Path>,
+    ) -> impl Future<Output = Result<()>> {
+        self.transact(move |db| {
+            db.execute(
+                "DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2",
+                params![worktree_id, delete_path.to_str()],
+            )?;
+            Ok(())
+        })
+    }
+
+    pub fn insert_file(
+        &self,
+        worktree_id: i64,
+        path: Arc<Path>,
+        mtime: SystemTime,
+        spans: Vec<Span>,
+    ) -> impl Future<Output = Result<()>> {
+        self.transact(move |db| {
+            // Return the existing ID, if both the file and mtime match
+            let mtime = Timestamp::from(mtime);
+
+            db.execute(
+                "
+                REPLACE INTO files
+                (worktree_id, relative_path, mtime_seconds, mtime_nanos)
+                VALUES (?1, ?2, ?3, ?4)
+                ",
+                params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos],
+            )?;
+
+            let file_id = db.last_insert_rowid();
+
+            let mut query = db.prepare(
+                "
+                INSERT INTO spans
+                (file_id, start_byte, end_byte, name, embedding, digest)
+                VALUES (?1, ?2, ?3, ?4, ?5, ?6)
+                ",
+            )?;
+
+            for span in spans {
+                query.execute(params![
+                    file_id,
+                    span.range.start.to_string(),
+                    span.range.end.to_string(),
+                    span.name,
+                    span.embedding,
+                    span.digest
+                ])?;
+            }
+
+            Ok(())
+        })
+    }
+
+    pub fn worktree_previously_indexed(
+        &self,
+        worktree_root_path: &Path,
+    ) -> impl Future<Output = Result<bool>> {
+        let worktree_root_path = worktree_root_path.to_string_lossy().into_owned();
+        self.transact(move |db| {
+            let mut worktree_query =
+                db.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?;
+            let worktree_id = worktree_query
+                .query_row(params![worktree_root_path], |row| Ok(row.get::<_, i64>(0)?));
+
+            if worktree_id.is_ok() {
+                return Ok(true);
+            } else {
+                return Ok(false);
+            }
+        })
+    }
+
+    pub fn embeddings_for_digests(
+        &self,
+        digests: Vec<SpanDigest>,
+    ) -> impl Future<Output = Result<HashMap<SpanDigest, Embedding>>> {
+        self.transact(move |db| {
+            let mut query = db.prepare(
+                "
+                SELECT digest, embedding
+                FROM spans
+                WHERE digest IN rarray(?)
+                ",
+            )?;
+            let mut embeddings_by_digest = HashMap::default();
+            let digests = Rc::new(
+                digests
+                    .into_iter()
+                    .map(|p| Value::Blob(p.0.to_vec()))
+                    .collect::<Vec<_>>(),
+            );
+            let rows = query.query_map(params![digests], |row| {
+                Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?))
+            })?;
+
+            for row in rows {
+                if let Ok(row) = row {
+                    embeddings_by_digest.insert(row.0, row.1);
+                }
+            }
+
+            Ok(embeddings_by_digest)
+        })
+    }
+
+    pub fn embeddings_for_files(
+        &self,
+        worktree_id_file_paths: HashMap<i64, Vec<Arc<Path>>>,
+    ) -> impl Future<Output = Result<HashMap<SpanDigest, Embedding>>> {
+        self.transact(move |db| {
+            let mut query = db.prepare(
+                "
+                SELECT digest, embedding
+                FROM spans
+                LEFT JOIN files ON files.id = spans.file_id
+                WHERE files.worktree_id = ? AND files.relative_path IN rarray(?)
+            ",
+            )?;
+            let mut embeddings_by_digest = HashMap::default();
+            for (worktree_id, file_paths) in worktree_id_file_paths {
+                let file_paths = Rc::new(
+                    file_paths
+                        .into_iter()
+                        .map(|p| Value::Text(p.to_string_lossy().into_owned()))
+                        .collect::<Vec<_>>(),
+                );
+                let rows = query.query_map(params![worktree_id, file_paths], |row| {
+                    Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?))
+                })?;
+
+                for row in rows {
+                    if let Ok(row) = row {
+                        embeddings_by_digest.insert(row.0, row.1);
+                    }
+                }
+            }
+
+            Ok(embeddings_by_digest)
+        })
+    }
+
+    pub fn find_or_create_worktree(
+        &self,
+        worktree_root_path: Arc<Path>,
+    ) -> impl Future<Output = Result<i64>> {
+        self.transact(move |db| {
+            let mut worktree_query =
+                db.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?;
+            let worktree_id = worktree_query
+                .query_row(params![worktree_root_path.to_string_lossy()], |row| {
+                    Ok(row.get::<_, i64>(0)?)
+                });
+
+            if worktree_id.is_ok() {
+                return Ok(worktree_id?);
+            }
+
+            // If worktree_id is Err, insert new worktree
+            db.execute(
+                "INSERT into worktrees (absolute_path) VALUES (?1)",
+                params![worktree_root_path.to_string_lossy()],
+            )?;
+            Ok(db.last_insert_rowid())
+        })
+    }
+
+    pub fn get_file_mtimes(
+        &self,
+        worktree_id: i64,
+    ) -> impl Future<Output = Result<HashMap<PathBuf, SystemTime>>> {
+        self.transact(move |db| {
+            let mut statement = db.prepare(
+                "
+                SELECT relative_path, mtime_seconds, mtime_nanos
+                FROM files
+                WHERE worktree_id = ?1
+                ORDER BY relative_path",
+            )?;
+            let mut result: HashMap<PathBuf, SystemTime> = HashMap::default();
+            for row in statement.query_map(params![worktree_id], |row| {
+                Ok((
+                    row.get::<_, String>(0)?.into(),
+                    Timestamp {
+                        seconds: row.get(1)?,
+                        nanos: row.get(2)?,
+                    }
+                    .into(),
+                ))
+            })? {
+                let row = row?;
+                result.insert(row.0, row.1);
+            }
+            Ok(result)
+        })
+    }
+
+    pub fn top_k_search(
+        &self,
+        query_embedding: &Embedding,
+        limit: usize,
+        file_ids: &[i64],
+    ) -> impl Future<Output = Result<Vec<(i64, OrderedFloat<f32>)>>> {
+        let file_ids = file_ids.to_vec();
+        let query = query_embedding.clone().0;
+        let query = Array1::from_vec(query);
+        self.transact(move |db| {
+            let mut query_statement = db.prepare(
+                "
+                    SELECT
+                        id, embedding
+                    FROM
+                        spans
+                    WHERE
+                        file_id IN rarray(?)
+                    ",
+            )?;
+
+            let deserialized_rows = query_statement
+                .query_map(params![ids_to_sql(&file_ids)], |row| {
+                    Ok((row.get::<_, usize>(0)?, row.get::<_, Embedding>(1)?))
+                })?
+                .filter_map(|row| row.ok())
+                .collect::<Vec<(usize, Embedding)>>();
+
+            if deserialized_rows.len() == 0 {
+                return Ok(Vec::new());
+            }
+
+            // Get Length of Embeddings Returned
+            let embedding_len = deserialized_rows[0].1 .0.len();
+
+            let batch_n = 1000;
+            let mut batches = Vec::new();
+            let mut batch_ids = Vec::new();
+            let mut batch_embeddings: Vec<f32> = Vec::new();
+            deserialized_rows.iter().for_each(|(id, embedding)| {
+                batch_ids.push(id);
+                batch_embeddings.extend(&embedding.0);
+
+                if batch_ids.len() == batch_n {
+                    let embeddings = std::mem::take(&mut batch_embeddings);
+                    let ids = std::mem::take(&mut batch_ids);
+                    let array =
+                        Array2::from_shape_vec((ids.len(), embedding_len.clone()), embeddings);
+                    match array {
+                        Ok(array) => {
+                            batches.push((ids, array));
+                        }
+                        Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err),
+                    }
+                }
+            });
+
+            if batch_ids.len() > 0 {
+                let array = Array2::from_shape_vec(
+                    (batch_ids.len(), embedding_len),
+                    batch_embeddings.clone(),
+                );
+                match array {
+                    Ok(array) => {
+                        batches.push((batch_ids.clone(), array));
+                    }
+                    Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err),
+                }
+            }
+
+            let mut ids: Vec<usize> = Vec::new();
+            let mut results = Vec::new();
+            for (batch_ids, array) in batches {
+                let scores = array
+                    .dot(&query.t())
+                    .to_vec()
+                    .iter()
+                    .map(|score| OrderedFloat(*score))
+                    .collect::<Vec<OrderedFloat<f32>>>();
+                results.extend(scores);
+                ids.extend(batch_ids);
+            }
+
+            let sorted_idx = argsort(&results);
+            let mut sorted_results = Vec::new();
+            let last_idx = limit.min(sorted_idx.len());
+            for idx in &sorted_idx[0..last_idx] {
+                sorted_results.push((ids[*idx] as i64, results[*idx]))
+            }
+
+            Ok(sorted_results)
+        })
+    }
+
+    pub fn retrieve_included_file_ids(
+        &self,
+        worktree_ids: &[i64],
+        includes: &[PathMatcher],
+        excludes: &[PathMatcher],
+    ) -> impl Future<Output = Result<Vec<i64>>> {
+        let worktree_ids = worktree_ids.to_vec();
+        let includes = includes.to_vec();
+        let excludes = excludes.to_vec();
+        self.transact(move |db| {
+            let mut file_query = db.prepare(
+                "
+                SELECT
+                    id, relative_path
+                FROM
+                    files
+                WHERE
+                    worktree_id IN rarray(?)
+                ",
+            )?;
+
+            let mut file_ids = Vec::<i64>::new();
+            let mut rows = file_query.query([ids_to_sql(&worktree_ids)])?;
+
+            while let Some(row) = rows.next()? {
+                let file_id = row.get(0)?;
+                let relative_path = row.get_ref(1)?.as_str()?;
+                let included =
+                    includes.is_empty() || includes.iter().any(|glob| glob.is_match(relative_path));
+                let excluded = excludes.iter().any(|glob| glob.is_match(relative_path));
+                if included && !excluded {
+                    file_ids.push(file_id);
+                }
+            }
+
+            anyhow::Ok(file_ids)
+        })
+    }
+
+    pub fn spans_for_ids(
+        &self,
+        ids: &[i64],
+    ) -> impl Future<Output = Result<Vec<(i64, PathBuf, Range<usize>)>>> {
+        let ids = ids.to_vec();
+        self.transact(move |db| {
+            let mut statement = db.prepare(
+                "
+                    SELECT
+                        spans.id,
+                        files.worktree_id,
+                        files.relative_path,
+                        spans.start_byte,
+                        spans.end_byte
+                    FROM
+                        spans, files
+                    WHERE
+                        spans.file_id = files.id AND
+                        spans.id in rarray(?)
+                ",
+            )?;
+
+            let result_iter = statement.query_map(params![ids_to_sql(&ids)], |row| {
+                Ok((
+                    row.get::<_, i64>(0)?,
+                    row.get::<_, i64>(1)?,
+                    row.get::<_, String>(2)?.into(),
+                    row.get(3)?..row.get(4)?,
+                ))
+            })?;
+
+            let mut values_by_id = HashMap::<i64, (i64, PathBuf, Range<usize>)>::default();
+            for row in result_iter {
+                let (id, worktree_id, path, range) = row?;
+                values_by_id.insert(id, (worktree_id, path, range));
+            }
+
+            let mut results = Vec::with_capacity(ids.len());
+            for id in &ids {
+                let value = values_by_id
+                    .remove(id)
+                    .ok_or(anyhow!("missing span id {}", id))?;
+                results.push(value);
+            }
+
+            Ok(results)
+        })
+    }
+}
+
+fn ids_to_sql(ids: &[i64]) -> Rc<Vec<rusqlite::types::Value>> {
+    Rc::new(
+        ids.iter()
+            .copied()
+            .map(|v| rusqlite::types::Value::from(v))
+            .collect::<Vec<_>>(),
+    )
+}

crates/semantic_index2/src/embedding_queue.rs 🔗

@@ -0,0 +1,169 @@
+use crate::{parsing::Span, JobHandle};
+use ai::embedding::EmbeddingProvider;
+use gpui::BackgroundExecutor;
+use parking_lot::Mutex;
+use smol::channel;
+use std::{mem, ops::Range, path::Path, sync::Arc, time::SystemTime};
+
+#[derive(Clone)]
+pub struct FileToEmbed {
+    pub worktree_id: i64,
+    pub path: Arc<Path>,
+    pub mtime: SystemTime,
+    pub spans: Vec<Span>,
+    pub job_handle: JobHandle,
+}
+
+impl std::fmt::Debug for FileToEmbed {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("FileToEmbed")
+            .field("worktree_id", &self.worktree_id)
+            .field("path", &self.path)
+            .field("mtime", &self.mtime)
+            .field("spans", &self.spans)
+            .finish_non_exhaustive()
+    }
+}
+
+impl PartialEq for FileToEmbed {
+    fn eq(&self, other: &Self) -> bool {
+        self.worktree_id == other.worktree_id
+            && self.path == other.path
+            && self.mtime == other.mtime
+            && self.spans == other.spans
+    }
+}
+
+pub struct EmbeddingQueue {
+    embedding_provider: Arc<dyn EmbeddingProvider>,
+    pending_batch: Vec<FileFragmentToEmbed>,
+    executor: BackgroundExecutor,
+    pending_batch_token_count: usize,
+    finished_files_tx: channel::Sender<FileToEmbed>,
+    finished_files_rx: channel::Receiver<FileToEmbed>,
+}
+
+#[derive(Clone)]
+pub struct FileFragmentToEmbed {
+    file: Arc<Mutex<FileToEmbed>>,
+    span_range: Range<usize>,
+}
+
+impl EmbeddingQueue {
+    pub fn new(
+        embedding_provider: Arc<dyn EmbeddingProvider>,
+        executor: BackgroundExecutor,
+    ) -> Self {
+        let (finished_files_tx, finished_files_rx) = channel::unbounded();
+        Self {
+            embedding_provider,
+            executor,
+            pending_batch: Vec::new(),
+            pending_batch_token_count: 0,
+            finished_files_tx,
+            finished_files_rx,
+        }
+    }
+
+    pub fn push(&mut self, file: FileToEmbed) {
+        if file.spans.is_empty() {
+            self.finished_files_tx.try_send(file).unwrap();
+            return;
+        }
+
+        let file = Arc::new(Mutex::new(file));
+
+        self.pending_batch.push(FileFragmentToEmbed {
+            file: file.clone(),
+            span_range: 0..0,
+        });
+
+        let mut fragment_range = &mut self.pending_batch.last_mut().unwrap().span_range;
+        for (ix, span) in file.lock().spans.iter().enumerate() {
+            let span_token_count = if span.embedding.is_none() {
+                span.token_count
+            } else {
+                0
+            };
+
+            let next_token_count = self.pending_batch_token_count + span_token_count;
+            if next_token_count > self.embedding_provider.max_tokens_per_batch() {
+                let range_end = fragment_range.end;
+                self.flush();
+                self.pending_batch.push(FileFragmentToEmbed {
+                    file: file.clone(),
+                    span_range: range_end..range_end,
+                });
+                fragment_range = &mut self.pending_batch.last_mut().unwrap().span_range;
+            }
+
+            fragment_range.end = ix + 1;
+            self.pending_batch_token_count += span_token_count;
+        }
+    }
+
+    pub fn flush(&mut self) {
+        let batch = mem::take(&mut self.pending_batch);
+        self.pending_batch_token_count = 0;
+        if batch.is_empty() {
+            return;
+        }
+
+        let finished_files_tx = self.finished_files_tx.clone();
+        let embedding_provider = self.embedding_provider.clone();
+
+        self.executor
+            .spawn(async move {
+                let mut spans = Vec::new();
+                for fragment in &batch {
+                    let file = fragment.file.lock();
+                    spans.extend(
+                        file.spans[fragment.span_range.clone()]
+                            .iter()
+                            .filter(|d| d.embedding.is_none())
+                            .map(|d| d.content.clone()),
+                    );
+                }
+
+                // If spans is 0, just send the fragment to the finished files if its the last one.
+                if spans.is_empty() {
+                    for fragment in batch.clone() {
+                        if let Some(file) = Arc::into_inner(fragment.file) {
+                            finished_files_tx.try_send(file.into_inner()).unwrap();
+                        }
+                    }
+                    return;
+                };
+
+                match embedding_provider.embed_batch(spans).await {
+                    Ok(embeddings) => {
+                        let mut embeddings = embeddings.into_iter();
+                        for fragment in batch {
+                            for span in &mut fragment.file.lock().spans[fragment.span_range.clone()]
+                                .iter_mut()
+                                .filter(|d| d.embedding.is_none())
+                            {
+                                if let Some(embedding) = embeddings.next() {
+                                    span.embedding = Some(embedding);
+                                } else {
+                                    log::error!("number of embeddings != number of documents");
+                                }
+                            }
+
+                            if let Some(file) = Arc::into_inner(fragment.file) {
+                                finished_files_tx.try_send(file.into_inner()).unwrap();
+                            }
+                        }
+                    }
+                    Err(error) => {
+                        log::error!("{:?}", error);
+                    }
+                }
+            })
+            .detach();
+    }
+
+    pub fn finished_files(&self) -> channel::Receiver<FileToEmbed> {
+        self.finished_files_rx.clone()
+    }
+}

crates/semantic_index2/src/parsing.rs 🔗

@@ -0,0 +1,414 @@
+use ai::{
+    embedding::{Embedding, EmbeddingProvider},
+    models::TruncationDirection,
+};
+use anyhow::{anyhow, Result};
+use language::{Grammar, Language};
+use rusqlite::{
+    types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef},
+    ToSql,
+};
+use sha1::{Digest, Sha1};
+use std::{
+    borrow::Cow,
+    cmp::{self, Reverse},
+    collections::HashSet,
+    ops::Range,
+    path::Path,
+    sync::Arc,
+};
+use tree_sitter::{Parser, QueryCursor};
+
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
+pub struct SpanDigest(pub [u8; 20]);
+
+impl FromSql for SpanDigest {
+    fn column_result(value: ValueRef) -> FromSqlResult<Self> {
+        let blob = value.as_blob()?;
+        let bytes =
+            blob.try_into()
+                .map_err(|_| rusqlite::types::FromSqlError::InvalidBlobSize {
+                    expected_size: 20,
+                    blob_size: blob.len(),
+                })?;
+        return Ok(SpanDigest(bytes));
+    }
+}
+
+impl ToSql for SpanDigest {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
+        self.0.to_sql()
+    }
+}
+
+impl From<&'_ str> for SpanDigest {
+    fn from(value: &'_ str) -> Self {
+        let mut sha1 = Sha1::new();
+        sha1.update(value);
+        Self(sha1.finalize().into())
+    }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Span {
+    pub name: String,
+    pub range: Range<usize>,
+    pub content: String,
+    pub embedding: Option<Embedding>,
+    pub digest: SpanDigest,
+    pub token_count: usize,
+}
+
+const CODE_CONTEXT_TEMPLATE: &str =
+    "The below code snippet is from file '<path>'\n\n```<language>\n<item>\n```";
+const ENTIRE_FILE_TEMPLATE: &str =
+    "The below snippet is from file '<path>'\n\n```<language>\n<item>\n```";
+const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file '<path>'\n\n<item>";
+pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = &[
+    "TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML", "Scheme",
+];
+
+pub struct CodeContextRetriever {
+    pub parser: Parser,
+    pub cursor: QueryCursor,
+    pub embedding_provider: Arc<dyn EmbeddingProvider>,
+}
+
+// Every match has an item, this represents the fundamental treesitter symbol and anchors the search
+// Every match has one or more 'name' captures. These indicate the display range of the item for deduplication.
+// If there are preceeding comments, we track this with a context capture
+// If there is a piece that should be collapsed in hierarchical queries, we capture it with a collapse capture
+// If there is a piece that should be kept inside a collapsed node, we capture it with a keep capture
+#[derive(Debug, Clone)]
+pub struct CodeContextMatch {
+    pub start_col: usize,
+    pub item_range: Option<Range<usize>>,
+    pub name_range: Option<Range<usize>>,
+    pub context_ranges: Vec<Range<usize>>,
+    pub collapse_ranges: Vec<Range<usize>>,
+}
+
+impl CodeContextRetriever {
+    pub fn new(embedding_provider: Arc<dyn EmbeddingProvider>) -> Self {
+        Self {
+            parser: Parser::new(),
+            cursor: QueryCursor::new(),
+            embedding_provider,
+        }
+    }
+
+    fn parse_entire_file(
+        &self,
+        relative_path: Option<&Path>,
+        language_name: Arc<str>,
+        content: &str,
+    ) -> Result<Vec<Span>> {
+        let document_span = ENTIRE_FILE_TEMPLATE
+            .replace(
+                "<path>",
+                &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
+            )
+            .replace("<language>", language_name.as_ref())
+            .replace("<item>", &content);
+        let digest = SpanDigest::from(document_span.as_str());
+        let model = self.embedding_provider.base_model();
+        let document_span = model.truncate(
+            &document_span,
+            model.capacity()?,
+            ai::models::TruncationDirection::End,
+        )?;
+        let token_count = model.count_tokens(&document_span)?;
+
+        Ok(vec![Span {
+            range: 0..content.len(),
+            content: document_span,
+            embedding: Default::default(),
+            name: language_name.to_string(),
+            digest,
+            token_count,
+        }])
+    }
+
+    fn parse_markdown_file(
+        &self,
+        relative_path: Option<&Path>,
+        content: &str,
+    ) -> Result<Vec<Span>> {
+        let document_span = MARKDOWN_CONTEXT_TEMPLATE
+            .replace(
+                "<path>",
+                &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
+            )
+            .replace("<item>", &content);
+        let digest = SpanDigest::from(document_span.as_str());
+
+        let model = self.embedding_provider.base_model();
+        let document_span = model.truncate(
+            &document_span,
+            model.capacity()?,
+            ai::models::TruncationDirection::End,
+        )?;
+        let token_count = model.count_tokens(&document_span)?;
+
+        Ok(vec![Span {
+            range: 0..content.len(),
+            content: document_span,
+            embedding: None,
+            name: "Markdown".to_string(),
+            digest,
+            token_count,
+        }])
+    }
+
+    fn get_matches_in_file(
+        &mut self,
+        content: &str,
+        grammar: &Arc<Grammar>,
+    ) -> Result<Vec<CodeContextMatch>> {
+        let embedding_config = grammar
+            .embedding_config
+            .as_ref()
+            .ok_or_else(|| anyhow!("no embedding queries"))?;
+        self.parser.set_language(grammar.ts_language).unwrap();
+
+        let tree = self
+            .parser
+            .parse(&content, None)
+            .ok_or_else(|| anyhow!("parsing failed"))?;
+
+        let mut captures: Vec<CodeContextMatch> = Vec::new();
+        let mut collapse_ranges: Vec<Range<usize>> = Vec::new();
+        let mut keep_ranges: Vec<Range<usize>> = Vec::new();
+        for mat in self.cursor.matches(
+            &embedding_config.query,
+            tree.root_node(),
+            content.as_bytes(),
+        ) {
+            let mut start_col = 0;
+            let mut item_range: Option<Range<usize>> = None;
+            let mut name_range: Option<Range<usize>> = None;
+            let mut context_ranges: Vec<Range<usize>> = Vec::new();
+            collapse_ranges.clear();
+            keep_ranges.clear();
+            for capture in mat.captures {
+                if capture.index == embedding_config.item_capture_ix {
+                    item_range = Some(capture.node.byte_range());
+                    start_col = capture.node.start_position().column;
+                } else if Some(capture.index) == embedding_config.name_capture_ix {
+                    name_range = Some(capture.node.byte_range());
+                } else if Some(capture.index) == embedding_config.context_capture_ix {
+                    context_ranges.push(capture.node.byte_range());
+                } else if Some(capture.index) == embedding_config.collapse_capture_ix {
+                    collapse_ranges.push(capture.node.byte_range());
+                } else if Some(capture.index) == embedding_config.keep_capture_ix {
+                    keep_ranges.push(capture.node.byte_range());
+                }
+            }
+
+            captures.push(CodeContextMatch {
+                start_col,
+                item_range,
+                name_range,
+                context_ranges,
+                collapse_ranges: subtract_ranges(&collapse_ranges, &keep_ranges),
+            });
+        }
+        Ok(captures)
+    }
+
+    pub fn parse_file_with_template(
+        &mut self,
+        relative_path: Option<&Path>,
+        content: &str,
+        language: Arc<Language>,
+    ) -> Result<Vec<Span>> {
+        let language_name = language.name();
+
+        if PARSEABLE_ENTIRE_FILE_TYPES.contains(&language_name.as_ref()) {
+            return self.parse_entire_file(relative_path, language_name, &content);
+        } else if ["Markdown", "Plain Text"].contains(&language_name.as_ref()) {
+            return self.parse_markdown_file(relative_path, &content);
+        }
+
+        let mut spans = self.parse_file(content, language)?;
+        for span in &mut spans {
+            let document_content = CODE_CONTEXT_TEMPLATE
+                .replace(
+                    "<path>",
+                    &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
+                )
+                .replace("<language>", language_name.as_ref())
+                .replace("item", &span.content);
+
+            let model = self.embedding_provider.base_model();
+            let document_content = model.truncate(
+                &document_content,
+                model.capacity()?,
+                TruncationDirection::End,
+            )?;
+            let token_count = model.count_tokens(&document_content)?;
+
+            span.content = document_content;
+            span.token_count = token_count;
+        }
+        Ok(spans)
+    }
+
+    pub fn parse_file(&mut self, content: &str, language: Arc<Language>) -> Result<Vec<Span>> {
+        let grammar = language
+            .grammar()
+            .ok_or_else(|| anyhow!("no grammar for language"))?;
+
+        // Iterate through query matches
+        let matches = self.get_matches_in_file(content, grammar)?;
+
+        let language_scope = language.default_scope();
+        let placeholder = language_scope.collapsed_placeholder();
+
+        let mut spans = Vec::new();
+        let mut collapsed_ranges_within = Vec::new();
+        let mut parsed_name_ranges = HashSet::new();
+        for (i, context_match) in matches.iter().enumerate() {
+            // Items which are collapsible but not embeddable have no item range
+            let item_range = if let Some(item_range) = context_match.item_range.clone() {
+                item_range
+            } else {
+                continue;
+            };
+
+            // Checks for deduplication
+            let name;
+            if let Some(name_range) = context_match.name_range.clone() {
+                name = content
+                    .get(name_range.clone())
+                    .map_or(String::new(), |s| s.to_string());
+                if parsed_name_ranges.contains(&name_range) {
+                    continue;
+                }
+                parsed_name_ranges.insert(name_range);
+            } else {
+                name = String::new();
+            }
+
+            collapsed_ranges_within.clear();
+            'outer: for remaining_match in &matches[(i + 1)..] {
+                for collapsed_range in &remaining_match.collapse_ranges {
+                    if item_range.start <= collapsed_range.start
+                        && item_range.end >= collapsed_range.end
+                    {
+                        collapsed_ranges_within.push(collapsed_range.clone());
+                    } else {
+                        break 'outer;
+                    }
+                }
+            }
+
+            collapsed_ranges_within.sort_by_key(|r| (r.start, Reverse(r.end)));
+
+            let mut span_content = String::new();
+            for context_range in &context_match.context_ranges {
+                add_content_from_range(
+                    &mut span_content,
+                    content,
+                    context_range.clone(),
+                    context_match.start_col,
+                );
+                span_content.push_str("\n");
+            }
+
+            let mut offset = item_range.start;
+            for collapsed_range in &collapsed_ranges_within {
+                if collapsed_range.start > offset {
+                    add_content_from_range(
+                        &mut span_content,
+                        content,
+                        offset..collapsed_range.start,
+                        context_match.start_col,
+                    );
+                    offset = collapsed_range.start;
+                }
+
+                if collapsed_range.end > offset {
+                    span_content.push_str(placeholder);
+                    offset = collapsed_range.end;
+                }
+            }
+
+            if offset < item_range.end {
+                add_content_from_range(
+                    &mut span_content,
+                    content,
+                    offset..item_range.end,
+                    context_match.start_col,
+                );
+            }
+
+            let sha1 = SpanDigest::from(span_content.as_str());
+            spans.push(Span {
+                name,
+                content: span_content,
+                range: item_range.clone(),
+                embedding: None,
+                digest: sha1,
+                token_count: 0,
+            })
+        }
+
+        return Ok(spans);
+    }
+}
+
+pub(crate) fn subtract_ranges(
+    ranges: &[Range<usize>],
+    ranges_to_subtract: &[Range<usize>],
+) -> Vec<Range<usize>> {
+    let mut result = Vec::new();
+
+    let mut ranges_to_subtract = ranges_to_subtract.iter().peekable();
+
+    for range in ranges {
+        let mut offset = range.start;
+
+        while offset < range.end {
+            if let Some(range_to_subtract) = ranges_to_subtract.peek() {
+                if offset < range_to_subtract.start {
+                    let next_offset = cmp::min(range_to_subtract.start, range.end);
+                    result.push(offset..next_offset);
+                    offset = next_offset;
+                } else {
+                    let next_offset = cmp::min(range_to_subtract.end, range.end);
+                    offset = next_offset;
+                }
+
+                if offset >= range_to_subtract.end {
+                    ranges_to_subtract.next();
+                }
+            } else {
+                result.push(offset..range.end);
+                offset = range.end;
+            }
+        }
+    }
+
+    result
+}
+
+fn add_content_from_range(
+    output: &mut String,
+    content: &str,
+    range: Range<usize>,
+    start_col: usize,
+) {
+    for mut line in content.get(range.clone()).unwrap_or("").lines() {
+        for _ in 0..start_col {
+            if line.starts_with(' ') {
+                line = &line[1..];
+            } else {
+                break;
+            }
+        }
+        output.push_str(line);
+        output.push('\n');
+    }
+    output.pop();
+}

crates/semantic_index2/src/semantic_index.rs 🔗

@@ -0,0 +1,1280 @@
+mod db;
+mod embedding_queue;
+mod parsing;
+pub mod semantic_index_settings;
+
+#[cfg(test)]
+mod semantic_index_tests;
+
+use crate::semantic_index_settings::SemanticIndexSettings;
+use ai::embedding::{Embedding, EmbeddingProvider};
+use ai::providers::open_ai::OpenAIEmbeddingProvider;
+use anyhow::{anyhow, Context as _, Result};
+use collections::{BTreeMap, HashMap, HashSet};
+use db::VectorDatabase;
+use embedding_queue::{EmbeddingQueue, FileToEmbed};
+use futures::{future, FutureExt, StreamExt};
+use gpui::{
+    AppContext, AsyncAppContext, BorrowWindow, Context, Model, ModelContext, Task, ViewContext,
+    WeakModel,
+};
+use language::{Anchor, Bias, Buffer, Language, LanguageRegistry};
+use lazy_static::lazy_static;
+use ordered_float::OrderedFloat;
+use parking_lot::Mutex;
+use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES};
+use postage::watch;
+use project::{Fs, PathChange, Project, ProjectEntryId, Worktree, WorktreeId};
+use settings::Settings;
+use smol::channel;
+use std::{
+    cmp::Reverse,
+    env,
+    future::Future,
+    mem,
+    ops::Range,
+    path::{Path, PathBuf},
+    sync::{Arc, Weak},
+    time::{Duration, Instant, SystemTime},
+};
+use util::paths::PathMatcher;
+use util::{channel::RELEASE_CHANNEL_NAME, http::HttpClient, paths::EMBEDDINGS_DIR, ResultExt};
+use workspace::Workspace;
+
+const SEMANTIC_INDEX_VERSION: usize = 11;
+const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60);
+const EMBEDDING_QUEUE_FLUSH_TIMEOUT: Duration = Duration::from_millis(250);
+
+lazy_static! {
+    static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok();
+}
+
+pub fn init(
+    fs: Arc<dyn Fs>,
+    http_client: Arc<dyn HttpClient>,
+    language_registry: Arc<LanguageRegistry>,
+    cx: &mut AppContext,
+) {
+    SemanticIndexSettings::register(cx);
+
+    let db_file_path = EMBEDDINGS_DIR
+        .join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
+        .join("embeddings_db");
+
+    cx.observe_new_views(
+        |workspace: &mut Workspace, cx: &mut ViewContext<Workspace>| {
+            let Some(semantic_index) = SemanticIndex::global(cx) else {
+                return;
+            };
+            let project = workspace.project().clone();
+
+            if project.read(cx).is_local() {
+                cx.app_mut()
+                    .spawn(|mut cx| async move {
+                        let previously_indexed = semantic_index
+                            .update(&mut cx, |index, cx| {
+                                index.project_previously_indexed(&project, cx)
+                            })?
+                            .await?;
+                        if previously_indexed {
+                            semantic_index
+                                .update(&mut cx, |index, cx| index.index_project(project, cx))?
+                                .await?;
+                        }
+                        anyhow::Ok(())
+                    })
+                    .detach_and_log_err(cx);
+            }
+        },
+    )
+    .detach();
+
+    cx.spawn(move |cx| async move {
+        let semantic_index = SemanticIndex::new(
+            fs,
+            db_file_path,
+            Arc::new(OpenAIEmbeddingProvider::new(
+                http_client,
+                cx.background_executor().clone(),
+            )),
+            language_registry,
+            cx.clone(),
+        )
+        .await?;
+
+        cx.update(|cx| cx.set_global(semantic_index.clone()))?;
+
+        anyhow::Ok(())
+    })
+    .detach();
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum SemanticIndexStatus {
+    NotAuthenticated,
+    NotIndexed,
+    Indexed,
+    Indexing {
+        remaining_files: usize,
+        rate_limit_expiry: Option<Instant>,
+    },
+}
+
+pub struct SemanticIndex {
+    fs: Arc<dyn Fs>,
+    db: VectorDatabase,
+    embedding_provider: Arc<dyn EmbeddingProvider>,
+    language_registry: Arc<LanguageRegistry>,
+    parsing_files_tx: channel::Sender<(Arc<HashMap<SpanDigest, Embedding>>, PendingFile)>,
+    _embedding_task: Task<()>,
+    _parsing_files_tasks: Vec<Task<()>>,
+    projects: HashMap<WeakModel<Project>, ProjectState>,
+}
+
+struct ProjectState {
+    worktrees: HashMap<WorktreeId, WorktreeState>,
+    pending_file_count_rx: watch::Receiver<usize>,
+    pending_file_count_tx: Arc<Mutex<watch::Sender<usize>>>,
+    pending_index: usize,
+    _subscription: gpui::Subscription,
+    _observe_pending_file_count: Task<()>,
+}
+
+enum WorktreeState {
+    Registering(RegisteringWorktreeState),
+    Registered(RegisteredWorktreeState),
+}
+
+impl WorktreeState {
+    fn is_registered(&self) -> bool {
+        matches!(self, Self::Registered(_))
+    }
+
+    fn paths_changed(
+        &mut self,
+        changes: Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>,
+        worktree: &Worktree,
+    ) {
+        let changed_paths = match self {
+            Self::Registering(state) => &mut state.changed_paths,
+            Self::Registered(state) => &mut state.changed_paths,
+        };
+
+        for (path, entry_id, change) in changes.iter() {
+            let Some(entry) = worktree.entry_for_id(*entry_id) else {
+                continue;
+            };
+            if entry.is_ignored || entry.is_symlink || entry.is_external || entry.is_dir() {
+                continue;
+            }
+            changed_paths.insert(
+                path.clone(),
+                ChangedPathInfo {
+                    mtime: entry.mtime,
+                    is_deleted: *change == PathChange::Removed,
+                },
+            );
+        }
+    }
+}
+
+struct RegisteringWorktreeState {
+    changed_paths: BTreeMap<Arc<Path>, ChangedPathInfo>,
+    done_rx: watch::Receiver<Option<()>>,
+    _registration: Task<()>,
+}
+
+impl RegisteringWorktreeState {
+    fn done(&self) -> impl Future<Output = ()> {
+        let mut done_rx = self.done_rx.clone();
+        async move {
+            while let Some(result) = done_rx.next().await {
+                if result.is_some() {
+                    break;
+                }
+            }
+        }
+    }
+}
+
+struct RegisteredWorktreeState {
+    db_id: i64,
+    changed_paths: BTreeMap<Arc<Path>, ChangedPathInfo>,
+}
+
+struct ChangedPathInfo {
+    mtime: SystemTime,
+    is_deleted: bool,
+}
+
+#[derive(Clone)]
+pub struct JobHandle {
+    /// The outer Arc is here to count the clones of a JobHandle instance;
+    /// when the last handle to a given job is dropped, we decrement a counter (just once).
+    tx: Arc<Weak<Mutex<watch::Sender<usize>>>>,
+}
+
+impl JobHandle {
+    fn new(tx: &Arc<Mutex<watch::Sender<usize>>>) -> Self {
+        *tx.lock().borrow_mut() += 1;
+        Self {
+            tx: Arc::new(Arc::downgrade(&tx)),
+        }
+    }
+}
+
+impl ProjectState {
+    fn new(subscription: gpui::Subscription, cx: &mut ModelContext<SemanticIndex>) -> Self {
+        let (pending_file_count_tx, pending_file_count_rx) = watch::channel_with(0);
+        let pending_file_count_tx = Arc::new(Mutex::new(pending_file_count_tx));
+        Self {
+            worktrees: Default::default(),
+            pending_file_count_rx: pending_file_count_rx.clone(),
+            pending_file_count_tx,
+            pending_index: 0,
+            _subscription: subscription,
+            _observe_pending_file_count: cx.spawn({
+                let mut pending_file_count_rx = pending_file_count_rx.clone();
+                |this, mut cx| async move {
+                    while let Some(_) = pending_file_count_rx.next().await {
+                        if this.update(&mut cx, |_, cx| cx.notify()).is_err() {
+                            break;
+                        }
+                    }
+                }
+            }),
+        }
+    }
+
+    fn worktree_id_for_db_id(&self, id: i64) -> Option<WorktreeId> {
+        self.worktrees
+            .iter()
+            .find_map(|(worktree_id, worktree_state)| match worktree_state {
+                WorktreeState::Registered(state) if state.db_id == id => Some(*worktree_id),
+                _ => None,
+            })
+    }
+}
+
+#[derive(Clone)]
+pub struct PendingFile {
+    worktree_db_id: i64,
+    relative_path: Arc<Path>,
+    absolute_path: PathBuf,
+    language: Option<Arc<Language>>,
+    modified_time: SystemTime,
+    job_handle: JobHandle,
+}
+
+#[derive(Clone)]
+pub struct SearchResult {
+    pub buffer: Model<Buffer>,
+    pub range: Range<Anchor>,
+    pub similarity: OrderedFloat<f32>,
+}
+
+impl SemanticIndex {
+    pub fn global(cx: &mut AppContext) -> Option<Model<SemanticIndex>> {
+        if cx.has_global::<Model<Self>>() {
+            Some(cx.global::<Model<SemanticIndex>>().clone())
+        } else {
+            None
+        }
+    }
+
+    pub fn authenticate(&mut self, cx: &mut AppContext) -> bool {
+        if !self.embedding_provider.has_credentials() {
+            self.embedding_provider.retrieve_credentials(cx);
+        } else {
+            return true;
+        }
+
+        self.embedding_provider.has_credentials()
+    }
+
+    pub fn is_authenticated(&self) -> bool {
+        self.embedding_provider.has_credentials()
+    }
+
+    pub fn enabled(cx: &AppContext) -> bool {
+        SemanticIndexSettings::get_global(cx).enabled
+    }
+
+    pub fn status(&self, project: &Model<Project>) -> SemanticIndexStatus {
+        if !self.is_authenticated() {
+            return SemanticIndexStatus::NotAuthenticated;
+        }
+
+        if let Some(project_state) = self.projects.get(&project.downgrade()) {
+            if project_state
+                .worktrees
+                .values()
+                .all(|worktree| worktree.is_registered())
+                && project_state.pending_index == 0
+            {
+                SemanticIndexStatus::Indexed
+            } else {
+                SemanticIndexStatus::Indexing {
+                    remaining_files: project_state.pending_file_count_rx.borrow().clone(),
+                    rate_limit_expiry: self.embedding_provider.rate_limit_expiration(),
+                }
+            }
+        } else {
+            SemanticIndexStatus::NotIndexed
+        }
+    }
+
+    pub async fn new(
+        fs: Arc<dyn Fs>,
+        database_path: PathBuf,
+        embedding_provider: Arc<dyn EmbeddingProvider>,
+        language_registry: Arc<LanguageRegistry>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Model<Self>> {
+        let t0 = Instant::now();
+        let database_path = Arc::from(database_path);
+        let db = VectorDatabase::new(fs.clone(), database_path, cx.background_executor().clone())
+            .await?;
+
+        log::trace!(
+            "db initialization took {:?} milliseconds",
+            t0.elapsed().as_millis()
+        );
+
+        cx.build_model(|cx| {
+            let t0 = Instant::now();
+            let embedding_queue =
+                EmbeddingQueue::new(embedding_provider.clone(), cx.background_executor().clone());
+            let _embedding_task = cx.background_executor().spawn({
+                let embedded_files = embedding_queue.finished_files();
+                let db = db.clone();
+                async move {
+                    while let Ok(file) = embedded_files.recv().await {
+                        db.insert_file(file.worktree_id, file.path, file.mtime, file.spans)
+                            .await
+                            .log_err();
+                    }
+                }
+            });
+
+            // Parse files into embeddable spans.
+            let (parsing_files_tx, parsing_files_rx) =
+                channel::unbounded::<(Arc<HashMap<SpanDigest, Embedding>>, PendingFile)>();
+            let embedding_queue = Arc::new(Mutex::new(embedding_queue));
+            let mut _parsing_files_tasks = Vec::new();
+            for _ in 0..cx.background_executor().num_cpus() {
+                let fs = fs.clone();
+                let mut parsing_files_rx = parsing_files_rx.clone();
+                let embedding_provider = embedding_provider.clone();
+                let embedding_queue = embedding_queue.clone();
+                let background = cx.background_executor().clone();
+                _parsing_files_tasks.push(cx.background_executor().spawn(async move {
+                    let mut retriever = CodeContextRetriever::new(embedding_provider.clone());
+                    loop {
+                        let mut timer = background.timer(EMBEDDING_QUEUE_FLUSH_TIMEOUT).fuse();
+                        let mut next_file_to_parse = parsing_files_rx.next().fuse();
+                        futures::select_biased! {
+                            next_file_to_parse = next_file_to_parse => {
+                                if let Some((embeddings_for_digest, pending_file)) = next_file_to_parse {
+                                    Self::parse_file(
+                                        &fs,
+                                        pending_file,
+                                        &mut retriever,
+                                        &embedding_queue,
+                                        &embeddings_for_digest,
+                                    )
+                                    .await
+                                } else {
+                                    break;
+                                }
+                            },
+                            _ = timer => {
+                                embedding_queue.lock().flush();
+                            }
+                        }
+                    }
+                }));
+            }
+
+            log::trace!(
+                "semantic index task initialization took {:?} milliseconds",
+                t0.elapsed().as_millis()
+            );
+            Self {
+                fs,
+                db,
+                embedding_provider,
+                language_registry,
+                parsing_files_tx,
+                _embedding_task,
+                _parsing_files_tasks,
+                projects: Default::default(),
+            }
+        })
+    }
+
+    async fn parse_file(
+        fs: &Arc<dyn Fs>,
+        pending_file: PendingFile,
+        retriever: &mut CodeContextRetriever,
+        embedding_queue: &Arc<Mutex<EmbeddingQueue>>,
+        embeddings_for_digest: &HashMap<SpanDigest, Embedding>,
+    ) {
+        let Some(language) = pending_file.language else {
+            return;
+        };
+
+        if let Some(content) = fs.load(&pending_file.absolute_path).await.log_err() {
+            if let Some(mut spans) = retriever
+                .parse_file_with_template(Some(&pending_file.relative_path), &content, language)
+                .log_err()
+            {
+                log::trace!(
+                    "parsed path {:?}: {} spans",
+                    pending_file.relative_path,
+                    spans.len()
+                );
+
+                for span in &mut spans {
+                    if let Some(embedding) = embeddings_for_digest.get(&span.digest) {
+                        span.embedding = Some(embedding.to_owned());
+                    }
+                }
+
+                embedding_queue.lock().push(FileToEmbed {
+                    worktree_id: pending_file.worktree_db_id,
+                    path: pending_file.relative_path,
+                    mtime: pending_file.modified_time,
+                    job_handle: pending_file.job_handle,
+                    spans,
+                });
+            }
+        }
+    }
+
+    pub fn project_previously_indexed(
+        &mut self,
+        project: &Model<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<bool>> {
+        let worktrees_indexed_previously = project
+            .read(cx)
+            .worktrees()
+            .map(|worktree| {
+                self.db
+                    .worktree_previously_indexed(&worktree.read(cx).abs_path())
+            })
+            .collect::<Vec<_>>();
+        cx.spawn(|_, _cx| async move {
+            let worktree_indexed_previously =
+                futures::future::join_all(worktrees_indexed_previously).await;
+
+            Ok(worktree_indexed_previously
+                .iter()
+                .filter(|worktree| worktree.is_ok())
+                .all(|v| v.as_ref().log_err().is_some_and(|v| v.to_owned())))
+        })
+    }
+
+    fn project_entries_changed(
+        &mut self,
+        project: Model<Project>,
+        worktree_id: WorktreeId,
+        changes: Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let Some(worktree) = project.read(cx).worktree_for_id(worktree_id.clone(), cx) else {
+            return;
+        };
+        let project = project.downgrade();
+        let Some(project_state) = self.projects.get_mut(&project) else {
+            return;
+        };
+
+        let worktree = worktree.read(cx);
+        let worktree_state =
+            if let Some(worktree_state) = project_state.worktrees.get_mut(&worktree_id) {
+                worktree_state
+            } else {
+                return;
+            };
+        worktree_state.paths_changed(changes, worktree);
+        if let WorktreeState::Registered(_) = worktree_state {
+            cx.spawn(|this, mut cx| async move {
+                cx.background_executor()
+                    .timer(BACKGROUND_INDEXING_DELAY)
+                    .await;
+                if let Some((this, project)) = this.upgrade().zip(project.upgrade()) {
+                    this.update(&mut cx, |this, cx| {
+                        this.index_project(project, cx).detach_and_log_err(cx)
+                    })?;
+                }
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+    }
+
+    fn register_worktree(
+        &mut self,
+        project: Model<Project>,
+        worktree: Model<Worktree>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let project = project.downgrade();
+        let project_state = if let Some(project_state) = self.projects.get_mut(&project) {
+            project_state
+        } else {
+            return;
+        };
+        let worktree = if let Some(worktree) = worktree.read(cx).as_local() {
+            worktree
+        } else {
+            return;
+        };
+        let worktree_abs_path = worktree.abs_path().clone();
+        let scan_complete = worktree.scan_complete();
+        let worktree_id = worktree.id();
+        let db = self.db.clone();
+        let language_registry = self.language_registry.clone();
+        let (mut done_tx, done_rx) = watch::channel();
+        let registration = cx.spawn(|this, mut cx| {
+            async move {
+                let register = async {
+                    scan_complete.await;
+                    let db_id = db.find_or_create_worktree(worktree_abs_path).await?;
+                    let mut file_mtimes = db.get_file_mtimes(db_id).await?;
+                    let worktree = if let Some(project) = project.upgrade() {
+                        project
+                            .read_with(&cx, |project, cx| project.worktree_for_id(worktree_id, cx))
+                            .ok()
+                            .flatten()
+                            .context("worktree not found")?
+                    } else {
+                        return anyhow::Ok(());
+                    };
+                    let worktree = worktree.read_with(&cx, |worktree, _| worktree.snapshot())?;
+                    let mut changed_paths = cx
+                        .background_executor()
+                        .spawn(async move {
+                            let mut changed_paths = BTreeMap::new();
+                            for file in worktree.files(false, 0) {
+                                let absolute_path = worktree.absolutize(&file.path);
+
+                                if file.is_external || file.is_ignored || file.is_symlink {
+                                    continue;
+                                }
+
+                                if let Ok(language) = language_registry
+                                    .language_for_file(&absolute_path, None)
+                                    .await
+                                {
+                                    // Test if file is valid parseable file
+                                    if !PARSEABLE_ENTIRE_FILE_TYPES
+                                        .contains(&language.name().as_ref())
+                                        && &language.name().as_ref() != &"Markdown"
+                                        && language
+                                            .grammar()
+                                            .and_then(|grammar| grammar.embedding_config.as_ref())
+                                            .is_none()
+                                    {
+                                        continue;
+                                    }
+
+                                    let stored_mtime = file_mtimes.remove(&file.path.to_path_buf());
+                                    let already_stored = stored_mtime
+                                        .map_or(false, |existing_mtime| {
+                                            existing_mtime == file.mtime
+                                        });
+
+                                    if !already_stored {
+                                        changed_paths.insert(
+                                            file.path.clone(),
+                                            ChangedPathInfo {
+                                                mtime: file.mtime,
+                                                is_deleted: false,
+                                            },
+                                        );
+                                    }
+                                }
+                            }
+
+                            // Clean up entries from database that are no longer in the worktree.
+                            for (path, mtime) in file_mtimes {
+                                changed_paths.insert(
+                                    path.into(),
+                                    ChangedPathInfo {
+                                        mtime,
+                                        is_deleted: true,
+                                    },
+                                );
+                            }
+
+                            anyhow::Ok(changed_paths)
+                        })
+                        .await?;
+                    this.update(&mut cx, |this, cx| {
+                        let project_state = this
+                            .projects
+                            .get_mut(&project)
+                            .context("project not registered")?;
+                        let project = project.upgrade().context("project was dropped")?;
+
+                        if let Some(WorktreeState::Registering(state)) =
+                            project_state.worktrees.remove(&worktree_id)
+                        {
+                            changed_paths.extend(state.changed_paths);
+                        }
+                        project_state.worktrees.insert(
+                            worktree_id,
+                            WorktreeState::Registered(RegisteredWorktreeState {
+                                db_id,
+                                changed_paths,
+                            }),
+                        );
+                        this.index_project(project, cx).detach_and_log_err(cx);
+
+                        anyhow::Ok(())
+                    })??;
+
+                    anyhow::Ok(())
+                };
+
+                if register.await.log_err().is_none() {
+                    // Stop tracking this worktree if the registration failed.
+                    this.update(&mut cx, |this, _| {
+                        this.projects.get_mut(&project).map(|project_state| {
+                            project_state.worktrees.remove(&worktree_id);
+                        });
+                    })
+                    .ok();
+                }
+
+                *done_tx.borrow_mut() = Some(());
+            }
+        });
+        project_state.worktrees.insert(
+            worktree_id,
+            WorktreeState::Registering(RegisteringWorktreeState {
+                changed_paths: Default::default(),
+                done_rx,
+                _registration: registration,
+            }),
+        );
+    }
+
+    fn project_worktrees_changed(&mut self, project: Model<Project>, cx: &mut ModelContext<Self>) {
+        let project_state = if let Some(project_state) = self.projects.get_mut(&project.downgrade())
+        {
+            project_state
+        } else {
+            return;
+        };
+
+        let mut worktrees = project
+            .read(cx)
+            .worktrees()
+            .filter(|worktree| worktree.read(cx).is_local())
+            .collect::<Vec<_>>();
+        let worktree_ids = worktrees
+            .iter()
+            .map(|worktree| worktree.read(cx).id())
+            .collect::<HashSet<_>>();
+
+        // Remove worktrees that are no longer present
+        project_state
+            .worktrees
+            .retain(|worktree_id, _| worktree_ids.contains(worktree_id));
+
+        // Register new worktrees
+        worktrees.retain(|worktree| {
+            let worktree_id = worktree.read(cx).id();
+            !project_state.worktrees.contains_key(&worktree_id)
+        });
+        for worktree in worktrees {
+            self.register_worktree(project.clone(), worktree, cx);
+        }
+    }
+
+    pub fn pending_file_count(&self, project: &Model<Project>) -> Option<watch::Receiver<usize>> {
+        Some(
+            self.projects
+                .get(&project.downgrade())?
+                .pending_file_count_rx
+                .clone(),
+        )
+    }
+
+    pub fn search_project(
+        &mut self,
+        project: Model<Project>,
+        query: String,
+        limit: usize,
+        includes: Vec<PathMatcher>,
+        excludes: Vec<PathMatcher>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<SearchResult>>> {
+        if query.is_empty() {
+            return Task::ready(Ok(Vec::new()));
+        }
+
+        let index = self.index_project(project.clone(), cx);
+        let embedding_provider = self.embedding_provider.clone();
+
+        cx.spawn(|this, mut cx| async move {
+            index.await?;
+            let t0 = Instant::now();
+
+            let query = embedding_provider
+                .embed_batch(vec![query])
+                .await?
+                .pop()
+                .context("could not embed query")?;
+            log::trace!("Embedding Search Query: {:?}ms", t0.elapsed().as_millis());
+
+            let search_start = Instant::now();
+            let modified_buffer_results = this.update(&mut cx, |this, cx| {
+                this.search_modified_buffers(
+                    &project,
+                    query.clone(),
+                    limit,
+                    &includes,
+                    &excludes,
+                    cx,
+                )
+            })?;
+            let file_results = this.update(&mut cx, |this, cx| {
+                this.search_files(project, query, limit, includes, excludes, cx)
+            })?;
+            let (modified_buffer_results, file_results) =
+                futures::join!(modified_buffer_results, file_results);
+
+            // Weave together the results from modified buffers and files.
+            let mut results = Vec::new();
+            let mut modified_buffers = HashSet::default();
+            for result in modified_buffer_results.log_err().unwrap_or_default() {
+                modified_buffers.insert(result.buffer.clone());
+                results.push(result);
+            }
+            for result in file_results.log_err().unwrap_or_default() {
+                if !modified_buffers.contains(&result.buffer) {
+                    results.push(result);
+                }
+            }
+            results.sort_by_key(|result| Reverse(result.similarity));
+            results.truncate(limit);
+            log::trace!("Semantic search took {:?}", search_start.elapsed());
+            Ok(results)
+        })
+    }
+
+    pub fn search_files(
+        &mut self,
+        project: Model<Project>,
+        query: Embedding,
+        limit: usize,
+        includes: Vec<PathMatcher>,
+        excludes: Vec<PathMatcher>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<SearchResult>>> {
+        let db_path = self.db.path().clone();
+        let fs = self.fs.clone();
+        cx.spawn(|this, mut cx| async move {
+            let database = VectorDatabase::new(
+                fs.clone(),
+                db_path.clone(),
+                cx.background_executor().clone(),
+            )
+            .await?;
+
+            let worktree_db_ids = this.read_with(&cx, |this, _| {
+                let project_state = this
+                    .projects
+                    .get(&project.downgrade())
+                    .context("project was not indexed")?;
+                let worktree_db_ids = project_state
+                    .worktrees
+                    .values()
+                    .filter_map(|worktree| {
+                        if let WorktreeState::Registered(worktree) = worktree {
+                            Some(worktree.db_id)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect::<Vec<i64>>();
+                anyhow::Ok(worktree_db_ids)
+            })??;
+
+            let file_ids = database
+                .retrieve_included_file_ids(&worktree_db_ids, &includes, &excludes)
+                .await?;
+
+            let batch_n = cx.background_executor().num_cpus();
+            let ids_len = file_ids.clone().len();
+            let minimum_batch_size = 50;
+
+            let batch_size = {
+                let size = ids_len / batch_n;
+                if size < minimum_batch_size {
+                    minimum_batch_size
+                } else {
+                    size
+                }
+            };
+
+            let mut batch_results = Vec::new();
+            for batch in file_ids.chunks(batch_size) {
+                let batch = batch.into_iter().map(|v| *v).collect::<Vec<i64>>();
+                let limit = limit.clone();
+                let fs = fs.clone();
+                let db_path = db_path.clone();
+                let query = query.clone();
+                if let Some(db) =
+                    VectorDatabase::new(fs, db_path.clone(), cx.background_executor().clone())
+                        .await
+                        .log_err()
+                {
+                    batch_results.push(async move {
+                        db.top_k_search(&query, limit, batch.as_slice()).await
+                    });
+                }
+            }
+
+            let batch_results = futures::future::join_all(batch_results).await;
+
+            let mut results = Vec::new();
+            for batch_result in batch_results {
+                if batch_result.is_ok() {
+                    for (id, similarity) in batch_result.unwrap() {
+                        let ix = match results
+                            .binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s))
+                        {
+                            Ok(ix) => ix,
+                            Err(ix) => ix,
+                        };
+
+                        results.insert(ix, (id, similarity));
+                        results.truncate(limit);
+                    }
+                }
+            }
+
+            let ids = results.iter().map(|(id, _)| *id).collect::<Vec<i64>>();
+            let scores = results
+                .into_iter()
+                .map(|(_, score)| score)
+                .collect::<Vec<_>>();
+            let spans = database.spans_for_ids(ids.as_slice()).await?;
+
+            let mut tasks = Vec::new();
+            let mut ranges = Vec::new();
+            let weak_project = project.downgrade();
+            project.update(&mut cx, |project, cx| {
+                let this = this.upgrade().context("index was dropped")?;
+                for (worktree_db_id, file_path, byte_range) in spans {
+                    let project_state =
+                        if let Some(state) = this.read(cx).projects.get(&weak_project) {
+                            state
+                        } else {
+                            return Err(anyhow!("project not added"));
+                        };
+                    if let Some(worktree_id) = project_state.worktree_id_for_db_id(worktree_db_id) {
+                        tasks.push(project.open_buffer((worktree_id, file_path), cx));
+                        ranges.push(byte_range);
+                    }
+                }
+
+                Ok(())
+            })??;
+
+            let buffers = futures::future::join_all(tasks).await;
+            Ok(buffers
+                .into_iter()
+                .zip(ranges)
+                .zip(scores)
+                .filter_map(|((buffer, range), similarity)| {
+                    let buffer = buffer.log_err()?;
+                    let range = buffer
+                        .read_with(&cx, |buffer, _| {
+                            let start = buffer.clip_offset(range.start, Bias::Left);
+                            let end = buffer.clip_offset(range.end, Bias::Right);
+                            buffer.anchor_before(start)..buffer.anchor_after(end)
+                        })
+                        .log_err()?;
+                    Some(SearchResult {
+                        buffer,
+                        range,
+                        similarity,
+                    })
+                })
+                .collect())
+        })
+    }
+
+    fn search_modified_buffers(
+        &self,
+        project: &Model<Project>,
+        query: Embedding,
+        limit: usize,
+        includes: &[PathMatcher],
+        excludes: &[PathMatcher],
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<SearchResult>>> {
+        let modified_buffers = project
+            .read(cx)
+            .opened_buffers()
+            .into_iter()
+            .filter_map(|buffer_handle| {
+                let buffer = buffer_handle.read(cx);
+                let snapshot = buffer.snapshot();
+                let excluded = snapshot.resolve_file_path(cx, false).map_or(false, |path| {
+                    excludes.iter().any(|matcher| matcher.is_match(&path))
+                });
+
+                let included = if includes.len() == 0 {
+                    true
+                } else {
+                    snapshot.resolve_file_path(cx, false).map_or(false, |path| {
+                        includes.iter().any(|matcher| matcher.is_match(&path))
+                    })
+                };
+
+                if buffer.is_dirty() && !excluded && included {
+                    Some((buffer_handle, snapshot))
+                } else {
+                    None
+                }
+            })
+            .collect::<HashMap<_, _>>();
+
+        let embedding_provider = self.embedding_provider.clone();
+        let fs = self.fs.clone();
+        let db_path = self.db.path().clone();
+        let background = cx.background_executor().clone();
+        cx.background_executor().spawn(async move {
+            let db = VectorDatabase::new(fs, db_path.clone(), background).await?;
+            let mut results = Vec::<SearchResult>::new();
+
+            let mut retriever = CodeContextRetriever::new(embedding_provider.clone());
+            for (buffer, snapshot) in modified_buffers {
+                let language = snapshot
+                    .language_at(0)
+                    .cloned()
+                    .unwrap_or_else(|| language::PLAIN_TEXT.clone());
+                let mut spans = retriever
+                    .parse_file_with_template(None, &snapshot.text(), language)
+                    .log_err()
+                    .unwrap_or_default();
+                if Self::embed_spans(&mut spans, embedding_provider.as_ref(), &db)
+                    .await
+                    .log_err()
+                    .is_some()
+                {
+                    for span in spans {
+                        let similarity = span.embedding.unwrap().similarity(&query);
+                        let ix = match results
+                            .binary_search_by_key(&Reverse(similarity), |result| {
+                                Reverse(result.similarity)
+                            }) {
+                            Ok(ix) => ix,
+                            Err(ix) => ix,
+                        };
+
+                        let range = {
+                            let start = snapshot.clip_offset(span.range.start, Bias::Left);
+                            let end = snapshot.clip_offset(span.range.end, Bias::Right);
+                            snapshot.anchor_before(start)..snapshot.anchor_after(end)
+                        };
+
+                        results.insert(
+                            ix,
+                            SearchResult {
+                                buffer: buffer.clone(),
+                                range,
+                                similarity,
+                            },
+                        );
+                        results.truncate(limit);
+                    }
+                }
+            }
+
+            Ok(results)
+        })
+    }
+
+    pub fn index_project(
+        &mut self,
+        project: Model<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        if !self.is_authenticated() {
+            if !self.authenticate(cx) {
+                return Task::ready(Err(anyhow!("user is not authenticated")));
+            }
+        }
+
+        if !self.projects.contains_key(&project.downgrade()) {
+            let subscription = cx.subscribe(&project, |this, project, event, cx| match event {
+                project::Event::WorktreeAdded | project::Event::WorktreeRemoved(_) => {
+                    this.project_worktrees_changed(project.clone(), cx);
+                }
+                project::Event::WorktreeUpdatedEntries(worktree_id, changes) => {
+                    this.project_entries_changed(project, *worktree_id, changes.clone(), cx);
+                }
+                _ => {}
+            });
+            let project_state = ProjectState::new(subscription, cx);
+            self.projects.insert(project.downgrade(), project_state);
+            self.project_worktrees_changed(project.clone(), cx);
+        }
+        let project_state = self.projects.get_mut(&project.downgrade()).unwrap();
+        project_state.pending_index += 1;
+        cx.notify();
+
+        let mut pending_file_count_rx = project_state.pending_file_count_rx.clone();
+        let db = self.db.clone();
+        let language_registry = self.language_registry.clone();
+        let parsing_files_tx = self.parsing_files_tx.clone();
+        let worktree_registration = self.wait_for_worktree_registration(&project, cx);
+
+        cx.spawn(|this, mut cx| async move {
+            worktree_registration.await?;
+
+            let mut pending_files = Vec::new();
+            let mut files_to_delete = Vec::new();
+            this.update(&mut cx, |this, cx| {
+                let project_state = this
+                    .projects
+                    .get_mut(&project.downgrade())
+                    .context("project was dropped")?;
+                let pending_file_count_tx = &project_state.pending_file_count_tx;
+
+                project_state
+                    .worktrees
+                    .retain(|worktree_id, worktree_state| {
+                        let worktree = if let Some(worktree) =
+                            project.read(cx).worktree_for_id(*worktree_id, cx)
+                        {
+                            worktree
+                        } else {
+                            return false;
+                        };
+                        let worktree_state =
+                            if let WorktreeState::Registered(worktree_state) = worktree_state {
+                                worktree_state
+                            } else {
+                                return true;
+                            };
+
+                        worktree_state.changed_paths.retain(|path, info| {
+                            if info.is_deleted {
+                                files_to_delete.push((worktree_state.db_id, path.clone()));
+                            } else {
+                                let absolute_path = worktree.read(cx).absolutize(path);
+                                let job_handle = JobHandle::new(pending_file_count_tx);
+                                pending_files.push(PendingFile {
+                                    absolute_path,
+                                    relative_path: path.clone(),
+                                    language: None,
+                                    job_handle,
+                                    modified_time: info.mtime,
+                                    worktree_db_id: worktree_state.db_id,
+                                });
+                            }
+
+                            false
+                        });
+                        true
+                    });
+
+                anyhow::Ok(())
+            })??;
+
+            cx.background_executor()
+                .spawn(async move {
+                    for (worktree_db_id, path) in files_to_delete {
+                        db.delete_file(worktree_db_id, path).await.log_err();
+                    }
+
+                    let embeddings_for_digest = {
+                        let mut files = HashMap::default();
+                        for pending_file in &pending_files {
+                            files
+                                .entry(pending_file.worktree_db_id)
+                                .or_insert(Vec::new())
+                                .push(pending_file.relative_path.clone());
+                        }
+                        Arc::new(
+                            db.embeddings_for_files(files)
+                                .await
+                                .log_err()
+                                .unwrap_or_default(),
+                        )
+                    };
+
+                    for mut pending_file in pending_files {
+                        if let Ok(language) = language_registry
+                            .language_for_file(&pending_file.relative_path, None)
+                            .await
+                        {
+                            if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref())
+                                && &language.name().as_ref() != &"Markdown"
+                                && language
+                                    .grammar()
+                                    .and_then(|grammar| grammar.embedding_config.as_ref())
+                                    .is_none()
+                            {
+                                continue;
+                            }
+                            pending_file.language = Some(language);
+                        }
+                        parsing_files_tx
+                            .try_send((embeddings_for_digest.clone(), pending_file))
+                            .ok();
+                    }
+
+                    // Wait until we're done indexing.
+                    while let Some(count) = pending_file_count_rx.next().await {
+                        if count == 0 {
+                            break;
+                        }
+                    }
+                })
+                .await;
+
+            this.update(&mut cx, |this, cx| {
+                let project_state = this
+                    .projects
+                    .get_mut(&project.downgrade())
+                    .context("project was dropped")?;
+                project_state.pending_index -= 1;
+                cx.notify();
+                anyhow::Ok(())
+            })??;
+
+            Ok(())
+        })
+    }
+
+    fn wait_for_worktree_registration(
+        &self,
+        project: &Model<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let project = project.downgrade();
+        cx.spawn(|this, cx| async move {
+            loop {
+                let mut pending_worktrees = Vec::new();
+                this.upgrade()
+                    .context("semantic index dropped")?
+                    .read_with(&cx, |this, _| {
+                        if let Some(project) = this.projects.get(&project) {
+                            for worktree in project.worktrees.values() {
+                                if let WorktreeState::Registering(worktree) = worktree {
+                                    pending_worktrees.push(worktree.done());
+                                }
+                            }
+                        }
+                    })?;
+
+                if pending_worktrees.is_empty() {
+                    break;
+                } else {
+                    future::join_all(pending_worktrees).await;
+                }
+            }
+            Ok(())
+        })
+    }
+
+    async fn embed_spans(
+        spans: &mut [Span],
+        embedding_provider: &dyn EmbeddingProvider,
+        db: &VectorDatabase,
+    ) -> Result<()> {
+        let mut batch = Vec::new();
+        let mut batch_tokens = 0;
+        let mut embeddings = Vec::new();
+
+        let digests = spans
+            .iter()
+            .map(|span| span.digest.clone())
+            .collect::<Vec<_>>();
+        let embeddings_for_digests = db
+            .embeddings_for_digests(digests)
+            .await
+            .log_err()
+            .unwrap_or_default();
+
+        for span in &*spans {
+            if embeddings_for_digests.contains_key(&span.digest) {
+                continue;
+            };
+
+            if batch_tokens + span.token_count > embedding_provider.max_tokens_per_batch() {
+                let batch_embeddings = embedding_provider
+                    .embed_batch(mem::take(&mut batch))
+                    .await?;
+                embeddings.extend(batch_embeddings);
+                batch_tokens = 0;
+            }
+
+            batch_tokens += span.token_count;
+            batch.push(span.content.clone());
+        }
+
+        if !batch.is_empty() {
+            let batch_embeddings = embedding_provider
+                .embed_batch(mem::take(&mut batch))
+                .await?;
+
+            embeddings.extend(batch_embeddings);
+        }
+
+        let mut embeddings = embeddings.into_iter();
+        for span in spans {
+            let embedding = if let Some(embedding) = embeddings_for_digests.get(&span.digest) {
+                Some(embedding.clone())
+            } else {
+                embeddings.next()
+            };
+            let embedding = embedding.context("failed to embed spans")?;
+            span.embedding = Some(embedding);
+        }
+        Ok(())
+    }
+}
+
+impl Drop for JobHandle {
+    fn drop(&mut self) {
+        if let Some(inner) = Arc::get_mut(&mut self.tx) {
+            // This is the last instance of the JobHandle (regardless of it's origin - whether it was cloned or not)
+            if let Some(tx) = inner.upgrade() {
+                let mut tx = tx.lock();
+                *tx.borrow_mut() -= 1;
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+    #[test]
+    fn test_job_handle() {
+        let (job_count_tx, job_count_rx) = watch::channel_with(0);
+        let tx = Arc::new(Mutex::new(job_count_tx));
+        let job_handle = JobHandle::new(&tx);
+
+        assert_eq!(1, *job_count_rx.borrow());
+        let new_job_handle = job_handle.clone();
+        assert_eq!(1, *job_count_rx.borrow());
+        drop(job_handle);
+        assert_eq!(1, *job_count_rx.borrow());
+        drop(new_job_handle);
+        assert_eq!(0, *job_count_rx.borrow());
+    }
+}

crates/semantic_index2/src/semantic_index_settings.rs 🔗

@@ -0,0 +1,28 @@
+use anyhow;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Settings;
+
+#[derive(Deserialize, Debug)]
+pub struct SemanticIndexSettings {
+    pub enabled: bool,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct SemanticIndexSettingsContent {
+    pub enabled: Option<bool>,
+}
+
+impl Settings for SemanticIndexSettings {
+    const KEY: Option<&'static str> = Some("semantic_index");
+
+    type FileContent = SemanticIndexSettingsContent;
+
+    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/semantic_index2/src/semantic_index_tests.rs 🔗

@@ -0,0 +1,1697 @@
+use crate::{
+    embedding_queue::EmbeddingQueue,
+    parsing::{subtract_ranges, CodeContextRetriever, Span, SpanDigest},
+    semantic_index_settings::SemanticIndexSettings,
+    FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT,
+};
+use ai::test::FakeEmbeddingProvider;
+
+use gpui::{Task, TestAppContext};
+use language::{Language, LanguageConfig, LanguageRegistry, ToOffset};
+use parking_lot::Mutex;
+use pretty_assertions::assert_eq;
+use project::{project_settings::ProjectSettings, FakeFs, Fs, Project};
+use rand::{rngs::StdRng, Rng};
+use serde_json::json;
+use settings::{Settings, SettingsStore};
+use std::{path::Path, sync::Arc, time::SystemTime};
+use unindent::Unindent;
+use util::{paths::PathMatcher, RandomCharIter};
+
+#[ctor::ctor]
+fn init_logger() {
+    if std::env::var("RUST_LOG").is_ok() {
+        env_logger::init();
+    }
+}
+
+#[gpui::test]
+async fn test_semantic_index(cx: &mut TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.background_executor.clone());
+    fs.insert_tree(
+        "/the-root",
+        json!({
+            "src": {
+                "file1.rs": "
+                    fn aaa() {
+                        println!(\"aaaaaaaaaaaa!\");
+                    }
+
+                    fn zzzzz() {
+                        println!(\"SLEEPING\");
+                    }
+                ".unindent(),
+                "file2.rs": "
+                    fn bbb() {
+                        println!(\"bbbbbbbbbbbbb!\");
+                    }
+                    struct pqpqpqp {}
+                ".unindent(),
+                "file3.toml": "
+                    ZZZZZZZZZZZZZZZZZZ = 5
+                ".unindent(),
+            }
+        }),
+    )
+    .await;
+
+    let languages = Arc::new(LanguageRegistry::new(Task::ready(())));
+    let rust_language = rust_lang();
+    let toml_language = toml_lang();
+    languages.add(rust_language);
+    languages.add(toml_language);
+
+    let db_dir = tempdir::TempDir::new("vector-store").unwrap();
+    let db_path = db_dir.path().join("db.sqlite");
+
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+    let semantic_index = SemanticIndex::new(
+        fs.clone(),
+        db_path,
+        embedding_provider.clone(),
+        languages,
+        cx.to_async(),
+    )
+    .await
+    .unwrap();
+
+    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+
+    let search_results = semantic_index.update(cx, |store, cx| {
+        store.search_project(
+            project.clone(),
+            "aaaaaabbbbzz".to_string(),
+            5,
+            vec![],
+            vec![],
+            cx,
+        )
+    });
+    let pending_file_count =
+        semantic_index.read_with(cx, |index, _| index.pending_file_count(&project).unwrap());
+    cx.background_executor.run_until_parked();
+    assert_eq!(*pending_file_count.borrow(), 3);
+    cx.background_executor
+        .advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT);
+    assert_eq!(*pending_file_count.borrow(), 0);
+
+    let search_results = search_results.await.unwrap();
+    assert_search_results(
+        &search_results,
+        &[
+            (Path::new("src/file1.rs").into(), 0),
+            (Path::new("src/file2.rs").into(), 0),
+            (Path::new("src/file3.toml").into(), 0),
+            (Path::new("src/file1.rs").into(), 45),
+            (Path::new("src/file2.rs").into(), 45),
+        ],
+        cx,
+    );
+
+    // Test Include Files Functonality
+    let include_files = vec![PathMatcher::new("*.rs").unwrap()];
+    let exclude_files = vec![PathMatcher::new("*.rs").unwrap()];
+    let rust_only_search_results = semantic_index
+        .update(cx, |store, cx| {
+            store.search_project(
+                project.clone(),
+                "aaaaaabbbbzz".to_string(),
+                5,
+                include_files,
+                vec![],
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+    assert_search_results(
+        &rust_only_search_results,
+        &[
+            (Path::new("src/file1.rs").into(), 0),
+            (Path::new("src/file2.rs").into(), 0),
+            (Path::new("src/file1.rs").into(), 45),
+            (Path::new("src/file2.rs").into(), 45),
+        ],
+        cx,
+    );
+
+    let no_rust_search_results = semantic_index
+        .update(cx, |store, cx| {
+            store.search_project(
+                project.clone(),
+                "aaaaaabbbbzz".to_string(),
+                5,
+                vec![],
+                exclude_files,
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+    assert_search_results(
+        &no_rust_search_results,
+        &[(Path::new("src/file3.toml").into(), 0)],
+        cx,
+    );
+
+    fs.save(
+        "/the-root/src/file2.rs".as_ref(),
+        &"
+            fn dddd() { println!(\"ddddd!\"); }
+            struct pqpqpqp {}
+        "
+        .unindent()
+        .into(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+
+    cx.background_executor
+        .advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT);
+
+    let prev_embedding_count = embedding_provider.embedding_count();
+    let index = semantic_index.update(cx, |store, cx| store.index_project(project.clone(), cx));
+    cx.background_executor.run_until_parked();
+    assert_eq!(*pending_file_count.borrow(), 1);
+    cx.background_executor
+        .advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT);
+    assert_eq!(*pending_file_count.borrow(), 0);
+    index.await.unwrap();
+
+    assert_eq!(
+        embedding_provider.embedding_count() - prev_embedding_count,
+        1
+    );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_embedding_batching(cx: &mut TestAppContext, mut rng: StdRng) {
+    let (outstanding_job_count, _) = postage::watch::channel_with(0);
+    let outstanding_job_count = Arc::new(Mutex::new(outstanding_job_count));
+
+    let files = (1..=3)
+        .map(|file_ix| FileToEmbed {
+            worktree_id: 5,
+            path: Path::new(&format!("path-{file_ix}")).into(),
+            mtime: SystemTime::now(),
+            spans: (0..rng.gen_range(4..22))
+                .map(|document_ix| {
+                    let content_len = rng.gen_range(10..100);
+                    let content = RandomCharIter::new(&mut rng)
+                        .with_simple_text()
+                        .take(content_len)
+                        .collect::<String>();
+                    let digest = SpanDigest::from(content.as_str());
+                    Span {
+                        range: 0..10,
+                        embedding: None,
+                        name: format!("document {document_ix}"),
+                        content,
+                        digest,
+                        token_count: rng.gen_range(10..30),
+                    }
+                })
+                .collect(),
+            job_handle: JobHandle::new(&outstanding_job_count),
+        })
+        .collect::<Vec<_>>();
+
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+
+    let mut queue = EmbeddingQueue::new(embedding_provider.clone(), cx.background_executor.clone());
+    for file in &files {
+        queue.push(file.clone());
+    }
+    queue.flush();
+
+    cx.background_executor.run_until_parked();
+    let finished_files = queue.finished_files();
+    let mut embedded_files: Vec<_> = files
+        .iter()
+        .map(|_| finished_files.try_recv().expect("no finished file"))
+        .collect();
+
+    let expected_files: Vec<_> = files
+        .iter()
+        .map(|file| {
+            let mut file = file.clone();
+            for doc in &mut file.spans {
+                doc.embedding = Some(embedding_provider.embed_sync(doc.content.as_ref()));
+            }
+            file
+        })
+        .collect();
+
+    embedded_files.sort_by_key(|f| f.path.clone());
+
+    assert_eq!(embedded_files, expected_files);
+}
+
+#[track_caller]
+fn assert_search_results(
+    actual: &[SearchResult],
+    expected: &[(Arc<Path>, usize)],
+    cx: &TestAppContext,
+) {
+    let actual = actual
+        .iter()
+        .map(|search_result| {
+            search_result.buffer.read_with(cx, |buffer, _cx| {
+                (
+                    buffer.file().unwrap().path().clone(),
+                    search_result.range.start.to_offset(buffer),
+                )
+            })
+        })
+        .collect::<Vec<_>>();
+    assert_eq!(actual, expected);
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_rust() {
+    let language = rust_lang();
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+    let text = "
+        /// A doc comment
+        /// that spans multiple lines
+        #[gpui::test]
+        fn a() {
+            b
+        }
+
+        impl C for D {
+        }
+
+        impl E {
+            // This is also a preceding comment
+            pub fn function_1() -> Option<()> {
+                unimplemented!();
+            }
+
+            // This is a preceding comment
+            fn function_2() -> Result<()> {
+                unimplemented!();
+            }
+        }
+
+        #[derive(Clone)]
+        struct D {
+            name: String
+        }
+    "
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (
+                "
+                /// A doc comment
+                /// that spans multiple lines
+                #[gpui::test]
+                fn a() {
+                    b
+                }"
+                .unindent(),
+                text.find("fn a").unwrap(),
+            ),
+            (
+                "
+                impl C for D {
+                }"
+                .unindent(),
+                text.find("impl C").unwrap(),
+            ),
+            (
+                "
+                impl E {
+                    // This is also a preceding comment
+                    pub fn function_1() -> Option<()> { /* ... */ }
+
+                    // This is a preceding comment
+                    fn function_2() -> Result<()> { /* ... */ }
+                }"
+                .unindent(),
+                text.find("impl E").unwrap(),
+            ),
+            (
+                "
+                // This is also a preceding comment
+                pub fn function_1() -> Option<()> {
+                    unimplemented!();
+                }"
+                .unindent(),
+                text.find("pub fn function_1").unwrap(),
+            ),
+            (
+                "
+                // This is a preceding comment
+                fn function_2() -> Result<()> {
+                    unimplemented!();
+                }"
+                .unindent(),
+                text.find("fn function_2").unwrap(),
+            ),
+            (
+                "
+                #[derive(Clone)]
+                struct D {
+                    name: String
+                }"
+                .unindent(),
+                text.find("struct D").unwrap(),
+            ),
+        ],
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_json() {
+    let language = json_lang();
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+    let text = r#"
+        {
+            "array": [1, 2, 3, 4],
+            "string": "abcdefg",
+            "nested_object": {
+                "array_2": [5, 6, 7, 8],
+                "string_2": "hijklmnop",
+                "boolean": true,
+                "none": null
+            }
+        }
+    "#
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[(
+            r#"
+                {
+                    "array": [],
+                    "string": "",
+                    "nested_object": {
+                        "array_2": [],
+                        "string_2": "",
+                        "boolean": true,
+                        "none": null
+                    }
+                }"#
+            .unindent(),
+            text.find("{").unwrap(),
+        )],
+    );
+
+    let text = r#"
+        [
+            {
+                "name": "somebody",
+                "age": 42
+            },
+            {
+                "name": "somebody else",
+                "age": 43
+            }
+        ]
+    "#
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[(
+            r#"
+            [{
+                    "name": "",
+                    "age": 42
+                }]"#
+            .unindent(),
+            text.find("[").unwrap(),
+        )],
+    );
+}
+
+fn assert_documents_eq(
+    documents: &[Span],
+    expected_contents_and_start_offsets: &[(String, usize)],
+) {
+    assert_eq!(
+        documents
+            .iter()
+            .map(|document| (document.content.clone(), document.range.start))
+            .collect::<Vec<_>>(),
+        expected_contents_and_start_offsets
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_javascript() {
+    let language = js_lang();
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+    let text = "
+        /* globals importScripts, backend */
+        function _authorize() {}
+
+        /**
+         * Sometimes the frontend build is way faster than backend.
+         */
+        export async function authorizeBank() {
+            _authorize(pushModal, upgradingAccountId, {});
+        }
+
+        export class SettingsPage {
+            /* This is a test setting */
+            constructor(page) {
+                this.page = page;
+            }
+        }
+
+        /* This is a test comment */
+        class TestClass {}
+
+        /* Schema for editor_events in Clickhouse. */
+        export interface ClickhouseEditorEvent {
+            installation_id: string
+            operation: string
+        }
+        "
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (
+                "
+            /* globals importScripts, backend */
+            function _authorize() {}"
+                    .unindent(),
+                37,
+            ),
+            (
+                "
+            /**
+             * Sometimes the frontend build is way faster than backend.
+             */
+            export async function authorizeBank() {
+                _authorize(pushModal, upgradingAccountId, {});
+            }"
+                .unindent(),
+                131,
+            ),
+            (
+                "
+                export class SettingsPage {
+                    /* This is a test setting */
+                    constructor(page) {
+                        this.page = page;
+                    }
+                }"
+                .unindent(),
+                225,
+            ),
+            (
+                "
+                /* This is a test setting */
+                constructor(page) {
+                    this.page = page;
+                }"
+                .unindent(),
+                290,
+            ),
+            (
+                "
+                /* This is a test comment */
+                class TestClass {}"
+                    .unindent(),
+                374,
+            ),
+            (
+                "
+                /* Schema for editor_events in Clickhouse. */
+                export interface ClickhouseEditorEvent {
+                    installation_id: string
+                    operation: string
+                }"
+                .unindent(),
+                440,
+            ),
+        ],
+    )
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_lua() {
+    let language = lua_lang();
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+    let text = r#"
+        -- Creates a new class
+        -- @param baseclass The Baseclass of this class, or nil.
+        -- @return A new class reference.
+        function classes.class(baseclass)
+            -- Create the class definition and metatable.
+            local classdef = {}
+            -- Find the super class, either Object or user-defined.
+            baseclass = baseclass or classes.Object
+            -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable.
+            setmetatable(classdef, { __index = baseclass })
+            -- All class instances have a reference to the class object.
+            classdef.class = classdef
+            --- Recursivly allocates the inheritance tree of the instance.
+            -- @param mastertable The 'root' of the inheritance tree.
+            -- @return Returns the instance with the allocated inheritance tree.
+            function classdef.alloc(mastertable)
+                -- All class instances have a reference to a superclass object.
+                local instance = { super = baseclass.alloc(mastertable) }
+                -- Any functions this instance does not know of will 'look up' to the superclass definition.
+                setmetatable(instance, { __index = classdef, __newindex = mastertable })
+                return instance
+            end
+        end
+        "#.unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (r#"
+                -- Creates a new class
+                -- @param baseclass The Baseclass of this class, or nil.
+                -- @return A new class reference.
+                function classes.class(baseclass)
+                    -- Create the class definition and metatable.
+                    local classdef = {}
+                    -- Find the super class, either Object or user-defined.
+                    baseclass = baseclass or classes.Object
+                    -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable.
+                    setmetatable(classdef, { __index = baseclass })
+                    -- All class instances have a reference to the class object.
+                    classdef.class = classdef
+                    --- Recursivly allocates the inheritance tree of the instance.
+                    -- @param mastertable The 'root' of the inheritance tree.
+                    -- @return Returns the instance with the allocated inheritance tree.
+                    function classdef.alloc(mastertable)
+                        --[ ... ]--
+                        --[ ... ]--
+                    end
+                end"#.unindent(),
+            114),
+            (r#"
+            --- Recursivly allocates the inheritance tree of the instance.
+            -- @param mastertable The 'root' of the inheritance tree.
+            -- @return Returns the instance with the allocated inheritance tree.
+            function classdef.alloc(mastertable)
+                -- All class instances have a reference to a superclass object.
+                local instance = { super = baseclass.alloc(mastertable) }
+                -- Any functions this instance does not know of will 'look up' to the superclass definition.
+                setmetatable(instance, { __index = classdef, __newindex = mastertable })
+                return instance
+            end"#.unindent(), 809),
+        ]
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_elixir() {
+    let language = elixir_lang();
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+    let text = r#"
+        defmodule File.Stream do
+            @moduledoc """
+            Defines a `File.Stream` struct returned by `File.stream!/3`.
+
+            The following fields are public:
+
+            * `path`          - the file path
+            * `modes`         - the file modes
+            * `raw`           - a boolean indicating if bin functions should be used
+            * `line_or_bytes` - if reading should read lines or a given number of bytes
+            * `node`          - the node the file belongs to
+
+            """
+
+            defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
+
+            @type t :: %__MODULE__{}
+
+            @doc false
+            def __build__(path, modes, line_or_bytes) do
+            raw = :lists.keyfind(:encoding, 1, modes) == false
+
+            modes =
+                case raw do
+                true ->
+                    case :lists.keyfind(:read_ahead, 1, modes) do
+                    {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)]
+                    {:read_ahead, _} -> [:raw | modes]
+                    false -> [:raw, :read_ahead | modes]
+                    end
+
+                false ->
+                    modes
+                end
+
+            %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()}
+
+            end"#
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[(
+            r#"
+        defmodule File.Stream do
+            @moduledoc """
+            Defines a `File.Stream` struct returned by `File.stream!/3`.
+
+            The following fields are public:
+
+            * `path`          - the file path
+            * `modes`         - the file modes
+            * `raw`           - a boolean indicating if bin functions should be used
+            * `line_or_bytes` - if reading should read lines or a given number of bytes
+            * `node`          - the node the file belongs to
+
+            """
+
+            defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
+
+            @type t :: %__MODULE__{}
+
+            @doc false
+            def __build__(path, modes, line_or_bytes) do
+            raw = :lists.keyfind(:encoding, 1, modes) == false
+
+            modes =
+                case raw do
+                true ->
+                    case :lists.keyfind(:read_ahead, 1, modes) do
+                    {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)]
+                    {:read_ahead, _} -> [:raw | modes]
+                    false -> [:raw, :read_ahead | modes]
+                    end
+
+                false ->
+                    modes
+                end
+
+            %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()}
+
+            end"#
+                .unindent(),
+            0,
+        ),(r#"
+            @doc false
+            def __build__(path, modes, line_or_bytes) do
+            raw = :lists.keyfind(:encoding, 1, modes) == false
+
+            modes =
+                case raw do
+                true ->
+                    case :lists.keyfind(:read_ahead, 1, modes) do
+                    {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)]
+                    {:read_ahead, _} -> [:raw | modes]
+                    false -> [:raw, :read_ahead | modes]
+                    end
+
+                false ->
+                    modes
+                end
+
+            %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()}
+
+            end"#.unindent(), 574)],
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_cpp() {
+    let language = cpp_lang();
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+    let text = "
+    /**
+     * @brief Main function
+     * @returns 0 on exit
+     */
+    int main() { return 0; }
+
+    /**
+    * This is a test comment
+    */
+    class MyClass {       // The class
+        public:           // Access specifier
+        int myNum;        // Attribute (int variable)
+        string myString;  // Attribute (string variable)
+    };
+
+    // This is a test comment
+    enum Color { red, green, blue };
+
+    /** This is a preceding block comment
+     * This is the second line
+     */
+    struct {           // Structure declaration
+        int myNum;       // Member (int variable)
+        string myString; // Member (string variable)
+    } myStructure;
+
+    /**
+     * @brief Matrix class.
+     */
+    template <typename T,
+              typename = typename std::enable_if<
+                std::is_integral<T>::value || std::is_floating_point<T>::value,
+                bool>::type>
+    class Matrix2 {
+        std::vector<std::vector<T>> _mat;
+
+        public:
+            /**
+            * @brief Constructor
+            * @tparam Integer ensuring integers are being evaluated and not other
+            * data types.
+            * @param size denoting the size of Matrix as size x size
+            */
+            template <typename Integer,
+                    typename = typename std::enable_if<std::is_integral<Integer>::value,
+                    Integer>::type>
+            explicit Matrix(const Integer size) {
+                for (size_t i = 0; i < size; ++i) {
+                    _mat.emplace_back(std::vector<T>(size, 0));
+                }
+            }
+    }"
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (
+                "
+        /**
+         * @brief Main function
+         * @returns 0 on exit
+         */
+        int main() { return 0; }"
+                    .unindent(),
+                54,
+            ),
+            (
+                "
+                /**
+                * This is a test comment
+                */
+                class MyClass {       // The class
+                    public:           // Access specifier
+                    int myNum;        // Attribute (int variable)
+                    string myString;  // Attribute (string variable)
+                }"
+                .unindent(),
+                112,
+            ),
+            (
+                "
+                // This is a test comment
+                enum Color { red, green, blue }"
+                    .unindent(),
+                322,
+            ),
+            (
+                "
+                /** This is a preceding block comment
+                 * This is the second line
+                 */
+                struct {           // Structure declaration
+                    int myNum;       // Member (int variable)
+                    string myString; // Member (string variable)
+                } myStructure;"
+                    .unindent(),
+                425,
+            ),
+            (
+                "
+                /**
+                 * @brief Matrix class.
+                 */
+                template <typename T,
+                          typename = typename std::enable_if<
+                            std::is_integral<T>::value || std::is_floating_point<T>::value,
+                            bool>::type>
+                class Matrix2 {
+                    std::vector<std::vector<T>> _mat;
+
+                    public:
+                        /**
+                        * @brief Constructor
+                        * @tparam Integer ensuring integers are being evaluated and not other
+                        * data types.
+                        * @param size denoting the size of Matrix as size x size
+                        */
+                        template <typename Integer,
+                                typename = typename std::enable_if<std::is_integral<Integer>::value,
+                                Integer>::type>
+                        explicit Matrix(const Integer size) {
+                            for (size_t i = 0; i < size; ++i) {
+                                _mat.emplace_back(std::vector<T>(size, 0));
+                            }
+                        }
+                }"
+                .unindent(),
+                612,
+            ),
+            (
+                "
+                explicit Matrix(const Integer size) {
+                    for (size_t i = 0; i < size; ++i) {
+                        _mat.emplace_back(std::vector<T>(size, 0));
+                    }
+                }"
+                .unindent(),
+                1226,
+            ),
+        ],
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_ruby() {
+    let language = ruby_lang();
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+    let text = r#"
+        # This concern is inspired by "sudo mode" on GitHub. It
+        # is a way to re-authenticate a user before allowing them
+        # to see or perform an action.
+        #
+        # Add `before_action :require_challenge!` to actions you
+        # want to protect.
+        #
+        # The user will be shown a page to enter the challenge (which
+        # is either the password, or just the username when no
+        # password exists). Upon passing, there is a grace period
+        # during which no challenge will be asked from the user.
+        #
+        # Accessing challenge-protected resources during the grace
+        # period will refresh the grace period.
+        module ChallengableConcern
+            extend ActiveSupport::Concern
+
+            CHALLENGE_TIMEOUT = 1.hour.freeze
+
+            def require_challenge!
+                return if skip_challenge?
+
+                if challenge_passed_recently?
+                    session[:challenge_passed_at] = Time.now.utc
+                    return
+                end
+
+                @challenge = Form::Challenge.new(return_to: request.url)
+
+                if params.key?(:form_challenge)
+                    if challenge_passed?
+                        session[:challenge_passed_at] = Time.now.utc
+                    else
+                        flash.now[:alert] = I18n.t('challenge.invalid_password')
+                        render_challenge
+                    end
+                else
+                    render_challenge
+                end
+            end
+
+            def challenge_passed?
+                current_user.valid_password?(challenge_params[:current_password])
+            end
+        end
+
+        class Animal
+            include Comparable
+
+            attr_reader :legs
+
+            def initialize(name, legs)
+                @name, @legs = name, legs
+            end
+
+            def <=>(other)
+                legs <=> other.legs
+            end
+        end
+
+        # Singleton method for car object
+        def car.wheels
+            puts "There are four wheels"
+        end"#
+        .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (
+                r#"
+        # This concern is inspired by "sudo mode" on GitHub. It
+        # is a way to re-authenticate a user before allowing them
+        # to see or perform an action.
+        #
+        # Add `before_action :require_challenge!` to actions you
+        # want to protect.
+        #
+        # The user will be shown a page to enter the challenge (which
+        # is either the password, or just the username when no
+        # password exists). Upon passing, there is a grace period
+        # during which no challenge will be asked from the user.
+        #
+        # Accessing challenge-protected resources during the grace
+        # period will refresh the grace period.
+        module ChallengableConcern
+            extend ActiveSupport::Concern
+
+            CHALLENGE_TIMEOUT = 1.hour.freeze
+
+            def require_challenge!
+                # ...
+            end
+
+            def challenge_passed?
+                # ...
+            end
+        end"#
+                    .unindent(),
+                558,
+            ),
+            (
+                r#"
+            def require_challenge!
+                return if skip_challenge?
+
+                if challenge_passed_recently?
+                    session[:challenge_passed_at] = Time.now.utc
+                    return
+                end
+
+                @challenge = Form::Challenge.new(return_to: request.url)
+
+                if params.key?(:form_challenge)
+                    if challenge_passed?
+                        session[:challenge_passed_at] = Time.now.utc
+                    else
+                        flash.now[:alert] = I18n.t('challenge.invalid_password')
+                        render_challenge
+                    end
+                else
+                    render_challenge
+                end
+            end"#
+                    .unindent(),
+                663,
+            ),
+            (
+                r#"
+                def challenge_passed?
+                    current_user.valid_password?(challenge_params[:current_password])
+                end"#
+                    .unindent(),
+                1254,
+            ),
+            (
+                r#"
+                class Animal
+                    include Comparable
+
+                    attr_reader :legs
+
+                    def initialize(name, legs)
+                        # ...
+                    end
+
+                    def <=>(other)
+                        # ...
+                    end
+                end"#
+                    .unindent(),
+                1363,
+            ),
+            (
+                r#"
+                def initialize(name, legs)
+                    @name, @legs = name, legs
+                end"#
+                    .unindent(),
+                1427,
+            ),
+            (
+                r#"
+                def <=>(other)
+                    legs <=> other.legs
+                end"#
+                    .unindent(),
+                1501,
+            ),
+            (
+                r#"
+                # Singleton method for car object
+                def car.wheels
+                    puts "There are four wheels"
+                end"#
+                    .unindent(),
+                1591,
+            ),
+        ],
+    );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_php() {
+    let language = php_lang();
+    let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+    let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+    let text = r#"
+        <?php
+
+        namespace LevelUp\Experience\Concerns;
+
+        /*
+        This is a multiple-lines comment block
+        that spans over multiple
+        lines
+        */
+        function functionName() {
+            echo "Hello world!";
+        }
+
+        trait HasAchievements
+        {
+            /**
+            * @throws \Exception
+            */
+            public function grantAchievement(Achievement $achievement, $progress = null): void
+            {
+                if ($progress > 100) {
+                    throw new Exception(message: 'Progress cannot be greater than 100');
+                }
+
+                if ($this->achievements()->find($achievement->id)) {
+                    throw new Exception(message: 'User already has this Achievement');
+                }
+
+                $this->achievements()->attach($achievement, [
+                    'progress' => $progress ?? null,
+                ]);
+
+                $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this)));
+            }
+
+            public function achievements(): BelongsToMany
+            {
+                return $this->belongsToMany(related: Achievement::class)
+                ->withPivot(columns: 'progress')
+                ->where('is_secret', false)
+                ->using(AchievementUser::class);
+            }
+        }
+
+        interface Multiplier
+        {
+            public function qualifies(array $data): bool;
+
+            public function setMultiplier(): int;
+        }
+
+        enum AuditType: string
+        {
+            case Add = 'add';
+            case Remove = 'remove';
+            case Reset = 'reset';
+            case LevelUp = 'level_up';
+        }
+
+        ?>"#
+    .unindent();
+
+    let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+    assert_documents_eq(
+        &documents,
+        &[
+            (
+                r#"
+        /*
+        This is a multiple-lines comment block
+        that spans over multiple
+        lines
+        */
+        function functionName() {
+            echo "Hello world!";
+        }"#
+                .unindent(),
+                123,
+            ),
+            (
+                r#"
+        trait HasAchievements
+        {
+            /**
+            * @throws \Exception
+            */
+            public function grantAchievement(Achievement $achievement, $progress = null): void
+            {/* ... */}
+
+            public function achievements(): BelongsToMany
+            {/* ... */}
+        }"#
+                .unindent(),
+                177,
+            ),
+            (r#"
+            /**
+            * @throws \Exception
+            */
+            public function grantAchievement(Achievement $achievement, $progress = null): void
+            {
+                if ($progress > 100) {
+                    throw new Exception(message: 'Progress cannot be greater than 100');
+                }
+
+                if ($this->achievements()->find($achievement->id)) {
+                    throw new Exception(message: 'User already has this Achievement');
+                }
+
+                $this->achievements()->attach($achievement, [
+                    'progress' => $progress ?? null,
+                ]);
+
+                $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this)));
+            }"#.unindent(), 245),
+            (r#"
+                public function achievements(): BelongsToMany
+                {
+                    return $this->belongsToMany(related: Achievement::class)
+                    ->withPivot(columns: 'progress')
+                    ->where('is_secret', false)
+                    ->using(AchievementUser::class);
+                }"#.unindent(), 902),
+            (r#"
+                interface Multiplier
+                {
+                    public function qualifies(array $data): bool;
+
+                    public function setMultiplier(): int;
+                }"#.unindent(),
+                1146),
+            (r#"
+                enum AuditType: string
+                {
+                    case Add = 'add';
+                    case Remove = 'remove';
+                    case Reset = 'reset';
+                    case LevelUp = 'level_up';
+                }"#.unindent(), 1265)
+        ],
+    );
+}
+
+fn js_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Javascript".into(),
+                path_suffixes: vec!["js".into()],
+                ..Default::default()
+            },
+            Some(tree_sitter_typescript::language_tsx()),
+        )
+        .with_embedding_query(
+            &r#"
+
+            (
+                (comment)* @context
+                .
+                [
+                (export_statement
+                    (function_declaration
+                        "async"? @name
+                        "function" @name
+                        name: (_) @name))
+                (function_declaration
+                    "async"? @name
+                    "function" @name
+                    name: (_) @name)
+                ] @item
+            )
+
+            (
+                (comment)* @context
+                .
+                [
+                (export_statement
+                    (class_declaration
+                        "class" @name
+                        name: (_) @name))
+                (class_declaration
+                    "class" @name
+                    name: (_) @name)
+                ] @item
+            )
+
+            (
+                (comment)* @context
+                .
+                [
+                (export_statement
+                    (interface_declaration
+                        "interface" @name
+                        name: (_) @name))
+                (interface_declaration
+                    "interface" @name
+                    name: (_) @name)
+                ] @item
+            )
+
+            (
+                (comment)* @context
+                .
+                [
+                (export_statement
+                    (enum_declaration
+                        "enum" @name
+                        name: (_) @name))
+                (enum_declaration
+                    "enum" @name
+                    name: (_) @name)
+                ] @item
+            )
+
+            (
+                (comment)* @context
+                .
+                (method_definition
+                    [
+                        "get"
+                        "set"
+                        "async"
+                        "*"
+                        "static"
+                    ]* @name
+                    name: (_) @name) @item
+            )
+
+                    "#
+            .unindent(),
+        )
+        .unwrap(),
+    )
+}
+
+fn rust_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".into()],
+                collapsed_placeholder: " /* ... */ ".to_string(),
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                [(line_comment) (attribute_item)]* @context
+                .
+                [
+                    (struct_item
+                        name: (_) @name)
+
+                    (enum_item
+                        name: (_) @name)
+
+                    (impl_item
+                        trait: (_)? @name
+                        "for"? @name
+                        type: (_) @name)
+
+                    (trait_item
+                        name: (_) @name)
+
+                    (function_item
+                        name: (_) @name
+                        body: (block
+                            "{" @keep
+                            "}" @keep) @collapse)
+
+                    (macro_definition
+                        name: (_) @name)
+                ] @item
+            )
+
+            (attribute_item) @collapse
+            (use_declaration) @collapse
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn json_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "JSON".into(),
+                path_suffixes: vec!["json".into()],
+                ..Default::default()
+            },
+            Some(tree_sitter_json::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (document) @item
+
+            (array
+                "[" @keep
+                .
+                (object)? @keep
+                "]" @keep) @collapse
+
+            (pair value: (string
+                "\"" @keep
+                "\"" @keep) @collapse)
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn toml_lang() -> Arc<Language> {
+    Arc::new(Language::new(
+        LanguageConfig {
+            name: "TOML".into(),
+            path_suffixes: vec!["toml".into()],
+            ..Default::default()
+        },
+        Some(tree_sitter_toml::language()),
+    ))
+}
+
+fn cpp_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "CPP".into(),
+                path_suffixes: vec!["cpp".into()],
+                ..Default::default()
+            },
+            Some(tree_sitter_cpp::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (comment)* @context
+                .
+                (function_definition
+                    (type_qualifier)? @name
+                    type: (_)? @name
+                    declarator: [
+                        (function_declarator
+                            declarator: (_) @name)
+                        (pointer_declarator
+                            "*" @name
+                            declarator: (function_declarator
+                            declarator: (_) @name))
+                        (pointer_declarator
+                            "*" @name
+                            declarator: (pointer_declarator
+                                "*" @name
+                            declarator: (function_declarator
+                                declarator: (_) @name)))
+                        (reference_declarator
+                            ["&" "&&"] @name
+                            (function_declarator
+                            declarator: (_) @name))
+                    ]
+                    (type_qualifier)? @name) @item
+                )
+
+            (
+                (comment)* @context
+                .
+                (template_declaration
+                    (class_specifier
+                        "class" @name
+                        name: (_) @name)
+                        ) @item
+            )
+
+            (
+                (comment)* @context
+                .
+                (class_specifier
+                    "class" @name
+                    name: (_) @name) @item
+                )
+
+            (
+                (comment)* @context
+                .
+                (enum_specifier
+                    "enum" @name
+                    name: (_) @name) @item
+                )
+
+            (
+                (comment)* @context
+                .
+                (declaration
+                    type: (struct_specifier
+                    "struct" @name)
+                    declarator: (_) @name) @item
+            )
+
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn lua_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Lua".into(),
+                path_suffixes: vec!["lua".into()],
+                collapsed_placeholder: "--[ ... ]--".to_string(),
+                ..Default::default()
+            },
+            Some(tree_sitter_lua::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (comment)* @context
+                .
+                (function_declaration
+                    "function" @name
+                    name: (_) @name
+                    (comment)* @collapse
+                    body: (block) @collapse
+                ) @item
+            )
+        "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn php_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "PHP".into(),
+                path_suffixes: vec!["php".into()],
+                collapsed_placeholder: "/* ... */".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_php::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (comment)* @context
+                .
+                [
+                    (function_definition
+                        "function" @name
+                        name: (_) @name
+                        body: (_
+                            "{" @keep
+                            "}" @keep) @collapse
+                        )
+
+                    (trait_declaration
+                        "trait" @name
+                        name: (_) @name)
+
+                    (method_declaration
+                        "function" @name
+                        name: (_) @name
+                        body: (_
+                            "{" @keep
+                            "}" @keep) @collapse
+                        )
+
+                    (interface_declaration
+                        "interface" @name
+                        name: (_) @name
+                        )
+
+                    (enum_declaration
+                        "enum" @name
+                        name: (_) @name
+                        )
+
+                ] @item
+            )
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn ruby_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Ruby".into(),
+                path_suffixes: vec!["rb".into()],
+                collapsed_placeholder: "# ...".to_string(),
+                ..Default::default()
+            },
+            Some(tree_sitter_ruby::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (comment)* @context
+                .
+                [
+                (module
+                    "module" @name
+                    name: (_) @name)
+                (method
+                    "def" @name
+                    name: (_) @name
+                    body: (body_statement) @collapse)
+                (class
+                    "class" @name
+                    name: (_) @name)
+                (singleton_method
+                    "def" @name
+                    object: (_) @name
+                    "." @name
+                    name: (_) @name
+                    body: (body_statement) @collapse)
+                ] @item
+            )
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+fn elixir_lang() -> Arc<Language> {
+    Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Elixir".into(),
+                path_suffixes: vec!["rs".into()],
+                ..Default::default()
+            },
+            Some(tree_sitter_elixir::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                (unary_operator
+                    operator: "@"
+                    operand: (call
+                        target: (identifier) @unary
+                        (#match? @unary "^(doc)$"))
+                    ) @context
+                .
+                (call
+                target: (identifier) @name
+                (arguments
+                [
+                (identifier) @name
+                (call
+                target: (identifier) @name)
+                (binary_operator
+                left: (call
+                target: (identifier) @name)
+                operator: "when")
+                ])
+                (#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
+                )
+
+            (call
+                target: (identifier) @name
+                (arguments (alias) @name)
+                (#any-match? @name "^(defmodule|defprotocol)$")) @item
+            "#,
+        )
+        .unwrap(),
+    )
+}
+
+#[gpui::test]
+fn test_subtract_ranges() {
+    // collapsed_ranges: Vec<Range<usize>>, keep_ranges: Vec<Range<usize>>
+
+    assert_eq!(
+        subtract_ranges(&[0..5, 10..21], &[0..1, 4..5]),
+        vec![1..4, 10..21]
+    );
+
+    assert_eq!(subtract_ranges(&[0..5], &[1..2]), &[0..1, 2..5]);
+}
+
+fn init_test(cx: &mut TestAppContext) {
+    cx.update(|cx| {
+        let settings_store = SettingsStore::test(cx);
+        cx.set_global(settings_store);
+        SemanticIndexSettings::register(cx);
+        ProjectSettings::register(cx);
+    });
+}

crates/storybook2/src/stories.rs 🔗

@@ -1,3 +1,5 @@
+mod auto_height_editor;
+mod cursor;
 mod focus;
 mod kitchen_sink;
 mod picker;
@@ -5,6 +7,8 @@ mod scroll;
 mod text;
 mod z_index;
 
+pub use auto_height_editor::*;
+pub use cursor::*;
 pub use focus::*;
 pub use kitchen_sink::*;
 pub use picker::*;

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

@@ -0,0 +1,34 @@
+use editor::Editor;
+use gpui::{
+    div, white, Div, KeyBinding, ParentElement, Render, Styled, View, ViewContext, VisualContext,
+    WindowContext,
+};
+
+pub struct AutoHeightEditorStory {
+    editor: View<Editor>,
+}
+
+impl AutoHeightEditorStory {
+    pub fn new(cx: &mut WindowContext) -> View<Self> {
+        cx.bind_keys([KeyBinding::new("enter", editor::Newline, Some("Editor"))]);
+        cx.build_view(|cx| Self {
+            editor: cx.build_view(|cx| {
+                let mut editor = Editor::auto_height(3, cx);
+                editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
+                editor
+            }),
+        })
+    }
+}
+
+impl Render for AutoHeightEditorStory {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        div()
+            .size_full()
+            .bg(white())
+            .text_sm()
+            .child(div().w_32().bg(gpui::black()).child(self.editor.clone()))
+    }
+}

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

@@ -0,0 +1,111 @@
+use gpui::{Div, Render, Stateful};
+use story::Story;
+use ui::prelude::*;
+
+pub struct CursorStory;
+
+impl Render for CursorStory {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        let all_cursors: [(&str, Box<dyn Fn(Stateful<Div>) -> Stateful<Div>>); 19] = [
+            (
+                "cursor_default",
+                Box::new(|el: Stateful<Div>| el.cursor_default()),
+            ),
+            (
+                "cursor_pointer",
+                Box::new(|el: Stateful<Div>| el.cursor_pointer()),
+            ),
+            (
+                "cursor_text",
+                Box::new(|el: Stateful<Div>| el.cursor_text()),
+            ),
+            (
+                "cursor_move",
+                Box::new(|el: Stateful<Div>| el.cursor_move()),
+            ),
+            (
+                "cursor_not_allowed",
+                Box::new(|el: Stateful<Div>| el.cursor_not_allowed()),
+            ),
+            (
+                "cursor_context_menu",
+                Box::new(|el: Stateful<Div>| el.cursor_context_menu()),
+            ),
+            (
+                "cursor_crosshair",
+                Box::new(|el: Stateful<Div>| el.cursor_crosshair()),
+            ),
+            (
+                "cursor_vertical_text",
+                Box::new(|el: Stateful<Div>| el.cursor_vertical_text()),
+            ),
+            (
+                "cursor_alias",
+                Box::new(|el: Stateful<Div>| el.cursor_alias()),
+            ),
+            (
+                "cursor_copy",
+                Box::new(|el: Stateful<Div>| el.cursor_copy()),
+            ),
+            (
+                "cursor_no_drop",
+                Box::new(|el: Stateful<Div>| el.cursor_no_drop()),
+            ),
+            (
+                "cursor_grab",
+                Box::new(|el: Stateful<Div>| el.cursor_grab()),
+            ),
+            (
+                "cursor_grabbing",
+                Box::new(|el: Stateful<Div>| el.cursor_grabbing()),
+            ),
+            (
+                "cursor_col_resize",
+                Box::new(|el: Stateful<Div>| el.cursor_col_resize()),
+            ),
+            (
+                "cursor_row_resize",
+                Box::new(|el: Stateful<Div>| el.cursor_row_resize()),
+            ),
+            (
+                "cursor_n_resize",
+                Box::new(|el: Stateful<Div>| el.cursor_n_resize()),
+            ),
+            (
+                "cursor_e_resize",
+                Box::new(|el: Stateful<Div>| el.cursor_e_resize()),
+            ),
+            (
+                "cursor_s_resize",
+                Box::new(|el: Stateful<Div>| el.cursor_s_resize()),
+            ),
+            (
+                "cursor_w_resize",
+                Box::new(|el: Stateful<Div>| el.cursor_w_resize()),
+            ),
+        ];
+
+        Story::container()
+            .flex()
+            .gap_1()
+            .child(Story::title("cursor"))
+            .children(all_cursors.map(|(name, apply_cursor)| {
+                div().gap_1().flex().text_color(gpui::white()).child(
+                    div()
+                        .flex()
+                        .items_center()
+                        .justify_center()
+                        .id(name)
+                        .map(apply_cursor)
+                        .w_64()
+                        .h_8()
+                        .bg(gpui::red())
+                        .active(|style| style.bg(gpui::green()))
+                        .text_sm()
+                        .child(Story::label(name)),
+                )
+            }))
+    }
+}

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

@@ -1,6 +1,6 @@
 use gpui::{
-    blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText,
-    TextRun, View, VisualContext, WindowContext,
+    blue, div, green, red, white, Div, HighlightStyle, InteractiveText, ParentElement, Render,
+    Styled, StyledText, View, VisualContext, WindowContext,
 };
 use ui::v_stack;
 
@@ -59,13 +59,11 @@ impl Render for TextStory {
             ))).child(
                 InteractiveText::new(
                     "interactive",
-                    StyledText::new("Hello world, how is it going?").with_runs(vec![
-                        cx.text_style().to_run(6),
-                        TextRun {
+                    StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
+                        (6..11, HighlightStyle {
                             background_color: Some(green()),
-                            ..cx.text_style().to_run(5)
-                        },
-                        cx.text_style().to_run(18),
+                            ..Default::default()
+                        }),
                     ]),
                 )
                 .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {

crates/storybook2/src/story_selector.rs 🔗

@@ -12,10 +12,12 @@ use ui::prelude::*;
 #[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
 #[strum(serialize_all = "snake_case")]
 pub enum ComponentStory {
+    AutoHeightEditor,
     Avatar,
     Button,
     Checkbox,
     ContextMenu,
+    Cursor,
     Disclosure,
     Focus,
     Icon,
@@ -23,6 +25,7 @@ pub enum ComponentStory {
     Keybinding,
     Label,
     List,
+    ListHeader,
     ListItem,
     Scroll,
     Text,
@@ -33,10 +36,12 @@ pub enum ComponentStory {
 impl ComponentStory {
     pub fn story(&self, cx: &mut WindowContext) -> AnyView {
         match self {
+            Self::AutoHeightEditor => AutoHeightEditorStory::new(cx).into(),
             Self::Avatar => cx.build_view(|_| ui::AvatarStory).into(),
             Self::Button => cx.build_view(|_| ui::ButtonStory).into(),
             Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(),
             Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(),
+            Self::Cursor => cx.build_view(|_| crate::stories::CursorStory).into(),
             Self::Disclosure => cx.build_view(|_| ui::DisclosureStory).into(),
             Self::Focus => FocusStory::view(cx).into(),
             Self::Icon => cx.build_view(|_| ui::IconStory).into(),
@@ -44,6 +49,7 @@ impl ComponentStory {
             Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
             Self::Label => cx.build_view(|_| ui::LabelStory).into(),
             Self::List => cx.build_view(|_| ui::ListStory).into(),
+            Self::ListHeader => cx.build_view(|_| ui::ListHeaderStory).into(),
             Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
             Self::Scroll => ScrollStory::view(cx).into(),
             Self::Text => TextStory::view(cx).into(),

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -299,11 +299,8 @@ impl TerminalView {
         cx: &mut ViewContext<Self>,
     ) {
         self.context_menu = Some(ContextMenu::build(cx, |menu, cx| {
-            menu.action("Clear", Box::new(Clear), cx).action(
-                "Close",
-                Box::new(CloseActiveItem { save_intent: None }),
-                cx,
-            )
+            menu.action("Clear", Box::new(Clear))
+                .action("Close", Box::new(CloseActiveItem { save_intent: None }))
         }));
         dbg!(&position);
         // todo!()
@@ -1173,6 +1170,7 @@ mod tests {
                 })
             })
             .await
+            .unwrap()
             .unwrap();
 
         (wt, entry)

crates/theme2/src/one_themes.rs 🔗

@@ -52,13 +52,13 @@ pub(crate) fn one_dark() -> Theme {
                 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),
+                element_disabled: SystemColors::default().transparent,
                 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),
+                ghost_element_disabled: SystemColors::default().transparent,
                 text: hsla(221. / 360., 11. / 100., 86. / 100., 1.0),
                 text_muted: hsla(218.0 / 360., 7. / 100., 46. / 100., 1.0),
                 text_placeholder: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0),

crates/theme_selector2/src/theme_selector.rs 🔗

@@ -2,14 +2,14 @@ use feature_flags::FeatureFlagAppExt;
 use fs::Fs;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, SharedString, View,
-    ViewContext, VisualContext, WeakView,
+    actions, AppContext, DismissEvent, Div, EventEmitter, FocusableView, Render, SharedString,
+    View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use settings::{update_settings_file, SettingsStore};
 use std::sync::Arc;
 use theme::{Theme, ThemeRegistry, ThemeSettings};
-use ui::{prelude::*, ListItem};
+use ui::{prelude::*, v_stack, ListItem};
 use util::ResultExt;
 use workspace::{ui::HighlightedLabel, Workspace};
 
@@ -65,10 +65,10 @@ impl FocusableView for ThemeSelector {
 }
 
 impl Render for ThemeSelector {
-    type Element = View<Picker<ThemeSelectorDelegate>>;
+    type Element = Div;
 
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
-        self.picker.clone()
+        v_stack().min_w_96().child(self.picker.clone())
     }
 }
 
@@ -98,7 +98,7 @@ impl ThemeSelectorDelegate {
         let original_theme = cx.theme().clone();
 
         let staff_mode = cx.is_staff();
-        let registry = cx.global::<Arc<ThemeRegistry>>();
+        let registry = cx.global::<ThemeRegistry>();
         let theme_names = registry.list(staff_mode).collect::<Vec<_>>();
         //todo!(theme sorting)
         // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
@@ -126,7 +126,7 @@ impl ThemeSelectorDelegate {
 
     fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
         if let Some(mat) = self.matches.get(self.selected_index) {
-            let registry = cx.global::<Arc<ThemeRegistry>>();
+            let registry = cx.global::<ThemeRegistry>();
             match registry.get(&mat.string) {
                 Ok(theme) => {
                     Self::set_theme(theme, cx);

crates/ui2/src/components.rs 🔗

@@ -9,6 +9,8 @@ mod keybinding;
 mod label;
 mod list;
 mod popover;
+mod popover_menu;
+mod right_click_menu;
 mod stack;
 mod tooltip;
 
@@ -26,6 +28,8 @@ pub use keybinding::*;
 pub use label::*;
 pub use list::*;
 pub use popover::*;
+pub use popover_menu::*;
+pub use right_click_menu::*;
 pub use stack::*;
 pub use tooltip::*;
 

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

@@ -1,13 +1,22 @@
-use gpui::AnyView;
+use gpui::{AnyView, DefiniteLength};
 
 use crate::prelude::*;
-use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Label, LineHeightStyle};
+use crate::{
+    ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize, Label, LineHeightStyle,
+};
+
+use super::button_icon::ButtonIcon;
 
 #[derive(IntoElement)]
 pub struct Button {
     base: ButtonLike,
     label: SharedString,
     label_color: Option<Color>,
+    selected_label: Option<SharedString>,
+    icon: Option<Icon>,
+    icon_size: Option<IconSize>,
+    icon_color: Option<Color>,
+    selected_icon: Option<Icon>,
 }
 
 impl Button {
@@ -16,6 +25,11 @@ impl Button {
             base: ButtonLike::new(id),
             label: label.into(),
             label_color: None,
+            selected_label: None,
+            icon: None,
+            icon_size: None,
+            icon_color: None,
+            selected_icon: None,
         }
     }
 
@@ -23,6 +37,31 @@ impl Button {
         self.label_color = label_color.into();
         self
     }
+
+    pub fn selected_label<L: Into<SharedString>>(mut self, label: impl Into<Option<L>>) -> Self {
+        self.selected_label = label.into().map(Into::into);
+        self
+    }
+
+    pub fn icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+        self.icon = icon.into();
+        self
+    }
+
+    pub fn icon_size(mut self, icon_size: impl Into<Option<IconSize>>) -> Self {
+        self.icon_size = icon_size.into();
+        self
+    }
+
+    pub fn icon_color(mut self, icon_color: impl Into<Option<Color>>) -> Self {
+        self.icon_color = icon_color.into();
+        self
+    }
+
+    pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+        self.selected_icon = icon.into();
+        self
+    }
 }
 
 impl Selectable for Button {
@@ -49,6 +88,18 @@ impl Clickable for Button {
     }
 }
 
+impl FixedWidth for Button {
+    fn width(mut self, width: DefiniteLength) -> Self {
+        self.base = self.base.width(width);
+        self
+    }
+
+    fn full_width(mut self) -> Self {
+        self.base = self.base.full_width();
+        self
+    }
+}
+
 impl ButtonCommon for Button {
     fn id(&self) -> &ElementId {
         self.base.id()
@@ -74,18 +125,35 @@ impl RenderOnce for Button {
     type Rendered = ButtonLike;
 
     fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
-        let label_color = if self.base.disabled {
+        let is_disabled = self.base.disabled;
+        let is_selected = self.base.selected;
+
+        let label = self
+            .selected_label
+            .filter(|_| is_selected)
+            .unwrap_or(self.label);
+
+        let label_color = if is_disabled {
             Color::Disabled
-        } else if self.base.selected {
+        } else if is_selected {
             Color::Selected
         } else {
-            Color::Default
+            self.label_color.unwrap_or_default()
         };
 
-        self.base.child(
-            Label::new(self.label)
-                .color(label_color)
-                .line_height_style(LineHeightStyle::UILabel),
-        )
+        self.base
+            .children(self.icon.map(|icon| {
+                ButtonIcon::new(icon)
+                    .disabled(is_disabled)
+                    .selected(is_selected)
+                    .selected_icon(self.selected_icon)
+                    .size(self.icon_size)
+                    .color(self.icon_color)
+            }))
+            .child(
+                Label::new(label)
+                    .color(label_color)
+                    .line_height_style(LineHeightStyle::UILabel),
+            )
     }
 }

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

@@ -0,0 +1,84 @@
+use crate::{prelude::*, Icon, IconElement, IconSize};
+
+/// An icon that appears within a button.
+///
+/// Can be used as either an icon alongside a label, like in [`Button`](crate::Button),
+/// or as a standalone icon, like in [`IconButton`](crate::IconButton).
+#[derive(IntoElement)]
+pub(super) struct ButtonIcon {
+    icon: Icon,
+    size: IconSize,
+    color: Color,
+    disabled: bool,
+    selected: bool,
+    selected_icon: Option<Icon>,
+}
+
+impl ButtonIcon {
+    pub fn new(icon: Icon) -> Self {
+        Self {
+            icon,
+            size: IconSize::default(),
+            color: Color::default(),
+            disabled: false,
+            selected: false,
+            selected_icon: None,
+        }
+    }
+
+    pub fn size(mut self, size: impl Into<Option<IconSize>>) -> Self {
+        if let Some(size) = size.into() {
+            self.size = size;
+        }
+
+        self
+    }
+
+    pub fn color(mut self, color: impl Into<Option<Color>>) -> Self {
+        if let Some(color) = color.into() {
+            self.color = color;
+        }
+
+        self
+    }
+
+    pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+        self.selected_icon = icon.into();
+        self
+    }
+}
+
+impl Disableable for ButtonIcon {
+    fn disabled(mut self, disabled: bool) -> Self {
+        self.disabled = disabled;
+        self
+    }
+}
+
+impl Selectable for ButtonIcon {
+    fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+}
+
+impl RenderOnce for ButtonIcon {
+    type Rendered = IconElement;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        let icon = self
+            .selected_icon
+            .filter(|_| self.selected)
+            .unwrap_or(self.icon);
+
+        let icon_color = if self.disabled {
+            Color::Disabled
+        } else if self.selected {
+            Color::Selected
+        } else {
+            self.color
+        };
+
+        IconElement::new(icon).size(self.size).color(icon_color)
+    }
+}

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

@@ -1,3 +1,4 @@
+use gpui::{relative, DefiniteLength};
 use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
 use smallvec::SmallVec;
 
@@ -5,18 +6,50 @@ use crate::h_stack;
 use crate::prelude::*;
 
 pub trait ButtonCommon: Clickable + Disableable {
+    /// A unique element ID to identify the button.
     fn id(&self) -> &ElementId;
+
+    /// The visual style of the button.
+    ///
+    /// Mosty commonly will be [`ButtonStyle::Subtle`], or [`ButtonStyle::Filled`]
+    /// for an emphasized button.
     fn style(self, style: ButtonStyle) -> Self;
+
+    /// The size of the button.
+    ///
+    /// Most buttons will use the default size.
+    ///
+    /// [`ButtonSize`] can also be used to help build non-button elements
+    /// that are consistently sized with buttons.
     fn size(self, size: ButtonSize) -> Self;
+
+    /// The tooltip that shows when a user hovers over the button.
+    ///
+    /// Nearly all interactable elements should have a tooltip. Some example
+    /// exceptions might a scroll bar, or a slider.
     fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
 }
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
 pub enum ButtonStyle {
-    #[default]
+    /// A filled button with a solid background color. Provides emphasis versus
+    /// the more common subtle button.
     Filled,
-    // Tinted,
+
+    /// 🚧 Under construction 🚧
+    ///
+    /// Used to emphasize a button in some way, like a selected state, or a semantic
+    /// coloring like an error or success button.
+    Tinted,
+
+    /// The default button style, used for most buttons. Has a transparent background,
+    /// but has a background color to indicate states like hover and active.
+    #[default]
     Subtle,
+
+    /// Used for buttons that only change forground color on hover and active states.
+    ///
+    /// TODO: Better docs for this.
     Transparent,
 }
 
@@ -40,6 +73,12 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::Tinted => ButtonLikeStyles {
+                background: gpui::red(),
+                border_color: gpui::red(),
+                label_color: gpui::red(),
+                icon_color: gpui::red(),
+            },
             ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_background,
                 border_color: transparent_black(),
@@ -63,6 +102,12 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::Tinted => ButtonLikeStyles {
+                background: gpui::red(),
+                border_color: gpui::red(),
+                label_color: gpui::red(),
+                icon_color: gpui::red(),
+            },
             ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_hover,
                 border_color: transparent_black(),
@@ -88,6 +133,12 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::Tinted => ButtonLikeStyles {
+                background: gpui::red(),
+                border_color: gpui::red(),
+                label_color: gpui::red(),
+                icon_color: gpui::red(),
+            },
             ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_active,
                 border_color: transparent_black(),
@@ -114,6 +165,12 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::Tinted => ButtonLikeStyles {
+                background: gpui::red(),
+                border_color: gpui::red(),
+                label_color: gpui::red(),
+                icon_color: gpui::red(),
+            },
             ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_background,
                 border_color: cx.theme().colors().border_focused,
@@ -137,6 +194,12 @@ impl ButtonStyle {
                 label_color: Color::Disabled.color(cx),
                 icon_color: Color::Disabled.color(cx),
             },
+            ButtonStyle::Tinted => ButtonLikeStyles {
+                background: gpui::red(),
+                border_color: gpui::red(),
+                label_color: gpui::red(),
+                icon_color: gpui::red(),
+            },
             ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_disabled,
                 border_color: cx.theme().colors().border_disabled,
@@ -153,6 +216,8 @@ impl ButtonStyle {
     }
 }
 
+/// ButtonSize can also be used to help build  non-button elements
+/// that are consistently sized with buttons.
 #[derive(Default, PartialEq, Clone, Copy)]
 pub enum ButtonSize {
     #[default]
@@ -171,12 +236,18 @@ impl ButtonSize {
     }
 }
 
+/// A button-like element that can be used to create a custom button when
+/// prebuilt buttons are not sufficient. Use this sparingly, as it is
+/// unconstrained and may make the UI feel less consistent.
+///
+/// This is also used to build the prebuilt buttons.
 #[derive(IntoElement)]
 pub struct ButtonLike {
     id: ElementId,
     pub(super) style: ButtonStyle,
     pub(super) disabled: bool,
     pub(super) selected: bool,
+    pub(super) width: Option<DefiniteLength>,
     size: ButtonSize,
     tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
     on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
@@ -190,6 +261,7 @@ impl ButtonLike {
             style: ButtonStyle::default(),
             disabled: false,
             selected: false,
+            width: None,
             size: ButtonSize::Default,
             tooltip: None,
             children: SmallVec::new(),
@@ -219,6 +291,18 @@ impl Clickable for ButtonLike {
     }
 }
 
+impl FixedWidth for ButtonLike {
+    fn width(mut self, width: DefiniteLength) -> Self {
+        self.width = Some(width);
+        self
+    }
+
+    fn full_width(mut self) -> Self {
+        self.width = Some(relative(1.));
+        self
+    }
+}
+
 impl ButtonCommon for ButtonLike {
     fn id(&self) -> &ElementId {
         &self.id
@@ -252,14 +336,19 @@ impl RenderOnce for ButtonLike {
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
         h_stack()
             .id(self.id.clone())
+            .group("")
+            .flex_none()
             .h(self.size.height())
+            .when_some(self.width, |this, width| this.w(width))
             .rounded_md()
-            .cursor_pointer()
             .gap_1()
             .px_1()
             .bg(self.style.enabled(cx).background)
-            .hover(|hover| hover.bg(self.style.hovered(cx).background))
-            .active(|active| active.bg(self.style.active(cx).background))
+            .when(!self.disabled, |this| {
+                this.cursor_pointer()
+                    .hover(|hover| hover.bg(self.style.hovered(cx).background))
+                    .active(|active| active.bg(self.style.active(cx).background))
+            })
             .when_some(
                 self.on_click.filter(|_| !self.disabled),
                 |this, on_click| {
@@ -270,7 +359,11 @@ impl RenderOnce for ButtonLike {
                 },
             )
             .when_some(self.tooltip, |this, tooltip| {
-                this.tooltip(move |cx| tooltip(cx))
+                if !self.selected {
+                    this.tooltip(move |cx| tooltip(cx))
+                } else {
+                    this
+                }
             })
             .children(self.children)
     }

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

@@ -1,7 +1,9 @@
-use gpui::{Action, AnyView};
+use gpui::{Action, AnyView, DefiniteLength};
 
 use crate::prelude::*;
-use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconElement, IconSize};
+use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize};
+
+use super::button_icon::ButtonIcon;
 
 #[derive(IntoElement)]
 pub struct IconButton {
@@ -9,6 +11,7 @@ pub struct IconButton {
     icon: Icon,
     icon_size: IconSize,
     icon_color: Color,
+    selected_icon: Option<Icon>,
 }
 
 impl IconButton {
@@ -18,6 +21,7 @@ impl IconButton {
             icon,
             icon_size: IconSize::default(),
             icon_color: Color::Default,
+            selected_icon: None,
         }
     }
 
@@ -31,6 +35,11 @@ impl IconButton {
         self
     }
 
+    pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+        self.selected_icon = icon.into();
+        self
+    }
+
     pub fn action(self, action: Box<dyn Action>) -> Self {
         self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
     }
@@ -60,6 +69,18 @@ impl Clickable for IconButton {
     }
 }
 
+impl FixedWidth for IconButton {
+    fn width(mut self, width: DefiniteLength) -> Self {
+        self.base = self.base.width(width);
+        self
+    }
+
+    fn full_width(mut self) -> Self {
+        self.base = self.base.full_width();
+        self
+    }
+}
+
 impl ButtonCommon for IconButton {
     fn id(&self) -> &ElementId {
         self.base.id()
@@ -85,18 +106,16 @@ impl RenderOnce for IconButton {
     type Rendered = ButtonLike;
 
     fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
-        let icon_color = if self.base.disabled {
-            Color::Disabled
-        } else if self.base.selected {
-            Color::Selected
-        } else {
-            self.icon_color
-        };
+        let is_disabled = self.base.disabled;
+        let is_selected = self.base.selected;
 
         self.base.child(
-            IconElement::new(self.icon)
+            ButtonIcon::new(self.icon)
+                .disabled(is_disabled)
+                .selected(is_selected)
+                .selected_icon(self.selected_icon)
                 .size(self.icon_size)
-                .color(icon_color),
+                .color(self.icon_color),
         )
     }
 }

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

@@ -1,21 +1,22 @@
 use crate::{
-    h_stack, prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader,
+    h_stack, prelude::*, v_stack, Icon, IconElement, KeyBinding, Label, List, ListItem,
+    ListSeparator, ListSubHeader,
 };
 use gpui::{
-    overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DismissEvent, DispatchPhase,
-    Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, MouseButton,
-    MouseDownEvent, Pixels, Point, Render, View, VisualContext,
+    px, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
+    IntoElement, Render, View, VisualContext,
 };
 use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
-use std::{cell::RefCell, rc::Rc};
+use std::{rc::Rc, time::Duration};
 
 pub enum ContextMenuItem {
     Separator,
     Header(SharedString),
     Entry {
         label: SharedString,
+        icon: Option<Icon>,
         handler: Rc<dyn Fn(&mut WindowContext)>,
-        key_binding: Option<KeyBinding>,
+        action: Option<Box<dyn Action>>,
     },
 }
 
@@ -23,6 +24,7 @@ pub struct ContextMenu {
     items: Vec<ContextMenuItem>,
     focus_handle: FocusHandle,
     selected_index: Option<usize>,
+    delayed: bool,
 }
 
 impl FocusableView for ContextMenu {
@@ -45,6 +47,7 @@ impl ContextMenu {
                     items: Default::default(),
                     focus_handle: cx.focus_handle(),
                     selected_index: None,
+                    delayed: false,
                 },
                 cx,
             )
@@ -69,21 +72,28 @@ impl ContextMenu {
         self.items.push(ContextMenuItem::Entry {
             label: label.into(),
             handler: Rc::new(on_click),
-            key_binding: None,
+            icon: None,
+            action: None,
         });
         self
     }
 
-    pub fn action(
-        mut self,
-        label: impl Into<SharedString>,
-        action: Box<dyn Action>,
-        cx: &mut WindowContext,
-    ) -> Self {
+    pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
+        self.items.push(ContextMenuItem::Entry {
+            label: label.into(),
+            action: Some(action.boxed_clone()),
+            handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
+            icon: None,
+        });
+        self
+    }
+
+    pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
         self.items.push(ContextMenuItem::Entry {
             label: label.into(),
-            key_binding: KeyBinding::for_action(&*action, cx),
+            action: Some(action.boxed_clone()),
             handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
+            icon: Some(Icon::Link),
         });
         self
     }
@@ -143,6 +153,37 @@ impl ContextMenu {
             self.select_last(&Default::default(), cx);
         }
     }
+
+    pub fn on_action_dispatch(&mut self, dispatched: &Box<dyn Action>, cx: &mut ViewContext<Self>) {
+        if let Some(ix) = self.items.iter().position(|item| {
+            if let ContextMenuItem::Entry {
+                action: Some(action),
+                ..
+            } = item
+            {
+                action.partial_eq(&**dispatched)
+            } else {
+                false
+            }
+        }) {
+            self.selected_index = Some(ix);
+            self.delayed = true;
+            cx.notify();
+            let action = dispatched.boxed_clone();
+            cx.spawn(|this, mut cx| async move {
+                cx.background_executor()
+                    .timer(Duration::from_millis(50))
+                    .await;
+                this.update(&mut cx, |this, cx| {
+                    cx.dispatch_action(action);
+                    this.cancel(&Default::default(), cx)
+                })
+            })
+            .detach_and_log_err(cx);
+        } else {
+            cx.propagate()
+        }
+    }
 }
 
 impl ContextMenuItem {
@@ -167,6 +208,22 @@ impl Render for ContextMenu {
                 .on_action(cx.listener(ContextMenu::select_prev))
                 .on_action(cx.listener(ContextMenu::confirm))
                 .on_action(cx.listener(ContextMenu::cancel))
+                .when(!self.delayed, |mut el| {
+                    for item in self.items.iter() {
+                        if let ContextMenuItem::Entry {
+                            action: Some(action),
+                            ..
+                        } = item
+                        {
+                            el = el.on_boxed_action(
+                                action,
+                                cx.listener(ContextMenu::on_action_dispatch),
+                            );
+                        }
+                    }
+                    el
+                })
+                .on_blur(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx)))
                 .flex_none()
                 .child(
                     List::new().children(self.items.iter().enumerate().map(
@@ -176,28 +233,38 @@ impl Render for ContextMenu {
                                 ListSubHeader::new(header.clone()).into_any_element()
                             }
                             ContextMenuItem::Entry {
-                                label: entry,
-                                handler: callback,
-                                key_binding,
+                                label,
+                                handler,
+                                icon,
+                                action,
                             } => {
-                                let callback = callback.clone();
+                                let handler = handler.clone();
                                 let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
 
-                                ListItem::new(entry.clone())
+                                let label_element = if let Some(icon) = icon {
+                                    h_stack()
+                                        .gap_1()
+                                        .child(Label::new(label.clone()))
+                                        .child(IconElement::new(*icon))
+                                        .into_any_element()
+                                } else {
+                                    Label::new(label.clone()).into_any_element()
+                                };
+
+                                ListItem::new(label.clone())
                                     .child(
                                         h_stack()
                                             .w_full()
                                             .justify_between()
-                                            .child(Label::new(entry.clone()))
-                                            .children(
-                                                key_binding
-                                                    .clone()
-                                                    .map(|binding| div().ml_1().child(binding)),
-                                            ),
+                                            .child(label_element)
+                                            .children(action.as_ref().and_then(|action| {
+                                                KeyBinding::for_action(&**action, cx)
+                                                    .map(|binding| div().ml_1().child(binding))
+                                            })),
                                     )
                                     .selected(Some(ix) == self.selected_index)
                                     .on_click(move |event, cx| {
-                                        callback(cx);
+                                        handler(cx);
                                         dismiss(event, cx)
                                     })
                                     .into_any_element()
@@ -208,174 +275,3 @@ impl Render for ContextMenu {
         )
     }
 }
-
-pub struct MenuHandle<M: ManagedView> {
-    id: ElementId,
-    child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
-    menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
-    anchor: Option<AnchorCorner>,
-    attach: Option<AnchorCorner>,
-}
-
-impl<M: ManagedView> MenuHandle<M> {
-    pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
-        self.menu_builder = Some(Rc::new(f));
-        self
-    }
-
-    pub fn child<R: IntoElement>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
-        self.child_builder = Some(Box::new(|b| f(b).into_element().into_any()));
-        self
-    }
-
-    /// anchor defines which corner of the menu to anchor to the attachment point
-    /// (by default the cursor position, but see attach)
-    pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
-        self.anchor = Some(anchor);
-        self
-    }
-
-    /// attach defines which corner of the handle to attach the menu's anchor to
-    pub fn attach(mut self, attach: AnchorCorner) -> Self {
-        self.attach = Some(attach);
-        self
-    }
-}
-
-pub fn menu_handle<M: ManagedView>(id: impl Into<ElementId>) -> MenuHandle<M> {
-    MenuHandle {
-        id: id.into(),
-        child_builder: None,
-        menu_builder: None,
-        anchor: None,
-        attach: None,
-    }
-}
-
-pub struct MenuHandleState<M> {
-    menu: Rc<RefCell<Option<View<M>>>>,
-    position: Rc<RefCell<Point<Pixels>>>,
-    child_layout_id: Option<LayoutId>,
-    child_element: Option<AnyElement>,
-    menu_element: Option<AnyElement>,
-}
-
-impl<M: ManagedView> Element for MenuHandle<M> {
-    type State = MenuHandleState<M>;
-
-    fn layout(
-        &mut self,
-        element_state: Option<Self::State>,
-        cx: &mut WindowContext,
-    ) -> (gpui::LayoutId, Self::State) {
-        let (menu, position) = if let Some(element_state) = element_state {
-            (element_state.menu, element_state.position)
-        } else {
-            (Rc::default(), Rc::default())
-        };
-
-        let mut menu_layout_id = None;
-
-        let menu_element = menu.borrow_mut().as_mut().map(|menu| {
-            let mut overlay = overlay().snap_to_window();
-            if let Some(anchor) = self.anchor {
-                overlay = overlay.anchor(anchor);
-            }
-            overlay = overlay.position(*position.borrow());
-
-            let mut element = overlay.child(menu.clone()).into_any();
-            menu_layout_id = Some(element.layout(cx));
-            element
-        });
-
-        let mut child_element = self
-            .child_builder
-            .take()
-            .map(|child_builder| (child_builder)(menu.borrow().is_some()));
-
-        let child_layout_id = child_element
-            .as_mut()
-            .map(|child_element| child_element.layout(cx));
-
-        let layout_id = cx.request_layout(
-            &gpui::Style::default(),
-            menu_layout_id.into_iter().chain(child_layout_id),
-        );
-
-        (
-            layout_id,
-            MenuHandleState {
-                menu,
-                position,
-                child_element,
-                child_layout_id,
-                menu_element,
-            },
-        )
-    }
-
-    fn paint(
-        self,
-        bounds: Bounds<gpui::Pixels>,
-        element_state: &mut Self::State,
-        cx: &mut WindowContext,
-    ) {
-        if let Some(child) = element_state.child_element.take() {
-            child.paint(cx);
-        }
-
-        if let Some(menu) = element_state.menu_element.take() {
-            menu.paint(cx);
-            return;
-        }
-
-        let Some(builder) = self.menu_builder else {
-            return;
-        };
-        let menu = element_state.menu.clone();
-        let position = element_state.position.clone();
-        let attach = self.attach.clone();
-        let child_layout_id = element_state.child_layout_id.clone();
-
-        cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
-            if phase == DispatchPhase::Bubble
-                && event.button == MouseButton::Right
-                && bounds.contains_point(&event.position)
-            {
-                cx.stop_propagation();
-                cx.prevent_default();
-
-                let new_menu = (builder)(cx);
-                let menu2 = menu.clone();
-                cx.subscribe(&new_menu, move |_modal, _: &DismissEvent, cx| {
-                    *menu2.borrow_mut() = None;
-                    cx.notify();
-                })
-                .detach();
-                cx.focus_view(&new_menu);
-                *menu.borrow_mut() = Some(new_menu);
-
-                *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
-                    attach
-                        .unwrap()
-                        .corner(cx.layout_bounds(child_layout_id.unwrap()))
-                } else {
-                    cx.mouse_position()
-                };
-                cx.notify();
-            }
-        });
-    }
-}
-
-impl<M: ManagedView> IntoElement for MenuHandle<M> {
-    type Element = Self;
-
-    fn element_id(&self) -> Option<gpui::ElementId> {
-        Some(self.id.clone())
-    }
-
-    fn into_element(self) -> Self::Element {
-        self
-    }
-}

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

@@ -27,6 +27,7 @@ pub enum Icon {
     Bolt,
     CaseSensitive,
     Check,
+    Copy,
     ChevronDown,
     ChevronLeft,
     ChevronRight,
@@ -54,6 +55,7 @@ pub enum Icon {
     FolderX,
     Hash,
     InlayHint,
+    Link,
     MagicWand,
     MagnifyingGlass,
     MailOpen,
@@ -99,6 +101,7 @@ impl Icon {
             Icon::Bolt => "icons/bolt.svg",
             Icon::CaseSensitive => "icons/case_insensitive.svg",
             Icon::Check => "icons/check.svg",
+            Icon::Copy => "icons/copy.svg",
             Icon::ChevronDown => "icons/chevron_down.svg",
             Icon::ChevronLeft => "icons/chevron_left.svg",
             Icon::ChevronRight => "icons/chevron_right.svg",
@@ -126,6 +129,7 @@ impl Icon {
             Icon::FolderX => "icons/stop_sharing.svg",
             Icon::Hash => "icons/hash.svg",
             Icon::InlayHint => "icons/inlay_hint.svg",
+            Icon::Link => "icons/link.svg",
             Icon::MagicWand => "icons/magic-wand.svg",
             Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
             Icon::MailOpen => "icons/mail-open.svg",

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

@@ -1,5 +1,5 @@
 use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
-use gpui::{relative, rems, Action, Div, IntoElement, Keystroke};
+use gpui::{relative, rems, Action, Div, FocusHandle, IntoElement, Keystroke};
 
 #[derive(IntoElement, Clone)]
 pub struct KeyBinding {
@@ -49,12 +49,21 @@ impl RenderOnce for KeyBinding {
 
 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))
     }
 
+    // like for_action(), but lets you specify the context from which keybindings
+    // are matched.
+    pub fn for_action_in(
+        action: &dyn Action,
+        focus: &FocusHandle,
+        cx: &mut WindowContext,
+    ) -> Option<Self> {
+        let key_binding = cx.bindings_for_action_in(action, focus).last().cloned()?;
+        Some(Self::new(key_binding))
+    }
+
     fn icon_for_key(keystroke: &Keystroke) -> Option<Icon> {
         let mut icon: Option<Icon> = None;
 

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

@@ -1,6 +1,8 @@
+use std::ops::Range;
+
 use crate::prelude::*;
 use crate::styled_ext::StyledExt;
-use gpui::{relative, Div, IntoElement, StyledText, TextRun, WindowContext};
+use gpui::{relative, Div, HighlightStyle, IntoElement, StyledText, WindowContext};
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
 pub enum LabelSize {
@@ -99,38 +101,32 @@ impl RenderOnce for HighlightedLabel {
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
         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: Vec<TextRun> = Vec::new();
-
-        for (char_ix, char) in self.label.char_indices() {
-            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 mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
+
+        while let Some(start_ix) = highlight_indices.next() {
+            let mut end_ix = start_ix;
+
+            loop {
+                end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8();
+                if let Some(&next_ix) = highlight_indices.peek() {
+                    if next_ix == end_ix {
+                        end_ix = next_ix;
+                        highlight_indices.next();
+                        continue;
+                    }
                 }
+                break;
             }
 
-            let last_run = runs.last_mut();
-            let start_new_run = if let Some(last_run) = last_run {
-                if color == last_run.color {
-                    last_run.len += char.len_utf8();
-                    false
-                } else {
-                    true
-                }
-            } else {
-                true
-            };
-
-            if start_new_run {
-                text_style.color = color;
-                runs.push(text_style.to_run(char.len_utf8()))
-            }
+            highlights.push((
+                start_ix..end_ix,
+                HighlightStyle {
+                    color: Some(highlight_color),
+                    ..Default::default()
+                },
+            ));
         }
 
         div()
@@ -150,7 +146,7 @@ impl RenderOnce for HighlightedLabel {
                 LabelSize::Default => this.text_ui(),
                 LabelSize::Small => this.text_ui_sm(),
             })
-            .child(StyledText::new(self.label).with_runs(runs))
+            .child(StyledText::new(self.label).with_highlights(&cx.text_style(), highlights))
     }
 }
 

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

@@ -1,73 +1,11 @@
+mod list;
 mod list_header;
 mod list_item;
 mod list_separator;
 mod list_sub_header;
 
-use gpui::{AnyElement, Div};
-use smallvec::SmallVec;
-
-use crate::prelude::*;
-use crate::{v_stack, Label};
-
+pub use list::*;
 pub use list_header::*;
 pub use list_item::*;
 pub use list_separator::*;
 pub use list_sub_header::*;
-
-#[derive(IntoElement)]
-pub struct List {
-    /// Message to display when the list is empty
-    /// Defaults to "No items"
-    empty_message: SharedString,
-    header: Option<ListHeader>,
-    toggle: Option<bool>,
-    children: SmallVec<[AnyElement; 2]>,
-}
-
-impl List {
-    pub fn new() -> Self {
-        Self {
-            empty_message: "No items".into(),
-            header: None,
-            toggle: None,
-            children: SmallVec::new(),
-        }
-    }
-
-    pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
-        self.empty_message = empty_message.into();
-        self
-    }
-
-    pub fn header(mut self, header: ListHeader) -> Self {
-        self.header = Some(header);
-        self
-    }
-
-    pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
-        self.toggle = toggle.into();
-        self
-    }
-}
-
-impl ParentElement for List {
-    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
-        &mut self.children
-    }
-}
-
-impl RenderOnce for List {
-    type Rendered = Div;
-
-    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
-        v_stack()
-            .w_full()
-            .py_1()
-            .children(self.header.map(|header| header))
-            .map(|this| match (self.children.is_empty(), self.toggle) {
-                (false, _) => this.children(self.children),
-                (true, Some(false)) => this,
-                (true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)),
-            })
-    }
-}

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

@@ -0,0 +1,60 @@
+use gpui::{AnyElement, Div};
+use smallvec::SmallVec;
+
+use crate::{prelude::*, v_stack, Label, ListHeader};
+
+#[derive(IntoElement)]
+pub struct List {
+    /// Message to display when the list is empty
+    /// Defaults to "No items"
+    empty_message: SharedString,
+    header: Option<ListHeader>,
+    toggle: Option<bool>,
+    children: SmallVec<[AnyElement; 2]>,
+}
+
+impl List {
+    pub fn new() -> Self {
+        Self {
+            empty_message: "No items".into(),
+            header: None,
+            toggle: None,
+            children: SmallVec::new(),
+        }
+    }
+
+    pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
+        self.empty_message = empty_message.into();
+        self
+    }
+
+    pub fn header(mut self, header: impl Into<Option<ListHeader>>) -> Self {
+        self.header = header.into();
+        self
+    }
+
+    pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
+        self.toggle = toggle.into();
+        self
+    }
+}
+
+impl ParentElement for List {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
+}
+
+impl RenderOnce for List {
+    type Rendered = Div;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        v_stack().w_full().py_1().children(self.header).map(|this| {
+            match (self.children.is_empty(), self.toggle) {
+                (false, _) => this.children(self.children),
+                (true, Some(false)) => this,
+                (true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)),
+            }
+        })
+    }
+}

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

@@ -1,22 +1,16 @@
 use std::rc::Rc;
 
-use gpui::{ClickEvent, Div};
+use gpui::{AnyElement, ClickEvent, Div};
+use smallvec::SmallVec;
 
 use crate::prelude::*;
-use crate::{h_stack, Disclosure, Icon, IconButton, IconElement, IconSize, Label};
-
-pub enum ListHeaderMeta {
-    Tools(Vec<IconButton>),
-    // TODO: This should be a button
-    Button(Label),
-    Text(Label),
-}
+use crate::{h_stack, Disclosure, Icon, IconElement, IconSize, Label};
 
 #[derive(IntoElement)]
 pub struct ListHeader {
     label: SharedString,
     left_icon: Option<Icon>,
-    meta: Option<ListHeaderMeta>,
+    meta: SmallVec<[AnyElement; 2]>,
     toggle: Option<bool>,
     on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
     inset: bool,
@@ -28,7 +22,7 @@ impl ListHeader {
         Self {
             label: label.into(),
             left_icon: None,
-            meta: None,
+            meta: SmallVec::new(),
             inset: false,
             toggle: None,
             on_toggle: None,
@@ -49,17 +43,13 @@ impl ListHeader {
         self
     }
 
-    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
-        self.left_icon = left_icon;
+    pub fn left_icon(mut self, left_icon: impl Into<Option<Icon>>) -> Self {
+        self.left_icon = left_icon.into();
         self
     }
 
-    pub fn right_button(self, button: IconButton) -> Self {
-        self.meta(Some(ListHeaderMeta::Tools(vec![button])))
-    }
-
-    pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
-        self.meta = meta;
+    pub fn meta(mut self, meta: impl IntoElement) -> Self {
+        self.meta.push(meta.into_any_element());
         self
     }
 }
@@ -75,18 +65,6 @@ impl RenderOnce for ListHeader {
     type Rendered = Div;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let meta = match self.meta {
-            Some(ListHeaderMeta::Tools(icons)) => div().child(
-                h_stack()
-                    .gap_2()
-                    .items_center()
-                    .children(icons.into_iter().map(|i| i.icon_color(Color::Muted))),
-            ),
-            Some(ListHeaderMeta::Button(label)) => div().child(label),
-            Some(ListHeaderMeta::Text(label)) => div().child(label),
-            None => div(),
-        };
-
         h_stack().w_full().relative().child(
             div()
                 .h_5()
@@ -120,7 +98,7 @@ impl RenderOnce for ListHeader {
                                 .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
                         ),
                 )
-                .child(meta),
+                .child(h_stack().gap_2().items_center().children(self.meta)),
         )
     }
 }

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

@@ -1,7 +1,8 @@
 use std::rc::Rc;
 
 use gpui::{
-    px, AnyElement, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, Stateful,
+    px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels,
+    Stateful,
 };
 use smallvec::SmallVec;
 
@@ -21,6 +22,7 @@ pub struct ListItem {
     inset: bool,
     on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
     on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
     on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
     children: SmallVec<[AnyElement; 2]>,
 }
@@ -38,6 +40,7 @@ impl ListItem {
             on_click: None,
             on_secondary_mouse_down: None,
             on_toggle: None,
+            tooltip: None,
             children: SmallVec::new(),
         }
     }
@@ -55,6 +58,11 @@ impl ListItem {
         self
     }
 
+    pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.tooltip = Some(Box::new(tooltip));
+        self
+    }
+
     pub fn inset(mut self, inset: bool) -> Self {
         self.inset = inset;
         self
@@ -149,6 +157,7 @@ impl RenderOnce for ListItem {
                     (on_mouse_down)(event, cx)
                 })
             })
+            .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
             .child(
                 div()
                     .when(self.inset, |this| this.px_2())

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

@@ -0,0 +1,231 @@
+use std::{cell::RefCell, rc::Rc};
+
+use gpui::{
+    overlay, point, px, rems, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase,
+    Element, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseDownEvent,
+    ParentElement, Pixels, Point, View, VisualContext, WindowContext,
+};
+
+use crate::{Clickable, Selectable};
+
+pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {}
+
+impl<T: IntoElement + Clickable + Selectable + 'static> PopoverTrigger for T {}
+
+pub struct PopoverMenu<M: ManagedView> {
+    id: ElementId,
+    child_builder: Option<
+        Box<
+            dyn FnOnce(
+                    Rc<RefCell<Option<View<M>>>>,
+                    Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
+                ) -> AnyElement
+                + 'static,
+        >,
+    >,
+    menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
+    anchor: AnchorCorner,
+    attach: Option<AnchorCorner>,
+    offset: Option<Point<Pixels>>,
+}
+
+impl<M: ManagedView> PopoverMenu<M> {
+    pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
+        self.menu_builder = Some(Rc::new(f));
+        self
+    }
+
+    pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
+        self.child_builder = Some(Box::new(|menu, builder| {
+            let open = menu.borrow().is_some();
+            t.selected(open)
+                .when_some(builder, |el, builder| {
+                    el.on_click({
+                        move |_, cx| {
+                            let new_menu = (builder)(cx);
+                            let menu2 = menu.clone();
+                            let previous_focus_handle = cx.focused();
+
+                            cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
+                                if modal.focus_handle(cx).contains_focused(cx) {
+                                    if previous_focus_handle.is_some() {
+                                        cx.focus(&previous_focus_handle.as_ref().unwrap())
+                                    }
+                                }
+                                *menu2.borrow_mut() = None;
+                                cx.notify();
+                            })
+                            .detach();
+                            cx.focus_view(&new_menu);
+                            *menu.borrow_mut() = Some(new_menu);
+                        }
+                    })
+                })
+                .into_any_element()
+        }));
+        self
+    }
+
+    /// anchor defines which corner of the menu to anchor to the attachment point
+    /// (by default the cursor position, but see attach)
+    pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
+        self.anchor = anchor;
+        self
+    }
+
+    /// attach defines which corner of the handle to attach the menu's anchor to
+    pub fn attach(mut self, attach: AnchorCorner) -> Self {
+        self.attach = Some(attach);
+        self
+    }
+
+    /// offset offsets the position of the content by that many pixels.
+    pub fn offset(mut self, offset: Point<Pixels>) -> Self {
+        self.offset = Some(offset);
+        self
+    }
+
+    fn resolved_attach(&self) -> AnchorCorner {
+        self.attach.unwrap_or_else(|| match self.anchor {
+            AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
+            AnchorCorner::TopRight => AnchorCorner::BottomRight,
+            AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
+            AnchorCorner::BottomRight => AnchorCorner::TopRight,
+        })
+    }
+
+    fn resolved_offset(&self, cx: &WindowContext) -> Point<Pixels> {
+        self.offset.unwrap_or_else(|| {
+            // Default offset = 4px padding + 1px border
+            let offset = rems(5. / 16.) * cx.rem_size();
+            match self.anchor {
+                AnchorCorner::TopRight | AnchorCorner::BottomRight => point(offset, px(0.)),
+                AnchorCorner::TopLeft | AnchorCorner::BottomLeft => point(-offset, px(0.)),
+            }
+        })
+    }
+}
+
+pub fn popover_menu<M: ManagedView>(id: impl Into<ElementId>) -> PopoverMenu<M> {
+    PopoverMenu {
+        id: id.into(),
+        child_builder: None,
+        menu_builder: None,
+        anchor: AnchorCorner::TopLeft,
+        attach: None,
+        offset: None,
+    }
+}
+
+pub struct PopoverMenuState<M> {
+    child_layout_id: Option<LayoutId>,
+    child_element: Option<AnyElement>,
+    child_bounds: Option<Bounds<Pixels>>,
+    menu_element: Option<AnyElement>,
+    menu: Rc<RefCell<Option<View<M>>>>,
+}
+
+impl<M: ManagedView> Element for PopoverMenu<M> {
+    type State = PopoverMenuState<M>;
+
+    fn layout(
+        &mut self,
+        element_state: Option<Self::State>,
+        cx: &mut WindowContext,
+    ) -> (gpui::LayoutId, Self::State) {
+        let mut menu_layout_id = None;
+
+        let (menu, child_bounds) = if let Some(element_state) = element_state {
+            (element_state.menu, element_state.child_bounds)
+        } else {
+            (Rc::default(), None)
+        };
+
+        let menu_element = menu.borrow_mut().as_mut().map(|menu| {
+            let mut overlay = overlay().snap_to_window().anchor(self.anchor);
+
+            if let Some(child_bounds) = child_bounds {
+                overlay = overlay.position(
+                    self.resolved_attach().corner(child_bounds) + self.resolved_offset(cx),
+                );
+            }
+
+            let mut element = overlay.child(menu.clone()).into_any();
+            menu_layout_id = Some(element.layout(cx));
+            element
+        });
+
+        let mut child_element = self
+            .child_builder
+            .take()
+            .map(|child_builder| (child_builder)(menu.clone(), self.menu_builder.clone()));
+
+        let child_layout_id = child_element
+            .as_mut()
+            .map(|child_element| child_element.layout(cx));
+
+        let layout_id = cx.request_layout(
+            &gpui::Style::default(),
+            menu_layout_id.into_iter().chain(child_layout_id),
+        );
+
+        (
+            layout_id,
+            PopoverMenuState {
+                menu,
+                child_element,
+                child_layout_id,
+                menu_element,
+                child_bounds,
+            },
+        )
+    }
+
+    fn paint(
+        self,
+        _: Bounds<gpui::Pixels>,
+        element_state: &mut Self::State,
+        cx: &mut WindowContext,
+    ) {
+        if let Some(child) = element_state.child_element.take() {
+            child.paint(cx);
+        }
+
+        if let Some(child_layout_id) = element_state.child_layout_id.take() {
+            element_state.child_bounds = Some(cx.layout_bounds(child_layout_id));
+        }
+
+        if let Some(menu) = element_state.menu_element.take() {
+            menu.paint(cx);
+
+            if let Some(child_bounds) = element_state.child_bounds {
+                let interactive_bounds = InteractiveBounds {
+                    bounds: child_bounds,
+                    stacking_order: cx.stacking_order().clone(),
+                };
+
+                // Mouse-downing outside the menu dismisses it, so we don't
+                // want a click on the toggle to re-open it.
+                cx.on_mouse_event(move |e: &MouseDownEvent, phase, cx| {
+                    if phase == DispatchPhase::Bubble
+                        && interactive_bounds.visibly_contains(&e.position, cx)
+                    {
+                        cx.stop_propagation()
+                    }
+                })
+            }
+        }
+    }
+}
+
+impl<M: ManagedView> IntoElement for PopoverMenu<M> {
+    type Element = Self;
+
+    fn element_id(&self) -> Option<gpui::ElementId> {
+        Some(self.id.clone())
+    }
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}

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

@@ -0,0 +1,185 @@
+use std::{cell::RefCell, rc::Rc};
+
+use gpui::{
+    overlay, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, Element, ElementId,
+    IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
+    View, VisualContext, WindowContext,
+};
+
+pub struct RightClickMenu<M: ManagedView> {
+    id: ElementId,
+    child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
+    menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
+    anchor: Option<AnchorCorner>,
+    attach: Option<AnchorCorner>,
+}
+
+impl<M: ManagedView> RightClickMenu<M> {
+    pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
+        self.menu_builder = Some(Rc::new(f));
+        self
+    }
+
+    pub fn trigger<E: IntoElement + 'static>(mut self, e: E) -> Self {
+        self.child_builder = Some(Box::new(move |_| e.into_any_element()));
+        self
+    }
+
+    /// anchor defines which corner of the menu to anchor to the attachment point
+    /// (by default the cursor position, but see attach)
+    pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
+        self.anchor = Some(anchor);
+        self
+    }
+
+    /// attach defines which corner of the handle to attach the menu's anchor to
+    pub fn attach(mut self, attach: AnchorCorner) -> Self {
+        self.attach = Some(attach);
+        self
+    }
+}
+
+pub fn right_click_menu<M: ManagedView>(id: impl Into<ElementId>) -> RightClickMenu<M> {
+    RightClickMenu {
+        id: id.into(),
+        child_builder: None,
+        menu_builder: None,
+        anchor: None,
+        attach: None,
+    }
+}
+
+pub struct MenuHandleState<M> {
+    menu: Rc<RefCell<Option<View<M>>>>,
+    position: Rc<RefCell<Point<Pixels>>>,
+    child_layout_id: Option<LayoutId>,
+    child_element: Option<AnyElement>,
+    menu_element: Option<AnyElement>,
+}
+
+impl<M: ManagedView> Element for RightClickMenu<M> {
+    type State = MenuHandleState<M>;
+
+    fn layout(
+        &mut self,
+        element_state: Option<Self::State>,
+        cx: &mut WindowContext,
+    ) -> (gpui::LayoutId, Self::State) {
+        let (menu, position) = if let Some(element_state) = element_state {
+            (element_state.menu, element_state.position)
+        } else {
+            (Rc::default(), Rc::default())
+        };
+
+        let mut menu_layout_id = None;
+
+        let menu_element = menu.borrow_mut().as_mut().map(|menu| {
+            let mut overlay = overlay().snap_to_window();
+            if let Some(anchor) = self.anchor {
+                overlay = overlay.anchor(anchor);
+            }
+            overlay = overlay.position(*position.borrow());
+
+            let mut element = overlay.child(menu.clone()).into_any();
+            menu_layout_id = Some(element.layout(cx));
+            element
+        });
+
+        let mut child_element = self
+            .child_builder
+            .take()
+            .map(|child_builder| (child_builder)(menu.borrow().is_some()));
+
+        let child_layout_id = child_element
+            .as_mut()
+            .map(|child_element| child_element.layout(cx));
+
+        let layout_id = cx.request_layout(
+            &gpui::Style::default(),
+            menu_layout_id.into_iter().chain(child_layout_id),
+        );
+
+        (
+            layout_id,
+            MenuHandleState {
+                menu,
+                position,
+                child_element,
+                child_layout_id,
+                menu_element,
+            },
+        )
+    }
+
+    fn paint(
+        self,
+        bounds: Bounds<gpui::Pixels>,
+        element_state: &mut Self::State,
+        cx: &mut WindowContext,
+    ) {
+        if let Some(child) = element_state.child_element.take() {
+            child.paint(cx);
+        }
+
+        if let Some(menu) = element_state.menu_element.take() {
+            menu.paint(cx);
+            return;
+        }
+
+        let Some(builder) = self.menu_builder else {
+            return;
+        };
+        let menu = element_state.menu.clone();
+        let position = element_state.position.clone();
+        let attach = self.attach.clone();
+        let child_layout_id = element_state.child_layout_id.clone();
+
+        cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
+            if phase == DispatchPhase::Bubble
+                && event.button == MouseButton::Right
+                && bounds.contains_point(&event.position)
+            {
+                cx.stop_propagation();
+                cx.prevent_default();
+
+                let new_menu = (builder)(cx);
+                let menu2 = menu.clone();
+                let previous_focus_handle = cx.focused();
+
+                cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
+                    if modal.focus_handle(cx).contains_focused(cx) {
+                        if previous_focus_handle.is_some() {
+                            cx.focus(&previous_focus_handle.as_ref().unwrap())
+                        }
+                    }
+                    *menu2.borrow_mut() = None;
+                    cx.notify();
+                })
+                .detach();
+                cx.focus_view(&new_menu);
+                *menu.borrow_mut() = Some(new_menu);
+
+                *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
+                    attach
+                        .unwrap()
+                        .corner(cx.layout_bounds(child_layout_id.unwrap()))
+                } else {
+                    cx.mouse_position()
+                };
+                cx.notify();
+            }
+        });
+    }
+}
+
+impl<M: ManagedView> IntoElement for RightClickMenu<M> {
+    type Element = Self;
+
+    fn element_id(&self) -> Option<gpui::ElementId> {
+        Some(self.id.clone())
+    }
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}

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

@@ -8,6 +8,7 @@ mod icon_button;
 mod keybinding;
 mod label;
 mod list;
+mod list_header;
 mod list_item;
 
 pub use avatar::*;
@@ -20,4 +21,5 @@ pub use icon_button::*;
 pub use keybinding::*;
 pub use label::*;
 pub use list::*;
+pub use list_header::*;
 pub use list_item::*;

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

@@ -1,7 +1,7 @@
 use gpui::{Div, Render};
 use story::Story;
 
-use crate::prelude::*;
+use crate::{prelude::*, Icon};
 use crate::{Button, ButtonStyle};
 
 pub struct ButtonStory;
@@ -14,6 +14,24 @@ impl Render for ButtonStory {
             .child(Story::title_for::<Button>())
             .child(Story::label("Default"))
             .child(Button::new("default_filled", "Click me"))
+            .child(Story::label("Selected"))
+            .child(Button::new("selected_filled", "Click me").selected(true))
+            .child(Story::label("Selected with `selected_label`"))
+            .child(
+                Button::new("selected_label_filled", "Click me")
+                    .selected(true)
+                    .selected_label("I have been selected"),
+            )
+            .child(Story::label("With `label_color`"))
+            .child(Button::new("filled_with_label_color", "Click me").color(Color::Created))
+            .child(Story::label("With `icon`"))
+            .child(Button::new("filled_with_icon", "Click me").icon(Icon::FileGit))
+            .child(Story::label("Selected with `icon`"))
+            .child(
+                Button::new("filled_and_selected_with_icon", "Click me")
+                    .selected(true)
+                    .icon(Icon::FileGit),
+            )
             .child(Story::label("Default (Subtle)"))
             .child(Button::new("default_subtle", "Click me").style(ButtonStyle::Subtle))
             .child(Story::label("Default (Transparent)"))

crates/ui2/src/components/stories/context_menu.rs 🔗

@@ -2,7 +2,7 @@ use gpui::{actions, Action, AnchorCorner, Div, Render, View};
 use story::Story;
 
 use crate::prelude::*;
-use crate::{menu_handle, ContextMenu, Label};
+use crate::{right_click_menu, ContextMenu, Label};
 
 actions!(PrintCurrentDate, PrintBestFood);
 
@@ -45,25 +45,13 @@ impl Render for ContextMenuStory {
                     .flex_col()
                     .justify_between()
                     .child(
-                        menu_handle("test2")
-                            .child(|is_open| {
-                                Label::new(if is_open {
-                                    "TOP LEFT"
-                                } else {
-                                    "RIGHT CLICK ME"
-                                })
-                            })
+                        right_click_menu("test2")
+                            .trigger(Label::new("TOP LEFT"))
                             .menu(move |cx| build_menu(cx, "top left")),
                     )
                     .child(
-                        menu_handle("test1")
-                            .child(|is_open| {
-                                Label::new(if is_open {
-                                    "BOTTOM LEFT"
-                                } else {
-                                    "RIGHT CLICK ME"
-                                })
-                            })
+                        right_click_menu("test1")
+                            .trigger(Label::new("BOTTOM LEFT"))
                             .anchor(AnchorCorner::BottomLeft)
                             .attach(AnchorCorner::TopLeft)
                             .menu(move |cx| build_menu(cx, "bottom left")),
@@ -75,26 +63,14 @@ impl Render for ContextMenuStory {
                     .flex_col()
                     .justify_between()
                     .child(
-                        menu_handle("test3")
-                            .child(|is_open| {
-                                Label::new(if is_open {
-                                    "TOP RIGHT"
-                                } else {
-                                    "RIGHT CLICK ME"
-                                })
-                            })
+                        right_click_menu("test3")
+                            .trigger(Label::new("TOP RIGHT"))
                             .anchor(AnchorCorner::TopRight)
                             .menu(move |cx| build_menu(cx, "top right")),
                     )
                     .child(
-                        menu_handle("test4")
-                            .child(|is_open| {
-                                Label::new(if is_open {
-                                    "BOTTOM RIGHT"
-                                } else {
-                                    "RIGHT CLICK ME"
-                                })
-                            })
+                        right_click_menu("test4")
+                            .trigger(Label::new("BOTTOM RIGHT"))
                             .anchor(AnchorCorner::BottomRight)
                             .attach(AnchorCorner::TopRight)
                             .menu(move |cx| build_menu(cx, "bottom right")),

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

@@ -20,6 +20,14 @@ impl Render for IconButtonStory {
                     .w_8()
                     .child(IconButton::new("icon_a", Icon::Hash).selected(true)),
             )
+            .child(Story::label("Selected with `selected_icon`"))
+            .child(
+                div().w_8().child(
+                    IconButton::new("icon_a", Icon::AudioOn)
+                        .selected(true)
+                        .selected_icon(Icon::AudioOff),
+                ),
+            )
             .child(Story::label("Disabled"))
             .child(
                 div()

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

@@ -22,12 +22,12 @@ impl Render for ListStory {
             .child(Story::label("With sections"))
             .child(
                 List::new()
-                    .child(ListHeader::new("Fruits"))
+                    .header(ListHeader::new("Produce"))
+                    .child(ListSubHeader::new("Fruits"))
                     .child(ListItem::new("apple").child("Apple"))
                     .child(ListItem::new("banana").child("Banana"))
                     .child(ListItem::new("cherry").child("Cherry"))
                     .child(ListSeparator)
-                    .child(ListHeader::new("Vegetables"))
                     .child(ListSubHeader::new("Root Vegetables"))
                     .child(ListItem::new("carrot").child("Carrot"))
                     .child(ListItem::new("potato").child("Potato"))

crates/ui2/src/components/stories/list_header.rs 🔗

@@ -0,0 +1,33 @@
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::{prelude::*, IconButton};
+use crate::{Icon, ListHeader};
+
+pub struct ListHeaderStory;
+
+impl Render for ListHeaderStory {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        Story::container()
+            .child(Story::title_for::<ListHeader>())
+            .child(Story::label("Default"))
+            .child(ListHeader::new("Section 1"))
+            .child(Story::label("With left icon"))
+            .child(ListHeader::new("Section 2").left_icon(Icon::Bell))
+            .child(Story::label("With left icon and meta"))
+            .child(
+                ListHeader::new("Section 3")
+                    .left_icon(Icon::BellOff)
+                    .meta(IconButton::new("action_1", Icon::Bolt)),
+            )
+            .child(Story::label("With multiple meta"))
+            .child(
+                ListHeader::new("Section 4")
+                    .meta(IconButton::new("action_1", Icon::Bolt))
+                    .meta(IconButton::new("action_2", Icon::ExclamationTriangle))
+                    .meta(IconButton::new("action_3", Icon::Plus)),
+            )
+    }
+}

crates/ui2/src/styled_ext.rs 🔗

@@ -1,4 +1,6 @@
 use gpui::{px, Styled, WindowContext};
+use settings::Settings;
+use theme::ThemeSettings;
 
 use crate::prelude::*;
 use crate::{ElevationIndex, UITextSize};
@@ -60,6 +62,18 @@ pub trait StyledExt: Styled + Sized {
         self.text_size(size)
     }
 
+    /// The font size for buffer text.
+    ///
+    /// Retrieves the default font size, or the user's custom font size if set.
+    ///
+    /// This should only be used for text that is displayed in a buffer,
+    /// or other places that text needs to match the user's buffer font size.
+    fn text_buffer(self, cx: &mut WindowContext) -> Self {
+        let settings = ThemeSettings::get_global(cx);
+
+        self.text_size(settings.buffer_font_size)
+    }
+
     /// The [`Surface`](ui2::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements
     ///
     /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`

crates/util/src/paths.rs 🔗

@@ -219,9 +219,11 @@ impl PathMatcher {
     }
 
     pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
-        other.as_ref().starts_with(&self.maybe_path)
-            || self.glob.is_match(&other)
-            || self.check_with_end_separator(other.as_ref())
+        let other_path = other.as_ref();
+        other_path.starts_with(&self.maybe_path)
+            || other_path.ends_with(&self.maybe_path)
+            || self.glob.is_match(other_path)
+            || self.check_with_end_separator(other_path)
     }
 
     fn check_with_end_separator(&self, path: &Path) -> bool {
@@ -418,4 +420,14 @@ mod tests {
             "Path matcher {path_matcher} should match {path:?}"
         );
     }
+
+    #[test]
+    fn project_search() {
+        let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
+        let path_matcher = PathMatcher::new("**/node_modules/**").unwrap();
+        assert!(
+            path_matcher.is_match(&path),
+            "Path matcher {path_matcher} should match {path:?}"
+        );
+    }
 }

crates/workspace/src/pane.rs 🔗

@@ -481,18 +481,21 @@ impl Pane {
 
     pub(crate) fn open_item(
         &mut self,
-        project_entry_id: ProjectEntryId,
+        project_entry_id: Option<ProjectEntryId>,
         focus_item: bool,
         cx: &mut ViewContext<Self>,
         build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
     ) -> Box<dyn ItemHandle> {
         let mut existing_item = None;
-        for (index, item) in self.items.iter().enumerate() {
-            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id]
-            {
-                let item = item.boxed_clone();
-                existing_item = Some((index, item));
-                break;
+        if let Some(project_entry_id) = project_entry_id {
+            for (index, item) in self.items.iter().enumerate() {
+                if item.is_singleton(cx)
+                    && item.project_entry_ids(cx).as_slice() == [project_entry_id]
+                {
+                    let item = item.boxed_clone();
+                    existing_item = Some((index, item));
+                    break;
+                }
             }
         }
 

crates/workspace/src/workspace.rs 🔗

@@ -2129,13 +2129,13 @@ impl Workspace {
         })
     }
 
-    pub(crate) fn load_path(
+    fn load_path(
         &mut self,
         path: ProjectPath,
         cx: &mut ViewContext<Self>,
     ) -> Task<
         Result<(
-            ProjectEntryId,
+            Option<ProjectEntryId>,
             impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
         )>,
     > {

crates/workspace2/Cargo.toml 🔗

@@ -10,33 +10,33 @@ doctest = false
 
 [features]
 test-support = [
-    "call2/test-support",
-    "client2/test-support",
-    "project2/test-support",
-    "settings2/test-support",
+    "call/test-support",
+    "client/test-support",
+    "project/test-support",
+    "settings/test-support",
     "gpui/test-support",
-    "fs2/test-support"
+    "fs/test-support"
 ]
 
 [dependencies]
-db2 = { path = "../db2" }
-client2 = { path = "../client2" }
+db = { path = "../db2", package = "db2" }
+call = { path = "../call2", package = "call2" }
+client = { path = "../client2", package = "client2" }
 collections = { path = "../collections" }
 # context_menu = { path = "../context_menu" }
-fs2 = { path = "../fs2" }
+fs = { path = "../fs2", package = "fs2" }
 gpui = { package = "gpui2", path = "../gpui2" }
-install_cli2 = { path = "../install_cli2" }
-language2 = { path = "../language2" }
+install_cli = { path = "../install_cli2", package = "install_cli2" }
+language = { path = "../language2", package = "language2" }
 #menu = { path = "../menu" }
 node_runtime = { path = "../node_runtime" }
-project2 = { path = "../project2" }
-settings2 = { path = "../settings2" }
-terminal2 = { path = "../terminal2" }
-theme2 = { path = "../theme2" }
+project = { path = "../project2", package = "project2" }
+settings = { path = "../settings2", package = "settings2" }
+terminal = { path = "../terminal2", package = "terminal2" }
+theme = { path = "../theme2", package = "theme2" }
 util = { path = "../util" }
 ui = { package = "ui2", path = "../ui2" }
 
-async-trait.workspace = true
 async-recursion = "1.0.0"
 itertools = "0.10"
 bincode = "1.2.1"
@@ -54,13 +54,13 @@ smallvec.workspace = true
 uuid.workspace = true
 
 [dev-dependencies]
-call2 = { path = "../call2", features = ["test-support"] }
-client2 = { path = "../client2", features = ["test-support"] }
-gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
-project2 = { path = "../project2", features = ["test-support"] }
-settings2 = { path = "../settings2", features = ["test-support"] }
-fs2 = { path = "../fs2", features = ["test-support"] }
-db2 = { path = "../db2", features = ["test-support"] }
+call = { path = "../call2", package = "call2", features = ["test-support"] }
+client = { path = "../client2", package = "client2", features = ["test-support"] }
+gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
+project = { path = "../project2", package = "project2", features = ["test-support"] }
+settings = { path = "../settings2", package = "settings2", features = ["test-support"] }
+fs = { path = "../fs2", package = "fs2", features = ["test-support"] }
+db = { path = "../db2", package = "db2", features = ["test-support"] }
 
 indoc.workspace = true
 env_logger.workspace = true

crates/workspace2/src/dock.rs 🔗

@@ -7,8 +7,8 @@ use gpui::{
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
-use ui::prelude::*;
-use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip};
+use ui::{h_stack, ContextMenu, IconButton, Tooltip};
+use ui::{prelude::*, right_click_menu};
 
 pub enum PanelEvent {
     ChangePosition,
@@ -702,7 +702,7 @@ impl Render for PanelButtons {
                 };
 
                 Some(
-                    menu_handle(name)
+                    right_click_menu(name)
                         .menu(move |cx| {
                             const POSITIONS: [DockPosition; 3] = [
                                 DockPosition::Left,
@@ -726,14 +726,14 @@ impl Render for PanelButtons {
                         })
                         .anchor(menu_anchor)
                         .attach(menu_attach)
-                        .child(move |_is_open| {
+                        .trigger(
                             IconButton::new(name, icon)
                                 .selected(is_active_button)
                                 .action(action.boxed_clone())
                                 .tooltip(move |cx| {
                                     Tooltip::for_action(tooltip.clone(), &*action, cx)
-                                })
-                        }),
+                                }),
+                        ),
                 )
             });
 

crates/workspace2/src/item.rs 🔗

@@ -7,7 +7,7 @@ use crate::{
     ViewId, Workspace, WorkspaceId,
 };
 use anyhow::Result;
-use client2::{
+use client::{
     proto::{self, PeerId},
     Client,
 };
@@ -16,10 +16,10 @@ use gpui::{
     HighlightStyle, Model, Pixels, Point, SharedString, Task, View, ViewContext, WeakView,
     WindowContext,
 };
-use project2::{Project, ProjectEntryId, ProjectPath};
+use project::{Project, ProjectEntryId, ProjectPath};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings2::Settings;
+use settings::Settings;
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -33,7 +33,7 @@ use std::{
     },
     time::Duration,
 };
-use theme2::Theme;
+use theme::Theme;
 
 #[derive(Deserialize)]
 pub struct ItemSettings {
@@ -110,7 +110,7 @@ pub trait Item: FocusableView + EventEmitter<ItemEvent> {
     fn for_each_project_item(
         &self,
         _: &AppContext,
-        _: &mut dyn FnMut(EntityId, &dyn project2::Item),
+        _: &mut dyn FnMut(EntityId, &dyn project::Item),
     ) {
     }
     fn is_singleton(&self, _cx: &AppContext) -> bool {
@@ -222,7 +222,7 @@ pub trait ItemHandle: 'static + Send {
     fn for_each_project_item(
         &self,
         _: &AppContext,
-        _: &mut dyn FnMut(EntityId, &dyn project2::Item),
+        _: &mut dyn FnMut(EntityId, &dyn project::Item),
     );
     fn is_singleton(&self, cx: &AppContext) -> bool;
     fn boxed_clone(&self) -> Box<dyn ItemHandle>;
@@ -347,7 +347,7 @@ impl<T: Item> ItemHandle for View<T> {
     fn for_each_project_item(
         &self,
         cx: &AppContext,
-        f: &mut dyn FnMut(EntityId, &dyn project2::Item),
+        f: &mut dyn FnMut(EntityId, &dyn project::Item),
     ) {
         self.read(cx).for_each_project_item(cx, f)
     }
@@ -375,6 +375,7 @@ impl<T: Item> ItemHandle for View<T> {
         pane: View<Pane>,
         cx: &mut ViewContext<Workspace>,
     ) {
+        let weak_item = self.downgrade();
         let history = pane.read(cx).nav_history_for_item(self);
         self.update(cx, |this, cx| {
             this.set_nav_history(history, cx);
@@ -491,16 +492,15 @@ impl<T: Item> ItemHandle for View<T> {
                     }
                 }));
 
-            // todo!()
-            // cx.observe_focus(self, move |workspace, item, focused, cx| {
-            //     if !focused
-            //         && WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange
-            //     {
-            //         Pane::autosave_item(&item, workspace.project.clone(), cx)
-            //             .detach_and_log_err(cx);
-            //     }
-            // })
-            // .detach();
+            cx.on_blur(&self.focus_handle(cx), move |workspace, cx| {
+                if WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange {
+                    if let Some(item) = weak_item.upgrade() {
+                        Pane::autosave_item(&item, workspace.project.clone(), cx)
+                            .detach_and_log_err(cx);
+                    }
+                }
+            })
+            .detach();
 
             let item_id = self.item_id();
             cx.observe_release(self, move |workspace, _, _| {
@@ -640,7 +640,7 @@ impl<T: Item> WeakItemHandle for WeakView<T> {
 }
 
 pub trait ProjectItem: Item {
-    type Item: project2::Item;
+    type Item: project::Item;
 
     fn for_project_item(
         project: Model<Project>,
@@ -662,19 +662,19 @@ pub trait FollowableEvents {
 pub trait FollowableItem: Item {
     type FollowableEvent: FollowableEvents;
     fn remote_id(&self) -> Option<ViewId>;
-    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+    fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
     fn from_state_proto(
         pane: View<Pane>,
         project: View<Workspace>,
         id: ViewId,
         state: &mut Option<proto::view::Variant>,
-        cx: &mut AppContext,
+        cx: &mut WindowContext,
     ) -> Option<Task<Result<View<Self>>>>;
     fn add_event_to_update_proto(
         &self,
         event: &Self::FollowableEvent,
         update: &mut Option<proto::update_view::Variant>,
-        cx: &AppContext,
+        cx: &WindowContext,
     ) -> bool;
     fn apply_update_proto(
         &mut self,
@@ -682,20 +682,20 @@ pub trait FollowableItem: Item {
         message: proto::update_view::Variant,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>>;
-    fn is_project_item(&self, cx: &AppContext) -> bool;
+    fn is_project_item(&self, cx: &WindowContext) -> bool;
 
     fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
 }
 
 pub trait FollowableItemHandle: ItemHandle {
-    fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
+    fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId>;
     fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
-    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+    fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
     fn add_event_to_update_proto(
         &self,
         event: &dyn Any,
         update: &mut Option<proto::update_view::Variant>,
-        cx: &AppContext,
+        cx: &WindowContext,
     ) -> bool;
     fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent>;
     fn apply_update_proto(
@@ -704,11 +704,11 @@ pub trait FollowableItemHandle: ItemHandle {
         message: proto::update_view::Variant,
         cx: &mut WindowContext,
     ) -> Task<Result<()>>;
-    fn is_project_item(&self, cx: &AppContext) -> bool;
+    fn is_project_item(&self, cx: &WindowContext) -> bool;
 }
 
 impl<T: FollowableItem> FollowableItemHandle for View<T> {
-    fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId> {
+    fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId> {
         self.read(cx).remote_id().or_else(|| {
             client.peer_id().map(|creator| ViewId {
                 creator,
@@ -721,7 +721,7 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
         self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
     }
 
-    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+    fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
         self.read(cx).to_state_proto(cx)
     }
 
@@ -729,7 +729,7 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
         &self,
         event: &dyn Any,
         update: &mut Option<proto::update_view::Variant>,
-        cx: &AppContext,
+        cx: &WindowContext,
     ) -> bool {
         if let Some(event) = event.downcast_ref() {
             self.read(cx).add_event_to_update_proto(event, update, cx)
@@ -754,305 +754,315 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
         self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
     }
 
-    fn is_project_item(&self, cx: &AppContext) -> bool {
+    fn is_project_item(&self, cx: &WindowContext) -> bool {
         self.read(cx).is_project_item(cx)
     }
 }
 
-// #[cfg(any(test, feature = "test-support"))]
-// pub mod test {
-//     use super::{Item, ItemEvent};
-//     use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
-//     use gpui::{
-//         elements::Empty, AnyElement, AppContext, Element, Entity, Model, Task, View,
-//         ViewContext, View, WeakViewHandle,
-//     };
-//     use project2::{Project, ProjectEntryId, ProjectPath, WorktreeId};
-//     use smallvec::SmallVec;
-//     use std::{any::Any, borrow::Cow, cell::Cell, path::Path};
-
-//     pub struct TestProjectItem {
-//         pub entry_id: Option<ProjectEntryId>,
-//         pub project_path: Option<ProjectPath>,
-//     }
-
-//     pub struct TestItem {
-//         pub workspace_id: WorkspaceId,
-//         pub state: String,
-//         pub label: String,
-//         pub save_count: usize,
-//         pub save_as_count: usize,
-//         pub reload_count: usize,
-//         pub is_dirty: bool,
-//         pub is_singleton: bool,
-//         pub has_conflict: bool,
-//         pub project_items: Vec<Model<TestProjectItem>>,
-//         pub nav_history: Option<ItemNavHistory>,
-//         pub tab_descriptions: Option<Vec<&'static str>>,
-//         pub tab_detail: Cell<Option<usize>>,
-//     }
-
-//     impl Entity for TestProjectItem {
-//         type Event = ();
-//     }
-
-//     impl project2::Item for TestProjectItem {
-//         fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
-//             self.entry_id
-//         }
-
-//         fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
-//             self.project_path.clone()
-//         }
-//     }
-
-//     pub enum TestItemEvent {
-//         Edit,
-//     }
-
-//     impl Clone for TestItem {
-//         fn clone(&self) -> Self {
-//             Self {
-//                 state: self.state.clone(),
-//                 label: self.label.clone(),
-//                 save_count: self.save_count,
-//                 save_as_count: self.save_as_count,
-//                 reload_count: self.reload_count,
-//                 is_dirty: self.is_dirty,
-//                 is_singleton: self.is_singleton,
-//                 has_conflict: self.has_conflict,
-//                 project_items: self.project_items.clone(),
-//                 nav_history: None,
-//                 tab_descriptions: None,
-//                 tab_detail: Default::default(),
-//                 workspace_id: self.workspace_id,
-//             }
-//         }
-//     }
-
-//     impl TestProjectItem {
-//         pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model<Self> {
-//             let entry_id = Some(ProjectEntryId::from_proto(id));
-//             let project_path = Some(ProjectPath {
-//                 worktree_id: WorktreeId::from_usize(0),
-//                 path: Path::new(path).into(),
-//             });
-//             cx.add_model(|_| Self {
-//                 entry_id,
-//                 project_path,
-//             })
-//         }
-
-//         pub fn new_untitled(cx: &mut AppContext) -> Model<Self> {
-//             cx.add_model(|_| Self {
-//                 project_path: None,
-//                 entry_id: None,
-//             })
-//         }
-//     }
-
-//     impl TestItem {
-//         pub fn new() -> Self {
-//             Self {
-//                 state: String::new(),
-//                 label: String::new(),
-//                 save_count: 0,
-//                 save_as_count: 0,
-//                 reload_count: 0,
-//                 is_dirty: false,
-//                 has_conflict: false,
-//                 project_items: Vec::new(),
-//                 is_singleton: true,
-//                 nav_history: None,
-//                 tab_descriptions: None,
-//                 tab_detail: Default::default(),
-//                 workspace_id: 0,
-//             }
-//         }
-
-//         pub fn new_deserialized(id: WorkspaceId) -> Self {
-//             let mut this = Self::new();
-//             this.workspace_id = id;
-//             this
-//         }
-
-//         pub fn with_label(mut self, state: &str) -> Self {
-//             self.label = state.to_string();
-//             self
-//         }
-
-//         pub fn with_singleton(mut self, singleton: bool) -> Self {
-//             self.is_singleton = singleton;
-//             self
-//         }
-
-//         pub fn with_dirty(mut self, dirty: bool) -> Self {
-//             self.is_dirty = dirty;
-//             self
-//         }
-
-//         pub fn with_conflict(mut self, has_conflict: bool) -> Self {
-//             self.has_conflict = has_conflict;
-//             self
-//         }
-
-//         pub fn with_project_items(mut self, items: &[Model<TestProjectItem>]) -> Self {
-//             self.project_items.clear();
-//             self.project_items.extend(items.iter().cloned());
-//             self
-//         }
-
-//         pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
-//             self.push_to_nav_history(cx);
-//             self.state = state;
-//         }
-
-//         fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
-//             if let Some(history) = &mut self.nav_history {
-//                 history.push(Some(Box::new(self.state.clone())), cx);
-//             }
-//         }
-//     }
-
-//     impl Entity for TestItem {
-//         type Event = TestItemEvent;
-//     }
-
-//     impl View for TestItem {
-//         fn ui_name() -> &'static str {
-//             "TestItem"
-//         }
-
-//         fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
-//             Empty::new().into_any()
-//         }
-//     }
-
-//     impl Item for TestItem {
-//         fn tab_description(&self, detail: usize, _: &AppContext) -> Option<Cow<str>> {
-//             self.tab_descriptions.as_ref().and_then(|descriptions| {
-//                 let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
-//                 Some(description.into())
-//             })
-//         }
-
-//         fn tab_content<V: 'static>(
-//             &self,
-//             detail: Option<usize>,
-//             _: &theme2::Tab,
-//             _: &AppContext,
-//         ) -> AnyElement<V> {
-//             self.tab_detail.set(detail);
-//             Empty::new().into_any()
-//         }
-
-//         fn for_each_project_item(
-//             &self,
-//             cx: &AppContext,
-//             f: &mut dyn FnMut(usize, &dyn project2::Item),
-//         ) {
-//             self.project_items
-//                 .iter()
-//                 .for_each(|item| f(item.id(), item.read(cx)))
-//         }
-
-// fn is_singleton(&self, _: &AppContext) -> bool {
-//     self.is_singleton
-// }
-
-//         fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
-//             self.nav_history = Some(history);
-//         }
-
-//         fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
-//             let state = *state.downcast::<String>().unwrap_or_default();
-//             if state != self.state {
-//                 self.state = state;
-//                 true
-//             } else {
-//                 false
-//             }
-//         }
-
-//         fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
-//             self.push_to_nav_history(cx);
-//         }
-
-//         fn clone_on_split(
-//             &self,
-//             _workspace_id: WorkspaceId,
-//             _: &mut ViewContext<Self>,
-//         ) -> Option<Self>
-//         where
-//             Self: Sized,
-//         {
-//             Some(self.clone())
-//         }
-
-//         fn is_dirty(&self, _: &AppContext) -> bool {
-//             self.is_dirty
-//         }
-
-//         fn has_conflict(&self, _: &AppContext) -> bool {
-//             self.has_conflict
-//         }
-
-//         fn can_save(&self, cx: &AppContext) -> bool {
-//             !self.project_items.is_empty()
-//                 && self
-//                     .project_items
-//                     .iter()
-//                     .all(|item| item.read(cx).entry_id.is_some())
-//         }
-
-//         fn save(
-//             &mut self,
-//             _: Model<Project>,
-//             _: &mut ViewContext<Self>,
-//         ) -> Task<anyhow::Result<()>> {
-//             self.save_count += 1;
-//             self.is_dirty = false;
-//             Task::ready(Ok(()))
-//         }
-
-//         fn save_as(
-//             &mut self,
-//             _: Model<Project>,
-//             _: std::path::PathBuf,
-//             _: &mut ViewContext<Self>,
-//         ) -> Task<anyhow::Result<()>> {
-//             self.save_as_count += 1;
-//             self.is_dirty = false;
-//             Task::ready(Ok(()))
-//         }
-
-//         fn reload(
-//             &mut self,
-//             _: Model<Project>,
-//             _: &mut ViewContext<Self>,
-//         ) -> Task<anyhow::Result<()>> {
-//             self.reload_count += 1;
-//             self.is_dirty = false;
-//             Task::ready(Ok(()))
-//         }
-
-//         fn to_item_events(_: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
-//             [ItemEvent::UpdateTab, ItemEvent::Edit].into()
-//         }
-
-//         fn serialized_item_kind() -> Option<&'static str> {
-//             Some("TestItem")
-//         }
-
-//         fn deserialize(
-//             _project: Model<Project>,
-//             _workspace: WeakViewHandle<Workspace>,
-//             workspace_id: WorkspaceId,
-//             _item_id: ItemId,
-//             cx: &mut ViewContext<Pane>,
-//         ) -> Task<anyhow::Result<View<Self>>> {
-//             let view = cx.add_view(|_cx| Self::new_deserialized(workspace_id));
-//             Task::Ready(Some(anyhow::Ok(view)))
-//         }
-//     }
-// }
+#[cfg(any(test, feature = "test-support"))]
+pub mod test {
+    use super::{Item, ItemEvent};
+    use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
+    use gpui::{
+        AnyElement, AppContext, Context as _, Div, EntityId, EventEmitter, FocusableView,
+        IntoElement, Model, Render, SharedString, Task, View, ViewContext, VisualContext, WeakView,
+    };
+    use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
+    use std::{any::Any, cell::Cell, path::Path};
+
+    pub struct TestProjectItem {
+        pub entry_id: Option<ProjectEntryId>,
+        pub project_path: Option<ProjectPath>,
+    }
+
+    pub struct TestItem {
+        pub workspace_id: WorkspaceId,
+        pub state: String,
+        pub label: String,
+        pub save_count: usize,
+        pub save_as_count: usize,
+        pub reload_count: usize,
+        pub is_dirty: bool,
+        pub is_singleton: bool,
+        pub has_conflict: bool,
+        pub project_items: Vec<Model<TestProjectItem>>,
+        pub nav_history: Option<ItemNavHistory>,
+        pub tab_descriptions: Option<Vec<&'static str>>,
+        pub tab_detail: Cell<Option<usize>>,
+        focus_handle: gpui::FocusHandle,
+    }
+
+    impl project::Item for TestProjectItem {
+        fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
+            self.entry_id
+        }
+
+        fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
+            self.project_path.clone()
+        }
+    }
+
+    pub enum TestItemEvent {
+        Edit,
+    }
+
+    // impl Clone for TestItem {
+    //     fn clone(&self) -> Self {
+    //         Self {
+    //             state: self.state.clone(),
+    //             label: self.label.clone(),
+    //             save_count: self.save_count,
+    //             save_as_count: self.save_as_count,
+    //             reload_count: self.reload_count,
+    //             is_dirty: self.is_dirty,
+    //             is_singleton: self.is_singleton,
+    //             has_conflict: self.has_conflict,
+    //             project_items: self.project_items.clone(),
+    //             nav_history: None,
+    //             tab_descriptions: None,
+    //             tab_detail: Default::default(),
+    //             workspace_id: self.workspace_id,
+    //             focus_handle: self.focus_handle.clone(),
+    //         }
+    //     }
+    // }
+
+    impl TestProjectItem {
+        pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model<Self> {
+            let entry_id = Some(ProjectEntryId::from_proto(id));
+            let project_path = Some(ProjectPath {
+                worktree_id: WorktreeId::from_usize(0),
+                path: Path::new(path).into(),
+            });
+            cx.build_model(|_| Self {
+                entry_id,
+                project_path,
+            })
+        }
+
+        pub fn new_untitled(cx: &mut AppContext) -> Model<Self> {
+            cx.build_model(|_| Self {
+                project_path: None,
+                entry_id: None,
+            })
+        }
+    }
+
+    impl TestItem {
+        pub fn new(cx: &mut ViewContext<Self>) -> Self {
+            Self {
+                state: String::new(),
+                label: String::new(),
+                save_count: 0,
+                save_as_count: 0,
+                reload_count: 0,
+                is_dirty: false,
+                has_conflict: false,
+                project_items: Vec::new(),
+                is_singleton: true,
+                nav_history: None,
+                tab_descriptions: None,
+                tab_detail: Default::default(),
+                workspace_id: 0,
+                focus_handle: cx.focus_handle(),
+            }
+        }
+
+        pub fn new_deserialized(id: WorkspaceId, cx: &mut ViewContext<Self>) -> Self {
+            let mut this = Self::new(cx);
+            this.workspace_id = id;
+            this
+        }
+
+        pub fn with_label(mut self, state: &str) -> Self {
+            self.label = state.to_string();
+            self
+        }
+
+        pub fn with_singleton(mut self, singleton: bool) -> Self {
+            self.is_singleton = singleton;
+            self
+        }
+
+        pub fn with_dirty(mut self, dirty: bool) -> Self {
+            self.is_dirty = dirty;
+            self
+        }
+
+        pub fn with_conflict(mut self, has_conflict: bool) -> Self {
+            self.has_conflict = has_conflict;
+            self
+        }
+
+        pub fn with_project_items(mut self, items: &[Model<TestProjectItem>]) -> Self {
+            self.project_items.clear();
+            self.project_items.extend(items.iter().cloned());
+            self
+        }
+
+        pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
+            self.push_to_nav_history(cx);
+            self.state = state;
+        }
+
+        fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
+            if let Some(history) = &mut self.nav_history {
+                history.push(Some(Box::new(self.state.clone())), cx);
+            }
+        }
+    }
+
+    impl Render for TestItem {
+        type Element = Div;
+
+        fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
+            gpui::div()
+        }
+    }
+
+    impl EventEmitter<ItemEvent> for TestItem {}
+
+    impl FocusableView for TestItem {
+        fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+            self.focus_handle.clone()
+        }
+    }
+
+    impl Item for TestItem {
+        fn tab_description(&self, detail: usize, _: &AppContext) -> Option<SharedString> {
+            self.tab_descriptions.as_ref().and_then(|descriptions| {
+                let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
+                Some(description.into())
+            })
+        }
+
+        fn tab_content(
+            &self,
+            detail: Option<usize>,
+            cx: &ui::prelude::WindowContext,
+        ) -> AnyElement {
+            self.tab_detail.set(detail);
+            gpui::div().into_any_element()
+        }
+
+        fn for_each_project_item(
+            &self,
+            cx: &AppContext,
+            f: &mut dyn FnMut(EntityId, &dyn project::Item),
+        ) {
+            self.project_items
+                .iter()
+                .for_each(|item| f(item.entity_id(), item.read(cx)))
+        }
+
+        fn is_singleton(&self, _: &AppContext) -> bool {
+            self.is_singleton
+        }
+
+        fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
+            self.nav_history = Some(history);
+        }
+
+        fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
+            let state = *state.downcast::<String>().unwrap_or_default();
+            if state != self.state {
+                self.state = state;
+                true
+            } else {
+                false
+            }
+        }
+
+        fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+            self.push_to_nav_history(cx);
+        }
+
+        fn clone_on_split(
+            &self,
+            _workspace_id: WorkspaceId,
+            cx: &mut ViewContext<Self>,
+        ) -> Option<View<Self>>
+        where
+            Self: Sized,
+        {
+            Some(cx.build_view(|cx| Self {
+                state: self.state.clone(),
+                label: self.label.clone(),
+                save_count: self.save_count,
+                save_as_count: self.save_as_count,
+                reload_count: self.reload_count,
+                is_dirty: self.is_dirty,
+                is_singleton: self.is_singleton,
+                has_conflict: self.has_conflict,
+                project_items: self.project_items.clone(),
+                nav_history: None,
+                tab_descriptions: None,
+                tab_detail: Default::default(),
+                workspace_id: self.workspace_id,
+                focus_handle: cx.focus_handle(),
+            }))
+        }
+
+        fn is_dirty(&self, _: &AppContext) -> bool {
+            self.is_dirty
+        }
+
+        fn has_conflict(&self, _: &AppContext) -> bool {
+            self.has_conflict
+        }
+
+        fn can_save(&self, cx: &AppContext) -> bool {
+            !self.project_items.is_empty()
+                && self
+                    .project_items
+                    .iter()
+                    .all(|item| item.read(cx).entry_id.is_some())
+        }
+
+        fn save(
+            &mut self,
+            _: Model<Project>,
+            _: &mut ViewContext<Self>,
+        ) -> Task<anyhow::Result<()>> {
+            self.save_count += 1;
+            self.is_dirty = false;
+            Task::ready(Ok(()))
+        }
+
+        fn save_as(
+            &mut self,
+            _: Model<Project>,
+            _: std::path::PathBuf,
+            _: &mut ViewContext<Self>,
+        ) -> Task<anyhow::Result<()>> {
+            self.save_as_count += 1;
+            self.is_dirty = false;
+            Task::ready(Ok(()))
+        }
+
+        fn reload(
+            &mut self,
+            _: Model<Project>,
+            _: &mut ViewContext<Self>,
+        ) -> Task<anyhow::Result<()>> {
+            self.reload_count += 1;
+            self.is_dirty = false;
+            Task::ready(Ok(()))
+        }
+
+        fn serialized_item_kind() -> Option<&'static str> {
+            Some("TestItem")
+        }
+
+        fn deserialize(
+            _project: Model<Project>,
+            _workspace: WeakView<Workspace>,
+            workspace_id: WorkspaceId,
+            _item_id: ItemId,
+            cx: &mut ViewContext<Pane>,
+        ) -> Task<anyhow::Result<View<Self>>> {
+            let view = cx.build_view(|cx| Self::new_deserialized(workspace_id, cx));
+            Task::Ready(Some(anyhow::Ok(view)))
+        }
+    }
+}

crates/workspace2/src/notifications.rs 🔗

@@ -135,24 +135,22 @@ impl Workspace {
     }
 
     pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
-        todo!()
-        // self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
-        // self.show_notification(toast.id, cx, |cx| {
-        //     cx.add_view(|_cx| match toast.on_click.as_ref() {
-        //         Some((click_msg, on_click)) => {
-        //             let on_click = on_click.clone();
-        //             simple_message_notification::MessageNotification::new(toast.msg.clone())
-        //                 .with_click_message(click_msg.clone())
-        //                 .on_click(move |cx| on_click(cx))
-        //         }
-        //         None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
-        //     })
-        // })
+        self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
+        self.show_notification(toast.id, cx, |cx| {
+            cx.build_view(|_cx| match toast.on_click.as_ref() {
+                Some((click_msg, on_click)) => {
+                    let on_click = on_click.clone();
+                    simple_message_notification::MessageNotification::new(toast.msg.clone())
+                        .with_click_message(click_msg.clone())
+                        .on_click(move |cx| on_click(cx))
+                }
+                None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
+            })
+        })
     }
 
     pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
-        todo!()
-        // self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
+        self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
     }
 
     fn dismiss_notification_internal(
@@ -179,33 +177,10 @@ pub mod simple_message_notification {
         ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
         ViewContext,
     };
-    use serde::Deserialize;
-    use std::{borrow::Cow, sync::Arc};
+    use std::sync::Arc;
     use ui::prelude::*;
     use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
 
-    #[derive(Clone, Default, Deserialize, PartialEq)]
-    pub struct OsOpen(pub Cow<'static, str>);
-
-    impl OsOpen {
-        pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
-            OsOpen(url.into())
-        }
-    }
-
-    // todo!()
-    //     impl_actions!(message_notifications, [OsOpen]);
-    //
-    // todo!()
-    //     pub fn init(cx: &mut AppContext) {
-    //         cx.add_action(MessageNotification::dismiss);
-    //         cx.add_action(
-    //             |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
-    //                 cx.platform().open_url(open_action.0.as_ref());
-    //             },
-    //         )
-    //     }
-
     enum NotificationMessage {
         Text(SharedString),
         Element(fn(TextStyle, &AppContext) -> AnyElement),
@@ -213,7 +188,7 @@ pub mod simple_message_notification {
 
     pub struct MessageNotification {
         message: NotificationMessage,
-        on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>) + Send + Sync>>,
+        on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
         click_message: Option<SharedString>,
     }
 
@@ -252,7 +227,7 @@ pub mod simple_message_notification {
 
         pub fn on_click<F>(mut self, on_click: F) -> Self
         where
-            F: 'static + Send + Sync + Fn(&mut ViewContext<Self>),
+            F: 'static + Fn(&mut ViewContext<Self>),
         {
             self.on_click = Some(Arc::new(on_click));
             self

crates/workspace2/src/pane.rs 🔗

@@ -2,19 +2,20 @@ use crate::{
     item::{Item, ItemHandle, ItemSettings, WeakItemHandle},
     toolbar::Toolbar,
     workspace_settings::{AutosaveSetting, WorkspaceSettings},
-    SplitDirection, Workspace,
+    NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace,
 };
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
-    actions, prelude::*, Action, AppContext, AsyncWindowContext, Div, EntityId, EventEmitter,
-    FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render, Task, View,
-    ViewContext, VisualContext, WeakView, WindowContext,
+    actions, overlay, prelude::*, rems, Action, AnchorCorner, AnyWeakView, AppContext,
+    AsyncWindowContext, DismissEvent, Div, EntityId, EventEmitter, FocusHandle, Focusable,
+    FocusableView, Model, Pixels, Point, PromptLevel, Render, Task, View, ViewContext,
+    VisualContext, WeakView, WindowContext,
 };
 use parking_lot::Mutex;
-use project2::{Project, ProjectEntryId, ProjectPath};
+use project::{Project, ProjectEntryId, ProjectPath};
 use serde::Deserialize;
-use settings2::Settings;
+use settings::Settings;
 use std::{
     any::Any,
     cmp, fmt, mem,
@@ -25,8 +26,10 @@ use std::{
     },
 };
 
-use ui::v_stack;
-use ui::{prelude::*, Color, Icon, IconButton, IconElement, Tooltip};
+use ui::{
+    h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, Label, Tooltip,
+};
+use ui::{v_stack, ContextMenu};
 use util::truncate_and_remove_front;
 
 #[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
@@ -50,7 +53,7 @@ pub enum SaveIntent {
 
 //todo!("Do we need the default bound on actions? Decide soon")
 // #[register_action]
-#[derive(Clone, Deserialize, PartialEq, Debug)]
+#[derive(Action, Clone, Deserialize, PartialEq, Debug)]
 pub struct ActivateItem(pub usize);
 
 // #[derive(Clone, PartialEq)]
@@ -143,17 +146,24 @@ impl fmt::Debug for Event {
     }
 }
 
+struct FocusedView {
+    view: AnyWeakView,
+    focus_handle: FocusHandle,
+}
+
 pub struct Pane {
     focus_handle: FocusHandle,
     items: Vec<Box<dyn ItemHandle>>,
     activation_history: Vec<EntityId>,
     zoomed: bool,
     active_item_index: usize,
-    //     last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
+    last_focused_view_by_item: HashMap<EntityId, FocusHandle>,
     autoscroll: bool,
     nav_history: NavHistory,
     toolbar: View<Toolbar>,
-    //     tab_bar_context_menu: TabBarContextMenu,
+    tab_bar_focus_handle: FocusHandle,
+    new_item_menu: Option<View<ContextMenu>>,
+    split_item_menu: Option<View<ContextMenu>>,
     //     tab_context_menu: ViewHandle<ContextMenu>,
     workspace: WeakView<Workspace>,
     project: Model<Project>,
@@ -306,7 +316,7 @@ impl Pane {
             activation_history: Vec::new(),
             zoomed: false,
             active_item_index: 0,
-            // last_focused_view_by_item: Default::default(),
+            last_focused_view_by_item: Default::default(),
             autoscroll: false,
             nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
                 mode: NavigationMode::Normal,
@@ -318,6 +328,9 @@ impl Pane {
                 next_timestamp,
             }))),
             toolbar: cx.build_view(|_| Toolbar::new()),
+            tab_bar_focus_handle: cx.focus_handle(),
+            new_item_menu: None,
+            split_item_menu: None,
             // tab_bar_context_menu: TabBarContextMenu {
             //     kind: TabBarContextMenuKind::New,
             //     handle: context_menu,
@@ -392,9 +405,48 @@ impl Pane {
     }
 
     pub fn has_focus(&self, cx: &WindowContext) -> bool {
+        // todo!(); // inline this manually
         self.focus_handle.contains_focused(cx)
     }
 
+    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
+        if !self.has_focus(cx) {
+            cx.emit(Event::Focus);
+            cx.notify();
+        }
+
+        self.toolbar.update(cx, |toolbar, cx| {
+            toolbar.focus_changed(true, cx);
+        });
+
+        if let Some(active_item) = self.active_item() {
+            if self.focus_handle.is_focused(cx) {
+                // Pane was focused directly. We need to either focus a view inside the active item,
+                // or focus the active item itself
+                if let Some(weak_last_focused_view) =
+                    self.last_focused_view_by_item.get(&active_item.item_id())
+                {
+                    weak_last_focused_view.focus(cx);
+                    return;
+                }
+
+                active_item.focus_handle(cx).focus(cx);
+            } else if !self.tab_bar_focus_handle.contains_focused(cx) {
+                if let Some(focused) = cx.focused() {
+                    self.last_focused_view_by_item
+                        .insert(active_item.item_id(), focused);
+                }
+            }
+        }
+    }
+
+    fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
+        self.toolbar.update(cx, |toolbar, cx| {
+            toolbar.focus_changed(false, cx);
+        });
+        cx.notify();
+    }
+
     pub fn active_item_index(&self) -> usize {
         self.active_item_index
     }
@@ -457,24 +509,49 @@ impl Pane {
         !self.nav_history.0.lock().forward_stack.is_empty()
     }
 
+    fn navigate_backward(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(workspace) = self.workspace.upgrade() {
+            let pane = cx.view().downgrade();
+            cx.window_context().defer(move |cx| {
+                workspace.update(cx, |workspace, cx| {
+                    workspace.go_back(pane, cx).detach_and_log_err(cx)
+                })
+            })
+        }
+    }
+
+    fn navigate_forward(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(workspace) = self.workspace.upgrade() {
+            let pane = cx.view().downgrade();
+            cx.window_context().defer(move |cx| {
+                workspace.update(cx, |workspace, cx| {
+                    workspace.go_forward(pane, cx).detach_and_log_err(cx)
+                })
+            })
+        }
+    }
+
     fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
         self.toolbar.update(cx, |_, cx| cx.notify());
     }
 
     pub(crate) fn open_item(
         &mut self,
-        project_entry_id: ProjectEntryId,
+        project_entry_id: Option<ProjectEntryId>,
         focus_item: bool,
         cx: &mut ViewContext<Self>,
         build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
     ) -> Box<dyn ItemHandle> {
         let mut existing_item = None;
-        for (index, item) in self.items.iter().enumerate() {
-            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id]
-            {
-                let item = item.boxed_clone();
-                existing_item = Some((index, item));
-                break;
+        if let Some(project_entry_id) = project_entry_id {
+            for (index, item) in self.items.iter().enumerate() {
+                if item.is_singleton(cx)
+                    && item.project_entry_ids(cx).as_slice() == [project_entry_id]
+                {
+                    let item = item.boxed_clone();
+                    existing_item = Some((index, item));
+                    break;
+                }
             }
         }
 
@@ -630,21 +707,16 @@ impl Pane {
             .position(|i| i.item_id() == item.item_id())
     }
 
-    // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
-    //     // Potentially warn the user of the new keybinding
-    //     let workspace_handle = self.workspace().clone();
-    //     cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
-    //         .detach();
-
-    //     if self.zoomed {
-    //         cx.emit(Event::ZoomOut);
-    //     } else if !self.items.is_empty() {
-    //         if !self.has_focus {
-    //             cx.focus_self();
-    //         }
-    //         cx.emit(Event::ZoomIn);
-    //     }
-    // }
+    pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+        if self.zoomed {
+            cx.emit(Event::ZoomOut);
+        } else if !self.items.is_empty() {
+            if !self.focus_handle.contains_focused(cx) {
+                cx.focus_self();
+            }
+            cx.emit(Event::ZoomIn);
+        }
+    }
 
     pub fn activate_item(
         &mut self,
@@ -1381,7 +1453,7 @@ impl Pane {
         let close_right = ItemSettings::get_global(cx).close_position.right();
         let is_active = ix == self.active_item_index;
 
-        div()
+        let tab = div()
             .group("")
             .id(ix)
             .cursor_pointer()
@@ -1455,43 +1527,68 @@ impl Pane {
                     .children((!close_right).then(|| close_icon()))
                     .child(label)
                     .children(close_right.then(|| close_icon())),
-            )
+            );
+
+        right_click_menu(ix).trigger(tab).menu(|cx| {
+            ContextMenu::build(cx, |menu, cx| {
+                menu.action(
+                    "Close Active Item",
+                    CloseActiveItem { save_intent: None }.boxed_clone(),
+                )
+                .action("Close Inactive Items", CloseInactiveItems.boxed_clone())
+                .action("Close Clean Items", CloseCleanItems.boxed_clone())
+                .action("Close Items To The Left", CloseItemsToTheLeft.boxed_clone())
+                .action(
+                    "Close Items To The Right",
+                    CloseItemsToTheRight.boxed_clone(),
+                )
+                .action(
+                    "Close All Items",
+                    CloseAllItems { save_intent: None }.boxed_clone(),
+                )
+            })
+        })
     }
 
     fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
         div()
-            .group("tab_bar")
             .id("tab_bar")
+            .group("tab_bar")
+            .track_focus(&self.tab_bar_focus_handle)
             .w_full()
+            // 30px @ 16px/rem
+            .h(rems(1.875))
+            .overflow_hidden()
             .flex()
+            .flex_none()
             .bg(cx.theme().colors().tab_bar_background)
             // Left Side
             .child(
-                div()
-                    .relative()
-                    .px_1()
+                h_stack()
+                    .px_2()
                     .flex()
                     .flex_none()
-                    .gap_2()
+                    .gap_1()
                     // Nav Buttons
                     .child(
-                        div()
-                            .right_0()
-                            .flex()
-                            .items_center()
-                            .gap_px()
-                            .child(
-                                div().border().border_color(gpui::red()).child(
-                                    IconButton::new("navigate_backward", Icon::ArrowLeft)
-                                        .disabled(!self.can_navigate_backward()),
-                                ),
-                            )
-                            .child(
-                                div().border().border_color(gpui::red()).child(
-                                    IconButton::new("navigate_forward", Icon::ArrowRight)
-                                        .disabled(!self.can_navigate_forward()),
-                                ),
-                            ),
+                        div().border().border_color(gpui::red()).child(
+                            IconButton::new("navigate_backward", Icon::ArrowLeft)
+                                .on_click({
+                                    let view = cx.view().clone();
+                                    move |_, cx| view.update(cx, Self::navigate_backward)
+                                })
+                                .disabled(!self.can_navigate_backward()),
+                        ),
+                    )
+                    .child(
+                        div().border().border_color(gpui::red()).child(
+                            IconButton::new("navigate_forward", Icon::ArrowRight)
+                                .on_click({
+                                    let view = cx.view().clone();
+                                    move |_, cx| view.update(cx, Self::navigate_backward)
+                                })
+                                .disabled(!self.can_navigate_forward()),
+                        ),
                     ),
             )
             .child(
@@ -1520,20 +1617,78 @@ impl Pane {
                             .gap_px()
                             .child(
                                 div()
+                                    .bg(gpui::blue())
                                     .border()
                                     .border_color(gpui::red())
-                                    .child(IconButton::new("plus", Icon::Plus)),
+                                    .child(IconButton::new("plus", Icon::Plus).on_click(
+                                        cx.listener(|this, _, cx| {
+                                            let menu = ContextMenu::build(cx, |menu, cx| {
+                                                menu.action("New File", NewFile.boxed_clone())
+                                                    .action(
+                                                        "New Terminal",
+                                                        NewCenterTerminal.boxed_clone(),
+                                                    )
+                                                    .action("New Search", NewSearch.boxed_clone())
+                                            });
+                                            cx.subscribe(
+                                                &menu,
+                                                |this, _, event: &DismissEvent, cx| {
+                                                    this.focus(cx);
+                                                    this.new_item_menu = None;
+                                                },
+                                            )
+                                            .detach();
+                                            this.new_item_menu = Some(menu);
+                                        }),
+                                    ))
+                                    .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| {
+                                        el.child(Self::render_menu_overlay(new_item_menu))
+                                    }),
                             )
                             .child(
                                 div()
                                     .border()
                                     .border_color(gpui::red())
-                                    .child(IconButton::new("split", Icon::Split)),
+                                    .child(IconButton::new("split", Icon::Split).on_click(
+                                        cx.listener(|this, _, cx| {
+                                            let menu = ContextMenu::build(cx, |menu, cx| {
+                                                menu.action("Split Right", SplitRight.boxed_clone())
+                                                    .action("Split Left", SplitLeft.boxed_clone())
+                                                    .action("Split Up", SplitUp.boxed_clone())
+                                                    .action("Split Down", SplitDown.boxed_clone())
+                                            });
+                                            cx.subscribe(
+                                                &menu,
+                                                |this, _, event: &DismissEvent, cx| {
+                                                    this.focus(cx);
+                                                    this.split_item_menu = None;
+                                                },
+                                            )
+                                            .detach();
+                                            this.split_item_menu = Some(menu);
+                                        }),
+                                    ))
+                                    .when_some(
+                                        self.split_item_menu.as_ref(),
+                                        |el, split_item_menu| {
+                                            el.child(Self::render_menu_overlay(split_item_menu))
+                                        },
+                                    ),
                             ),
                     ),
             )
     }
 
+    fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
+        div()
+            .absolute()
+            .z_index(1)
+            .bottom_0()
+            .right_0()
+            .size_0()
+            .child(overlay().anchor(AnchorCorner::TopRight).child(menu.clone()))
+    }
+
     //     fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
     //         let theme = theme::current(cx).clone();
 
@@ -1932,40 +2087,78 @@ impl Render for Pane {
     type Element = Focusable<Div>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let this = cx.view().downgrade();
+
         v_stack()
             .key_context("Pane")
             .track_focus(&self.focus_handle)
-            .on_action(cx.listener(|pane: &mut Pane, _: &SplitLeft, cx| {
-                pane.split(SplitDirection::Left, cx)
-            }))
+            .on_focus_in({
+                let this = this.clone();
+                move |event, cx| {
+                    this.update(cx, |this, cx| this.focus_in(cx)).ok();
+                }
+            })
+            .on_focus_out({
+                let this = this.clone();
+                move |event, cx| {
+                    this.update(cx, |this, cx| this.focus_out(cx)).ok();
+                }
+            })
+            .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
+            .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
             .on_action(
-                cx.listener(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)),
+                cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
             )
-            .on_action(cx.listener(|pane: &mut Pane, _: &SplitRight, cx| {
-                pane.split(SplitDirection::Right, cx)
+            .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
+            .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
+            .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
+            .on_action(cx.listener(Pane::toggle_zoom))
+            .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
+                pane.activate_item(action.0, true, true, cx);
+            }))
+            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
+                pane.activate_item(pane.items.len() - 1, true, true, cx);
             }))
-            .on_action(cx.listener(|pane: &mut Pane, _: &SplitDown, cx| {
-                pane.split(SplitDirection::Down, cx)
+            .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
+                pane.activate_prev_item(true, cx);
+            }))
+            .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
+                pane.activate_next_item(true, cx);
+            }))
+            .on_action(
+                cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
+                    pane.close_active_item(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                }),
+            )
+            .on_action(
+                cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
+                    pane.close_inactive_items(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                }),
+            )
+            .on_action(
+                cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
+                    pane.close_clean_items(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                }),
+            )
+            .on_action(
+                cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
+                    pane.close_items_to_the_left(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                }),
+            )
+            .on_action(
+                cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
+                    pane.close_items_to_the_right(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                }),
+            )
+            .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
+                pane.close_all_items(action, cx)
+                    .map(|task| task.detach_and_log_err(cx));
             }))
-            //     cx.add_action(Pane::toggle_zoom);
-            //     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
-            //         pane.activate_item(action.0, true, true, cx);
-            //     });
-            //     cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
-            //         pane.activate_item(pane.items.len() - 1, true, true, cx);
-            //     });
-            //     cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
-            //         pane.activate_prev_item(true, cx);
-            //     });
-            //     cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
-            //         pane.activate_next_item(true, cx);
-            //     });
-            //     cx.add_async_action(Pane::close_active_item);
-            //     cx.add_async_action(Pane::close_inactive_items);
-            //     cx.add_async_action(Pane::close_clean_items);
-            //     cx.add_async_action(Pane::close_items_to_the_left);
-            //     cx.add_async_action(Pane::close_items_to_the_right);
-            //     cx.add_async_action(Pane::close_all_items);
             .size_full()
             .on_action(
                 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
@@ -1978,8 +2171,11 @@ impl Render for Pane {
             .child(if let Some(item) = self.active_item() {
                 div().flex().flex_1().child(item.to_any())
             } else {
-                // todo!()
-                div().child("Empty Pane")
+                h_stack()
+                    .items_center()
+                    .size_full()
+                    .justify_center()
+                    .child(Label::new("Open a file or project to get started.").color(Color::Muted))
             })
 
         // enum MouseNavigationHandler {}

crates/workspace2/src/pane_group.rs 🔗

@@ -1,18 +1,20 @@
 use crate::{AppState, FollowerState, Pane, Workspace};
 use anyhow::{anyhow, bail, Result};
+use call::{ActiveCall, ParticipantLocation};
 use collections::HashMap;
-use db2::sqlez::{
+use db::sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
     statement::Statement,
 };
 use gpui::{
-    point, size, AnyWeakView, Bounds, Div, IntoElement, Model, Pixels, Point, View, ViewContext,
+    point, size, AnyWeakView, Bounds, Div, Entity as _, IntoElement, Model, Pixels, Point, View,
+    ViewContext,
 };
 use parking_lot::Mutex;
-use project2::Project;
+use project::Project;
 use serde::Deserialize;
 use std::sync::Arc;
-use ui::prelude::*;
+use ui::{prelude::*, Button};
 
 const HANDLE_HITBOX_SIZE: f32 = 4.0;
 const HORIZONTAL_MIN_SIZE: f32 = 80.;
@@ -126,6 +128,7 @@ impl PaneGroup {
         &self,
         project: &Model<Project>,
         follower_states: &HashMap<View<Pane>, FollowerState>,
+        active_call: Option<&Model<ActiveCall>>,
         active_pane: &View<Pane>,
         zoomed: Option<&AnyWeakView>,
         app_state: &Arc<AppState>,
@@ -135,6 +138,7 @@ impl PaneGroup {
             project,
             0,
             follower_states,
+            active_call,
             active_pane,
             zoomed,
             app_state,
@@ -196,6 +200,7 @@ impl Member {
         project: &Model<Project>,
         basis: usize,
         follower_states: &HashMap<View<Pane>, FollowerState>,
+        active_call: Option<&Model<ActiveCall>>,
         active_pane: &View<Pane>,
         zoomed: Option<&AnyWeakView>,
         app_state: &Arc<AppState>,
@@ -203,19 +208,89 @@ impl Member {
     ) -> impl IntoElement {
         match self {
             Member::Pane(pane) => {
-                // todo!()
-                // let pane_element = if Some(pane.into()) == zoomed {
-                //     None
-                // } else {
-                //     Some(pane)
-                // };
-
-                div().size_full().child(pane.clone()).into_any()
-
-                //         Stack::new()
-                //             .with_child(pane_element.contained().with_border(leader_border))
-                //             .with_children(leader_status_box)
-                //             .into_any()
+                let leader = follower_states.get(pane).and_then(|state| {
+                    let room = active_call?.read(cx).room()?.read(cx);
+                    room.remote_participant_for_peer_id(state.leader_id)
+                });
+
+                let mut leader_border = None;
+                let mut leader_status_box = None;
+                if let Some(leader) = &leader {
+                    let mut leader_color = cx
+                        .theme()
+                        .players()
+                        .color_for_participant(leader.participant_index.0)
+                        .cursor;
+                    leader_color.fade_out(0.3);
+                    leader_border = Some(leader_color);
+
+                    leader_status_box = match leader.location {
+                        ParticipantLocation::SharedProject {
+                            project_id: leader_project_id,
+                        } => {
+                            if Some(leader_project_id) == project.read(cx).remote_id() {
+                                None
+                            } else {
+                                let leader_user = leader.user.clone();
+                                let leader_user_id = leader.user.id;
+                                Some(
+                                    Button::new(
+                                        ("leader-status", pane.entity_id()),
+                                        format!(
+                                            "Follow {} to their active project",
+                                            leader_user.github_login,
+                                        ),
+                                    )
+                                    .on_click(cx.listener(
+                                        move |this, _, cx| {
+                                            crate::join_remote_project(
+                                                leader_project_id,
+                                                leader_user_id,
+                                                this.app_state().clone(),
+                                                cx,
+                                            )
+                                            .detach_and_log_err(cx);
+                                        },
+                                    )),
+                                )
+                            }
+                        }
+                        ParticipantLocation::UnsharedProject => Some(Button::new(
+                            ("leader-status", pane.entity_id()),
+                            format!(
+                                "{} is viewing an unshared Zed project",
+                                leader.user.github_login
+                            ),
+                        )),
+                        ParticipantLocation::External => Some(Button::new(
+                            ("leader-status", pane.entity_id()),
+                            format!(
+                                "{} is viewing a window outside of Zed",
+                                leader.user.github_login
+                            ),
+                        )),
+                    };
+                }
+
+                div()
+                    .relative()
+                    .size_full()
+                    .child(pane.clone())
+                    .when_some(leader_border, |this, color| {
+                        this.border_2().border_color(color)
+                    })
+                    .when_some(leader_status_box, |this, status_box| {
+                        this.child(
+                            div()
+                                .absolute()
+                                .w_96()
+                                .bottom_3()
+                                .right_3()
+                                .z_index(1)
+                                .child(status_box),
+                        )
+                    })
+                    .into_any()
 
                 // let el = div()
                 //     .flex()

crates/workspace2/src/persistence.rs 🔗

@@ -5,7 +5,7 @@ pub mod model;
 use std::path::Path;
 
 use anyhow::{anyhow, bail, Context, Result};
-use db2::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
+use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
 use gpui::WindowBounds;
 
 use util::{unzip_option, ResultExt};
@@ -552,7 +552,7 @@ impl WorkspaceDb {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use db2::open_test_db;
+    use db::open_test_db;
     use gpui;
 
     #[gpui::test]

crates/workspace2/src/persistence/model.rs 🔗

@@ -3,12 +3,12 @@ use crate::{
 };
 use anyhow::{Context, Result};
 use async_recursion::async_recursion;
-use db2::sqlez::{
+use db::sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
     statement::Statement,
 };
 use gpui::{AsyncWindowContext, Model, Task, View, WeakView, WindowBounds};
-use project2::Project;
+use project::Project;
 use std::{
     path::{Path, PathBuf},
     sync::Arc,

crates/workspace2/src/searchable.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
     AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WeakView,
     WindowContext,
 };
-use project2::search::SearchQuery;
+use project::search::SearchQuery;
 
 use crate::{
     item::{Item, WeakItemHandle},

crates/call2/src/shared_screen.rs → crates/workspace2/src/shared_screen.rs 🔗

@@ -1,5 +1,9 @@
-use crate::participant::{Frame, RemoteVideoTrack};
+use crate::{
+    item::{Item, ItemEvent},
+    ItemNavHistory, WorkspaceId,
+};
 use anyhow::Result;
+use call::participant::{Frame, RemoteVideoTrack};
 use client::{proto::PeerId, User};
 use futures::StreamExt;
 use gpui::{
@@ -9,7 +13,6 @@ use gpui::{
 };
 use std::sync::{Arc, Weak};
 use ui::{h_stack, Icon, IconElement};
-use workspace::{item::Item, ItemNavHistory, WorkspaceId};
 
 pub enum Event {
     Close,
@@ -56,7 +59,7 @@ impl SharedScreen {
 }
 
 impl EventEmitter<Event> for SharedScreen {}
-impl EventEmitter<workspace::item::ItemEvent> for SharedScreen {}
+impl EventEmitter<ItemEvent> for SharedScreen {}
 
 impl FocusableView for SharedScreen {
     fn focus_handle(&self, _: &AppContext) -> FocusHandle {

crates/workspace2/src/status_bar.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
     WindowContext,
 };
 use ui::prelude::*;
-use ui::{h_stack, Button, Icon, IconButton};
+use ui::{h_stack, Icon, IconButton};
 use util::ResultExt;
 
 pub trait StatusItemView: Render {
@@ -52,49 +52,12 @@ impl Render for StatusBar {
                 h_stack()
                     .gap_4()
                     .child(
-                        h_stack()
-                            .gap_1()
-                            .child(
-                                // TODO: Line / column numbers
-                                div()
-                                    .border()
-                                    .border_color(gpui::red())
-                                    .child(Button::new("status_line_column_numbers", "15:22")),
-                            )
-                            .child(
-                                // TODO: Language picker
-                                div()
-                                    .border()
-                                    .border_color(gpui::red())
-                                    .child(Button::new("status_buffer_language", "Rust")),
-                            ),
-                    )
-                    .child(
-                        h_stack()
-                            .gap_1()
-                            .child(
-                                // Github tool
-                                div()
-                                    .border()
-                                    .border_color(gpui::red())
-                                    .child(IconButton::new("status-copilot", Icon::Copilot)),
-                            )
-                            .child(
-                                // Feedback Tool
-                                div()
-                                    .border()
-                                    .border_color(gpui::red())
-                                    .child(IconButton::new("status-feedback", Icon::Envelope)),
-                            ),
-                    )
-                    .child(
-                        // Bottom Dock
                         h_stack().gap_1().child(
-                            // Terminal
+                            // Feedback Tool
                             div()
                                 .border()
                                 .border_color(gpui::red())
-                                .child(IconButton::new("status-terminal", Icon::Terminal)),
+                                .child(IconButton::new("status-feedback", Icon::Envelope)),
                         ),
                     )
                     .child(
@@ -133,7 +96,7 @@ impl StatusBar {
         h_stack()
             .items_center()
             .gap_2()
-            .children(self.right_items.iter().map(|item| item.to_any()))
+            .children(self.right_items.iter().rev().map(|item| item.to_any()))
     }
 }
 

crates/workspace2/src/toolbar.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
     ViewContext, WindowContext,
 };
 use ui::prelude::*;
-use ui::{h_stack, v_stack, ButtonLike, Color, Icon, IconButton, Label};
+use ui::{h_stack, v_stack, Icon, IconButton};
 
 pub enum ToolbarItemEvent {
     ChangeLocation(ToolbarItemLocation),
@@ -80,7 +80,6 @@ impl Render for Toolbar {
     type Element = Div;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        //dbg!(&self.items.len());
         v_stack()
             .border_b()
             .border_color(cx.theme().colors().border_variant)
@@ -88,17 +87,8 @@ impl Render for Toolbar {
             .child(
                 h_stack()
                     .justify_between()
-                    .child(
-                        // Toolbar left side
-                        h_stack().border().border_color(gpui::red()).p_1().child(
-                            ButtonLike::new("breadcrumb")
-                                .child(Label::new("crates/workspace2/src/toolbar.rs"))
-                                .child(Label::new("›").color(Color::Muted))
-                                .child(Label::new("impl Render for Toolbar"))
-                                .child(Label::new("›").color(Color::Muted))
-                                .child(Label::new("fn render")),
-                        ),
-                    )
+                    // Toolbar left side
+                    .children(self.items.iter().map(|(child, _)| child.to_any()))
                     // Toolbar right side
                     .child(
                         h_stack()
@@ -117,7 +107,6 @@ impl Render for Toolbar {
                             ),
                     ),
             )
-            .children(self.items.iter().map(|(child, _)| child.to_any()))
     }
 }
 

crates/workspace2/src/workspace2.rs 🔗

@@ -10,15 +10,16 @@ mod persistence;
 pub mod searchable;
 // todo!()
 mod modal_layer;
+pub mod shared_screen;
 mod status_bar;
 mod toolbar;
 mod workspace_settings;
 
 use anyhow::{anyhow, Context as _, Result};
-use async_trait::async_trait;
-use client2::{
+use call::ActiveCall;
+use client::{
     proto::{self, PeerId},
-    Client, TypedEnvelope, User, UserStore,
+    Client, Status, TypedEnvelope, UserStore,
 };
 use collections::{hash_map, HashMap, HashSet};
 use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
@@ -28,16 +29,16 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
-    AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
-    FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext,
-    ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription, Task,
-    View, ViewContext, VisualContext, WeakModel, WeakView, WindowBounds, WindowContext,
+    actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AnyWindowHandle, AppContext,
+    AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter,
+    FocusHandle, FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model,
+    ModelContext, ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled,
+    Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext,
     WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
-use language2::{LanguageRegistry, Rope};
+use language::{LanguageRegistry, Rope};
 use lazy_static::lazy_static;
 pub use modal_layer::*;
 use node_runtime::NodeRuntime;
@@ -49,9 +50,10 @@ pub use persistence::{
     WorkspaceDb, DB,
 };
 use postage::stream::Stream;
-use project2::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
+use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
 use serde::Deserialize;
-use settings2::Settings;
+use settings::Settings;
+use shared_screen::SharedScreen;
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use std::{
@@ -62,7 +64,7 @@ use std::{
     sync::{atomic::AtomicUsize, Arc},
     time::Duration,
 };
-use theme2::{ActiveTheme, ThemeSettings};
+use theme::{ActiveTheme, ThemeSettings};
 pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 pub use ui;
 use ui::{h_stack, v_stack, StyledExt};
@@ -210,6 +212,7 @@ pub fn init_settings(cx: &mut AppContext) {
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     init_settings(cx);
     notifications::init(cx);
+
     //     cx.add_global_action({
     //         let app_state = Arc::downgrade(&app_state);
     //         move |_: &Open, cx: &mut AppContext| {
@@ -248,7 +251,7 @@ type FollowableItemBuilder = fn(
     View<Workspace>,
     ViewId,
     &mut Option<proto::view::Variant>,
-    &mut AppContext,
+    &mut WindowContext,
 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
 type FollowableItemBuilders = HashMap<
     TypeId,
@@ -302,8 +305,7 @@ pub struct AppState {
     pub client: Arc<Client>,
     pub user_store: Model<UserStore>,
     pub workspace_store: Model<WorkspaceStore>,
-    pub fs: Arc<dyn fs2::Fs>,
-    pub call_factory: CallFactory,
+    pub fs: Arc<dyn fs::Fs>,
     pub build_window_options:
         fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
     pub node_runtime: Arc<dyn NodeRuntime>,
@@ -313,7 +315,7 @@ pub struct WorkspaceStore {
     workspaces: HashSet<WindowHandle<Workspace>>,
     followers: Vec<Follower>,
     client: Arc<Client>,
-    _subscriptions: Vec<client2::Subscription>,
+    _subscriptions: Vec<client::Subscription>,
 }
 
 #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
@@ -322,89 +324,26 @@ struct Follower {
     peer_id: PeerId,
 }
 
-#[cfg(any(test, feature = "test-support"))]
-pub struct TestCallHandler;
-
-#[cfg(any(test, feature = "test-support"))]
-impl CallHandler for TestCallHandler {
-    fn peer_state(
-        &mut self,
-        id: PeerId,
-        project: &Model<Project>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<(bool, bool)> {
-        None
-    }
-
-    fn shared_screen_for_peer(
-        &self,
-        peer_id: PeerId,
-        pane: &View<Pane>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<Box<dyn ItemHandle>> {
-        None
-    }
-
-    fn room_id(&self, cx: &AppContext) -> Option<u64> {
-        None
-    }
-
-    fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
-        Task::ready(Err(anyhow!("TestCallHandler should not be hanging up")))
-    }
-
-    fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
-        None
-    }
-
-    fn invite(
-        &mut self,
-        called_user_id: u64,
-        initial_project: Option<Model<Project>>,
-        cx: &mut AppContext,
-    ) -> Task<Result<()>> {
-        unimplemented!()
-    }
-
-    fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
-        None
-    }
-
-    fn is_muted(&self, cx: &AppContext) -> Option<bool> {
-        None
-    }
-
-    fn toggle_mute(&self, cx: &mut AppContext) {}
-
-    fn toggle_screen_share(&self, cx: &mut AppContext) {}
-
-    fn toggle_deafen(&self, cx: &mut AppContext) {}
-
-    fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
-        None
-    }
-}
-
 impl AppState {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut AppContext) -> Arc<Self> {
         use node_runtime::FakeNodeRuntime;
-        use settings2::SettingsStore;
+        use settings::SettingsStore;
 
         if !cx.has_global::<SettingsStore>() {
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
         }
 
-        let fs = fs2::FakeFs::new(cx.background_executor().clone());
+        let fs = fs::FakeFs::new(cx.background_executor().clone());
         let languages = Arc::new(LanguageRegistry::test());
         let http_client = util::http::FakeHttpClient::with_404_response();
         let client = Client::new(http_client.clone(), cx);
         let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx));
         let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
 
-        theme2::init(theme2::LoadThemes::JustBase, cx);
-        client2::init(&client, cx);
+        theme::init(theme::LoadThemes::JustBase, cx);
+        client::init(&client, cx);
         crate::init_settings(cx);
 
         Arc::new(Self {
@@ -415,7 +354,6 @@ impl AppState {
             workspace_store,
             node_runtime: FakeNodeRuntime::new(),
             build_window_options: |_, _, _| Default::default(),
-            call_factory: |_| Box::new(TestCallHandler),
         })
     }
 }
@@ -472,40 +410,6 @@ pub enum Event {
     WorkspaceCreated(WeakView<Workspace>),
 }
 
-#[async_trait(?Send)]
-pub trait CallHandler {
-    fn peer_state(
-        &mut self,
-        id: PeerId,
-        project: &Model<Project>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<(bool, bool)>;
-    fn shared_screen_for_peer(
-        &self,
-        peer_id: PeerId,
-        pane: &View<Pane>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<Box<dyn ItemHandle>>;
-    fn room_id(&self, cx: &AppContext) -> Option<u64>;
-    fn is_in_room(&self, cx: &mut ViewContext<Workspace>) -> bool {
-        self.room_id(cx).is_some()
-    }
-    fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>>;
-    fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>>;
-    fn invite(
-        &mut self,
-        called_user_id: u64,
-        initial_project: Option<Model<Project>>,
-        cx: &mut AppContext,
-    ) -> Task<Result<()>>;
-    fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>>;
-    fn is_muted(&self, cx: &AppContext) -> Option<bool>;
-    fn is_deafened(&self, cx: &AppContext) -> Option<bool>;
-    fn toggle_mute(&self, cx: &mut AppContext);
-    fn toggle_deafen(&self, cx: &mut AppContext);
-    fn toggle_screen_share(&self, cx: &mut AppContext);
-}
-
 pub struct Workspace {
     window_self: WindowHandle<Self>,
     weak_self: WeakView<Self>,
@@ -526,10 +430,10 @@ pub struct Workspace {
     titlebar_item: Option<AnyView>,
     notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
     project: Model<Project>,
-    call_handler: Box<dyn CallHandler>,
     follower_states: HashMap<View<Pane>, FollowerState>,
     last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
     window_edited: bool,
+    active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
     leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
     database_id: WorkspaceId,
     app_state: Arc<AppState>,
@@ -557,7 +461,6 @@ struct FollowerState {
 
 enum WorkspaceBounds {}
 
-type CallFactory = fn(&mut ViewContext<Workspace>) -> Box<dyn CallHandler>;
 impl Workspace {
     pub fn new(
         workspace_id: WorkspaceId,
@@ -568,29 +471,29 @@ impl Workspace {
         cx.observe(&project, |_, _, cx| cx.notify()).detach();
         cx.subscribe(&project, move |this, _, event, cx| {
             match event {
-                project2::Event::RemoteIdChanged(_) => {
+                project::Event::RemoteIdChanged(_) => {
                     this.update_window_title(cx);
                 }
 
-                project2::Event::CollaboratorLeft(peer_id) => {
+                project::Event::CollaboratorLeft(peer_id) => {
                     this.collaborator_left(*peer_id, cx);
                 }
 
-                project2::Event::WorktreeRemoved(_) | project2::Event::WorktreeAdded => {
+                project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
                     this.update_window_title(cx);
                     this.serialize_workspace(cx);
                 }
 
-                project2::Event::DisconnectedFromHost => {
+                project::Event::DisconnectedFromHost => {
                     this.update_window_edited(cx);
                     cx.blur();
                 }
 
-                project2::Event::Closed => {
+                project::Event::Closed => {
                     cx.remove_window();
                 }
 
-                project2::Event::DeletedEntry(entry_id) => {
+                project::Event::DeletedEntry(entry_id) => {
                     for pane in this.panes.iter() {
                         pane.update(cx, |pane, cx| {
                             pane.handle_deleted_project_item(*entry_id, cx)
@@ -598,7 +501,7 @@ impl Workspace {
                     }
                 }
 
-                project2::Event::Notification(message) => this.show_notification(0, cx, |cx| {
+                project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
                     cx.build_view(|_| MessageNotification::new(message.clone()))
                 }),
 
@@ -649,19 +552,9 @@ impl Workspace {
             mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
         let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
             while let Some((leader_id, update)) = leader_updates_rx.next().await {
-                let mut cx2 = cx.clone();
-                let t = this.clone();
-
-                Workspace::process_leader_update(&this, leader_id, update, &mut cx)
+                Self::process_leader_update(&this, leader_id, update, &mut cx)
                     .await
                     .log_err();
-
-                // this.update(&mut cx, |this, cxx| {
-                //     this.call_handler
-                //         .process_leader_update(leader_id, update, cx2)
-                // })?
-                // .await
-                // .log_err();
             }
 
             Ok(())
@@ -694,6 +587,14 @@ impl Workspace {
         //     drag_and_drop.register_container(weak_handle.clone());
         // });
 
+        let mut active_call = None;
+        if cx.has_global::<Model<ActiveCall>>() {
+            let call = cx.global::<Model<ActiveCall>>().clone();
+            let mut subscriptions = Vec::new();
+            subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
+            active_call = Some((call, subscriptions));
+        }
+
         let subscriptions = vec![
             cx.observe_window_activation(Self::on_window_activation_changed),
             cx.observe_window_bounds(move |_, cx| {
@@ -770,8 +671,7 @@ impl Workspace {
             follower_states: Default::default(),
             last_leaders_by_pane: Default::default(),
             window_edited: false,
-
-            call_handler: (app_state.call_factory)(cx),
+            active_call,
             database_id: workspace_id,
             app_state,
             _observe_current_user,
@@ -1218,7 +1118,7 @@ impl Workspace {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<bool>> {
         //todo!(saveing)
-
+        let active_call = self.active_call().cloned();
         let window = cx.window_handle();
 
         cx.spawn(|this, mut cx| async move {
@@ -1229,27 +1129,27 @@ impl Workspace {
                     .count()
             })?;
 
-            if !quitting
-                && workspace_count == 1
-                && this
-                    .update(&mut cx, |this, cx| this.call_handler.is_in_room(cx))
-                    .log_err()
-                    .unwrap_or_default()
-            {
-                let answer = window.update(&mut cx, |_, cx| {
-                    cx.prompt(
-                        PromptLevel::Warning,
-                        "Do you want to leave the current call?",
-                        &["Close window and hang up", "Cancel"],
-                    )
-                })?;
+            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.update(&mut cx, |_, cx| {
+                        cx.prompt(
+                            PromptLevel::Warning,
+                            "Do you want to leave the current call?",
+                            &["Close window and hang up", "Cancel"],
+                        )
+                    })?;
 
-                if answer.await.log_err() == Some(1) {
-                    return anyhow::Ok(false);
-                } else {
-                    this.update(&mut cx, |this, cx| this.call_handler.hang_up(cx))?
-                        .await
-                        .log_err();
+                    if answer.await.log_err() == Some(1) {
+                        return anyhow::Ok(false);
+                    } else {
+                        active_call
+                            .update(&mut cx, |call, cx| call.hang_up(cx))?
+                            .await
+                            .log_err();
+                    }
                 }
             }
 
@@ -1451,7 +1351,7 @@ impl Workspace {
                             .map(|entry| entry.id);
                             if let Some(entry_id) = entry_id {
                                 workspace.project.update(cx, |_, cx| {
-                                    cx.emit(project2::Event::ActiveEntryChanged(Some(entry_id)));
+                                    cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
                                 })
                             }
                         })
@@ -1813,8 +1713,7 @@ impl Workspace {
         });
         cx.subscribe(&pane, Self::handle_pane_event).detach();
         self.panes.push(pane.clone());
-        // todo!()
-        // cx.focus(&pane);
+        cx.focus_view(&pane);
         cx.emit(Event::PaneAdded(pane.clone()));
         pane
     }
@@ -1955,13 +1854,13 @@ impl Workspace {
         })
     }
 
-    pub(crate) fn load_path(
+    fn load_path(
         &mut self,
         path: ProjectPath,
         cx: &mut ViewContext<Self>,
     ) -> Task<
         Result<(
-            ProjectEntryId,
+            Option<ProjectEntryId>,
             impl 'static + Send + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
         )>,
     > {
@@ -1989,7 +1888,7 @@ impl Workspace {
     where
         T: ProjectItem,
     {
-        use project2::Item as _;
+        use project::Item as _;
 
         let entry_id = project_item.read(cx).entry_id(cx);
         if let Some(item) = entry_id
@@ -2014,7 +1913,7 @@ impl Workspace {
     where
         T: ProjectItem,
     {
-        use project2::Item as _;
+        use project::Item as _;
 
         let entry_id = project_item.read(cx).entry_id(cx);
         if let Some(item) = entry_id
@@ -2034,7 +1933,7 @@ impl Workspace {
     pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
         if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
             self.active_pane.update(cx, |pane, cx| {
-                pane.add_item(shared_screen, false, true, None, cx)
+                pane.add_item(Box::new(shared_screen), false, true, None, cx)
             });
         }
     }
@@ -2372,60 +2271,60 @@ impl Workspace {
         cx.notify();
     }
 
-    //     fn start_following(
-    //         &mut self,
-    //         leader_id: PeerId,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let pane = self.active_pane().clone();
-
-    //         self.last_leaders_by_pane
-    //             .insert(pane.downgrade(), leader_id);
-    //         self.unfollow(&pane, cx);
-    //         self.follower_states.insert(
-    //             pane.clone(),
-    //             FollowerState {
-    //                 leader_id,
-    //                 active_view_id: None,
-    //                 items_by_leader_view_id: Default::default(),
-    //             },
-    //         );
-    //         cx.notify();
-
-    //         let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
-    //         let project_id = self.project.read(cx).remote_id();
-    //         let request = self.app_state.client.request(proto::Follow {
-    //             room_id,
-    //             project_id,
-    //             leader_id: Some(leader_id),
-    //         });
+    fn start_following(
+        &mut self,
+        leader_id: PeerId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let pane = self.active_pane().clone();
+
+        self.last_leaders_by_pane
+            .insert(pane.downgrade(), leader_id);
+        self.unfollow(&pane, cx);
+        self.follower_states.insert(
+            pane.clone(),
+            FollowerState {
+                leader_id,
+                active_view_id: None,
+                items_by_leader_view_id: Default::default(),
+            },
+        );
+        cx.notify();
 
-    //         Some(cx.spawn(|this, mut cx| async move {
-    //             let response = request.await?;
-    //             this.update(&mut cx, |this, _| {
-    //                 let state = this
-    //                     .follower_states
-    //                     .get_mut(&pane)
-    //                     .ok_or_else(|| anyhow!("following interrupted"))?;
-    //                 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
-    //                     Some(ViewId::from_proto(active_view_id)?)
-    //                 } else {
-    //                     None
-    //                 };
-    //                 Ok::<_, anyhow::Error>(())
-    //             })??;
-    //             Self::add_views_from_leader(
-    //                 this.clone(),
-    //                 leader_id,
-    //                 vec![pane],
-    //                 response.views,
-    //                 &mut cx,
-    //             )
-    //             .await?;
-    //             this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
-    //             Ok(())
-    //         }))
-    //     }
+        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+        let project_id = self.project.read(cx).remote_id();
+        let request = self.app_state.client.request(proto::Follow {
+            room_id,
+            project_id,
+            leader_id: Some(leader_id),
+        });
+
+        Some(cx.spawn(|this, mut cx| async move {
+            let response = request.await?;
+            this.update(&mut cx, |this, _| {
+                let state = this
+                    .follower_states
+                    .get_mut(&pane)
+                    .ok_or_else(|| anyhow!("following interrupted"))?;
+                state.active_view_id = if let Some(active_view_id) = response.active_view_id {
+                    Some(ViewId::from_proto(active_view_id)?)
+                } else {
+                    None
+                };
+                Ok::<_, anyhow::Error>(())
+            })??;
+            Self::add_views_from_leader(
+                this.clone(),
+                leader_id,
+                vec![pane],
+                response.views,
+                &mut cx,
+            )
+            .await?;
+            this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
+            Ok(())
+        }))
+    }
 
     //     pub fn follow_next_collaborator(
     //         &mut self,
@@ -2464,67 +2363,67 @@ impl Workspace {
     //         self.follow(leader_id, cx)
     //     }
 
-    //     pub fn follow(
-    //         &mut self,
-    //         leader_id: PeerId,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
-    //         let project = self.project.read(cx);
+    pub fn follow(
+        &mut self,
+        leader_id: PeerId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
+        let project = self.project.read(cx);
 
-    //         let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
-    //             return None;
-    //         };
+        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
+            return None;
+        };
 
-    //         let other_project_id = match remote_participant.location {
-    //             call::ParticipantLocation::External => None,
-    //             call::ParticipantLocation::UnsharedProject => None,
-    //             call::ParticipantLocation::SharedProject { project_id } => {
-    //                 if Some(project_id) == project.remote_id() {
-    //                     None
-    //                 } else {
-    //                     Some(project_id)
-    //                 }
-    //             }
-    //         };
+        let other_project_id = match remote_participant.location {
+            call::ParticipantLocation::External => None,
+            call::ParticipantLocation::UnsharedProject => None,
+            call::ParticipantLocation::SharedProject { project_id } => {
+                if Some(project_id) == project.remote_id() {
+                    None
+                } else {
+                    Some(project_id)
+                }
+            }
+        };
 
-    //         // if they are active in another project, follow there.
-    //         if let Some(project_id) = other_project_id {
-    //             let app_state = self.app_state.clone();
-    //             return Some(crate::join_remote_project(
-    //                 project_id,
-    //                 remote_participant.user.id,
-    //                 app_state,
-    //                 cx,
-    //             ));
-    //         }
+        // if they are active in another project, follow there.
+        if let Some(project_id) = other_project_id {
+            let app_state = self.app_state.clone();
+            return Some(crate::join_remote_project(
+                project_id,
+                remote_participant.user.id,
+                app_state,
+                cx,
+            ));
+        }
 
-    //         // if you're already following, find the right pane and focus it.
-    //         for (pane, state) in &self.follower_states {
-    //             if leader_id == state.leader_id {
-    //                 cx.focus(pane);
-    //                 return None;
-    //             }
-    //         }
+        // if you're already following, find the right pane and focus it.
+        for (pane, state) in &self.follower_states {
+            if leader_id == state.leader_id {
+                cx.focus_view(pane);
+                return None;
+            }
+        }
 
-    //         // Otherwise, follow.
-    //         self.start_following(leader_id, cx)
-    //     }
+        // Otherwise, follow.
+        self.start_following(leader_id, cx)
+    }
 
     pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
-        let follower_states = &mut self.follower_states;
-        let state = follower_states.remove(pane)?;
+        let state = self.follower_states.remove(pane)?;
         let leader_id = state.leader_id;
         for (_, item) in state.items_by_leader_view_id {
             item.set_leader_peer_id(None, cx);
         }
 
-        if follower_states
+        if self
+            .follower_states
             .values()
             .all(|state| state.leader_id != state.leader_id)
         {
             let project_id = self.project.read(cx).remote_id();
-            let room_id = self.call_handler.room_id(cx)?;
+            let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
             self.app_state
                 .client
                 .send(proto::Unfollow {
@@ -2593,8 +2492,7 @@ impl Workspace {
             title.push_str(" ↗");
         }
 
-        // todo!()
-        // cx.set_window_title(&title);
+        cx.set_window_title(&title);
     }
 
     fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
@@ -2660,57 +2558,55 @@ impl Workspace {
         }
     }
 
-    //     // RPC handlers
+    // RPC handlers
 
     fn handle_follow(
         &mut self,
-        _follower_project_id: Option<u64>,
-        _cx: &mut ViewContext<Self>,
+        follower_project_id: Option<u64>,
+        cx: &mut ViewContext<Self>,
     ) -> proto::FollowResponse {
-        todo!()
+        let client = &self.app_state.client;
+        let project_id = self.project.read(cx).remote_id();
 
-        //     let client = &self.app_state.client;
-        //     let project_id = self.project.read(cx).remote_id();
+        let active_view_id = self.active_item(cx).and_then(|i| {
+            Some(
+                i.to_followable_item_handle(cx)?
+                    .remote_id(client, cx)?
+                    .to_proto(),
+            )
+        });
 
-        //     let active_view_id = self.active_item(cx).and_then(|i| {
-        //         Some(
-        //             i.to_followable_item_handle(cx)?
-        //                 .remote_id(client, cx)?
-        //                 .to_proto(),
-        //         )
-        //     });
+        cx.notify();
 
-        //     cx.notify();
-
-        //     self.last_active_view_id = active_view_id.clone();
-        //     proto::FollowResponse {
-        //         active_view_id,
-        //         views: self
-        //             .panes()
-        //             .iter()
-        //             .flat_map(|pane| {
-        //                 let leader_id = self.leader_for_pane(pane);
-        //                 pane.read(cx).items().filter_map({
-        //                     let cx = &cx;
-        //                     move |item| {
-        //                         let item = item.to_followable_item_handle(cx)?;
-        //                         if (project_id.is_none() || project_id != follower_project_id)
-        //                             && item.is_project_item(cx)
-        //                         {
-        //                             return None;
-        //                         }
-        //                         let id = item.remote_id(client, cx)?.to_proto();
-        //                         let variant = item.to_state_proto(cx)?;
-        //                         Some(proto::View {
-        //                             id: Some(id),
-        //                             leader_id,
-        //                             variant: Some(variant),
-        //                         })
-        //                     }
-        //                 })
-        //             })
-        //             .collect(),
-        //     }
+        self.last_active_view_id = active_view_id.clone();
+        proto::FollowResponse {
+            active_view_id,
+            views: self
+                .panes()
+                .iter()
+                .flat_map(|pane| {
+                    let leader_id = self.leader_for_pane(pane);
+                    pane.read(cx).items().filter_map({
+                        let cx = &cx;
+                        move |item| {
+                            let item = item.to_followable_item_handle(cx)?;
+                            if (project_id.is_none() || project_id != follower_project_id)
+                                && item.is_project_item(cx)
+                            {
+                                return None;
+                            }
+                            let id = item.remote_id(client, cx)?.to_proto();
+                            let variant = item.to_state_proto(cx)?;
+                            Some(proto::View {
+                                id: Some(id),
+                                leader_id,
+                                variant: Some(variant),
+                            })
+                        }
+                    })
+                })
+                .collect(),
+        }
     }
 
     fn handle_update_followers(
@@ -2730,6 +2626,8 @@ impl Workspace {
         update: proto::UpdateFollowers,
         cx: &mut AsyncWindowContext,
     ) -> Result<()> {
+        dbg!("process_leader_update", &update);
+
         match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
             proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
                 this.update(cx, |this, _| {
@@ -2881,9 +2779,8 @@ impl Workspace {
         } else {
             None
         };
-        let room_id = self.call_handler.room_id(cx)?;
         self.app_state().workspace_store.update(cx, |store, cx| {
-            store.update_followers(project_id, room_id, update, cx)
+            store.update_followers(project_id, update, cx)
         })
     }
 
@@ -2891,12 +2788,31 @@ impl Workspace {
         self.follower_states.get(pane).map(|state| state.leader_id)
     }
 
-    pub fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
+    fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
         cx.notify();
 
-        let (leader_in_this_project, leader_in_this_app) =
-            self.call_handler.peer_state(leader_id, &self.project, cx)?;
+        let call = self.active_call()?;
+        let room = call.read(cx).room()?.read(cx);
+        let participant = room.remote_participant_for_peer_id(leader_id)?;
         let mut items_to_activate = Vec::new();
+
+        let leader_in_this_app;
+        let leader_in_this_project;
+        match participant.location {
+            call::ParticipantLocation::SharedProject { project_id } => {
+                leader_in_this_app = true;
+                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
+            }
+            call::ParticipantLocation::UnsharedProject => {
+                leader_in_this_app = true;
+                leader_in_this_project = false;
+            }
+            call::ParticipantLocation::External => {
+                leader_in_this_app = false;
+                leader_in_this_project = false;
+            }
+        };
+
         for (pane, state) in &self.follower_states {
             if state.leader_id != leader_id {
                 continue;
@@ -2917,7 +2833,7 @@ impl Workspace {
             }
 
             if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
-                items_to_activate.push((pane.clone(), shared_screen));
+                items_to_activate.push((pane.clone(), Box::new(shared_screen)));
             }
         }
 
@@ -2926,8 +2842,8 @@ impl Workspace {
             if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
                 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
             } else {
-                pane.update(cx, |pane, mut cx| {
-                    pane.add_item(item.boxed_clone(), false, false, None, &mut cx)
+                pane.update(cx, |pane, cx| {
+                    pane.add_item(item.boxed_clone(), false, false, None, cx)
                 });
             }
 
@@ -2944,21 +2860,20 @@ impl Workspace {
         peer_id: PeerId,
         pane: &View<Pane>,
         cx: &mut ViewContext<Self>,
-    ) -> Option<Box<dyn ItemHandle>> {
-        self.call_handler.shared_screen_for_peer(peer_id, pane, cx)
-        // let call = self.active_call()?;
-        // let room = call.read(cx).room()?.read(cx);
-        // let participant = room.remote_participant_for_peer_id(peer_id)?;
-        // let track = participant.video_tracks.values().next()?.clone();
-        // let user = participant.user.clone();
-
-        // for item in pane.read(cx).items_of_type::<SharedScreen>() {
-        //     if item.read(cx).peer_id == peer_id {
-        //         return Some(item);
-        //     }
-        // }
+    ) -> Option<View<SharedScreen>> {
+        let call = self.active_call()?;
+        let room = call.read(cx).room()?.read(cx);
+        let participant = room.remote_participant_for_peer_id(peer_id)?;
+        let track = participant.video_tracks.values().next()?.clone();
+        let user = participant.user.clone();
+
+        for item in pane.read(cx).items_of_type::<SharedScreen>() {
+            if item.read(cx).peer_id == peer_id {
+                return Some(item);
+            }
+        }
 
-        // Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
+        Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
     }
 
     pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
@@ -2987,6 +2902,25 @@ impl Workspace {
         }
     }
 
+    fn active_call(&self) -> Option<&Model<ActiveCall>> {
+        self.active_call.as_ref().map(|(call, _)| call)
+    }
+
+    fn on_active_call_event(
+        &mut self,
+        _: Model<ActiveCall>,
+        event: &call::room::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            call::room::Event::ParticipantLocationChanged { participant_id }
+            | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
+                self.leader_updated(*participant_id, cx);
+            }
+            _ => {}
+        }
+    }
+
     pub fn database_id(&self) -> WorkspaceId {
         self.database_id
     }
@@ -3396,7 +3330,6 @@ impl Workspace {
             fs: project.read(cx).fs().clone(),
             build_window_options: |_, _, _| Default::default(),
             node_runtime: FakeNodeRuntime::new(),
-            call_factory: |_| Box::new(TestCallHandler),
         });
         let workspace = Self::new(0, project, app_state, cx);
         workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
@@ -3475,10 +3408,6 @@ impl Workspace {
         self.modal_layer
             .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
     }
-
-    pub fn call_state(&mut self) -> &mut dyn CallHandler {
-        &mut *self.call_handler
-    }
 }
 
 fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
@@ -3588,93 +3517,12 @@ fn open_items(
     })
 }
 
-// todo!()
-// fn notify_of_new_dock(workspace: &WeakView<Workspace>, cx: &mut AsyncAppContext) {
-//     const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
-//     const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
-//     const MESSAGE_ID: usize = 2;
-
-//     if workspace
-//         .read_with(cx, |workspace, cx| {
-//             workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
-//         })
-//         .unwrap_or(false)
-//     {
-//         return;
-//     }
-
-//     if db::kvp::KEY_VALUE_STORE
-//         .read_kvp(NEW_DOCK_HINT_KEY)
-//         .ok()
-//         .flatten()
-//         .is_some()
-//     {
-//         if !workspace
-//             .read_with(cx, |workspace, cx| {
-//                 workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
-//             })
-//             .unwrap_or(false)
-//         {
-//             cx.update(|cx| {
-//                 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
-//                     let entry = tracker
-//                         .entry(TypeId::of::<MessageNotification>())
-//                         .or_default();
-//                     if !entry.contains(&MESSAGE_ID) {
-//                         entry.push(MESSAGE_ID);
-//                     }
-//                 });
-//             });
-//         }
-
-//         return;
-//     }
-
-//     cx.spawn(|_| async move {
-//         db::kvp::KEY_VALUE_STORE
-//             .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
-//             .await
-//             .ok();
-//     })
-//     .detach();
-
-//     workspace
-//         .update(cx, |workspace, cx| {
-//             workspace.show_notification_once(2, cx, |cx| {
-//                 cx.build_view(|_| {
-//                     MessageNotification::new_element(|text, _| {
-//                         Text::new(
-//                             "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
-//                             text,
-//                         )
-//                         .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| {
-//                             let code_span_background_color = settings::get::<ThemeSettings>(cx)
-//                                 .theme
-//                                 .editor
-//                                 .document_highlight_read_background;
-
-//                             cx.scene().push_quad(gpui::Quad {
-//                                 bounds,
-//                                 background: Some(code_span_background_color),
-//                                 border: Default::default(),
-//                                 corner_radii: (2.0).into(),
-//                             })
-//                         })
-//                         .into_any()
-//                     })
-//                     .with_click_message("Read more about the new panel system")
-//                     .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
-//                 })
-//             })
-//         })
-//         .ok();
-
 fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
     const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
 
     workspace
         .update(cx, |workspace, cx| {
-            if (*db2::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
+            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
                 workspace.show_notification_once(0, cx, |cx| {
                     cx.build_view(|_| {
                         MessageNotification::new("Failed to load the database file.")

crates/zed/Cargo.toml 🔗

@@ -172,7 +172,7 @@ osx_info_plist_exts = ["resources/info/*"]
 osx_url_schemes = ["zed-dev"]
 
 [package.metadata.bundle-nightly]
-icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
+icon = ["resources/app-icon-nightly@2x.png", "resources/app-icon-nightly.png"]
 identifier = "dev.zed.Zed-Nightly"
 name = "Zed Nightly"
 osx_minimum_system_version = "10.15.7"

crates/zed/src/only_instance.rs 🔗

@@ -39,7 +39,7 @@ pub enum IsOnlyInstance {
 }
 
 pub fn ensure_only_instance() -> IsOnlyInstance {
-    if *db::ZED_STATELESS {
+    if *db::ZED_STATELESS || *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
         return IsOnlyInstance::Yes;
     }
 

crates/zed/src/zed.rs 🔗

@@ -615,8 +615,8 @@ fn open_local_settings_file(
                         .update(&mut cx, |project, cx| {
                             project.create_entry((tree_id, dir_path), true, cx)
                         })
-                        .ok_or_else(|| anyhow!("worktree was removed"))?
-                        .await?;
+                        .await
+                        .context("worktree was removed")?;
                 }
             }
 
@@ -625,8 +625,8 @@ fn open_local_settings_file(
                     .update(&mut cx, |project, cx| {
                         project.create_entry((tree_id, file_path), false, cx)
                     })
-                    .ok_or_else(|| anyhow!("worktree was removed"))?
-                    .await?;
+                    .await
+                    .context("worktree was removed")?;
             }
 
             let editor = workspace
@@ -763,7 +763,7 @@ mod tests {
         AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
     };
     use language::LanguageRegistry;
-    use project::{Project, ProjectPath};
+    use project::{project_settings::ProjectSettings, Project, ProjectPath};
     use serde_json::json;
     use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
     use std::{
@@ -1308,6 +1308,122 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+        cx.update(|cx| {
+            cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+                    project_settings.file_scan_exclusions =
+                        Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
+                });
+            });
+        });
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    ".gitignore": "ignored_dir\n",
+                    ".git": {
+                        "HEAD": "ref: refs/heads/main",
+                    },
+                    "regular_dir": {
+                        "file": "regular file contents",
+                    },
+                    "ignored_dir": {
+                        "ignored_subdir": {
+                            "file": "ignored subfile contents",
+                        },
+                        "file": "ignored file contents",
+                    },
+                    "excluded_dir": {
+                        "file": "excluded file contents",
+                    },
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+
+        let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
+        let paths_to_open = [
+            Path::new("/root/excluded_dir/file").to_path_buf(),
+            Path::new("/root/.git/HEAD").to_path_buf(),
+            Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
+        ];
+        let (opened_workspace, new_items) = cx
+            .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx))
+            .await
+            .unwrap();
+
+        assert_eq!(
+            opened_workspace.id(),
+            workspace.id(),
+            "Excluded files in subfolders of a workspace root should be opened in the workspace"
+        );
+        let mut opened_paths = cx.read(|cx| {
+            assert_eq!(
+                new_items.len(),
+                paths_to_open.len(),
+                "Expect to get the same number of opened items as submitted paths to open"
+            );
+            new_items
+                .iter()
+                .zip(paths_to_open.iter())
+                .map(|(i, path)| {
+                    match i {
+                        Some(Ok(i)) => {
+                            Some(i.project_path(cx).map(|p| p.path.display().to_string()))
+                        }
+                        Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
+                        None => None,
+                    }
+                    .flatten()
+                })
+                .collect::<Vec<_>>()
+        });
+        opened_paths.sort();
+        assert_eq!(
+            opened_paths,
+            vec![
+                None,
+                Some(".git/HEAD".to_string()),
+                Some("excluded_dir/file".to_string()),
+            ],
+            "Excluded files should get opened, excluded dir should not get opened"
+        );
+
+        let entries = cx.read(|cx| workspace.file_project_paths(cx));
+        assert_eq!(
+            initial_entries, entries,
+            "Workspace entries should not change after opening excluded files and directories paths"
+        );
+
+        cx.read(|cx| {
+            let pane = workspace.read(cx).active_pane().read(cx);
+            let mut opened_buffer_paths = pane
+                .items()
+                .map(|i| {
+                    i.project_path(cx)
+                        .expect("all excluded files that got open should have a path")
+                        .path
+                        .display()
+                        .to_string()
+                })
+                .collect::<Vec<_>>();
+            opened_buffer_paths.sort();
+            assert_eq!(
+                opened_buffer_paths,
+                vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
+                "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
+            );
+        });
+    }
+
     #[gpui::test]
     async fn test_save_conflicting_item(cx: &mut TestAppContext) {
         let app_state = init_test(cx);

crates/zed2/Cargo.toml 🔗

@@ -19,7 +19,7 @@ ai = { package = "ai2", path = "../ai2"}
 audio = { package = "audio2", path = "../audio2" }
 activity_indicator = { package = "activity_indicator2", path = "../activity_indicator2"}
 auto_update = { package = "auto_update2", path = "../auto_update2" }
-# breadcrumbs = { path = "../breadcrumbs" }
+breadcrumbs = { package = "breadcrumbs2", path = "../breadcrumbs2" }
 call = { package = "call2", path = "../call2" }
 channel = { package = "channel2", path = "../channel2" }
 cli = { path = "../cli" }
@@ -30,7 +30,7 @@ command_palette = { package="command_palette2", path = "../command_palette2" }
 client = { package = "client2", path = "../client2" }
 # clock = { path = "../clock" }
 copilot = { package = "copilot2", path = "../copilot2" }
-# copilot_button = { path = "../copilot_button" }
+copilot_button = { package = "copilot_button2", path = "../copilot_button2" }
 diagnostics = { package = "diagnostics2", path = "../diagnostics2" }
 db = { package = "db2", path = "../db2" }
 editor = { package="editor2", path = "../editor2" }
@@ -44,13 +44,13 @@ gpui = { package = "gpui2", path = "../gpui2" }
 install_cli = { package = "install_cli2", path = "../install_cli2" }
 journal = { package = "journal2", path = "../journal2" }
 language = { package = "language2", path = "../language2" }
-# language_selector = { path = "../language_selector" }
+language_selector = { package = "language_selector2", path = "../language_selector2" }
 lsp = { package = "lsp2", path = "../lsp2" }
 menu = { package = "menu2", path = "../menu2" }
 # language_tools = { path = "../language_tools" }
 node_runtime = { path = "../node_runtime" }
 # assistant = { path = "../assistant" }
-# outline = { path = "../outline" }
+outline = { package = "outline2", path = "../outline2" }
 # plugin_runtime = { path = "../plugin_runtime",optional = true }
 project = { package = "project2", path = "../project2" }
 project_panel = { package = "project_panel2", path = "../project_panel2" }
@@ -167,7 +167,7 @@ osx_info_plist_exts = ["resources/info/*"]
 osx_url_schemes = ["zed-dev"]
 
 [package.metadata.bundle-nightly]
-icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
+icon = ["resources/app-icon-nightly@2x.png", "resources/app-icon-nightly.png"]
 identifier = "dev.zed.Zed-Dev"
 name = "Zed Nightly"
 osx_minimum_system_version = "10.15.7"

crates/zed2/src/main.rs 🔗

@@ -191,7 +191,6 @@ fn main() {
             user_store: user_store.clone(),
             fs,
             build_window_options,
-            call_factory: call::Call::new,
             workspace_store,
             node_runtime,
         });
@@ -205,7 +204,7 @@ fn main() {
 
         go_to_line::init(cx);
         file_finder::init(cx);
-        // outline::init(cx);
+        outline::init(cx);
         // project_symbols::init(cx);
         project_panel::init(Assets, cx);
         channel::init(&client, user_store.clone(), cx);
@@ -216,7 +215,7 @@ fn main() {
         terminal_view::init(cx);
 
         // journal2::init(app_state.clone(), cx);
-        // language_selector::init(cx);
+        language_selector::init(cx);
         theme_selector::init(cx);
         // activity_indicator::init(cx);
         // language_tools::init(cx);

crates/zed2/src/only_instance.rs 🔗

@@ -39,7 +39,7 @@ pub enum IsOnlyInstance {
 }
 
 pub fn ensure_only_instance() -> IsOnlyInstance {
-    if *db::ZED_STATELESS {
+    if *db::ZED_STATELESS || *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
         return IsOnlyInstance::Yes;
     }
 

crates/zed2/src/zed2.rs 🔗

@@ -7,6 +7,7 @@ mod only_instance;
 mod open_listener;
 
 pub use assets::*;
+use breadcrumbs::Breadcrumbs;
 use collections::VecDeque;
 use editor::{Editor, MultiBuffer};
 use gpui::{
@@ -95,11 +96,11 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                 if let workspace::Event::PaneAdded(pane) = event {
                     pane.update(cx, |pane, cx| {
                         pane.toolbar().update(cx, |toolbar, cx| {
-                            // todo!()
-                            //     let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
-                            //     toolbar.add_item(breadcrumbs, cx);
+                            let breadcrumbs = cx.build_view(|_| Breadcrumbs::new(workspace));
+                            toolbar.add_item(breadcrumbs, cx);
                             let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
                             toolbar.add_item(buffer_search_bar.clone(), cx);
+                            // todo!()
                             //     let quick_action_bar = cx.add_view(|_| {
                             //         QuickActionBar::new(buffer_search_bar, workspace)
                             //     });
@@ -135,27 +136,31 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
         //         cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
         //     workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
 
-        //     let copilot =
-        //         cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
+        let copilot =
+            cx.build_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
         let diagnostic_summary =
             cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
         let activity_indicator =
             activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
-        //     let active_buffer_language =
-        //         cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
+        let active_buffer_language =
+            cx.build_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
         //     let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx));
         let feedback_button = cx
             .build_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace));
         //     let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
+        //     let feedback_button = cx.add_view(|_| {
+        //         feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
+        //     });
+        let cursor_position = cx.build_view(|_| editor::items::CursorPosition::new());
         workspace.status_bar().update(cx, |status_bar, cx| {
             status_bar.add_left_item(diagnostic_summary, cx);
             status_bar.add_left_item(activity_indicator, cx);
-
             status_bar.add_right_item(feedback_button, cx);
             // status_bar.add_right_item(copilot, cx);
-            // status_bar.add_right_item(active_buffer_language, cx);
+            status_bar.add_right_item(copilot, cx);
+            status_bar.add_right_item(active_buffer_language, cx);
             // status_bar.add_right_item(vim_mode_indicator, cx);
-            // status_bar.add_right_item(cursor_position, cx);
+            status_bar.add_right_item(cursor_position, cx);
         });
 
         auto_update::notify_of_any_new_update(cx);
@@ -580,8 +585,8 @@ fn open_local_settings_file(
                         .update(&mut cx, |project, cx| {
                             project.create_entry((tree_id, dir_path), true, cx)
                         })?
-                        .ok_or_else(|| anyhow!("worktree was removed"))?
-                        .await?;
+                        .await
+                        .context("worktree was removed")?;
                 }
             }
 
@@ -590,8 +595,8 @@ fn open_local_settings_file(
                     .update(&mut cx, |project, cx| {
                         project.create_entry((tree_id, file_path), false, cx)
                     })?
-                    .ok_or_else(|| anyhow!("worktree was removed"))?
-                    .await?;
+                    .await
+                    .context("worktree was removed")?;
             }
 
             let editor = workspace
@@ -716,3 +721,1846 @@ fn open_bundled_file(
     })
     .detach_and_log_err(cx);
 }
+
+// todo!()
+// #[cfg(test)]
+// mod tests {
+//     use super::*;
+//     use assets::Assets;
+//     use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
+//     use fs::{FakeFs, Fs};
+//     use gpui::{
+//         actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle,
+//         AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
+//     };
+//     use language::LanguageRegistry;
+//     use project::{project_settings::ProjectSettings, Project, ProjectPath};
+//     use serde_json::json;
+//     use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
+//     use std::{
+//         collections::HashSet,
+//         path::{Path, PathBuf},
+//     };
+//     use theme::{ThemeRegistry, ThemeSettings};
+//     use workspace::{
+//         item::{Item, ItemHandle},
+//         open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle,
+//     };
+
+//     #[gpui::test]
+//     async fn test_open_paths_action(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/root",
+//                 json!({
+//                     "a": {
+//                         "aa": null,
+//                         "ab": null,
+//                     },
+//                     "b": {
+//                         "ba": null,
+//                         "bb": null,
+//                     },
+//                     "c": {
+//                         "ca": null,
+//                         "cb": null,
+//                     },
+//                     "d": {
+//                         "da": null,
+//                         "db": null,
+//                     },
+//                 }),
+//             )
+//             .await;
+
+//         cx.update(|cx| {
+//             open_paths(
+//                 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
+//                 &app_state,
+//                 None,
+//                 cx,
+//             )
+//         })
+//         .await
+//         .unwrap();
+//         assert_eq!(cx.windows().len(), 1);
+
+//         cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(cx.windows().len(), 1);
+//         let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
+//         workspace_1.update(cx, |workspace, cx| {
+//             assert_eq!(workspace.worktrees(cx).count(), 2);
+//             assert!(workspace.left_dock().read(cx).is_open());
+//             assert!(workspace.active_pane().is_focused(cx));
+//         });
+
+//         cx.update(|cx| {
+//             open_paths(
+//                 &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
+//                 &app_state,
+//                 None,
+//                 cx,
+//             )
+//         })
+//         .await
+//         .unwrap();
+//         assert_eq!(cx.windows().len(), 2);
+
+//         // Replace existing windows
+//         let window = cx.windows()[0].downcast::<Workspace>().unwrap();
+//         cx.update(|cx| {
+//             open_paths(
+//                 &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
+//                 &app_state,
+//                 Some(window),
+//                 cx,
+//             )
+//         })
+//         .await
+//         .unwrap();
+//         assert_eq!(cx.windows().len(), 2);
+//         let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
+//         workspace_1.update(cx, |workspace, cx| {
+//             assert_eq!(
+//                 workspace
+//                     .worktrees(cx)
+//                     .map(|w| w.read(cx).abs_path())
+//                     .collect::<Vec<_>>(),
+//                 &[Path::new("/root/c").into(), Path::new("/root/d").into()]
+//             );
+//             assert!(workspace.left_dock().read(cx).is_open());
+//             assert!(workspace.active_pane().is_focused(cx));
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree("/root", json!({"a": "hey"}))
+//             .await;
+
+//         cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(cx.windows().len(), 1);
+
+//         // When opening the workspace, the window is not in a edited state.
+//         let window = cx.windows()[0].downcast::<Workspace>().unwrap();
+//         let workspace = window.root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+//         let editor = workspace.read_with(cx, |workspace, cx| {
+//             workspace
+//                 .active_item(cx)
+//                 .unwrap()
+//                 .downcast::<Editor>()
+//                 .unwrap()
+//         });
+//         assert!(!window.is_edited(cx));
+
+//         // Editing a buffer marks the window as edited.
+//         editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
+//         assert!(window.is_edited(cx));
+
+//         // Undoing the edit restores the window's edited state.
+//         editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
+//         assert!(!window.is_edited(cx));
+
+//         // Redoing the edit marks the window as edited again.
+//         editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
+//         assert!(window.is_edited(cx));
+
+//         // Closing the item restores the window's edited state.
+//         let close = pane.update(cx, |pane, cx| {
+//             drop(editor);
+//             pane.close_active_item(&Default::default(), cx).unwrap()
+//         });
+//         executor.run_until_parked();
+
+//         window.simulate_prompt_answer(1, cx);
+//         close.await.unwrap();
+//         assert!(!window.is_edited(cx));
+
+//         // Opening the buffer again doesn't impact the window's edited state.
+//         cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+//             .await
+//             .unwrap();
+//         let editor = workspace.read_with(cx, |workspace, cx| {
+//             workspace
+//                 .active_item(cx)
+//                 .unwrap()
+//                 .downcast::<Editor>()
+//                 .unwrap()
+//         });
+//         assert!(!window.is_edited(cx));
+
+//         // Editing the buffer marks the window as edited.
+//         editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
+//         assert!(window.is_edited(cx));
+
+//         // Ensure closing the window via the mouse gets preempted due to the
+//         // buffer having unsaved changes.
+//         assert!(!window.simulate_close(cx));
+//         executor.run_until_parked();
+//         assert_eq!(cx.windows().len(), 1);
+
+//         // The window is successfully closed after the user dismisses the prompt.
+//         window.simulate_prompt_answer(1, cx);
+//         executor.run_until_parked();
+//         assert_eq!(cx.windows().len(), 0);
+//     }
+
+//     #[gpui::test]
+//     async fn test_new_empty_workspace(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         cx.update(|cx| {
+//             open_new(&app_state, cx, |workspace, cx| {
+//                 Editor::new_file(workspace, &Default::default(), cx)
+//             })
+//         })
+//         .await;
+
+//         let window = cx
+//             .windows()
+//             .first()
+//             .unwrap()
+//             .downcast::<Workspace>()
+//             .unwrap();
+//         let workspace = window.root(cx);
+
+//         let editor = workspace.update(cx, |workspace, cx| {
+//             workspace
+//                 .active_item(cx)
+//                 .unwrap()
+//                 .downcast::<editor::Editor>()
+//                 .unwrap()
+//         });
+
+//         editor.update(cx, |editor, cx| {
+//             assert!(editor.text(cx).is_empty());
+//             assert!(!editor.is_dirty(cx));
+//         });
+
+//         let save_task = workspace.update(cx, |workspace, cx| {
+//             workspace.save_active_item(SaveIntent::Save, cx)
+//         });
+//         app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+//         cx.foreground().run_until_parked();
+//         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
+//         save_task.await.unwrap();
+//         editor.read_with(cx, |editor, cx| {
+//             assert!(!editor.is_dirty(cx));
+//             assert_eq!(editor.title(cx), "the-new-name");
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_open_entry(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/root",
+//                 json!({
+//                     "a": {
+//                         "file1": "contents 1",
+//                         "file2": "contents 2",
+//                         "file3": "contents 3",
+//                     },
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+//         let workspace = window.root(cx);
+
+//         let entries = cx.read(|cx| workspace.file_project_paths(cx));
+//         let file1 = entries[0].clone();
+//         let file2 = entries[1].clone();
+//         let file3 = entries[2].clone();
+
+//         // Open the first entry
+//         let entry_1 = workspace
+//             .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+//             .await
+//             .unwrap();
+//         cx.read(|cx| {
+//             let pane = workspace.read(cx).active_pane().read(cx);
+//             assert_eq!(
+//                 pane.active_item().unwrap().project_path(cx),
+//                 Some(file1.clone())
+//             );
+//             assert_eq!(pane.items_len(), 1);
+//         });
+
+//         // Open the second entry
+//         workspace
+//             .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+//             .await
+//             .unwrap();
+//         cx.read(|cx| {
+//             let pane = workspace.read(cx).active_pane().read(cx);
+//             assert_eq!(
+//                 pane.active_item().unwrap().project_path(cx),
+//                 Some(file2.clone())
+//             );
+//             assert_eq!(pane.items_len(), 2);
+//         });
+
+//         // Open the first entry again. The existing pane item is activated.
+//         let entry_1b = workspace
+//             .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(entry_1.id(), entry_1b.id());
+
+//         cx.read(|cx| {
+//             let pane = workspace.read(cx).active_pane().read(cx);
+//             assert_eq!(
+//                 pane.active_item().unwrap().project_path(cx),
+//                 Some(file1.clone())
+//             );
+//             assert_eq!(pane.items_len(), 2);
+//         });
+
+//         // Split the pane with the first entry, then open the second entry again.
+//         workspace
+//             .update(cx, |w, cx| {
+//                 w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
+//                 w.open_path(file2.clone(), None, true, cx)
+//             })
+//             .await
+//             .unwrap();
+
+//         workspace.read_with(cx, |w, cx| {
+//             assert_eq!(
+//                 w.active_pane()
+//                     .read(cx)
+//                     .active_item()
+//                     .unwrap()
+//                     .project_path(cx),
+//                 Some(file2.clone())
+//             );
+//         });
+
+//         // Open the third entry twice concurrently. Only one pane item is added.
+//         let (t1, t2) = workspace.update(cx, |w, cx| {
+//             (
+//                 w.open_path(file3.clone(), None, true, cx),
+//                 w.open_path(file3.clone(), None, true, cx),
+//             )
+//         });
+//         t1.await.unwrap();
+//         t2.await.unwrap();
+//         cx.read(|cx| {
+//             let pane = workspace.read(cx).active_pane().read(cx);
+//             assert_eq!(
+//                 pane.active_item().unwrap().project_path(cx),
+//                 Some(file3.clone())
+//             );
+//             let pane_entries = pane
+//                 .items()
+//                 .map(|i| i.project_path(cx).unwrap())
+//                 .collect::<Vec<_>>();
+//             assert_eq!(pane_entries, &[file1, file2, file3]);
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_open_paths(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/",
+//                 json!({
+//                     "dir1": {
+//                         "a.txt": ""
+//                     },
+//                     "dir2": {
+//                         "b.txt": ""
+//                     },
+//                     "dir3": {
+//                         "c.txt": ""
+//                     },
+//                     "d.txt": ""
+//                 }),
+//             )
+//             .await;
+
+//         cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(cx.windows().len(), 1);
+//         let workspace = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
+
+//         #[track_caller]
+//         fn assert_project_panel_selection(
+//             workspace: &Workspace,
+//             expected_worktree_path: &Path,
+//             expected_entry_path: &Path,
+//             cx: &AppContext,
+//         ) {
+//             let project_panel = [
+//                 workspace.left_dock().read(cx).panel::<ProjectPanel>(),
+//                 workspace.right_dock().read(cx).panel::<ProjectPanel>(),
+//                 workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
+//             ]
+//             .into_iter()
+//             .find_map(std::convert::identity)
+//             .expect("found no project panels")
+//             .read(cx);
+//             let (selected_worktree, selected_entry) = project_panel
+//                 .selected_entry(cx)
+//                 .expect("project panel should have a selected entry");
+//             assert_eq!(
+//                 selected_worktree.abs_path().as_ref(),
+//                 expected_worktree_path,
+//                 "Unexpected project panel selected worktree path"
+//             );
+//             assert_eq!(
+//                 selected_entry.path.as_ref(),
+//                 expected_entry_path,
+//                 "Unexpected project panel selected entry path"
+//             );
+//         }
+
+//         // Open a file within an existing worktree.
+//         workspace
+//             .update(cx, |view, cx| {
+//                 view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
+//             })
+//             .await;
+//         cx.read(|cx| {
+//             let workspace = workspace.read(cx);
+//             assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
+//             assert_eq!(
+//                 workspace
+//                     .active_pane()
+//                     .read(cx)
+//                     .active_item()
+//                     .unwrap()
+//                     .as_any()
+//                     .downcast_ref::<Editor>()
+//                     .unwrap()
+//                     .read(cx)
+//                     .title(cx),
+//                 "a.txt"
+//             );
+//         });
+
+//         // Open a file outside of any existing worktree.
+//         workspace
+//             .update(cx, |view, cx| {
+//                 view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
+//             })
+//             .await;
+//         cx.read(|cx| {
+//             let workspace = workspace.read(cx);
+//             assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
+//             let worktree_roots = workspace
+//                 .worktrees(cx)
+//                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+//                 .collect::<HashSet<_>>();
+//             assert_eq!(
+//                 worktree_roots,
+//                 vec!["/dir1", "/dir2/b.txt"]
+//                     .into_iter()
+//                     .map(Path::new)
+//                     .collect(),
+//             );
+//             assert_eq!(
+//                 workspace
+//                     .active_pane()
+//                     .read(cx)
+//                     .active_item()
+//                     .unwrap()
+//                     .as_any()
+//                     .downcast_ref::<Editor>()
+//                     .unwrap()
+//                     .read(cx)
+//                     .title(cx),
+//                 "b.txt"
+//             );
+//         });
+
+//         // Ensure opening a directory and one of its children only adds one worktree.
+//         workspace
+//             .update(cx, |view, cx| {
+//                 view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
+//             })
+//             .await;
+//         cx.read(|cx| {
+//             let workspace = workspace.read(cx);
+//             assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
+//             let worktree_roots = workspace
+//                 .worktrees(cx)
+//                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+//                 .collect::<HashSet<_>>();
+//             assert_eq!(
+//                 worktree_roots,
+//                 vec!["/dir1", "/dir2/b.txt", "/dir3"]
+//                     .into_iter()
+//                     .map(Path::new)
+//                     .collect(),
+//             );
+//             assert_eq!(
+//                 workspace
+//                     .active_pane()
+//                     .read(cx)
+//                     .active_item()
+//                     .unwrap()
+//                     .as_any()
+//                     .downcast_ref::<Editor>()
+//                     .unwrap()
+//                     .read(cx)
+//                     .title(cx),
+//                 "c.txt"
+//             );
+//         });
+
+//         // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
+//         workspace
+//             .update(cx, |view, cx| {
+//                 view.open_paths(vec!["/d.txt".into()], false, cx)
+//             })
+//             .await;
+//         cx.read(|cx| {
+//             let workspace = workspace.read(cx);
+//             assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
+//             let worktree_roots = workspace
+//                 .worktrees(cx)
+//                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+//                 .collect::<HashSet<_>>();
+//             assert_eq!(
+//                 worktree_roots,
+//                 vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
+//                     .into_iter()
+//                     .map(Path::new)
+//                     .collect(),
+//             );
+
+//             let visible_worktree_roots = workspace
+//                 .visible_worktrees(cx)
+//                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+//                 .collect::<HashSet<_>>();
+//             assert_eq!(
+//                 visible_worktree_roots,
+//                 vec!["/dir1", "/dir2/b.txt", "/dir3"]
+//                     .into_iter()
+//                     .map(Path::new)
+//                     .collect(),
+//             );
+
+//             assert_eq!(
+//                 workspace
+//                     .active_pane()
+//                     .read(cx)
+//                     .active_item()
+//                     .unwrap()
+//                     .as_any()
+//                     .downcast_ref::<Editor>()
+//                     .unwrap()
+//                     .read(cx)
+//                     .title(cx),
+//                 "d.txt"
+//             );
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         cx.update(|cx| {
+//             cx.update_global::<SettingsStore, _, _>(|store, cx| {
+//                 store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+//                     project_settings.file_scan_exclusions =
+//                         Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
+//                 });
+//             });
+//         });
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/root",
+//                 json!({
+//                     ".gitignore": "ignored_dir\n",
+//                     ".git": {
+//                         "HEAD": "ref: refs/heads/main",
+//                     },
+//                     "regular_dir": {
+//                         "file": "regular file contents",
+//                     },
+//                     "ignored_dir": {
+//                         "ignored_subdir": {
+//                             "file": "ignored subfile contents",
+//                         },
+//                         "file": "ignored file contents",
+//                     },
+//                     "excluded_dir": {
+//                         "file": "excluded file contents",
+//                     },
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+//         let workspace = window.root(cx);
+
+//         let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
+//         let paths_to_open = [
+//             Path::new("/root/excluded_dir/file").to_path_buf(),
+//             Path::new("/root/.git/HEAD").to_path_buf(),
+//             Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
+//         ];
+//         let (opened_workspace, new_items) = cx
+//             .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx))
+//             .await
+//             .unwrap();
+
+//         assert_eq!(
+//             opened_workspace.id(),
+//             workspace.id(),
+//             "Excluded files in subfolders of a workspace root should be opened in the workspace"
+//         );
+//         let mut opened_paths = cx.read(|cx| {
+//             assert_eq!(
+//                 new_items.len(),
+//                 paths_to_open.len(),
+//                 "Expect to get the same number of opened items as submitted paths to open"
+//             );
+//             new_items
+//                 .iter()
+//                 .zip(paths_to_open.iter())
+//                 .map(|(i, path)| {
+//                     match i {
+//                         Some(Ok(i)) => {
+//                             Some(i.project_path(cx).map(|p| p.path.display().to_string()))
+//                         }
+//                         Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
+//                         None => None,
+//                     }
+//                     .flatten()
+//                 })
+//                 .collect::<Vec<_>>()
+//         });
+//         opened_paths.sort();
+//         assert_eq!(
+//             opened_paths,
+//             vec![
+//                 None,
+//                 Some(".git/HEAD".to_string()),
+//                 Some("excluded_dir/file".to_string()),
+//             ],
+//             "Excluded files should get opened, excluded dir should not get opened"
+//         );
+
+//         let entries = cx.read(|cx| workspace.file_project_paths(cx));
+//         assert_eq!(
+//             initial_entries, entries,
+//             "Workspace entries should not change after opening excluded files and directories paths"
+//         );
+
+//         cx.read(|cx| {
+//             let pane = workspace.read(cx).active_pane().read(cx);
+//             let mut opened_buffer_paths = pane
+//                 .items()
+//                 .map(|i| {
+//                     i.project_path(cx)
+//                         .expect("all excluded files that got open should have a path")
+//                         .path
+//                         .display()
+//                         .to_string()
+//                 })
+//                 .collect::<Vec<_>>();
+//             opened_buffer_paths.sort();
+//             assert_eq!(
+//                 opened_buffer_paths,
+//                 vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
+//                 "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
+//             );
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_save_conflicting_item(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree("/root", json!({ "a.txt": "" }))
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+//         let workspace = window.root(cx);
+
+//         // Open a file within an existing worktree.
+//         workspace
+//             .update(cx, |view, cx| {
+//                 view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
+//             })
+//             .await;
+//         let editor = cx.read(|cx| {
+//             let pane = workspace.read(cx).active_pane().read(cx);
+//             let item = pane.active_item().unwrap();
+//             item.downcast::<Editor>().unwrap()
+//         });
+
+//         editor.update(cx, |editor, cx| editor.handle_input("x", cx));
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_file("/root/a.txt", "changed".to_string())
+//             .await;
+//         editor
+//             .condition(cx, |editor, cx| editor.has_conflict(cx))
+//             .await;
+//         cx.read(|cx| assert!(editor.is_dirty(cx)));
+
+//         let save_task = workspace.update(cx, |workspace, cx| {
+//             workspace.save_active_item(SaveIntent::Save, cx)
+//         });
+//         cx.foreground().run_until_parked();
+//         window.simulate_prompt_answer(0, cx);
+//         save_task.await.unwrap();
+//         editor.read_with(cx, |editor, cx| {
+//             assert!(!editor.is_dirty(cx));
+//             assert!(!editor.has_conflict(cx));
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+
+//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+//         project.update(cx, |project, _| project.languages().add(rust_lang()));
+//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+//         let workspace = window.root(cx);
+//         let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
+
+//         // Create a new untitled buffer
+//         cx.dispatch_action(window.into(), NewFile);
+//         let editor = workspace.read_with(cx, |workspace, cx| {
+//             workspace
+//                 .active_item(cx)
+//                 .unwrap()
+//                 .downcast::<Editor>()
+//                 .unwrap()
+//         });
+
+//         editor.update(cx, |editor, cx| {
+//             assert!(!editor.is_dirty(cx));
+//             assert_eq!(editor.title(cx), "untitled");
+//             assert!(Arc::ptr_eq(
+//                 &editor.language_at(0, cx).unwrap(),
+//                 &languages::PLAIN_TEXT
+//             ));
+//             editor.handle_input("hi", cx);
+//             assert!(editor.is_dirty(cx));
+//         });
+
+//         // Save the buffer. This prompts for a filename.
+//         let save_task = workspace.update(cx, |workspace, cx| {
+//             workspace.save_active_item(SaveIntent::Save, cx)
+//         });
+//         cx.foreground().run_until_parked();
+//         cx.simulate_new_path_selection(|parent_dir| {
+//             assert_eq!(parent_dir, Path::new("/root"));
+//             Some(parent_dir.join("the-new-name.rs"))
+//         });
+//         cx.read(|cx| {
+//             assert!(editor.is_dirty(cx));
+//             assert_eq!(editor.read(cx).title(cx), "untitled");
+//         });
+
+//         // When the save completes, the buffer's title is updated and the language is assigned based
+//         // on the path.
+//         save_task.await.unwrap();
+//         editor.read_with(cx, |editor, cx| {
+//             assert!(!editor.is_dirty(cx));
+//             assert_eq!(editor.title(cx), "the-new-name.rs");
+//             assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
+//         });
+
+//         // Edit the file and save it again. This time, there is no filename prompt.
+//         editor.update(cx, |editor, cx| {
+//             editor.handle_input(" there", cx);
+//             assert!(editor.is_dirty(cx));
+//         });
+//         let save_task = workspace.update(cx, |workspace, cx| {
+//             workspace.save_active_item(SaveIntent::Save, cx)
+//         });
+//         save_task.await.unwrap();
+//         assert!(!cx.did_prompt_for_new_path());
+//         editor.read_with(cx, |editor, cx| {
+//             assert!(!editor.is_dirty(cx));
+//             assert_eq!(editor.title(cx), "the-new-name.rs")
+//         });
+
+//         // Open the same newly-created file in another pane item. The new editor should reuse
+//         // the same buffer.
+//         cx.dispatch_action(window.into(), NewFile);
+//         workspace
+//             .update(cx, |workspace, cx| {
+//                 workspace.split_and_clone(
+//                     workspace.active_pane().clone(),
+//                     SplitDirection::Right,
+//                     cx,
+//                 );
+//                 workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
+//             })
+//             .await
+//             .unwrap();
+//         let editor2 = workspace.update(cx, |workspace, cx| {
+//             workspace
+//                 .active_item(cx)
+//                 .unwrap()
+//                 .downcast::<Editor>()
+//                 .unwrap()
+//         });
+//         cx.read(|cx| {
+//             assert_eq!(
+//                 editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
+//                 editor.read(cx).buffer().read(cx).as_singleton().unwrap()
+//             );
+//         })
+//     }
+
+//     #[gpui::test]
+//     async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+
+//         let project = Project::test(app_state.fs.clone(), [], cx).await;
+//         project.update(cx, |project, _| project.languages().add(rust_lang()));
+//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+//         let workspace = window.root(cx);
+
+//         // Create a new untitled buffer
+//         cx.dispatch_action(window.into(), NewFile);
+//         let editor = workspace.read_with(cx, |workspace, cx| {
+//             workspace
+//                 .active_item(cx)
+//                 .unwrap()
+//                 .downcast::<Editor>()
+//                 .unwrap()
+//         });
+
+//         editor.update(cx, |editor, cx| {
+//             assert!(Arc::ptr_eq(
+//                 &editor.language_at(0, cx).unwrap(),
+//                 &languages::PLAIN_TEXT
+//             ));
+//             editor.handle_input("hi", cx);
+//             assert!(editor.is_dirty(cx));
+//         });
+
+//         // Save the buffer. This prompts for a filename.
+//         let save_task = workspace.update(cx, |workspace, cx| {
+//             workspace.save_active_item(SaveIntent::Save, cx)
+//         });
+//         cx.foreground().run_until_parked();
+//         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
+//         save_task.await.unwrap();
+//         // The buffer is not dirty anymore and the language is assigned based on the path.
+//         editor.read_with(cx, |editor, cx| {
+//             assert!(!editor.is_dirty(cx));
+//             assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_pane_actions(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/root",
+//                 json!({
+//                     "a": {
+//                         "file1": "contents 1",
+//                         "file2": "contents 2",
+//                         "file3": "contents 3",
+//                     },
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+//         let workspace = window.root(cx);
+
+//         let entries = cx.read(|cx| workspace.file_project_paths(cx));
+//         let file1 = entries[0].clone();
+
+//         let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
+
+//         workspace
+//             .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+//             .await
+//             .unwrap();
+
+//         let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
+//             let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
+//             assert_eq!(editor.project_path(cx), Some(file1.clone()));
+//             let buffer = editor.update(cx, |editor, cx| {
+//                 editor.insert("dirt", cx);
+//                 editor.buffer().downgrade()
+//             });
+//             (editor.downgrade(), buffer)
+//         });
+
+//         cx.dispatch_action(window.into(), pane::SplitRight);
+//         let editor_2 = cx.update(|cx| {
+//             let pane_2 = workspace.read(cx).active_pane().clone();
+//             assert_ne!(pane_1, pane_2);
+
+//             let pane2_item = pane_2.read(cx).active_item().unwrap();
+//             assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
+
+//             pane2_item.downcast::<Editor>().unwrap().downgrade()
+//         });
+//         cx.dispatch_action(
+//             window.into(),
+//             workspace::CloseActiveItem { save_intent: None },
+//         );
+
+//         cx.foreground().run_until_parked();
+//         workspace.read_with(cx, |workspace, _| {
+//             assert_eq!(workspace.panes().len(), 1);
+//             assert_eq!(workspace.active_pane(), &pane_1);
+//         });
+
+//         cx.dispatch_action(
+//             window.into(),
+//             workspace::CloseActiveItem { save_intent: None },
+//         );
+//         cx.foreground().run_until_parked();
+//         window.simulate_prompt_answer(1, cx);
+//         cx.foreground().run_until_parked();
+
+//         workspace.read_with(cx, |workspace, cx| {
+//             assert_eq!(workspace.panes().len(), 1);
+//             assert!(workspace.active_item(cx).is_none());
+//         });
+
+//         cx.assert_dropped(editor_1);
+//         cx.assert_dropped(editor_2);
+//         cx.assert_dropped(buffer);
+//     }
+
+//     #[gpui::test]
+//     async fn test_navigation(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/root",
+//                 json!({
+//                     "a": {
+//                         "file1": "contents 1\n".repeat(20),
+//                         "file2": "contents 2\n".repeat(20),
+//                         "file3": "contents 3\n".repeat(20),
+//                     },
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+//         let workspace = cx
+//             .add_window(|cx| Workspace::test_new(project.clone(), cx))
+//             .root(cx);
+//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         let entries = cx.read(|cx| workspace.file_project_paths(cx));
+//         let file1 = entries[0].clone();
+//         let file2 = entries[1].clone();
+//         let file3 = entries[2].clone();
+
+//         let editor1 = workspace
+//             .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+//             .await
+//             .unwrap()
+//             .downcast::<Editor>()
+//             .unwrap();
+//         editor1.update(cx, |editor, cx| {
+//             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+//                 s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
+//             });
+//         });
+//         let editor2 = workspace
+//             .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+//             .await
+//             .unwrap()
+//             .downcast::<Editor>()
+//             .unwrap();
+//         let editor3 = workspace
+//             .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
+//             .await
+//             .unwrap()
+//             .downcast::<Editor>()
+//             .unwrap();
+
+//         editor3
+//             .update(cx, |editor, cx| {
+//                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+//                     s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
+//                 });
+//                 editor.newline(&Default::default(), cx);
+//                 editor.newline(&Default::default(), cx);
+//                 editor.move_down(&Default::default(), cx);
+//                 editor.move_down(&Default::default(), cx);
+//                 editor.save(project.clone(), cx)
+//             })
+//             .await
+//             .unwrap();
+//         editor3.update(cx, |editor, cx| {
+//             editor.set_scroll_position(vec2f(0., 12.5), cx)
+//         });
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file3.clone(), DisplayPoint::new(16, 0), 12.5)
+//         );
+
+//         workspace
+//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file3.clone(), DisplayPoint::new(0, 0), 0.)
+//         );
+
+//         workspace
+//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file2.clone(), DisplayPoint::new(0, 0), 0.)
+//         );
+
+//         workspace
+//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file1.clone(), DisplayPoint::new(10, 0), 0.)
+//         );
+
+//         workspace
+//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file1.clone(), DisplayPoint::new(0, 0), 0.)
+//         );
+
+//         // Go back one more time and ensure we don't navigate past the first item in the history.
+//         workspace
+//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file1.clone(), DisplayPoint::new(0, 0), 0.)
+//         );
+
+//         workspace
+//             .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file1.clone(), DisplayPoint::new(10, 0), 0.)
+//         );
+
+//         workspace
+//             .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file2.clone(), DisplayPoint::new(0, 0), 0.)
+//         );
+
+//         // Go forward to an item that has been closed, ensuring it gets re-opened at the same
+//         // location.
+//         pane.update(cx, |pane, cx| {
+//             let editor3_id = editor3.id();
+//             drop(editor3);
+//             pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
+//         })
+//         .await
+//         .unwrap();
+//         workspace
+//             .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file3.clone(), DisplayPoint::new(0, 0), 0.)
+//         );
+
+//         workspace
+//             .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file3.clone(), DisplayPoint::new(16, 0), 12.5)
+//         );
+
+//         workspace
+//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file3.clone(), DisplayPoint::new(0, 0), 0.)
+//         );
+
+//         // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
+//         pane.update(cx, |pane, cx| {
+//             let editor2_id = editor2.id();
+//             drop(editor2);
+//             pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
+//         })
+//         .await
+//         .unwrap();
+//         app_state
+//             .fs
+//             .remove_file(Path::new("/root/a/file2"), Default::default())
+//             .await
+//             .unwrap();
+//         cx.foreground().run_until_parked();
+
+//         workspace
+//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file1.clone(), DisplayPoint::new(10, 0), 0.)
+//         );
+//         workspace
+//             .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file3.clone(), DisplayPoint::new(0, 0), 0.)
+//         );
+
+//         // Modify file to collapse multiple nav history entries into the same location.
+//         // Ensure we don't visit the same location twice when navigating.
+//         editor1.update(cx, |editor, cx| {
+//             editor.change_selections(None, cx, |s| {
+//                 s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
+//             })
+//         });
+
+//         for _ in 0..5 {
+//             editor1.update(cx, |editor, cx| {
+//                 editor.change_selections(None, cx, |s| {
+//                     s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
+//                 });
+//             });
+//             editor1.update(cx, |editor, cx| {
+//                 editor.change_selections(None, cx, |s| {
+//                     s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
+//                 })
+//             });
+//         }
+
+//         editor1.update(cx, |editor, cx| {
+//             editor.transact(cx, |editor, cx| {
+//                 editor.change_selections(None, cx, |s| {
+//                     s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
+//                 });
+//                 editor.insert("", cx);
+//             })
+//         });
+
+//         editor1.update(cx, |editor, cx| {
+//             editor.change_selections(None, cx, |s| {
+//                 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+//             })
+//         });
+//         workspace
+//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file1.clone(), DisplayPoint::new(2, 0), 0.)
+//         );
+//         workspace
+//             .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             active_location(&workspace, cx),
+//             (file1.clone(), DisplayPoint::new(3, 0), 0.)
+//         );
+
+//         fn active_location(
+//             workspace: &ViewHandle<Workspace>,
+//             cx: &mut TestAppContext,
+//         ) -> (ProjectPath, DisplayPoint, f32) {
+//             workspace.update(cx, |workspace, cx| {
+//                 let item = workspace.active_item(cx).unwrap();
+//                 let editor = item.downcast::<Editor>().unwrap();
+//                 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
+//                     (
+//                         editor.selections.display_ranges(cx),
+//                         editor.scroll_position(cx),
+//                     )
+//                 });
+//                 (
+//                     item.project_path(cx).unwrap(),
+//                     selections[0].start,
+//                     scroll_position.y(),
+//                 )
+//             })
+//         }
+//     }
+
+//     #[gpui::test]
+//     async fn test_reopening_closed_items(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/root",
+//                 json!({
+//                     "a": {
+//                         "file1": "",
+//                         "file2": "",
+//                         "file3": "",
+//                         "file4": "",
+//                     },
+//                 }),
+//             )
+//             .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 pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+//         let entries = cx.read(|cx| workspace.file_project_paths(cx));
+//         let file1 = entries[0].clone();
+//         let file2 = entries[1].clone();
+//         let file3 = entries[2].clone();
+//         let file4 = entries[3].clone();
+
+//         let file1_item_id = workspace
+//             .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+//             .await
+//             .unwrap()
+//             .id();
+//         let file2_item_id = workspace
+//             .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+//             .await
+//             .unwrap()
+//             .id();
+//         let file3_item_id = workspace
+//             .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
+//             .await
+//             .unwrap()
+//             .id();
+//         let file4_item_id = workspace
+//             .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
+//             .await
+//             .unwrap()
+//             .id();
+//         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+//         // Close all the pane items in some arbitrary order.
+//         pane.update(cx, |pane, cx| {
+//             pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
+//         })
+//         .await
+//         .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+//         pane.update(cx, |pane, cx| {
+//             pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
+//         })
+//         .await
+//         .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+//         pane.update(cx, |pane, cx| {
+//             pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
+//         })
+//         .await
+//         .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+//         pane.update(cx, |pane, cx| {
+//             pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
+//         })
+//         .await
+//         .unwrap();
+//         assert_eq!(active_path(&workspace, cx), None);
+
+//         // Reopen all the closed items, ensuring they are reopened in the same order
+//         // in which they were closed.
+//         workspace
+//             .update(cx, Workspace::reopen_closed_item)
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+//         workspace
+//             .update(cx, Workspace::reopen_closed_item)
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+//         workspace
+//             .update(cx, Workspace::reopen_closed_item)
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+//         workspace
+//             .update(cx, Workspace::reopen_closed_item)
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+//         // Reopening past the last closed item is a no-op.
+//         workspace
+//             .update(cx, Workspace::reopen_closed_item)
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+//         // Reopening closed items doesn't interfere with navigation history.
+//         workspace
+//             .update(cx, |workspace, cx| {
+//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
+//             })
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+//         workspace
+//             .update(cx, |workspace, cx| {
+//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
+//             })
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+//         workspace
+//             .update(cx, |workspace, cx| {
+//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
+//             })
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+//         workspace
+//             .update(cx, |workspace, cx| {
+//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
+//             })
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+//         workspace
+//             .update(cx, |workspace, cx| {
+//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
+//             })
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+//         workspace
+//             .update(cx, |workspace, cx| {
+//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
+//             })
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+//         workspace
+//             .update(cx, |workspace, cx| {
+//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
+//             })
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+//         workspace
+//             .update(cx, |workspace, cx| {
+//                 workspace.go_back(workspace.active_pane().downgrade(), cx)
+//             })
+//             .await
+//             .unwrap();
+//         assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+//         fn active_path(
+//             workspace: &ViewHandle<Workspace>,
+//             cx: &TestAppContext,
+//         ) -> Option<ProjectPath> {
+//             workspace.read_with(cx, |workspace, cx| {
+//                 let item = workspace.active_item(cx)?;
+//                 item.project_path(cx)
+//             })
+//         }
+//     }
+
+//     #[gpui::test]
+//     async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
+//         struct TestView;
+
+//         impl Entity for TestView {
+//             type Event = ();
+//         }
+
+//         impl View for TestView {
+//             fn ui_name() -> &'static str {
+//                 "TestView"
+//             }
+
+//             fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+//                 Empty::new().into_any()
+//             }
+//         }
+
+//         let executor = cx.background();
+//         let fs = FakeFs::new(executor.clone());
+
+//         actions!(test, [A, B]);
+//         // From the Atom keymap
+//         actions!(workspace, [ActivatePreviousPane]);
+//         // From the JetBrains keymap
+//         actions!(pane, [ActivatePrevItem]);
+
+//         fs.save(
+//             "/settings.json".as_ref(),
+//             &r#"
+//             {
+//                 "base_keymap": "Atom"
+//             }
+//             "#
+//             .into(),
+//             Default::default(),
+//         )
+//         .await
+//         .unwrap();
+
+//         fs.save(
+//             "/keymap.json".as_ref(),
+//             &r#"
+//             [
+//                 {
+//                     "bindings": {
+//                         "backspace": "test::A"
+//                     }
+//                 }
+//             ]
+//             "#
+//             .into(),
+//             Default::default(),
+//         )
+//         .await
+//         .unwrap();
+
+//         cx.update(|cx| {
+//             cx.set_global(SettingsStore::test(cx));
+//             theme::init(Assets, cx);
+//             welcome::init(cx);
+
+//             cx.add_global_action(|_: &A, _cx| {});
+//             cx.add_global_action(|_: &B, _cx| {});
+//             cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
+//             cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
+
+//             let settings_rx = watch_config_file(
+//                 executor.clone(),
+//                 fs.clone(),
+//                 PathBuf::from("/settings.json"),
+//             );
+//             let keymap_rx =
+//                 watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
+
+//             handle_keymap_file_changes(keymap_rx, cx);
+//             handle_settings_file_changes(settings_rx, cx);
+//         });
+
+//         cx.foreground().run_until_parked();
+
+//         let window = cx.add_window(|_| TestView);
+
+//         // Test loading the keymap base at all
+//         assert_key_bindings_for(
+//             window.into(),
+//             cx,
+//             vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+//             line!(),
+//         );
+
+//         // Test modifying the users keymap, while retaining the base keymap
+//         fs.save(
+//             "/keymap.json".as_ref(),
+//             &r#"
+//             [
+//                 {
+//                     "bindings": {
+//                         "backspace": "test::B"
+//                     }
+//                 }
+//             ]
+//             "#
+//             .into(),
+//             Default::default(),
+//         )
+//         .await
+//         .unwrap();
+
+//         cx.foreground().run_until_parked();
+
+//         assert_key_bindings_for(
+//             window.into(),
+//             cx,
+//             vec![("backspace", &B), ("k", &ActivatePreviousPane)],
+//             line!(),
+//         );
+
+//         // Test modifying the base, while retaining the users keymap
+//         fs.save(
+//             "/settings.json".as_ref(),
+//             &r#"
+//             {
+//                 "base_keymap": "JetBrains"
+//             }
+//             "#
+//             .into(),
+//             Default::default(),
+//         )
+//         .await
+//         .unwrap();
+
+//         cx.foreground().run_until_parked();
+
+//         assert_key_bindings_for(
+//             window.into(),
+//             cx,
+//             vec![("backspace", &B), ("[", &ActivatePrevItem)],
+//             line!(),
+//         );
+
+//         #[track_caller]
+//         fn assert_key_bindings_for<'a>(
+//             window: AnyWindowHandle,
+//             cx: &TestAppContext,
+//             actions: Vec<(&'static str, &'a dyn Action)>,
+//             line: u32,
+//         ) {
+//             for (key, action) in actions {
+//                 // assert that...
+//                 assert!(
+//                     cx.available_actions(window, 0)
+//                         .into_iter()
+//                         .any(|(_, bound_action, b)| {
+//                             // action names match...
+//                             bound_action.name() == action.name()
+//                         && bound_action.namespace() == action.namespace()
+//                         // and key strokes contain the given key
+//                         && b.iter()
+//                             .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
+//                         }),
+//                     "On {} Failed to find {} with key binding {}",
+//                     line,
+//                     action.name(),
+//                     key
+//                 );
+//             }
+//         }
+//     }
+
+//     #[gpui::test]
+//     async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
+//         struct TestView;
+
+//         impl Entity for TestView {
+//             type Event = ();
+//         }
+
+//         impl View for TestView {
+//             fn ui_name() -> &'static str {
+//                 "TestView"
+//             }
+
+//             fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+//                 Empty::new().into_any()
+//             }
+//         }
+
+//         let executor = cx.background();
+//         let fs = FakeFs::new(executor.clone());
+
+//         actions!(test, [A, B]);
+//         // From the Atom keymap
+//         actions!(workspace, [ActivatePreviousPane]);
+//         // From the JetBrains keymap
+//         actions!(pane, [ActivatePrevItem]);
+
+//         fs.save(
+//             "/settings.json".as_ref(),
+//             &r#"
+//             {
+//                 "base_keymap": "Atom"
+//             }
+//             "#
+//             .into(),
+//             Default::default(),
+//         )
+//         .await
+//         .unwrap();
+
+//         fs.save(
+//             "/keymap.json".as_ref(),
+//             &r#"
+//             [
+//                 {
+//                     "bindings": {
+//                         "backspace": "test::A"
+//                     }
+//                 }
+//             ]
+//             "#
+//             .into(),
+//             Default::default(),
+//         )
+//         .await
+//         .unwrap();
+
+//         cx.update(|cx| {
+//             cx.set_global(SettingsStore::test(cx));
+//             theme::init(Assets, cx);
+//             welcome::init(cx);
+
+//             cx.add_global_action(|_: &A, _cx| {});
+//             cx.add_global_action(|_: &B, _cx| {});
+//             cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
+//             cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
+
+//             let settings_rx = watch_config_file(
+//                 executor.clone(),
+//                 fs.clone(),
+//                 PathBuf::from("/settings.json"),
+//             );
+//             let keymap_rx =
+//                 watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
+
+//             handle_keymap_file_changes(keymap_rx, cx);
+//             handle_settings_file_changes(settings_rx, cx);
+//         });
+
+//         cx.foreground().run_until_parked();
+
+//         let window = cx.add_window(|_| TestView);
+
+//         // Test loading the keymap base at all
+//         assert_key_bindings_for(
+//             window.into(),
+//             cx,
+//             vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+//             line!(),
+//         );
+
+//         // Test disabling the key binding for the base keymap
+//         fs.save(
+//             "/keymap.json".as_ref(),
+//             &r#"
+//             [
+//                 {
+//                     "bindings": {
+//                         "backspace": null
+//                     }
+//                 }
+//             ]
+//             "#
+//             .into(),
+//             Default::default(),
+//         )
+//         .await
+//         .unwrap();
+
+//         cx.foreground().run_until_parked();
+
+//         assert_key_bindings_for(
+//             window.into(),
+//             cx,
+//             vec![("k", &ActivatePreviousPane)],
+//             line!(),
+//         );
+
+//         // Test modifying the base, while retaining the users keymap
+//         fs.save(
+//             "/settings.json".as_ref(),
+//             &r#"
+//             {
+//                 "base_keymap": "JetBrains"
+//             }
+//             "#
+//             .into(),
+//             Default::default(),
+//         )
+//         .await
+//         .unwrap();
+
+//         cx.foreground().run_until_parked();
+
+//         assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!());
+
+//         #[track_caller]
+//         fn assert_key_bindings_for<'a>(
+//             window: AnyWindowHandle,
+//             cx: &TestAppContext,
+//             actions: Vec<(&'static str, &'a dyn Action)>,
+//             line: u32,
+//         ) {
+//             for (key, action) in actions {
+//                 // assert that...
+//                 assert!(
+//                     cx.available_actions(window, 0)
+//                         .into_iter()
+//                         .any(|(_, bound_action, b)| {
+//                             // action names match...
+//                             bound_action.name() == action.name()
+//                         && bound_action.namespace() == action.namespace()
+//                         // and key strokes contain the given key
+//                         && b.iter()
+//                             .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
+//                         }),
+//                     "On {} Failed to find {} with key binding {}",
+//                     line,
+//                     action.name(),
+//                     key
+//                 );
+//             }
+//         }
+//     }
+
+//     #[gpui::test]
+//     fn test_bundled_settings_and_themes(cx: &mut AppContext) {
+//         cx.platform()
+//             .fonts()
+//             .add_fonts(&[
+//                 Assets
+//                     .load("fonts/zed-sans/zed-sans-extended.ttf")
+//                     .unwrap()
+//                     .to_vec()
+//                     .into(),
+//                 Assets
+//                     .load("fonts/zed-mono/zed-mono-extended.ttf")
+//                     .unwrap()
+//                     .to_vec()
+//                     .into(),
+//                 Assets
+//                     .load("fonts/plex/IBMPlexSans-Regular.ttf")
+//                     .unwrap()
+//                     .to_vec()
+//                     .into(),
+//             ])
+//             .unwrap();
+//         let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
+//         let mut settings = SettingsStore::default();
+//         settings
+//             .set_default_settings(&settings::default_settings(), cx)
+//             .unwrap();
+//         cx.set_global(settings);
+//         theme::init(Assets, cx);
+
+//         let mut has_default_theme = false;
+//         for theme_name in themes.list(false).map(|meta| meta.name) {
+//             let theme = themes.get(&theme_name).unwrap();
+//             assert_eq!(theme.meta.name, theme_name);
+//             if theme.meta.name == settings::get::<ThemeSettings>(cx).theme.meta.name {
+//                 has_default_theme = true;
+//             }
+//         }
+//         assert!(has_default_theme);
+//     }
+
+//     #[gpui::test]
+//     fn test_bundled_languages(cx: &mut AppContext) {
+//         cx.set_global(SettingsStore::test(cx));
+//         let mut languages = LanguageRegistry::test();
+//         languages.set_executor(cx.background().clone());
+//         let languages = Arc::new(languages);
+//         let node_runtime = node_runtime::FakeNodeRuntime::new();
+//         languages::init(languages.clone(), node_runtime, cx);
+//         for name in languages.language_names() {
+//             languages.language_for_name(&name);
+//         }
+//         cx.foreground().run_until_parked();
+//     }
+
+//     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+//         cx.foreground().forbid_parking();
+//         cx.update(|cx| {
+//             let mut app_state = AppState::test(cx);
+//             let state = Arc::get_mut(&mut app_state).unwrap();
+//             state.initialize_workspace = initialize_workspace;
+//             state.build_window_options = build_window_options;
+//             theme::init((), cx);
+//             audio::init((), cx);
+//             channel::init(&app_state.client, app_state.user_store.clone(), cx);
+//             call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+//             notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+//             workspace::init(app_state.clone(), cx);
+//             Project::init_settings(cx);
+//             language::init(cx);
+//             editor::init(cx);
+//             project_panel::init_settings(cx);
+//             collab_ui::init(&app_state, cx);
+//             pane::init(cx);
+//             project_panel::init((), cx);
+//             terminal_view::init(cx);
+//             assistant::init(cx);
+//             app_state
+//         })
+//     }
+
+//     fn rust_lang() -> Arc<language::Language> {
+//         Arc::new(language::Language::new(
+//             language::LanguageConfig {
+//                 name: "Rust".into(),
+//                 path_suffixes: vec!["rs".to_string()],
+//                 ..Default::default()
+//             },
+//             Some(tree_sitter_rust::language()),
+//         ))
+//     }
+// }

test.rs 🔗

@@ -1,5618 +0,0 @@
-#![feature(prelude_import)]
-#![allow(dead_code, unused_variables)]
-#[prelude_import]
-use std::prelude::rust_2021::*;
-#[macro_use]
-extern crate std;
-use color::black;
-use components::button;
-use element::Element;
-use frame::frame;
-use gpui::{
-    geometry::{rect::RectF, vector::vec2f},
-    platform::WindowOptions,aa
-};
-use log::LevelFilter;a
-use simplelog::SimpleLogger;
-use themes::{rose_pine, ThemeColors};
-use view::view;a
-mod adapter {
-    use crate::element::AnyElement;
-    use crate::element::{LayoutContext, PaintContext};
-    use gpui::{geometry::rect::RectF, LayoutEngine};aaaa
-    use util::ResultExt;
-    pub struct Adapter<V>(pub(crate) AnyElement<V>);
-    impl<V: 'static> gpui::Element<V> for Adapter<V> {aa
-        type LayoutState = Option<LayaoutEngine>;
-        type PaintState = ();
-        fn layout(
-            &mut self,
-            constraint: gpui::SizeConstraint,
-            view: &mut V,
-            cx: &mut LayoutContext<V>,aa
-        ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
-            cx.push_layout_engine(LayoutEngine::new());
-            let node = self.0.layout(view, cx).log_err();a
-            if let Some(node) = node {
-                let layout_engine = cx.layout_engine().unwrap();
-                layout_engine.compute_layout(node, constraint.max).log_err();
-            }
-            let layout_engine = cx.pop_layout_engine();
-            if true {a
-                if !layout_engine.is_some() {
-                    ::core::panicking::panic("assertion failed: layout_engine.is_some()")
-                }
-            }
-            (constraint.max, layout_engine)a
-        }
-        fn paint(a
-            &mut self,
-            scene: &mut gpui::SceneBuilder,
-            bounds: RectF,
-            visible_bounds: RectF,
-            layout_engine: &mut Option<LayoutEngine>,
-            view: &mut V,
-            legacy_cx: &mut gpui::PaintContext<V>,aaa
-        ) -> Self::PaintState {
-            legacy_cx.push_layout_engine(layout_engine.take().unwrap());
-            let mut cx = PaintContext::new(legacy_cx, scene);
-            self.0.paint(view, &mut cx).log_err();
-            *layout_engine = legacy_cx.pop_layout_engine();
-            if true {
-                if !layout_engine.is_some() {
-                    ::core::panicking::panic("assertion failed: layout_engine.is_some()")
-                }
-            }
-        }
-        fn rect_for_text_range(
-            &self,
-            range_utf16: std::ops::Range<usize>,
-            bounds: RectF,
-            visible_bounds: RectF,
-            layout: &Self::LayoutState,
-            paint: &Self::PaintState,
-            view: &V,
-            cx: &gpui::ViewContext<V>,
-        ) -> Option<RectF> {
-            ::core::panicking::panic("not yet implemented")
-        }
-        fn debug(
-            &self,
-            bounds: RectF,
-            layout: &Self::LayoutState,
-            paint: &Self::PaintState,
-            view: &V,
-            cx: &gpui::ViewContext<V>,
-        ) -> gpui::serde_json::Value {
-            ::core::panicking::panic("not yet implemented")
-        }
-    }
-}
-mod color {
-    #![allow(dead_code)]
-    use smallvec::SmallVec;
-    use std::{num::ParseIntError, ops::Range};
-    pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
-        let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
-        let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
-        let b = (hex & 0xFF) as f32 / 255.0;
-        Rgba { r, g, b, a: 1.0 }.into()
-    }
-    pub struct Rgba {
-        pub r: f32,
-        pub g: f32,
-        pub b: f32,
-        pub a: f32,
-    }
-    #[automatically_derived]
-    impl ::core::clone::Clone for Rgba {
-        #[inline]
-        fn clone(&self) -> Rgba {
-            let _: ::core::clone::AssertParamIsClone<f32>;
-            *self
-        }
-    }
-    #[automatically_derived]
-    impl ::core::marker::Copy for Rgba {}
-    #[automatically_derived]
-    impl ::core::default::Default for Rgba {
-        #[inline]
-        fn default() -> Rgba {
-            Rgba {
-                r: ::core::default::Default::default(),
-                g: ::core::default::Default::default(),
-                b: ::core::default::Default::default(),
-                a: ::core::default::Default::default(),
-            }
-        }
-    }
-    #[automatically_derived]
-    impl ::core::fmt::Debug for Rgba {
-        fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
-            ::core::fmt::Formatter::debug_struct_field4_finish(
-                f, "Rgba", "r", &self.r, "g", &self.g, "b", &self.b, "a", &&self.a,
-            )
-        }
-    }
-    pub trait Lerp {
-        fn lerp(&self, level: f32) -> Hsla;
-    }
-    impl Lerp for Range<Hsla> {
-        fn lerp(&self, level: f32) -> Hsla {
-            let level = level.clamp(0., 1.);
-            Hsla {
-                h: self.start.h + (level * (self.end.h - self.start.h)),
-                s: self.start.s + (level * (self.end.s - self.start.s)),
-                l: self.start.l + (level * (self.end.l - self.start.l)),
-                a: self.start.a + (level * (self.end.a - self.start.a)),
-            }
-        }
-    }
-    impl From<gpui::color::Color> for Rgba {
-        fn from(value: gpui::color::Color) -> Self {
-            Self {
-                r: value.0.r as f32 / 255.0,
-                g: value.0.g as f32 / 255.0,
-                b: value.0.b as f32 / 255.0,
-                a: value.0.a as f32 / 255.0,
-            }
-        }
-    }
-    impl From<Hsla> for Rgba {
-        fn from(color: Hsla) -> Self {
-            let h = color.h;
-            let s = color.s;
-            let l = color.l;
-            let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
-            let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
-            let m = l - c / 2.0;
-            let cm = c + m;
-            let xm = x + m;
-            let (r, g, b) = match (h * 6.0).floor() as i32 {
-                0 | 6 => (cm, xm, m),
-                1 => (xm, cm, m),
-                2 => (m, cm, xm),
-                3 => (m, xm, cm),
-                4 => (xm, m, cm),
-                _ => (cm, m, xm),
-            };
-            Rgba {
-                r,
-                g,
-                b,
-                a: color.a,
-            }
-        }
-    }
-    impl TryFrom<&'_ str> for Rgba {
-        type Error = ParseIntError;
-        fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
-            let r = u8::from_str_radix(&value[1..3], 16)? as f32 / 255.0;
-            let g = u8::from_str_radix(&value[3..5], 16)? as f32 / 255.0;
-            let b = u8::from_str_radix(&value[5..7], 16)? as f32 / 255.0;
-            let a = if value.len() > 7 {
-                u8::from_str_radix(&value[7..9], 16)? as f32 / 255.0
-            } else {
-                1.0
-            };
-            Ok(Rgba { r, g, b, a })
-        }
-    }
-    impl Into<gpui::color::Color> for Rgba {
-        fn into(self) -> gpui::color::Color {
-            gpui::color::rgba(self.r, self.g, self.b, self.a)
-        }
-    }
-    pub struct Hsla {
-        pub h: f32,
-        pub s: f32,
-        pub l: f32,
-        pub a: f32,
-    }
-    #[automatically_derived]
-    impl ::core::default::Default for Hsla {
-        #[inline]
-        fn default() -> Hsla {
-            Hsla {
-                h: ::core::default::Default::default(),
-                s: ::core::default::Default::default(),
-                l: ::core::default::Default::default(),
-                a: ::core::default::Default::default(),
-            }
-        }
-    }
-    #[automatically_derived]
-    impl ::core::marker::Copy for Hsla {}
-    #[automatically_derived]
-    impl ::core::clone::Clone for Hsla {
-        #[inline]
-        fn clone(&self) -> Hsla {
-            let _: ::core::clone::AssertParamIsClone<f32>;
-            *self
-        }
-    }
-    #[automatically_derived]
-    impl ::core::fmt::Debug for Hsla {
-        fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
-            ::core::fmt::Formatter::debug_struct_field4_finish(
-                f, "Hsla", "h", &self.h, "s", &self.s, "l", &self.l, "a", &&self.a,
-            )
-        }
-    }
-    #[automatically_derived]
-    impl ::core::marker::StructuralPartialEq for Hsla {}
-    #[automatically_derived]
-    impl ::core::cmp::PartialEq for Hsla {
-        #[inline]
-        fn eq(&self, other: &Hsla) -> bool {
-            self.h == other.h && self.s == other.s && self.l == other.l && self.a == other.a
-        }
-    }
-    pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
-        Hsla {
-            h: h.clamp(0., 1.),
-            s: s.clamp(0., 1.),
-            l: l.clamp(0., 1.),
-            a: a.clamp(0., 1.),
-        }
-    }
-    pub fn black() -> Hsla {
-        Hsla {
-            h: 0.,
-            s: 0.,
-            l: 0.,
-            a: 1.,
-        }
-    }
-    impl From<Rgba> for Hsla {
-        fn from(color: Rgba) -> Self {
-            let r = color.r;
-            let g = color.g;
-            let b = color.b;
-            let max = r.max(g.max(b));
-            let min = r.min(g.min(b));
-            let delta = max - min;
-            let l = (max + min) / 2.0;
-            let s = if l == 0.0 || l == 1.0 {
-                0.0
-            } else if l < 0.5 {
-                delta / (2.0 * l)
-            } else {
-                delta / (2.0 - 2.0 * l)
-            };
-            let h = if delta == 0.0 {
-                0.0
-            } else if max == r {
-                ((g - b) / delta).rem_euclid(6.0) / 6.0
-            } else if max == g {
-                ((b - r) / delta + 2.0) / 6.0
-            } else {
-                ((r - g) / delta + 4.0) / 6.0
-            };
-            Hsla {
-                h,
-                s,
-                l,
-                a: color.a,
-            }
-        }
-    }
-    impl Hsla {
-        /// Scales the saturation and lightness by the given values, clamping at 1.0.
-        pub fn scale_sl(mut self, s: f32, l: f32) -> Self {
-            self.s = (self.s * s).clamp(0., 1.);
-            self.l = (self.l * l).clamp(0., 1.);
-            self
-        }
-        /// Increases the saturation of the color by a certain amount, with a max
-        /// value of 1.0.
-        pub fn saturate(mut self, amount: f32) -> Self {
-            self.s += amount;
-            self.s = self.s.clamp(0.0, 1.0);
-            self
-        }
-        /// Decreases the saturation of the color by a certain amount, with a min
-        /// value of 0.0.
-        pub fn desaturate(mut self, amount: f32) -> Self {
-            self.s -= amount;
-            self.s = self.s.max(0.0);
-            if self.s < 0.0 {
-                self.s = 0.0;
-            }
-            self
-        }
-        /// Brightens the color by increasing the lightness by a certain amount,
-        /// with a max value of 1.0.
-        pub fn brighten(mut self, amount: f32) -> Self {
-            self.l += amount;
-            self.l = self.l.clamp(0.0, 1.0);
-            self
-        }
-        /// Darkens the color by decreasing the lightness by a certain amount,
-        /// with a max value of 0.0.
-        pub fn darken(mut self, amount: f32) -> Self {
-            self.l -= amount;
-            self.l = self.l.clamp(0.0, 1.0);
-            self
-        }
-    }
-    impl From<gpui::color::Color> for Hsla {
-        fn from(value: gpui::color::Color) -> Self {
-            Rgba::from(value).into()
-        }
-    }
-    impl Into<gpui::color::Color> for Hsla {
-        fn into(self) -> gpui::color::Color {
-            Rgba::from(self).into()
-        }
-    }
-    pub struct ColorScale {
-        colors: SmallVec<[Hsla; 2]>,
-        positions: SmallVec<[f32; 2]>,
-    }
-    pub fn scale<I, C>(colors: I) -> ColorScale
-    where
-        I: IntoIterator<Item = C>,
-        C: Into<Hsla>,
-    {
-        let mut scale = ColorScale {
-            colors: colors.into_iter().map(Into::into).collect(),
-            positions: SmallVec::new(),
-        };
-        let num_colors: f32 = scale.colors.len() as f32 - 1.0;
-        scale.positions = (0..scale.colors.len())
-            .map(|i| i as f32 / num_colors)
-            .collect();
-        scale
-    }
-    impl ColorScale {
-        fn at(&self, t: f32) -> Hsla {
-            if true {
-                if !(0.0 <= t && t <= 1.0) {
-                    {
-                        ::core::panicking::panic_fmt(format_args!(
-                            "t value {0} is out of range. Expected value in range 0.0 to 1.0",
-                            t,
-                        ));
-                    }
-                }
-            }
-            let position = match self
-                .positions
-                .binary_search_by(|a| a.partial_cmp(&t).unwrap())
-            {
-                Ok(index) | Err(index) => index,
-            };
-            let lower_bound = position.saturating_sub(1);
-            let upper_bound = position.min(self.colors.len() - 1);
-            let lower_color = &self.colors[lower_bound];
-            let upper_color = &self.colors[upper_bound];
-            match upper_bound.checked_sub(lower_bound) {
-                Some(0) | None => *lower_color,
-                Some(_) => {
-                    let interval_t = (t - self.positions[lower_bound])
-                        / (self.positions[upper_bound] - self.positions[lower_bound]);
-                    let h = lower_color.h + interval_t * (upper_color.h - lower_color.h);
-                    let s = lower_color.s + interval_t * (upper_color.s - lower_color.s);
-                    let l = lower_color.l + interval_t * (upper_color.l - lower_color.l);
-                    let a = lower_color.a + interval_t * (upper_color.a - lower_color.a);
-                    Hsla { h, s, l, a }
-                }
-            }
-        }
-    }
-}
-mod components {
-    use crate::{
-        element::{Element, ElementMetadata},
-        frame,
-        text::ArcCow,
-        themes::rose_pine,
-    };
-    use gpui::{platform::MouseButton, ViewContext};
-    use gpui_macros::Element;
-    use std::{marker::PhantomData, rc::Rc};
-    struct ButtonHandlers<V, D> {
-        click: Option<Rc<dyn Fn(&mut V, &D, &mut ViewContext<V>)>>,
-    }
-    impl<V, D> Default for ButtonHandlers<V, D> {
-        fn default() -> Self {
-            Self { click: None }
-        }
-    }
-    #[element_crate = "crate"]
-    pub struct Button<V: 'static, D: 'static> {
-        metadata: ElementMetadata<V>,
-        handlers: ButtonHandlers<V, D>,
-        label: Option<ArcCow<'static, str>>,
-        icon: Option<ArcCow<'static, str>>,
-        data: Rc<D>,
-        view_type: PhantomData<V>,
-    }
-    impl<V: 'static, D: 'static> crate::element::Element<V> for Button<V, D> {
-        type Layout = crate::element::AnyElement<V>;
-        fn declared_style(&mut self) -> &mut crate::style::OptionalStyle {
-            &mut self.metadata.style
-        }
-        fn handlers_mut(&mut self) -> &mut Vec<crate::element::EventHandler<V>> {
-            &mut self.metadata.handlers
-        }
-        fn layout(
-            &mut self,
-            view: &mut V,
-            cx: &mut crate::element::LayoutContext<V>,
-        ) -> anyhow::Result<(taffy::tree::NodeId, Self::Layout)> {
-            let mut element = self.render(view, cx).into_any();
-            let node_id = element.layout(view, cx)?;
-            Ok((node_id, element))
-        }
-        fn paint<'a>(
-            &mut self,
-            layout: crate::element::Layout<'a, Self::Layout>,
-            view: &mut V,
-            cx: &mut crate::element::PaintContext<V>,
-        ) -> anyhow::Result<()> {
-            layout.from_element.paint(view, cx)?;
-            Ok(())
-        }
-    }
-    impl<V: 'static, D: 'static> crate::element::IntoElement<V> for Button<V, D> {
-        type Element = Self;
-        fn into_element(self) -> Self {
-            self
-        }
-    }
-    impl<V: 'static> Button<V, ()> {
-        fn new() -> Self {
-            Self {
-                metadata: Default::default(),
-                handlers: ButtonHandlers::default(),
-                label: None,
-                icon: None,
-                data: Rc::new(()),
-                view_type: PhantomData,
-            }
-        }
-        pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
-            Button {
-                metadata: Default::default(),
-                handlers: ButtonHandlers::default(),
-                label: self.label,
-                icon: self.icon,
-                data: Rc::new(data),
-                view_type: PhantomData,
-            }
-        }
-    }
-    impl<V: 'static, D: 'static> Button<V, D> {
-        pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
-            self.label = Some(label.into());
-            self
-        }
-        pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
-            self.icon = Some(icon.into());
-            self
-        }
-        pub fn click(self, handler: impl Fn(&mut V, &D, &mut ViewContext<V>) + 'static) -> Self {
-            let data = self.data.clone();
-            Element::click(self, MouseButton::Left, move |view, _, cx| {
-                handler(view, data.as_ref(), cx);
-            })
-        }
-    }
-    pub fn button<V>() -> Button<V, ()> {
-        Button::new()
-    }
-    impl<V: 'static, D: 'static> Button<V, D> {
-        fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
-            let button = frame()
-                .fill(rose_pine::dawn().error(0.5))
-                .h_4()
-                .children(self.label.clone());
-            if let Some(handler) = self.handlers.click.clone() {
-                let data = self.data.clone();
-                button.mouse_down(MouseButton::Left, move |view, event, cx| {
-                    handler(view, data.as_ref(), cx)
-                })
-            } else {
-                button
-            }
-        }
-    }
-}
-mod element {
-    pub use crate::paint_context::PaintContext;
-    use crate::{
-        adapter::Adapter,
-        color::Hsla,
-        hoverable::Hoverable,
-        style::{Display, Fill, OptionalStyle, Overflow, Position},
-    };
-    use anyhow::Result;
-    pub use gpui::LayoutContext;
-    use gpui::{
-        geometry::{DefinedLength, Length, OptionalPoint},
-        platform::{MouseButton, MouseButtonEvent},
-        EngineLayout, EventContext, RenderContext, ViewContext,
-    };
-    use gpui_macros::tailwind_lengths;
-    use std::{
-        any::{Any, TypeId},
-        cell::Cell,
-        rc::Rc,
-    };
-    pub use taffy::tree::NodeId;
-    pub struct Layout<'a, E: ?Sized> {
-        pub from_engine: EngineLayout,
-        pub from_element: &'a mut E,
-    }
-    pub struct ElementMetadata<V> {
-        pub style: OptionalStyle,
-        pub handlers: Vec<EventHandler<V>>,
-    }
-    pub struct EventHandler<V> {
-        handler: Rc<dyn Fn(&mut V, &dyn Any, &mut EventContext<V>)>,
-        event_type: TypeId,
-        outside_bounds: bool,
-    }
-    impl<V> Clone for EventHandler<V> {
-        fn clone(&self) -> Self {
-            Self {
-                handler: self.handler.clone(),
-                event_type: self.event_type,
-                outside_bounds: self.outside_bounds,
-            }
-        }
-    }
-    impl<V> Default for ElementMetadata<V> {
-        fn default() -> Self {
-            Self {
-                style: OptionalStyle::default(),
-                handlers: Vec::new(),
-            }
-        }
-    }
-    pub trait Element<V: 'static>: 'static {
-        type Layout: 'static;
-        fn declared_style(&mut self) -> &mut OptionalStyle;
-        fn computed_style(&mut self) -> &OptionalStyle {
-            self.declared_style()
-        }
-        fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>>;
-        fn layout(
-            &mut self,
-            view: &mut V,
-            cx: &mut LayoutContext<V>,
-        ) -> Result<(NodeId, Self::Layout)>;
-        fn paint<'a>(
-            &mut self,
-            layout: Layout<Self::Layout>,
-            view: &mut V,
-            cx: &mut PaintContext<V>,
-        ) -> Result<()>;
-        /// Convert to a dynamically-typed element suitable for layout and paint.
-        fn into_any(self) -> AnyElement<V>
-        where
-            Self: 'static + Sized,
-        {
-            AnyElement {
-                element: Box::new(self) as Box<dyn ElementObject<V>>,
-                layout: None,
-            }
-        }
-        fn adapt(self) -> Adapter<V>
-        where
-            Self: Sized,
-            Self: Element<V>,
-        {
-            Adapter(self.into_any())
-        }
-        fn click(
-            self,
-            button: MouseButton,
-            handler: impl Fn(&mut V, &MouseButtonEvent, &mut ViewContext<V>) + 'static,
-        ) -> Self
-        where
-            Self: Sized,
-        {
-            let pressed: Rc<Cell<bool>> = Default::default();
-            self.mouse_down(button, {
-                let pressed = pressed.clone();
-                move |_, _, _| {
-                    pressed.set(true);
-                }
-            })
-            .mouse_up_outside(button, {
-                let pressed = pressed.clone();
-                move |_, _, _| {
-                    pressed.set(false);
-                }
-            })
-            .mouse_up(button, move |view, event, event_cx| {
-                if pressed.get() {
-                    pressed.set(false);
-                    handler(view, event, event_cx);
-                }
-            })
-        }
-        fn mouse_down(
-            mut self,
-            button: MouseButton,
-            handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
-        ) -> Self
-        where
-            Self: Sized,
-        {
-            self.handlers_mut().push(EventHandler {
-                handler: Rc::new(move |view, event, event_cx| {
-                    let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
-                    if event.button == button && event.is_down {
-                        handler(view, event, event_cx);
-                    }
-                }),
-                event_type: TypeId::of::<MouseButtonEvent>(),
-                outside_bounds: false,
-            });
-            self
-        }
-        fn mouse_down_outside(
-            mut self,
-            button: MouseButton,
-            handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
-        ) -> Self
-        where
-            Self: Sized,
-        {
-            self.handlers_mut().push(EventHandler {
-                handler: Rc::new(move |view, event, event_cx| {
-                    let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
-                    if event.button == button && event.is_down {
-                        handler(view, event, event_cx);
-                    }
-                }),
-                event_type: TypeId::of::<MouseButtonEvent>(),
-                outside_bounds: true,
-            });
-            self
-        }
-        fn mouse_up(
-            mut self,
-            button: MouseButton,
-            handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
-        ) -> Self
-        where
-            Self: Sized,
-        {
-            self.handlers_mut().push(EventHandler {
-                handler: Rc::new(move |view, event, event_cx| {
-                    let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
-                    if event.button == button && !event.is_down {
-                        handler(view, event, event_cx);
-                    }
-                }),
-                event_type: TypeId::of::<MouseButtonEvent>(),
-                outside_bounds: false,
-            });
-            self
-        }
-        fn mouse_up_outside(
-            mut self,
-            button: MouseButton,
-            handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
-        ) -> Self
-        where
-            Self: Sized,
-        {
-            self.handlers_mut().push(EventHandler {
-                handler: Rc::new(move |view, event, event_cx| {
-                    let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
-                    if event.button == button && !event.is_down {
-                        handler(view, event, event_cx);
-                    }
-                }),
-                event_type: TypeId::of::<MouseButtonEvent>(),
-                outside_bounds: true,
-            });
-            self
-        }
-        fn block(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().display = Some(Display::Block);
-            self
-        }
-        fn flex(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().display = Some(Display::Flex);
-            self
-        }
-        fn grid(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().display = Some(Display::Grid);
-            self
-        }
-        fn overflow_visible(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().overflow = OptionalPoint {
-                x: Some(Overflow::Visible),
-                y: Some(Overflow::Visible),
-            };
-            self
-        }
-        fn overflow_hidden(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().overflow = OptionalPoint {
-                x: Some(Overflow::Hidden),
-                y: Some(Overflow::Hidden),
-            };
-            self
-        }
-        fn overflow_scroll(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().overflow = OptionalPoint {
-                x: Some(Overflow::Scroll),
-                y: Some(Overflow::Scroll),
-            };
-            self
-        }
-        fn overflow_x_visible(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().overflow.x = Some(Overflow::Visible);
-            self
-        }
-        fn overflow_x_hidden(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().overflow.x = Some(Overflow::Hidden);
-            self
-        }
-        fn overflow_x_scroll(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().overflow.x = Some(Overflow::Scroll);
-            self
-        }
-        fn overflow_y_visible(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().overflow.y = Some(Overflow::Visible);
-            self
-        }
-        fn overflow_y_hidden(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().overflow.y = Some(Overflow::Hidden);
-            self
-        }
-        fn overflow_y_scroll(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().overflow.y = Some(Overflow::Scroll);
-            self
-        }
-        fn relative(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().position = Some(Position::Relative);
-            self
-        }
-        fn absolute(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().position = Some(Position::Absolute);
-            self
-        }
-        fn inset_0(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Pixels(0.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_px(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Pixels(1.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_0_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.125).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_1(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.25).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_1_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.375).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_2(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.5).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_2_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.625).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_3(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.75).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_3_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.875).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_4(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.25).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_6(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.5).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_7(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.75).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_8(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_9(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.25).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_10(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.5).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_11(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.75).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_12(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(3.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_14(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(3.5).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_16(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(4.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_20(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(5.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_24(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(6.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_28(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(7.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_32(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(8.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_36(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(9.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_40(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(10.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_44(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(11.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_48(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(12.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_52(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(13.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_56(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(14.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_60(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(15.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_64(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(16.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_72(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(18.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_80(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(20.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_96(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(24.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_half(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_1_3rd(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(33.333333).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_2_3rd(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(66.666667).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_1_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(25.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_2_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_3_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(75.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_1_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(20.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_2_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(40.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_3_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(60.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_4_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(80.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_1_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(16.666667).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_2_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(33.333333).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_3_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_4_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(66.666667).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_5_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(83.333333).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_1_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(8.333333).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_2_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(16.666667).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_3_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(25.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_4_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(33.333333).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_5_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(41.666667).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_6_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_7_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(58.333333).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_8_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(66.666667).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_9_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(75.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_10_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(83.333333).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_11_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(91.666667).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn inset_full(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(100.).into();
-            {
-                let inset = self
-                    .computed_style()
-                    .inset
-                    .get_or_insert_with(Default::default);
-                inset.top = length;
-                inset.right = length;
-                inset.bottom = length;
-                inset.left = length;
-                self
-            }
-        }
-        fn w(mut self, width: impl Into<Length>) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().size.width = Some(width.into());
-            self
-        }
-        fn w_auto(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().size.width = Some(Length::Auto);
-            self
-        }
-        fn w_0(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Pixels(0.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_px(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Pixels(1.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_0_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.125).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_1(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.25).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_1_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.375).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_2(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.5).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_2_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.625).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_3(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.75).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_3_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.875).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_4(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.25).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_6(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.5).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_7(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.75).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_8(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_9(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.25).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_10(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.5).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_11(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.75).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_12(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(3.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_14(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(3.5).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_16(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(4.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_20(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(5.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_24(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(6.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_28(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(7.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_32(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(8.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_36(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(9.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_40(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(10.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_44(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(11.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_48(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(12.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_52(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(13.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_56(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(14.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_60(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(15.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_64(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(16.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_72(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(18.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_80(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(20.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_96(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(24.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_half(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_1_3rd(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(33.333333).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_2_3rd(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(66.666667).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_1_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(25.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_2_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_3_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(75.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_1_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(20.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_2_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(40.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_3_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(60.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_4_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(80.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_1_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(16.666667).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_2_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(33.333333).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_3_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_4_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(66.666667).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_5_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(83.333333).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_1_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(8.333333).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_2_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(16.666667).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_3_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(25.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_4_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(33.333333).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_5_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(41.666667).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_6_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_7_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(58.333333).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_8_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(66.666667).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_9_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(75.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_10_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(83.333333).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_11_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(91.666667).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn w_full(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(100.).into();
-            {
-                self.declared_style().size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_0(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Pixels(0.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_px(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Pixels(1.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_0_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.125).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_1(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.25).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_1_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.375).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_2(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.5).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_2_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.625).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_3(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.75).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_3_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.875).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_4(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.25).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_6(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.5).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_7(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.75).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_8(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_9(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.25).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_10(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.5).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_11(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.75).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_12(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(3.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_14(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(3.5).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_16(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(4.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_20(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(5.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_24(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(6.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_28(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(7.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_32(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(8.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_36(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(9.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_40(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(10.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_44(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(11.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_48(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(12.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_52(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(13.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_56(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(14.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_60(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(15.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_64(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(16.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_72(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(18.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_80(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(20.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_96(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(24.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_half(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_1_3rd(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(33.333333).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_2_3rd(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(66.666667).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_1_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(25.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_2_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_3_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(75.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_1_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(20.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_2_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(40.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_3_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(60.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_4_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(80.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_1_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(16.666667).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_2_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(33.333333).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_3_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_4_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(66.666667).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_5_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(83.333333).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_1_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(8.333333).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_2_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(16.666667).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_3_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(25.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_4_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(33.333333).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_5_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(41.666667).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_6_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_7_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(58.333333).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_8_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(66.666667).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_9_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(75.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_10_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(83.333333).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_11_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(91.666667).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn min_w_full(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(100.).into();
-            {
-                self.declared_style().min_size.width = Some(length);
-                self
-            }
-        }
-        fn h(mut self, height: impl Into<Length>) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().size.height = Some(height.into());
-            self
-        }
-        fn h_auto(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().size.height = Some(Length::Auto);
-            self
-        }
-        fn h_0(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Pixels(0.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_px(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Pixels(1.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_0_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(0.125).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_1(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(0.25).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_1_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(0.375).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_2(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(0.5).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_2_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(0.625).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_3(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(0.75).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_3_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(0.875).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_4(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(1.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(1.25).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_6(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(1.5).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_7(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(1.75).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_8(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(2.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_9(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(2.25).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_10(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(2.5).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_11(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(2.75).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_12(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(3.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_14(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(3.5).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_16(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(4.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_20(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(5.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_24(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(6.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_28(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(7.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_32(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(8.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_36(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(9.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_40(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(10.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_44(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(11.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_48(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(12.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_52(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(13.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_56(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(14.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_60(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(15.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_64(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(16.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_72(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(18.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_80(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(20.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_96(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Rems(24.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_half(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_1_3rd(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(33.333333).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_2_3rd(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(66.666667).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_1_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(25.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_2_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_3_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(75.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_1_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(20.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_2_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(40.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_3_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(60.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_4_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(80.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_1_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(16.666667).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_2_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(33.333333).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_3_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_4_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(66.666667).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_5_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(83.333333).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_1_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(8.333333).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_2_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(16.666667).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_3_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(25.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_4_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(33.333333).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_5_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(41.666667).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_6_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_7_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(58.333333).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_8_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(66.666667).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_9_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(75.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_10_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(83.333333).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_11_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(91.666667).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn h_full(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let height = DefinedLength::Percent(100.).into();
-            {
-                self.declared_style().size.height = Some(height);
-                self
-            }
-        }
-        fn min_h_0(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Pixels(0.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_px(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Pixels(1.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_0_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.125).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_1(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.25).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_1_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.375).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_2(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.5).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_2_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.625).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_3(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.75).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_3_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(0.875).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_4(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_5(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.25).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_6(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.5).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_7(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(1.75).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_8(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_9(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.25).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_10(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.5).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_11(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(2.75).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_12(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(3.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_14(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(3.5).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_16(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(4.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_20(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(5.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_24(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(6.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_28(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(7.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_32(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(8.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_36(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(9.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_40(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(10.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_44(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(11.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_48(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(12.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_52(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(13.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_56(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(14.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_60(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(15.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_64(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(16.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_72(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(18.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_80(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(20.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_96(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Rems(24.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_half(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_1_3rd(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(33.333333).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_2_3rd(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(66.666667).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_1_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(25.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_2_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_3_4th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(75.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_1_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(20.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_2_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(40.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_3_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(60.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_4_5th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(80.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_1_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(16.666667).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_2_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(33.333333).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_3_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_4_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(66.666667).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_5_6th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(83.333333).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_1_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(8.333333).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_2_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(16.666667).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_3_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(25.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_4_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(33.333333).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_5_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(41.666667).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_6_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(50.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_7_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(58.333333).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_8_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(66.666667).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_9_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(75.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_10_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(83.333333).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_11_12th(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(91.666667).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn min_h_full(mut self) -> Self
-        where
-            Self: Sized,
-        {
-            let length = DefinedLength::Percent(100.).into();
-            {
-                self.declared_style().min_size.height = Some(length);
-                self
-            }
-        }
-        fn hoverable(self) -> Hoverable<V, Self>
-        where
-            Self: Sized,
-        {
-            Hoverable::new(self)
-        }
-        fn fill(mut self, fill: impl Into<Fill>) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().fill = Some(Some(fill.into()));
-            self
-        }
-        fn text_color(mut self, color: impl Into<Hsla>) -> Self
-        where
-            Self: Sized,
-        {
-            self.declared_style().text_color = Some(Some(color.into()));
-            self
-        }
-    }
-    trait ElementObject<V> {
-        fn style(&mut self) -> &mut OptionalStyle;
-        fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>>;
-        fn layout(
-            &mut self,
-            view: &mut V,
-            cx: &mut LayoutContext<V>,
-        ) -> Result<(NodeId, Box<dyn Any>)>;
-        fn paint(
-            &mut self,
-            layout: Layout<dyn Any>,
-            view: &mut V,
-            cx: &mut PaintContext<V>,
-        ) -> Result<()>;
-    }
-    impl<V: 'static, E: Element<V>> ElementObject<V> for E {
-        fn style(&mut self) -> &mut OptionalStyle {
-            Element::declared_style(self)
-        }
-        fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
-            Element::handlers_mut(self)
-        }
-        fn layout(
-            &mut self,
-            view: &mut V,
-            cx: &mut LayoutContext<V>,
-        ) -> Result<(NodeId, Box<dyn Any>)> {
-            let (node_id, layout) = self.layout(view, cx)?;
-            let layout = Box::new(layout) as Box<dyn Any>;
-            Ok((node_id, layout))
-        }
-        fn paint(
-            &mut self,
-            layout: Layout<dyn Any>,
-            view: &mut V,
-            cx: &mut PaintContext<V>,
-        ) -> Result<()> {
-            let layout = Layout {
-                from_engine: layout.from_engine,
-                from_element: layout.from_element.downcast_mut::<E::Layout>().unwrap(),
-            };
-            self.paint(layout, view, cx)
-        }
-    }
-    /// A dynamically typed element.
-    pub struct AnyElement<V> {
-        element: Box<dyn ElementObject<V>>,
-        layout: Option<(NodeId, Box<dyn Any>)>,
-    }
-    impl<V: 'static> AnyElement<V> {
-        pub fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<NodeId> {
-            let pushed_text_style = self.push_text_style(cx);
-            let (node_id, layout) = self.element.layout(view, cx)?;
-            self.layout = Some((node_id, layout));
-            if pushed_text_style {
-                cx.pop_text_style();
-            }
-            Ok(node_id)
-        }
-        pub fn push_text_style(&mut self, cx: &mut impl RenderContext) -> bool {
-            let text_style = self.element.style().text_style();
-            if let Some(text_style) = text_style {
-                let mut current_text_style = cx.text_style();
-                text_style.apply(&mut current_text_style);
-                cx.push_text_style(current_text_style);
-                true
-            } else {
-                false
-            }
-        }
-        pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) -> Result<()> {
-            let pushed_text_style = self.push_text_style(cx);
-            let (layout_node_id, element_layout) =
-                self.layout.as_mut().expect("paint called before layout");
-            let layout = Layout {
-                from_engine: cx
-                    .layout_engine()
-                    .unwrap()
-                    .computed_layout(*layout_node_id)
-                    .expect("make sure you're using this within a gpui2 adapter element"),
-                from_element: element_layout.as_mut(),
-            };
-            let style = self.element.style();
-            let fill_color = style.fill.flatten().and_then(|fill| fill.color());
-            if let Some(fill_color) = fill_color {
-                cx.scene.push_quad(gpui::scene::Quad {
-                    bounds: layout.from_engine.bounds,
-                    background: Some(fill_color.into()),
-                    border: Default::default(),
-                    corner_radii: Default::default(),
-                });
-            }
-            for event_handler in self.element.handlers_mut().iter().cloned() {
-                let EngineLayout { order, bounds } = layout.from_engine;
-                let view_id = cx.view_id();
-                let view_event_handler = event_handler.handler.clone();
-                cx.scene
-                    .interactive_regions
-                    .push(gpui::scene::InteractiveRegion {
-                        order,
-                        bounds,
-                        outside_bounds: event_handler.outside_bounds,
-                        event_handler: Rc::new(move |view, event, window_cx, view_id| {
-                            let mut view_context = ViewContext::mutable(window_cx, view_id);
-                            let mut event_context = EventContext::new(&mut view_context);
-                            view_event_handler(
-                                view.downcast_mut().unwrap(),
-                                event,
-                                &mut event_context,
-                            );
-                        }),
-                        event_type: event_handler.event_type,
-                        view_id,
-                    });
-            }
-            self.element.paint(layout, view, cx)?;
-            if pushed_text_style {
-                cx.pop_text_style();
-            }
-            Ok(())
-        }
-    }
-    impl<V: 'static> Element<V> for AnyElement<V> {
-        type Layout = ();
-        fn declared_style(&mut self) -> &mut OptionalStyle {
-            self.element.style()
-        }
-        fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
-            self.element.handlers_mut()
-        }
-        fn layout(
-            &mut self,
-            view: &mut V,
-            cx: &mut LayoutContext<V>,
-        ) -> Result<(NodeId, Self::Layout)> {
-            Ok((self.layout(view, cx)?, ()))
-        }
-        fn paint(
-            &mut self,
-            layout: Layout<()>,
-            view: &mut V,
-            cx: &mut PaintContext<V>,
-        ) -> Result<()> {
-            self.paint(view, cx)
-        }
-    }
-    pub trait IntoElement<V: 'static> {
-        type Element: Element<V>;
-        fn into_element(self) -> Self::Element;
-        fn into_any_element(self) -> AnyElement<V>
-        where
-            Self: Sized,
-        {
-            self.into_element().into_any()
-        }
-    }
-}
-mod frame {
-    use crate::{
-        element::{
-            AnyElement, Element, EventHandler, IntoElement, Layout, LayoutContext, NodeId,
-            PaintContext,
-        },
-        style::{OptionalStyle, Style},
-    };
-    use anyhow::{anyhow, Result};
-    use gpui::LayoutNodeId;
-    use gpui_macros::IntoElement;
-    #[element_crate = "crate"]
-    pub struct Frame<V: 'static> {
-        style: OptionalStyle,
-        handlers: Vec<EventHandler<V>>,
-        children: Vec<AnyElement<V>>,
-    }
-    impl<V: 'static> crate::element::IntoElement<V> for Frame<V> {
-        type Element = Self;
-        fn into_element(self) -> Self {
-            self
-        }
-    }
-    pub fn frame<V>() -> Frame<V> {
-        Frame {
-            style: OptionalStyle::default(),
-            handlers: Vec::new(),
-            children: Vec::new(),
-        }
-    }
-    impl<V: 'static> Element<V> for Frame<V> {
-        type Layout = ();
-        fn declared_style(&mut self) -> &mut OptionalStyle {
-            &mut self.style
-        }
-        fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
-            &mut self.handlers
-        }
-        fn layout(
-            &mut self,
-            view: &mut V,
-            cx: &mut LayoutContext<V>,
-        ) -> Result<(NodeId, Self::Layout)> {
-            let child_layout_node_ids = self
-                .children
-                .iter_mut()
-                .map(|child| child.layout(view, cx))
-                .collect::<Result<Vec<LayoutNodeId>>>()?;
-            let rem_size = cx.rem_pixels();
-            let style: Style = self.style.into();
-            let node_id = cx
-                .layout_engine()
-                .ok_or_else(|| {
-                    ::anyhow::__private::must_use({
-                        let error =
-                            ::anyhow::__private::format_err(format_args!("no layout engine"));
-                        error
-                    })
-                })?
-                .add_node(style.to_taffy(rem_size), child_layout_node_ids)?;
-            Ok((node_id, ()))
-        }
-        fn paint(
-            &mut self,
-            layout: Layout<()>,
-            view: &mut V,
-            cx: &mut PaintContext<V>,
-        ) -> Result<()> {
-            for child in &mut self.children {
-                child.paint(view, cx)?;
-            }
-            Ok(())
-        }
-    }
-    impl<V: 'static> Frame<V> {
-        pub fn child(mut self, child: impl IntoElement<V>) -> Self {
-            self.children.push(child.into_any_element());
-            self
-        }
-        pub fn children<I, E>(mut self, children: I) -> Self
-        where
-            I: IntoIterator<Item = E>,
-            E: IntoElement<V>,
-        {
-            self.children
-                .extend(children.into_iter().map(|e| e.into_any_element()));
-            self
-        }
-    }
-}
-mod hoverable {
-    use crate::{
-        element::Element,
-        style::{OptionalStyle, Style},
-    };
-    use gpui::{
-        geometry::{rect::RectF, vector::Vector2F},
-        scene::MouseMove,
-        EngineLayout,
-    };
-    use std::{cell::Cell, marker::PhantomData, rc::Rc};
-    pub struct Hoverable<V, E> {
-        hover_style: OptionalStyle,
-        computed_style: Option<Style>,
-        view_type: PhantomData<V>,
-        child: E,
-    }
-    impl<V, E> Hoverable<V, E> {
-        pub fn new(child: E) -> Self {
-            Self {
-                hover_style: OptionalStyle::default(),
-                computed_style: None,
-                view_type: PhantomData,
-                child,
-            }
-        }
-    }
-    impl<V: 'static, E: Element<V>> Element<V> for Hoverable<V, E> {
-        type Layout = E::Layout;
-        fn declared_style(&mut self) -> &mut OptionalStyle {
-            &mut self.hover_style
-        }
-        fn computed_style(&mut self) -> &OptionalStyle {
-            ::core::panicking::panic("not yet implemented")
-        }
-        fn handlers_mut(&mut self) -> &mut Vec<crate::element::EventHandler<V>> {
-            self.child.handlers_mut()
-        }
-        fn layout(
-            &mut self,
-            view: &mut V,
-            cx: &mut gpui::LayoutContext<V>,
-        ) -> anyhow::Result<(taffy::tree::NodeId, Self::Layout)> {
-            self.child.layout(view, cx)
-        }
-        fn paint<'a>(
-            &mut self,
-            layout: crate::element::Layout<Self::Layout>,
-            view: &mut V,
-            cx: &mut crate::element::PaintContext<V>,
-        ) -> anyhow::Result<()> {
-            let EngineLayout { bounds, order } = layout.from_engine;
-            let window_bounds = RectF::new(Vector2F::zero(), cx.window_size());
-            let was_hovered = Rc::new(Cell::new(false));
-            self.child.paint(layout, view, cx)?;
-            cx.draw_interactive_region(
-                order,
-                window_bounds,
-                false,
-                move |view, event: &MouseMove, cx| {
-                    let is_hovered = bounds.contains_point(cx.mouse_position());
-                    if is_hovered != was_hovered.get() {
-                        was_hovered.set(is_hovered);
-                        cx.repaint();
-                    }
-                },
-            );
-            Ok(())
-        }
-    }
-}
-mod paint_context {
-    use derive_more::{Deref, DerefMut};
-    use gpui::{geometry::rect::RectF, EventContext, RenderContext, ViewContext};
-    pub use gpui::{LayoutContext, PaintContext as LegacyPaintContext};
-    use std::{any::TypeId, rc::Rc};
-    pub use taffy::tree::NodeId;
-    pub struct PaintContext<'a, 'b, 'c, 'd, V> {
-        #[deref]
-        #[deref_mut]
-        pub(crate) legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
-        pub(crate) scene: &'d mut gpui::SceneBuilder,
-    }
-    impl<'a, 'b, 'c, 'd, V> ::core::ops::Deref for PaintContext<'a, 'b, 'c, 'd, V> {
-        type Target = &'d mut LegacyPaintContext<'a, 'b, 'c, V>;
-        #[inline]
-        fn deref(&self) -> &Self::Target {
-            &self.legacy_cx
-        }
-    }
-    impl<'a, 'b, 'c, 'd, V> ::core::ops::DerefMut for PaintContext<'a, 'b, 'c, 'd, V> {
-        #[inline]
-        fn deref_mut(&mut self) -> &mut Self::Target {
-            &mut self.legacy_cx
-        }
-    }
-    impl<V> RenderContext for PaintContext<'_, '_, '_, '_, V> {
-        fn text_style(&self) -> gpui::fonts::TextStyle {
-            self.legacy_cx.text_style()
-        }
-        fn push_text_style(&mut self, style: gpui::fonts::TextStyle) {
-            self.legacy_cx.push_text_style(style)
-        }
-        fn pop_text_style(&mut self) {
-            self.legacy_cx.pop_text_style()
-        }
-    }
-    impl<'a, 'b, 'c, 'd, V: 'static> PaintContext<'a, 'b, 'c, 'd, V> {
-        pub fn new(
-            legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
-            scene: &'d mut gpui::SceneBuilder,
-        ) -> Self {
-            Self { legacy_cx, scene }
-        }
-        pub fn draw_interactive_region<E: 'static>(
-            &mut self,
-            order: u32,
-            bounds: RectF,
-            outside_bounds: bool,
-            event_handler: impl Fn(&mut V, &E, &mut EventContext<V>) + 'static,
-        ) {
-            self.scene
-                .interactive_regions
-                .push(gpui::scene::InteractiveRegion {
-                    order,
-                    bounds,
-                    outside_bounds,
-                    event_handler: Rc::new(move |view, event, window_cx, view_id| {
-                        let mut view_context = ViewContext::mutable(window_cx, view_id);
-                        let mut event_context = EventContext::new(&mut view_context);
-                        event_handler(
-                            view.downcast_mut().unwrap(),
-                            event.downcast_ref().unwrap(),
-                            &mut event_context,
-                        );
-                    }),
-                    event_type: TypeId::of::<E>(),
-                    view_id: self.view_id(),
-                });
-        }
-    }
-}
-mod style {
-    use crate::color::Hsla;
-    use gpui::geometry::{
-        DefinedLength, Edges, Length, OptionalEdges, OptionalPoint, OptionalSize, Point, Size,
-    };
-    use optional::Optional;
-    pub use taffy::style::{
-        AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
-        Overflow, Position,
-    };
-    pub struct Style {
-        /// What layout strategy should be used?
-        pub display: Display,
-        /// How children overflowing their container should affect layout
-        #[optional]
-        pub overflow: Point<Overflow>,
-        /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
-        pub scrollbar_width: f32,
-        /// What should the `position` value of this struct use as a base offset?
-        pub position: Position,
-        /// How should the position of this element be tweaked relative to the layout defined?
-        pub inset: Edges<Length>,
-        /// Sets the initial size of the item
-        #[optional]
-        pub size: Size<Length>,
-        /// Controls the minimum size of the item
-        #[optional]
-        pub min_size: Size<Length>,
-        /// Controls the maximum size of the item
-        #[optional]
-        pub max_size: Size<Length>,
-        /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
-        pub aspect_ratio: Option<f32>,
-        /// How large should the margin be on each side?
-        #[optional]
-        pub margin: Edges<Length>,
-        /// How large should the padding be on each side?
-        pub padding: Edges<DefinedLength>,
-        /// How large should the border be on each side?
-        pub border: Edges<DefinedLength>,
-        /// How this node's children aligned in the cross/block axis?
-        pub align_items: Option<AlignItems>,
-        /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
-        pub align_self: Option<AlignSelf>,
-        /// How should content contained within this item be aligned in the cross/block axis
-        pub align_content: Option<AlignContent>,
-        /// How should contained within this item be aligned in the main/inline axis
-        pub justify_content: Option<JustifyContent>,
-        /// How large should the gaps between items in a flex container be?
-        pub gap: Size<DefinedLength>,
-        /// Which direction does the main axis flow in?
-        pub flex_direction: FlexDirection,
-        /// Should elements wrap, or stay in a single line?
-        pub flex_wrap: FlexWrap,
-        /// Sets the initial main axis size of the item
-        pub flex_basis: Length,
-        /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive.
-        pub flex_grow: f32,
-        /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive.
-        pub flex_shrink: f32,
-        /// The fill color of this element
-        pub fill: Option<Fill>,
-        /// The color of text within this element. Cascades to children unless overridden.
-        pub text_color: Option<Hsla>,
-    }
-    #[automatically_derived]
-    impl ::core::clone::Clone for Style {
-        #[inline]
-        fn clone(&self) -> Style {
-            Style {
-                display: ::core::clone::Clone::clone(&self.display),
-                overflow: ::core::clone::Clone::clone(&self.overflow),
-                scrollbar_width: ::core::clone::Clone::clone(&self.scrollbar_width),
-                position: ::core::clone::Clone::clone(&self.position),
-                inset: ::core::clone::Clone::clone(&self.inset),
-                size: ::core::clone::Clone::clone(&self.size),
-                min_size: ::core::clone::Clone::clone(&self.min_size),
-                max_size: ::core::clone::Clone::clone(&self.max_size),
-                aspect_ratio: ::core::clone::Clone::clone(&self.aspect_ratio),
-                margin: ::core::clone::Clone::clone(&self.margin),
-                padding: ::core::clone::Clone::clone(&self.padding),
-                border: ::core::clone::Clone::clone(&self.border),
-                align_items: ::core::clone::Clone::clone(&self.align_items),
-                align_self: ::core::clone::Clone::clone(&self.align_self),
-                align_content: ::core::clone::Clone::clone(&self.align_content),
-                justify_content: ::core::clone::Clone::clone(&self.justify_content),
-                gap: ::core::clone::Clone::clone(&self.gap),
-                flex_direction: ::core::clone::Clone::clone(&self.flex_direction),
-                flex_wrap: ::core::clone::Clone::clone(&self.flex_wrap),
-                flex_basis: ::core::clone::Clone::clone(&self.flex_basis),
-                flex_grow: ::core::clone::Clone::clone(&self.flex_grow),
-                flex_shrink: ::core::clone::Clone::clone(&self.flex_shrink),
-                fill: ::core::clone::Clone::clone(&self.fill),
-                text_color: ::core::clone::Clone::clone(&self.text_color),
-            }
-        }
-    }
-    pub struct OptionalStyle {
-        pub display: Option<Display>,
-        pub overflow: OptionalPoint<Overflow>,
-        pub scrollbar_width: Option<f32>,
-        pub position: Option<Position>,
-        pub inset: Option<Edges<Length>>,
-        pub size: OptionalSize<Length>,
-        pub min_size: OptionalSize<Length>,
-        pub max_size: OptionalSize<Length>,
-        pub aspect_ratio: Option<Option<f32>>,
-        pub margin: OptionalEdges<Length>,
-        pub padding: Option<Edges<DefinedLength>>,
-        pub border: Option<Edges<DefinedLength>>,
-        pub align_items: Option<Option<AlignItems>>,
-        pub align_self: Option<Option<AlignSelf>>,
-        pub align_content: Option<Option<AlignContent>>,
-        pub justify_content: Option<Option<JustifyContent>>,
-        pub gap: Option<Size<DefinedLength>>,
-        pub flex_direction: Option<FlexDirection>,
-        pub flex_wrap: Option<FlexWrap>,
-        pub flex_basis: Option<Length>,
-        pub flex_grow: Option<f32>,
-        pub flex_shrink: Option<f32>,
-        pub fill: Option<Option<Fill>>,
-        pub text_color: Option<Option<Hsla>>,
-    }
-    #[automatically_derived]
-    impl ::core::default::Default for OptionalStyle {
-        #[inline]
-        fn default() -> OptionalStyle {
-            OptionalStyle {
-                display: ::core::default::Default::default(),
-                overflow: ::core::default::Default::default(),
-                scrollbar_width: ::core::default::Default::default(),
-                position: ::core::default::Default::default(),
-                inset: ::core::default::Default::default(),
-                size: ::core::default::Default::default(),
-                min_size: ::core::default::Default::default(),
-                max_size: ::core::default::Default::default(),
-                aspect_ratio: ::core::default::Default::default(),
-                margin: ::core::default::Default::default(),
-                padding: ::core::default::Default::default(),
-                border: ::core::default::Default::default(),
-                align_items: ::core::default::Default::default(),
-                align_self: ::core::default::Default::default(),
-                align_content: ::core::default::Default::default(),
-                justify_content: ::core::default::Default::default(),
-                gap: ::core::default::Default::default(),
-                flex_direction: ::core::default::Default::default(),
-                flex_wrap: ::core::default::Default::default(),
-                flex_basis: ::core::default::Default::default(),
-                flex_grow: ::core::default::Default::default(),
-                flex_shrink: ::core::default::Default::default(),
-                fill: ::core::default::Default::default(),
-                text_color: ::core::default::Default::default(),
-            }
-        }
-    }
-    #[automatically_derived]
-    impl ::core::clone::Clone for OptionalStyle {
-        #[inline]
-        fn clone(&self) -> OptionalStyle {
-            OptionalStyle {
-                display: ::core::clone::Clone::clone(&self.display),
-                overflow: ::core::clone::Clone::clone(&self.overflow),
-                scrollbar_width: ::core::clone::Clone::clone(&self.scrollbar_width),
-                position: ::core::clone::Clone::clone(&self.position),
-                inset: ::core::clone::Clone::clone(&self.inset),
-                size: ::core::clone::Clone::clone(&self.size),
-                min_size: ::core::clone::Clone::clone(&self.min_size),
-                max_size: ::core::clone::Clone::clone(&self.max_size),
-                aspect_ratio: ::core::clone::Clone::clone(&self.aspect_ratio),
-                margin: ::core::clone::Clone::clone(&self.margin),
-                padding: ::core::clone::Clone::clone(&self.padding),
-                border: ::core::clone::Clone::clone(&self.border),
-                align_items: ::core::clone::Clone::clone(&self.align_items),
-                align_self: ::core::clone::Clone::clone(&self.align_self),
-                align_content: ::core::clone::Clone::clone(&self.align_content),
-                justify_content: ::core::clone::Clone::clone(&self.justify_content),
-                gap: ::core::clone::Clone::clone(&self.gap),
-                flex_direction: ::core::clone::Clone::clone(&self.flex_direction),
-                flex_wrap: ::core::clone::Clone::clone(&self.flex_wrap),
-                flex_basis: ::core::clone::Clone::clone(&self.flex_basis),
-                flex_grow: ::core::clone::Clone::clone(&self.flex_grow),
-                flex_shrink: ::core::clone::Clone::clone(&self.flex_shrink),
-                fill: ::core::clone::Clone::clone(&self.fill),
-                text_color: ::core::clone::Clone::clone(&self.text_color),
-            }
-        }
-    }
-    impl Optional for OptionalStyle {
-        type Base = Style;
-        fn assign(&self, base: &mut Self::Base) {
-            if let Some(value) = self.display.clone() {
-                base.display = value;
-            }
-            if let Some(value) = self.overflow.clone() {
-                base.overflow = value;
-            }
-            if let Some(value) = self.scrollbar_width.clone() {
-                base.scrollbar_width = value;
-            }
-            if let Some(value) = self.position.clone() {
-                base.position = value;
-            }
-            if let Some(value) = self.inset.clone() {
-                base.inset = value;
-            }
-            if let Some(value) = self.size.clone() {
-                base.size = value;
-            }
-            if let Some(value) = self.min_size.clone() {
-                base.min_size = value;
-            }
-            if let Some(value) = self.max_size.clone() {
-                base.max_size = value;
-            }
-            if let Some(value) = self.aspect_ratio.clone() {
-                base.aspect_ratio = value;
-            }
-            if let Some(value) = self.margin.clone() {
-                base.margin = value;
-            }
-            if let Some(value) = self.padding.clone() {
-                base.padding = value;
-            }
-            if let Some(value) = self.border.clone() {
-                base.border = value;
-            }
-            if let Some(value) = self.align_items.clone() {
-                base.align_items = value;
-            }
-            if let Some(value) = self.align_self.clone() {
-                base.align_self = value;
-            }
-            if let Some(value) = self.align_content.clone() {
-                base.align_content = value;
-            }
-            if let Some(value) = self.justify_content.clone() {
-                base.justify_content = value;
-            }
-            if let Some(value) = self.gap.clone() {
-                base.gap = value;
-            }
-            if let Some(value) = self.flex_direction.clone() {
-                base.flex_direction = value;
-            }
-            if let Some(value) = self.flex_wrap.clone() {
-                base.flex_wrap = value;
-            }
-            if let Some(value) = self.flex_basis.clone() {
-                base.flex_basis = value;
-            }
-            if let Some(value) = self.flex_grow.clone() {
-                base.flex_grow = value;
-            }
-            if let Some(value) = self.flex_shrink.clone() {
-                base.flex_shrink = value;
-            }
-            if let Some(value) = self.fill.clone() {
-                base.fill = value;
-            }
-            if let Some(value) = self.text_color.clone() {
-                base.text_color = value;
-            }
-        }
-    }
-    impl From<OptionalStyle> for Style
-    where
-        Style: Default,
-    {
-        fn from(wrapper: OptionalStyle) -> Self {
-            let mut base = Self::default();
-            wrapper.assign(&mut base);
-            base
-        }
-    }
-    impl Style {
-        pub const DEFAULT: Style = Style {
-            display: Display::DEFAULT,
-            overflow: Point {
-                x: Overflow::Visible,
-                y: Overflow::Visible,
-            },
-            scrollbar_width: 0.0,
-            position: Position::Relative,
-            inset: Edges::auto(),
-            margin: Edges::<Length>::zero(),
-            padding: Edges::<DefinedLength>::zero(),
-            border: Edges::<DefinedLength>::zero(),
-            size: Size::auto(),
-            min_size: Size::auto(),
-            max_size: Size::auto(),
-            aspect_ratio: None,
-            gap: Size::zero(),
-            align_items: None,
-            align_self: None,
-            align_content: None,
-            justify_content: None,
-            flex_direction: FlexDirection::Row,
-            flex_wrap: FlexWrap::NoWrap,
-            flex_grow: 0.0,
-            flex_shrink: 1.0,
-            flex_basis: Length::Auto,
-            fill: None,
-            text_color: None,
-        };
-        pub fn new() -> Self {
-            Self::DEFAULT.clone()
-        }
-        pub fn to_taffy(&self, rem_size: f32) -> taffy::style::Style {
-            taffy::style::Style {
-                display: self.display,
-                overflow: self.overflow.clone().into(),
-                scrollbar_width: self.scrollbar_width,
-                position: self.position,
-                inset: self.inset.to_taffy(rem_size),
-                size: self.size.to_taffy(rem_size),
-                min_size: self.min_size.to_taffy(rem_size),
-                max_size: self.max_size.to_taffy(rem_size),
-                aspect_ratio: self.aspect_ratio,
-                margin: self.margin.to_taffy(rem_size),
-                padding: self.padding.to_taffy(rem_size),
-                border: self.border.to_taffy(rem_size),
-                align_items: self.align_items,
-                align_self: self.align_self,
-                align_content: self.align_content,
-                justify_content: self.justify_content,
-                gap: self.gap.to_taffy(rem_size),
-                flex_direction: self.flex_direction,
-                flex_wrap: self.flex_wrap,
-                flex_basis: self.flex_basis.to_taffy(rem_size).into(),
-                flex_grow: self.flex_grow,
-                flex_shrink: self.flex_shrink,
-                ..Default::default()
-            }
-        }
-    }
-    impl Default for Style {
-        fn default() -> Self {
-            Self::DEFAULT.clone()
-        }
-    }
-    impl OptionalStyle {
-        pub fn text_style(&self) -> Option<OptionalTextStyle> {
-            self.text_color.map(|color| OptionalTextStyle { color })
-        }
-    }
-    pub struct OptionalTextStyle {
-        color: Option<Hsla>,
-    }
-    impl OptionalTextStyle {
-        pub fn apply(&self, style: &mut gpui::fonts::TextStyle) {
-            if let Some(color) = self.color {
-                style.color = color.into();
-            }
-        }
-    }
-    pub enum Fill {
-        Color(Hsla),
-    }
-    #[automatically_derived]
-    impl ::core::clone::Clone for Fill {
-        #[inline]
-        fn clone(&self) -> Fill {
-            match self {
-                Fill::Color(__self_0) => Fill::Color(::core::clone::Clone::clone(__self_0)),
-            }
-        }
-    }
-    impl Fill {
-        pub fn color(&self) -> Option<Hsla> {
-            match self {
-                Fill::Color(color) => Some(*color),
-            }
-        }
-    }
-    impl Default for Fill {
-        fn default() -> Self {
-            Self::Color(Hsla::default())
-        }
-    }
-    impl From<Hsla> for Fill {
-        fn from(color: Hsla) -> Self {
-            Self::Color(color)
-        }
-    }
-}
-mod text {
-    use crate::{
-        element::{Element, ElementMetadata, EventHandler, IntoElement},
-        style::Style,
-    };
-    use gpui::{geometry::Size, text_layout::LineLayout, RenderContext};
-    use parking_lot::Mutex;
-    use std::sync::Arc;
-    impl<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
-        type Element = Text<V>;
-        fn into_element(self) -> Self::Element {
-            Text {
-                text: self.into(),
-                metadata: Default::default(),
-            }
-        }
-    }
-    pub struct Text<V> {
-        text: ArcCow<'static, str>,
-        metadata: ElementMetadata<V>,
-    }
-    impl<V: 'static> Element<V> for Text<V> {
-        type Layout = Arc<Mutex<Option<TextLayout>>>;
-        fn declared_style(&mut self) -> &mut crate::style::OptionalStyle {
-            &mut self.metadata.style
-        }
-        fn layout(
-            &mut self,
-            view: &mut V,
-            cx: &mut gpui::LayoutContext<V>,
-        ) -> anyhow::Result<(taffy::tree::NodeId, Self::Layout)> {
-            let rem_size = cx.rem_pixels();
-            let fonts = cx.platform().fonts();
-            let text_style = cx.text_style();
-            let line_height = cx.font_cache().line_height(text_style.font_size);
-            let layout_engine = cx.layout_engine().expect("no layout engine present");
-            let text = self.text.clone();
-            let layout = Arc::new(Mutex::new(None));
-            let style: Style = self.metadata.style.into();
-            let node_id = layout_engine.add_measured_node(style.to_taffy(rem_size), {
-                let layout = layout.clone();
-                move |params| {
-                    let line_layout = fonts.layout_line(
-                        text.as_ref(),
-                        text_style.font_size,
-                        &[(text.len(), text_style.to_run())],
-                    );
-                    let size = Size {
-                        width: line_layout.width,
-                        height: line_height,
-                    };
-                    layout.lock().replace(TextLayout {
-                        line_layout: Arc::new(line_layout),
-                        line_height,
-                    });
-                    size
-                }
-            })?;
-            Ok((node_id, layout))
-        }
-        fn paint<'a>(
-            &mut self,
-            layout: crate::element::Layout<Arc<Mutex<Option<TextLayout>>>>,
-            view: &mut V,
-            cx: &mut crate::element::PaintContext<V>,
-        ) -> anyhow::Result<()> {
-            let element_layout_lock = layout.from_element.lock();
-            let element_layout = element_layout_lock
-                .as_ref()
-                .expect("layout has not been performed");
-            let line_layout = element_layout.line_layout.clone();
-            let line_height = element_layout.line_height;
-            drop(element_layout_lock);
-            let text_style = cx.text_style();
-            let line = gpui::text_layout::Line::new(
-                line_layout,
-                &[(self.text.len(), text_style.to_run())],
-            );
-            line.paint(
-                cx.scene,
-                layout.from_engine.bounds.origin(),
-                layout.from_engine.bounds,
-                line_height,
-                cx.legacy_cx,
-            );
-            Ok(())
-        }
-        fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
-            &mut self.metadata.handlers
-        }
-    }
-    pub struct TextLayout {
-        line_layout: Arc<LineLayout>,
-        line_height: f32,
-    }
-    pub enum ArcCow<'a, T: ?Sized> {
-        Borrowed(&'a T),
-        Owned(Arc<T>),
-    }
-    impl<'a, T: ?Sized> Clone for ArcCow<'a, T> {
-        fn clone(&self) -> Self {
-            match self {
-                Self::Borrowed(borrowed) => Self::Borrowed(borrowed),
-                Self::Owned(owned) => Self::Owned(owned.clone()),
-            }
-        }
-    }
-    impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
-        fn from(s: &'a T) -> Self {
-            Self::Borrowed(s)
-        }
-    }
-    impl<T> From<Arc<T>> for ArcCow<'_, T> {
-        fn from(s: Arc<T>) -> Self {
-            Self::Owned(s)
-        }
-    }
-    impl From<String> for ArcCow<'_, str> {
-        fn from(value: String) -> Self {
-            Self::Owned(value.into())
-        }
-    }
-    impl<T: ?Sized> std::ops::Deref for ArcCow<'_, T> {
-        type Target = T;
-        fn deref(&self) -> &Self::Target {
-            match self {
-                ArcCow::Borrowed(s) => s,
-                ArcCow::Owned(s) => s.as_ref(),
-            }
-        }
-    }
-    impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
-        fn as_ref(&self) -> &T {
-            match self {
-                ArcCow::Borrowed(borrowed) => borrowed,
-                ArcCow::Owned(owned) => owned.as_ref(),
-            }
-        }
-    }
-}
-mod themes {
-    use crate::color::{Hsla, Lerp};
-    use std::ops::Range;
-    pub mod rose_pine {
-        use crate::{
-            color::{hsla, rgb, Hsla},
-            ThemeColors,
-        };
-        use std::ops::Range;
-        pub struct RosePineThemes {
-            pub default: RosePinePalette,
-            pub dawn: RosePinePalette,
-            pub moon: RosePinePalette,
-        }
-        pub struct RosePinePalette {
-            pub base: Hsla,
-            pub surface: Hsla,
-            pub overlay: Hsla,
-            pub muted: Hsla,
-            pub subtle: Hsla,
-            pub text: Hsla,
-            pub love: Hsla,
-            pub gold: Hsla,
-            pub rose: Hsla,
-            pub pine: Hsla,
-            pub foam: Hsla,
-            pub iris: Hsla,
-            pub highlight_low: Hsla,
-            pub highlight_med: Hsla,
-            pub highlight_high: Hsla,
-        }
-        #[automatically_derived]
-        impl ::core::clone::Clone for RosePinePalette {
-            #[inline]
-            fn clone(&self) -> RosePinePalette {
-                let _: ::core::clone::AssertParamIsClone<Hsla>;
-                *self
-            }
-        }
-        #[automatically_derived]
-        impl ::core::marker::Copy for RosePinePalette {}
-        #[automatically_derived]
-        impl ::core::fmt::Debug for RosePinePalette {
-            fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
-                let names: &'static _ = &[
-                    "base",
-                    "surface",
-                    "overlay",
-                    "muted",
-                    "subtle",
-                    "text",
-                    "love",
-                    "gold",
-                    "rose",
-                    "pine",
-                    "foam",
-                    "iris",
-                    "highlight_low",
-                    "highlight_med",
-                    "highlight_high",
-                ];
-                let values: &[&dyn::core::fmt::Debug] = &[
-                    &self.base,
-                    &self.surface,
-                    &self.overlay,
-                    &self.muted,
-                    &self.subtle,
-                    &self.text,
-                    &self.love,
-                    &self.gold,
-                    &self.rose,
-                    &self.pine,
-                    &self.foam,
-                    &self.iris,
-                    &self.highlight_low,
-                    &self.highlight_med,
-                    &&self.highlight_high,
-                ];
-                ::core::fmt::Formatter::debug_struct_fields_finish(
-                    f,
-                    "RosePinePalette",
-                    names,
-                    values,
-                )
-            }
-        }
-        impl RosePinePalette {
-            pub fn default() -> RosePinePalette {
-                RosePinePalette {
-                    base: rgb(0x191724),
-                    surface: rgb(0x1f1d2e),
-                    overlay: rgb(0x26233a),
-                    muted: rgb(0x6e6a86),
-                    subtle: rgb(0x908caa),
-                    text: rgb(0xe0def4),
-                    love: rgb(0xeb6f92),
-                    gold: rgb(0xf6c177),
-                    rose: rgb(0xebbcba),
-                    pine: rgb(0x31748f),
-                    foam: rgb(0x9ccfd8),
-                    iris: rgb(0xc4a7e7),
-                    highlight_low: rgb(0x21202e),
-                    highlight_med: rgb(0x403d52),
-                    highlight_high: rgb(0x524f67),
-                }
-            }
-            pub fn moon() -> RosePinePalette {
-                RosePinePalette {
-                    base: rgb(0x232136),
-                    surface: rgb(0x2a273f),
-                    overlay: rgb(0x393552),
-                    muted: rgb(0x6e6a86),
-                    subtle: rgb(0x908caa),
-                    text: rgb(0xe0def4),
-                    love: rgb(0xeb6f92),
-                    gold: rgb(0xf6c177),
-                    rose: rgb(0xea9a97),
-                    pine: rgb(0x3e8fb0),
-                    foam: rgb(0x9ccfd8),
-                    iris: rgb(0xc4a7e7),
-                    highlight_low: rgb(0x2a283e),
-                    highlight_med: rgb(0x44415a),
-                    highlight_high: rgb(0x56526e),
-                }
-            }
-            pub fn dawn() -> RosePinePalette {
-                RosePinePalette {
-                    base: rgb(0xfaf4ed),
-                    surface: rgb(0xfffaf3),
-                    overlay: rgb(0xf2e9e1),
-                    muted: rgb(0x9893a5),
-                    subtle: rgb(0x797593),
-                    text: rgb(0x575279),
-                    love: rgb(0xb4637a),
-                    gold: rgb(0xea9d34),
-                    rose: rgb(0xd7827e),
-                    pine: rgb(0x286983),
-                    foam: rgb(0x56949f),
-                    iris: rgb(0x907aa9),
-                    highlight_low: rgb(0xf4ede8),
-                    highlight_med: rgb(0xdfdad9),
-                    highlight_high: rgb(0xcecacd),
-                }
-            }
-        }
-        pub fn default() -> ThemeColors {
-            theme_colors(&RosePinePalette::default())
-        }
-        pub fn moon() -> ThemeColors {
-            theme_colors(&RosePinePalette::moon())
-        }
-        pub fn dawn() -> ThemeColors {
-            theme_colors(&RosePinePalette::dawn())
-        }
-        fn theme_colors(p: &RosePinePalette) -> ThemeColors {
-            ThemeColors {
-                base: scale_sl(p.base, (0.8, 0.8), (1.2, 1.2)),
-                surface: scale_sl(p.surface, (0.8, 0.8), (1.2, 1.2)),
-                overlay: scale_sl(p.overlay, (0.8, 0.8), (1.2, 1.2)),
-                muted: scale_sl(p.muted, (0.8, 0.8), (1.2, 1.2)),
-                subtle: scale_sl(p.subtle, (0.8, 0.8), (1.2, 1.2)),
-                text: scale_sl(p.text, (0.8, 0.8), (1.2, 1.2)),
-                highlight_low: scale_sl(p.highlight_low, (0.8, 0.8), (1.2, 1.2)),
-                highlight_med: scale_sl(p.highlight_med, (0.8, 0.8), (1.2, 1.2)),
-                highlight_high: scale_sl(p.highlight_high, (0.8, 0.8), (1.2, 1.2)),
-                success: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
-                warning: scale_sl(p.gold, (0.8, 0.8), (1.2, 1.2)),
-                error: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
-                inserted: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
-                deleted: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
-                modified: scale_sl(p.rose, (0.8, 0.8), (1.2, 1.2)),
-            }
-        }
-        /// Produces a range by multiplying the saturation and lightness of the base color by the given
-        /// start and end factors.
-        fn scale_sl(
-            base: Hsla,
-            (start_s, start_l): (f32, f32),
-            (end_s, end_l): (f32, f32),
-        ) -> Range<Hsla> {
-            let start = hsla(base.h, base.s * start_s, base.l * start_l, base.a);
-            let end = hsla(base.h, base.s * end_s, base.l * end_l, base.a);
-            Range { start, end }
-        }
-    }
-    pub struct ThemeColors {
-        pub base: Range<Hsla>,
-        pub surface: Range<Hsla>,
-        pub overlay: Range<Hsla>,
-        pub muted: Range<Hsla>,
-        pub subtle: Range<Hsla>,
-        pub text: Range<Hsla>,
-        pub highlight_low: Range<Hsla>,
-        pub highlight_med: Range<Hsla>,
-        pub highlight_high: Range<Hsla>,
-        pub success: Range<Hsla>,
-        pub warning: Range<Hsla>,
-        pub error: Range<Hsla>,
-        pub inserted: Range<Hsla>,
-        pub deleted: Range<Hsla>,
-        pub modified: Range<Hsla>,
-    }
-    impl ThemeColors {
-        pub fn base(&self, level: f32) -> Hsla {
-            self.base.lerp(level)
-        }
-        pub fn surface(&self, level: f32) -> Hsla {
-            self.surface.lerp(level)
-        }
-        pub fn overlay(&self, level: f32) -> Hsla {
-            self.overlay.lerp(level)
-        }
-        pub fn muted(&self, level: f32) -> Hsla {
-            self.muted.lerp(level)
-        }
-        pub fn subtle(&self, level: f32) -> Hsla {
-            self.subtle.lerp(level)
-        }
-        pub fn text(&self, level: f32) -> Hsla {
-            self.text.lerp(level)
-        }
-        pub fn highlight_low(&self, level: f32) -> Hsla {
-            self.highlight_low.lerp(level)
-        }
-        pub fn highlight_med(&self, level: f32) -> Hsla {
-            self.highlight_med.lerp(level)
-        }
-        pub fn highlight_high(&self, level: f32) -> Hsla {
-            self.highlight_high.lerp(level)
-        }
-        pub fn success(&self, level: f32) -> Hsla {
-            self.success.lerp(level)
-        }
-        pub fn warning(&self, level: f32) -> Hsla {
-            self.warning.lerp(level)
-        }
-        pub fn error(&self, level: f32) -> Hsla {
-            self.error.lerp(level)
-        }
-        pub fn inserted(&self, level: f32) -> Hsla {
-            self.inserted.lerp(level)
-        }
-        pub fn deleted(&self, level: f32) -> Hsla {
-            self.deleted.lerp(level)
-        }
-        pub fn modified(&self, level: f32) -> Hsla {
-            self.modified.lerp(level)
-        }
-    }
-}
-mod view {
-    use crate::element::{AnyElement, Element};
-    use gpui::{Element as _, ViewContext};
-    pub fn view<F, E>(mut render: F) -> ViewFn
-    where
-        F: 'static + FnMut(&mut ViewContext<ViewFn>) -> E,
-        E: Element<ViewFn>,
-    {
-        ViewFn(Box::new(move |cx| (render)(cx).into_any()))
-    }
-    pub struct ViewFn(Box<dyn FnMut(&mut ViewContext<ViewFn>) -> AnyElement<ViewFn>>);
-    impl gpui::Entity for ViewFn {
-        type Event = ();
-    }
-    impl gpui::View for ViewFn {
-        fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
-            (self.0)(cx).adapt().into_any()
-        }
-    }
-}
-fn main() {
-    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
-    gpui::App::new(()).unwrap().run(|cx| {
-        cx.add_window(
-            WindowOptions {
-                bounds: gpui::platform::WindowBounds::Fixed(RectF::new(
-                    vec2f(0., 0.),
-                    vec2f(400., 300.),
-                )),
-                center: true,
-                ..Default::default()
-            },
-            |_| view(|_| storybook(&rose_pine::moon())),
-        );
-        cx.platform().activate(true);
-    });
-}
-fn storybook<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
-    frame()
-        .text_color(black())
-        .h_full()
-        .w_half()
-        .fill(theme.success(0.5))
-        .child(button().label("Hello").click(|_, _, _| {
-            ::std::io::_print(format_args!("click!\n"));
-        }))
-}