Merge remote-tracking branch 'origin/main' into no-more-todos-in-editor

Antonio Scandurra created

# Conflicts:
#	crates/editor2/src/items.rs

Change summary

Cargo.lock                                       |    2 
Cargo.toml                                       |    2 
assets/keymaps/default.json                      |    7 
crates/assistant/src/assistant_panel.rs          |   39 
crates/collab/src/tests/integration_tests.rs     |   25 
crates/collab2/src/tests/integration_tests.rs    |   25 
crates/collab_ui2/src/collab_panel.rs            |    2 
crates/collab_ui2/src/collab_titlebar_item.rs    |    8 
crates/command_palette2/src/command_palette.rs   |    2 
crates/diagnostics/src/diagnostics.rs            |    8 
crates/diagnostics/src/items.rs                  |    6 
crates/diagnostics2/src/diagnostics.rs           |    6 
crates/diagnostics2/src/items.rs                 |    6 
crates/editor2/src/items.rs                      |  167 
crates/file_finder/src/file_finder.rs            |   47 
crates/file_finder2/src/file_finder.rs           |   41 
crates/gpui2/src/app/test_context.rs             |   50 
crates/gpui2/src/platform.rs                     |    5 
crates/gpui2/src/platform/test/platform.rs       |    8 
crates/gpui2/src/platform/test/window.rs         |   22 
crates/gpui2/src/view.rs                         |    4 
crates/gpui2/src/window.rs                       |    4 
crates/language/src/language.rs                  |   10 
crates/language2/src/language2.rs                |   10 
crates/lsp/src/lsp.rs                            |   33 
crates/lsp2/src/lsp2.rs                          |   33 
crates/project/src/project.rs                    |   37 
crates/project/src/project_tests.rs              |   93 
crates/project2/src/project2.rs                  |   56 
crates/project2/src/project_tests.rs             |   93 
crates/project_panel/src/project_panel.rs        |   14 
crates/project_panel2/src/project_panel.rs       |   29 
crates/search2/src/search.rs                     |   10 
crates/storybook2/src/story_selector.rs          |    2 
crates/theme_selector2/src/theme_selector.rs     |    8 
crates/ui2/src/components/button.rs              |    0 
crates/ui2/src/components/button/button.rs       |    8 
crates/ui2/src/components/button/button_like.rs  |  108 
crates/ui2/src/components/button/icon_button.rs  |    6 
crates/ui2/src/components/list.rs                |   66 
crates/ui2/src/components/list/list.rs           |   60 
crates/ui2/src/components/list/list_header.rs    |   46 
crates/ui2/src/components/list/list_item.rs      |   12 
crates/ui2/src/components/stories.rs             |    2 
crates/ui2/src/components/stories/button.rs      |   10 
crates/ui2/src/components/stories/list.rs        |    4 
crates/ui2/src/components/stories/list_header.rs |   33 
crates/ui2/src/slot.rs                           |   12 
crates/ui2/src/ui2.rs                            |    2 
crates/welcome2/src/base_keymap_picker.rs        |    6 
crates/workspace2/Cargo.toml                     |   42 
crates/workspace2/src/item.rs                    |  640 +++---
crates/workspace2/src/pane.rs                    |   50 
crates/workspace2/src/pane_group.rs              |    4 
crates/workspace2/src/persistence.rs             |    4 
crates/workspace2/src/persistence/model.rs       |    4 
crates/workspace2/src/searchable.rs              |    2 
crates/workspace2/src/status_bar.rs              |   25 
crates/workspace2/src/workspace2.rs              | 1760 ++++++++---------
crates/workspace2/src/workspace_settings.rs      |    2 
crates/zed/src/languages/json.rs                 |    1 
crates/zed/src/languages/php.rs                  |    1 
crates/zed/src/languages/tailwind.rs             |    6 
crates/zed/src/languages/typescript.rs           |   13 
crates/zed/src/languages/yaml.rs                 |    6 
crates/zed2/src/languages/json.rs                |    1 
crates/zed2/src/languages/php.rs                 |    1 
crates/zed2/src/languages/tailwind.rs            |    6 
crates/zed2/src/languages/typescript.rs          |   13 
crates/zed2/src/languages/yaml.rs                |    6 
crates/zed2/src/main.rs                          |    4 
crates/zed2/src/zed2.rs                          |    4 
72 files changed, 2,194 insertions(+), 1,690 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -9970,7 +9970,7 @@ dependencies = [
 [[package]]
 name = "tree-sitter"
 version = "0.20.10"
-source = "git+https://github.com/tree-sitter/tree-sitter?rev=3b0159d25559b603af566ade3c83d930bf466db1#3b0159d25559b603af566ade3c83d930bf466db1"
+source = "git+https://github.com/tree-sitter/tree-sitter?rev=b5f461a69bf3df7298b1903574d506179e6390b0#b5f461a69bf3df7298b1903574d506179e6390b0"
 dependencies = [
  "cc",
  "regex",

Cargo.toml πŸ”—

@@ -201,7 +201,7 @@ tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", re
 tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
 
 [patch.crates-io]
-tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" }
+tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "b5f461a69bf3df7298b1903574d506179e6390b0" }
 async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
 
 # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457

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/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/collab/src/tests/integration_tests.rs πŸ”—

@@ -3941,7 +3941,7 @@ async fn test_collaborating_with_diagnostics(
     // Ensure client B observes the new diagnostics.
     project_b.read_with(cx_b, |project, cx| {
         assert_eq!(
-            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
             &[(
                 ProjectPath {
                     worktree_id,
@@ -3961,14 +3961,14 @@ async fn test_collaborating_with_diagnostics(
     let project_c = client_c.build_remote_project(project_id, cx_c).await;
     let project_c_diagnostic_summaries =
         Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
-            project.diagnostic_summaries(cx).collect::<Vec<_>>()
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
         })));
     project_c.update(cx_c, |_, cx| {
         let summaries = project_c_diagnostic_summaries.clone();
         cx.subscribe(&project_c, {
             move |p, _, event, cx| {
                 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
-                    *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
+                    *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
                 }
             }
         })
@@ -4018,7 +4018,7 @@ async fn test_collaborating_with_diagnostics(
     deterministic.run_until_parked();
     project_b.read_with(cx_b, |project, cx| {
         assert_eq!(
-            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
             [(
                 ProjectPath {
                     worktree_id,
@@ -4034,7 +4034,7 @@ async fn test_collaborating_with_diagnostics(
     });
     project_c.read_with(cx_c, |project, cx| {
         assert_eq!(
-            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
             [(
                 ProjectPath {
                     worktree_id,
@@ -4097,13 +4097,22 @@ async fn test_collaborating_with_diagnostics(
     );
     deterministic.run_until_parked();
     project_a.read_with(cx_a, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+        assert_eq!(
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
+            []
+        )
     });
     project_b.read_with(cx_b, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+        assert_eq!(
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
+            []
+        )
     });
     project_c.read_with(cx_c, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+        assert_eq!(
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
+            []
+        )
     });
 }
 

crates/collab2/src/tests/integration_tests.rs πŸ”—

@@ -3688,7 +3688,7 @@ async fn test_collaborating_with_diagnostics(
 
     project_b.read_with(cx_b, |project, cx| {
         assert_eq!(
-            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
             &[(
                 ProjectPath {
                     worktree_id,
@@ -3708,14 +3708,14 @@ async fn test_collaborating_with_diagnostics(
     let project_c = client_c.build_remote_project(project_id, cx_c).await;
     let project_c_diagnostic_summaries =
         Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
-            project.diagnostic_summaries(cx).collect::<Vec<_>>()
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
         })));
     project_c.update(cx_c, |_, cx| {
         let summaries = project_c_diagnostic_summaries.clone();
         cx.subscribe(&project_c, {
             move |p, _, event, cx| {
                 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
-                    *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
+                    *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
                 }
             }
         })
@@ -3766,7 +3766,7 @@ async fn test_collaborating_with_diagnostics(
 
     project_b.read_with(cx_b, |project, cx| {
         assert_eq!(
-            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
             [(
                 ProjectPath {
                     worktree_id,
@@ -3783,7 +3783,7 @@ async fn test_collaborating_with_diagnostics(
 
     project_c.read_with(cx_c, |project, cx| {
         assert_eq!(
-            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
             [(
                 ProjectPath {
                     worktree_id,
@@ -3844,15 +3844,24 @@ async fn test_collaborating_with_diagnostics(
     executor.run_until_parked();
 
     project_a.read_with(cx_a, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+        assert_eq!(
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
+            []
+        )
     });
 
     project_b.read_with(cx_b, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+        assert_eq!(
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
+            []
+        )
     });
 
     project_c.read_with(cx_c, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+        assert_eq!(
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
+            []
+        )
     });
 }
 

crates/collab_ui2/src/collab_panel.rs πŸ”—

@@ -2511,7 +2511,7 @@ impl CollabPanel {
                 } else {
                     el.child(
                         ListHeader::new(text)
-                            .when_some(button, |el, button| el.right_button(button))
+                            .when_some(button, |el, button| el.meta(button))
                             .selected(is_selected),
                     )
                 }

crates/collab_ui2/src/collab_titlebar_item.rs πŸ”—

@@ -37,7 +37,7 @@ use gpui::{
 };
 use project::Project;
 use theme::ActiveTheme;
-use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle2, IconButton, KeyBinding, Tooltip};
+use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle, IconButton, KeyBinding, Tooltip};
 use util::ResultExt;
 use workspace::{notifications::NotifyResultExt, Workspace};
 
@@ -154,7 +154,7 @@ impl Render for CollabTitlebarItem {
                             .id("project_owner_indicator")
                             .child(
                                 Button::new("player", "player")
-                                    .style(ButtonStyle2::Subtle)
+                                    .style(ButtonStyle::Subtle)
                                     .color(Some(Color::Player(0))),
                             )
                             .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
@@ -167,7 +167,7 @@ impl Render for CollabTitlebarItem {
                             .id("titlebar_project_menu_button")
                             .child(
                                 Button::new("project_name", "project_name")
-                                    .style(ButtonStyle2::Subtle),
+                                    .style(ButtonStyle::Subtle),
                             )
                             .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
                     )
@@ -179,7 +179,7 @@ impl Render for CollabTitlebarItem {
                             .id("titlebar_git_menu_button")
                             .child(
                                 Button::new("branch_name", "branch_name")
-                                    .style(ButtonStyle2::Subtle)
+                                    .style(ButtonStyle::Subtle)
                                     .color(Some(Color::Muted)),
                             )
                             .tooltip(move |cx| {

crates/command_palette2/src/command_palette.rs πŸ”—

@@ -11,7 +11,7 @@ use gpui::{
 };
 use picker::{Picker, PickerDelegate};
 
-use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
+use ui::{h_stack, prelude::*, v_stack, HighlightedLabel, KeyBinding, ListItem};
 use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,

crates/diagnostics/src/diagnostics.rs πŸ”—

@@ -126,7 +126,7 @@ impl View for ProjectDiagnosticsEditor {
         json!({
             "project": json!({
                 "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
-                "summary": project.diagnostic_summary(cx),
+                "summary": project.diagnostic_summary(false, cx),
             }),
             "summary": self.summary,
             "paths_to_update": self.paths_to_update.iter().map(|(server_id, paths)|
@@ -195,7 +195,7 @@ impl ProjectDiagnosticsEditor {
         });
 
         let project = project_handle.read(cx);
-        let summary = project.diagnostic_summary(cx);
+        let summary = project.diagnostic_summary(false, cx);
         let mut this = Self {
             project: project_handle,
             summary,
@@ -241,7 +241,7 @@ impl ProjectDiagnosticsEditor {
         let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
             .project
             .read(cx)
-            .diagnostic_summaries(cx)
+            .diagnostic_summaries(false, cx)
             .fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
                 summaries.entry(server_id).or_default().insert(path);
                 summaries
@@ -320,7 +320,7 @@ impl ProjectDiagnosticsEditor {
                 .context("rechecking diagnostics for paths")?;
 
                 this.update(&mut cx, |this, cx| {
-                    this.summary = this.project.read(cx).diagnostic_summary(cx);
+                    this.summary = this.project.read(cx).diagnostic_summary(false, cx);
                     cx.emit(Event::TitleChanged);
                 })?;
                 anyhow::Ok(())

crates/diagnostics/src/items.rs πŸ”—

@@ -34,19 +34,19 @@ impl DiagnosticIndicator {
             }
             project::Event::DiskBasedDiagnosticsFinished { language_server_id }
             | project::Event::LanguageServerRemoved(language_server_id) => {
-                this.summary = project.read(cx).diagnostic_summary(cx);
+                this.summary = project.read(cx).diagnostic_summary(false, cx);
                 this.in_progress_checks.remove(language_server_id);
                 cx.notify();
             }
             project::Event::DiagnosticsUpdated { .. } => {
-                this.summary = project.read(cx).diagnostic_summary(cx);
+                this.summary = project.read(cx).diagnostic_summary(false, cx);
                 cx.notify();
             }
             _ => {}
         })
         .detach();
         Self {
-            summary: project.read(cx).diagnostic_summary(cx),
+            summary: project.read(cx).diagnostic_summary(false, cx),
             in_progress_checks: project
                 .read(cx)
                 .language_servers_running_disk_based_diagnostics()

crates/diagnostics2/src/diagnostics.rs πŸ”—

@@ -165,7 +165,7 @@ impl ProjectDiagnosticsEditor {
             });
 
         let project = project_handle.read(cx);
-        let summary = project.diagnostic_summary(cx);
+        let summary = project.diagnostic_summary(false, cx);
         let mut this = Self {
             project: project_handle,
             summary,
@@ -252,7 +252,7 @@ impl ProjectDiagnosticsEditor {
         let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
             .project
             .read(cx)
-            .diagnostic_summaries(cx)
+            .diagnostic_summaries(false, cx)
             .fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
                 summaries.entry(server_id).or_default().insert(path);
                 summaries
@@ -332,7 +332,7 @@ impl ProjectDiagnosticsEditor {
                 .context("rechecking diagnostics for paths")?;
 
                 this.update(&mut cx, |this, cx| {
-                    this.summary = this.project.read(cx).diagnostic_summary(cx);
+                    this.summary = this.project.read(cx).diagnostic_summary(false, cx);
                     cx.emit(ItemEvent::UpdateTab);
                     cx.emit(ItemEvent::UpdateBreadcrumbs);
                 })?;

crates/diagnostics2/src/items.rs πŸ”—

@@ -77,13 +77,13 @@ impl DiagnosticIndicator {
 
             project::Event::DiskBasedDiagnosticsFinished { language_server_id }
             | project::Event::LanguageServerRemoved(language_server_id) => {
-                this.summary = project.read(cx).diagnostic_summary(cx);
+                this.summary = project.read(cx).diagnostic_summary(false, cx);
                 this.in_progress_checks.remove(language_server_id);
                 cx.notify();
             }
 
             project::Event::DiagnosticsUpdated { .. } => {
-                this.summary = project.read(cx).diagnostic_summary(cx);
+                this.summary = project.read(cx).diagnostic_summary(false, cx);
                 cx.notify();
             }
 
@@ -92,7 +92,7 @@ impl DiagnosticIndicator {
         .detach();
 
         Self {
-            summary: project.read(cx).diagnostic_summary(cx),
+            summary: project.read(cx).diagnostic_summary(false, cx),
             in_progress_checks: project
                 .read(cx)
                 .language_servers_running_disk_based_diagnostics()

crates/editor2/src/items.rs πŸ”—

@@ -8,9 +8,10 @@ use anyhow::{anyhow, Context as _, Result};
 use collections::HashSet;
 use futures::future::try_join_all;
 use gpui::{
-    div, point, AnyElement, AppContext, AsyncAppContext, AsyncWindowContext, Context, 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},
@@ -1113,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>,

crates/file_finder/src/file_finder.rs πŸ”—

@@ -518,6 +518,7 @@ impl PickerDelegate for FileFinderDelegate {
     }
 
     fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
+        let raw_query = raw_query.trim();
         if raw_query.is_empty() {
             let project = self.project.read(cx);
             self.latest_search_id = post_inc(&mut self.search_count);
@@ -539,7 +540,6 @@ impl PickerDelegate for FileFinderDelegate {
             cx.notify();
             Task::ready(())
         } else {
-            let raw_query = &raw_query;
             let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
                 Ok::<_, std::convert::Infallible>(FileSearchQuery {
                     raw_query: raw_query.to_owned(),
@@ -735,6 +735,7 @@ mod tests {
         cx.dispatch_action(window.into(), Toggle);
 
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+
         finder
             .update(cx, |finder, cx| {
                 finder.delegate_mut().update_matches("bna".to_string(), cx)
@@ -743,7 +744,6 @@ mod tests {
         finder.read_with(cx, |finder, _| {
             assert_eq!(finder.delegate().matches.len(), 2);
         });
-
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
         cx.dispatch_action(window.into(), SelectNext);
         cx.dispatch_action(window.into(), Confirm);
@@ -762,6 +762,49 @@ mod tests {
                 "bandana"
             );
         });
+
+        for bandana_query in [
+            "bandana",
+            " bandana",
+            "bandana ",
+            " bandana ",
+            " ndan ",
+            " band ",
+        ] {
+            finder
+                .update(cx, |finder, cx| {
+                    finder
+                        .delegate_mut()
+                        .update_matches(bandana_query.to_string(), cx)
+                })
+                .await;
+            finder.read_with(cx, |finder, _| {
+                assert_eq!(
+                    finder.delegate().matches.len(),
+                    1,
+                    "Wrong number of matches for bandana query '{bandana_query}'"
+                );
+            });
+            let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+            cx.dispatch_action(window.into(), SelectNext);
+            cx.dispatch_action(window.into(), Confirm);
+            active_pane
+                .condition(cx, |pane, _| pane.active_item().is_some())
+                .await;
+            cx.read(|cx| {
+                let active_item = active_pane.read(cx).active_item().unwrap();
+                assert_eq!(
+                    active_item
+                        .as_any()
+                        .downcast_ref::<Editor>()
+                        .unwrap()
+                        .read(cx)
+                        .title(cx),
+                    "bandana",
+                    "Wrong match for bandana query '{bandana_query}'"
+                );
+            });
+        }
     }
 
     #[gpui::test]

crates/file_finder2/src/file_finder.rs πŸ”—

@@ -15,7 +15,7 @@ use std::{
     },
 };
 use text::Point;
-use ui::{v_stack, HighlightedLabel, ListItem};
+use ui::{prelude::*, v_stack, HighlightedLabel, ListItem};
 use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
 use workspace::Workspace;
 
@@ -552,6 +552,7 @@ impl PickerDelegate for FileFinderDelegate {
         raw_query: String,
         cx: &mut ViewContext<Picker<Self>>,
     ) -> Task<()> {
+        let raw_query = raw_query.trim();
         if raw_query.is_empty() {
             let project = self.project.read(cx);
             self.latest_search_id = post_inc(&mut self.search_count);
@@ -573,7 +574,6 @@ impl PickerDelegate for FileFinderDelegate {
             cx.notify();
             Task::ready(())
         } else {
-            let raw_query = &raw_query;
             let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
                 Ok::<_, std::convert::Infallible>(FileSearchQuery {
                     raw_query: raw_query.to_owned(),
@@ -766,18 +766,49 @@ mod tests {
         let (picker, workspace, cx) = build_find_picker(project, cx);
 
         cx.simulate_input("bna");
-
         picker.update(cx, |picker, _| {
             assert_eq!(picker.delegate.matches.len(), 2);
         });
-
         cx.dispatch_action(SelectNext);
         cx.dispatch_action(Confirm);
-
         cx.read(|cx| {
             let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
             assert_eq!(active_editor.read(cx).title(cx), "bandana");
         });
+
+        for bandana_query in [
+            "bandana",
+            " bandana",
+            "bandana ",
+            " bandana ",
+            " ndan ",
+            " band ",
+        ] {
+            picker
+                .update(cx, |picker, cx| {
+                    picker
+                        .delegate
+                        .update_matches(bandana_query.to_string(), cx)
+                })
+                .await;
+            picker.update(cx, |picker, _| {
+                assert_eq!(
+                    picker.delegate.matches.len(),
+                    1,
+                    "Wrong number of matches for bandana query '{bandana_query}'"
+                );
+            });
+            cx.dispatch_action(SelectNext);
+            cx.dispatch_action(Confirm);
+            cx.read(|cx| {
+                let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
+                assert_eq!(
+                    active_editor.read(cx).title(cx),
+                    "bandana",
+                    "Wrong match for bandana query '{bandana_query}'"
+                );
+            });
+        }
     }
 
     #[gpui::test]

crates/gpui2/src/app/test_context.rs πŸ”—

@@ -2,8 +2,8 @@ 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,
+    TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext, VisualContext, WindowContext,
+    WindowHandle, WindowOptions,
 };
 use anyhow::{anyhow, bail};
 use futures::{Stream, StreamExt};
@@ -502,6 +502,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 +522,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/platform.rs πŸ”—

@@ -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(&self) -> Option<&TestWindow> {
+        None
+    }
 }
 
 pub trait PlatformDispatcher: Send + Sync {

crates/gpui2/src/platform/test/platform.rs πŸ”—

@@ -189,13 +189,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()>) {}
 

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,
     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(&self) -> Option<&TestWindow> {
+        Some(self)
+    }
 }
 
 pub struct TestAtlasState {

crates/gpui2/src/view.rs πŸ”—

@@ -238,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 πŸ”—

@@ -685,6 +685,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()

crates/language/src/language.rs πŸ”—

@@ -197,8 +197,12 @@ impl CachedLspAdapter {
         self.adapter.code_action_kinds()
     }
 
-    pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
-        self.adapter.workspace_configuration(cx)
+    pub fn workspace_configuration(
+        &self,
+        workspace_root: &Path,
+        cx: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
+        self.adapter.workspace_configuration(workspace_root, cx)
     }
 
     pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
@@ -312,7 +316,7 @@ pub trait LspAdapter: 'static + Send + Sync {
         None
     }
 
-    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(&self, _: &Path, _: &mut AppContext) -> BoxFuture<'static, Value> {
         futures::future::ready(serde_json::json!({})).boxed()
     }
 

crates/language2/src/language2.rs πŸ”—

@@ -200,8 +200,12 @@ impl CachedLspAdapter {
         self.adapter.code_action_kinds()
     }
 
-    pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
-        self.adapter.workspace_configuration(cx)
+    pub fn workspace_configuration(
+        &self,
+        workspace_root: &Path,
+        cx: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
+        self.adapter.workspace_configuration(workspace_root, cx)
     }
 
     pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
@@ -315,7 +319,7 @@ pub trait LspAdapter: 'static + Send + Sync {
         None
     }
 
-    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(&self, _: &Path, _: &mut AppContext) -> BoxFuture<'static, Value> {
         futures::future::ready(serde_json::json!({})).boxed()
     }
 

crates/lsp/src/lsp.rs πŸ”—

@@ -429,8 +429,8 @@ impl LanguageServer {
         let root_uri = Url::from_file_path(&self.root_path).unwrap();
         #[allow(deprecated)]
         let params = InitializeParams {
-            process_id: Default::default(),
-            root_path: Default::default(),
+            process_id: None,
+            root_path: None,
             root_uri: Some(root_uri.clone()),
             initialization_options: options,
             capabilities: ClientCapabilities {
@@ -451,12 +451,15 @@ impl LanguageServer {
                     inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
                         refresh_support: Some(true),
                     }),
+                    diagnostic: Some(DiagnosticWorkspaceClientCapabilities {
+                        refresh_support: None,
+                    }),
                     ..Default::default()
                 }),
                 text_document: Some(TextDocumentClientCapabilities {
                     definition: Some(GotoCapability {
                         link_support: Some(true),
-                        ..Default::default()
+                        dynamic_registration: None,
                     }),
                     code_action: Some(CodeActionClientCapabilities {
                         code_action_literal_support: Some(CodeActionLiteralSupport {
@@ -501,7 +504,7 @@ impl LanguageServer {
                     }),
                     hover: Some(HoverClientCapabilities {
                         content_format: Some(vec![MarkupKind::Markdown]),
-                        ..Default::default()
+                        dynamic_registration: None,
                     }),
                     inlay_hint: Some(InlayHintClientCapabilities {
                         resolve_support: Some(InlayHintResolveClientCapabilities {
@@ -515,6 +518,20 @@ impl LanguageServer {
                         }),
                         dynamic_registration: Some(false),
                     }),
+                    publish_diagnostics: Some(PublishDiagnosticsClientCapabilities {
+                        related_information: Some(true),
+                        ..Default::default()
+                    }),
+                    formatting: Some(DynamicRegistrationClientCapabilities {
+                        dynamic_registration: None,
+                    }),
+                    on_type_formatting: Some(DynamicRegistrationClientCapabilities {
+                        dynamic_registration: None,
+                    }),
+                    diagnostic: Some(DiagnosticClientCapabilities {
+                        related_document_support: Some(true),
+                        dynamic_registration: None,
+                    }),
                     ..Default::default()
                 }),
                 experimental: Some(json!({
@@ -524,15 +541,15 @@ impl LanguageServer {
                     work_done_progress: Some(true),
                     ..Default::default()
                 }),
-                ..Default::default()
+                general: None,
             },
-            trace: Default::default(),
+            trace: None,
             workspace_folders: Some(vec![WorkspaceFolder {
                 uri: root_uri,
                 name: Default::default(),
             }]),
-            client_info: Default::default(),
-            locale: Default::default(),
+            client_info: None,
+            locale: None,
         };
 
         let response = self.request::<request::Initialize>(params).await?;

crates/lsp2/src/lsp2.rs πŸ”—

@@ -434,8 +434,8 @@ impl LanguageServer {
         let root_uri = Url::from_file_path(&self.root_path).unwrap();
         #[allow(deprecated)]
         let params = InitializeParams {
-            process_id: Default::default(),
-            root_path: Default::default(),
+            process_id: None,
+            root_path: None,
             root_uri: Some(root_uri.clone()),
             initialization_options: options,
             capabilities: ClientCapabilities {
@@ -456,12 +456,15 @@ impl LanguageServer {
                     inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
                         refresh_support: Some(true),
                     }),
+                    diagnostic: Some(DiagnosticWorkspaceClientCapabilities {
+                        refresh_support: None,
+                    }),
                     ..Default::default()
                 }),
                 text_document: Some(TextDocumentClientCapabilities {
                     definition: Some(GotoCapability {
                         link_support: Some(true),
-                        ..Default::default()
+                        dynamic_registration: None,
                     }),
                     code_action: Some(CodeActionClientCapabilities {
                         code_action_literal_support: Some(CodeActionLiteralSupport {
@@ -503,7 +506,7 @@ impl LanguageServer {
                     }),
                     hover: Some(HoverClientCapabilities {
                         content_format: Some(vec![MarkupKind::Markdown]),
-                        ..Default::default()
+                        dynamic_registration: None,
                     }),
                     inlay_hint: Some(InlayHintClientCapabilities {
                         resolve_support: Some(InlayHintResolveClientCapabilities {
@@ -517,6 +520,20 @@ impl LanguageServer {
                         }),
                         dynamic_registration: Some(false),
                     }),
+                    publish_diagnostics: Some(PublishDiagnosticsClientCapabilities {
+                        related_information: Some(true),
+                        ..Default::default()
+                    }),
+                    formatting: Some(DynamicRegistrationClientCapabilities {
+                        dynamic_registration: None,
+                    }),
+                    on_type_formatting: Some(DynamicRegistrationClientCapabilities {
+                        dynamic_registration: None,
+                    }),
+                    diagnostic: Some(DiagnosticClientCapabilities {
+                        related_document_support: Some(true),
+                        dynamic_registration: None,
+                    }),
                     ..Default::default()
                 }),
                 experimental: Some(json!({
@@ -526,15 +543,15 @@ impl LanguageServer {
                     work_done_progress: Some(true),
                     ..Default::default()
                 }),
-                ..Default::default()
+                general: None,
             },
-            trace: Default::default(),
+            trace: None,
             workspace_folders: Some(vec![WorkspaceFolder {
                 uri: root_uri,
                 name: Default::default(),
             }]),
-            client_info: Default::default(),
-            locale: Default::default(),
+            client_info: None,
+            locale: None,
         };
 
         let response = self.request::<request::Initialize>(params).await?;

crates/project/src/project.rs πŸ”—

@@ -2641,8 +2641,9 @@ impl Project {
                 });
 
                 for (adapter, server) in servers {
-                    let workspace_config =
-                        cx.update(|cx| adapter.workspace_configuration(cx)).await;
+                    let workspace_config = cx
+                        .update(|cx| adapter.workspace_configuration(server.root_path(), cx))
+                        .await;
                     server
                         .notify::<lsp::notification::DidChangeConfiguration>(
                             lsp::DidChangeConfigurationParams {
@@ -2753,7 +2754,7 @@ impl Project {
             stderr_capture.clone(),
             language.clone(),
             adapter.clone(),
-            worktree_path,
+            Arc::clone(&worktree_path),
             ProjectLspAdapterDelegate::new(self, cx),
             cx,
         ) {
@@ -2776,6 +2777,7 @@ impl Project {
             cx.spawn_weak(|this, mut cx| async move {
                 let result = Self::setup_and_insert_language_server(
                     this,
+                    &worktree_path,
                     override_options,
                     pending_server,
                     adapter.clone(),
@@ -2891,6 +2893,7 @@ impl Project {
 
     async fn setup_and_insert_language_server(
         this: WeakModelHandle<Self>,
+        worktree_path: &Path,
         override_initialization_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
         adapter: Arc<CachedLspAdapter>,
@@ -2903,6 +2906,7 @@ impl Project {
             this,
             override_initialization_options,
             pending_server,
+            worktree_path,
             adapter.clone(),
             server_id,
             cx,
@@ -2932,11 +2936,14 @@ impl Project {
         this: WeakModelHandle<Self>,
         override_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
+        worktree_path: &Path,
         adapter: Arc<CachedLspAdapter>,
         server_id: LanguageServerId,
         cx: &mut AsyncAppContext,
     ) -> Result<Arc<LanguageServer>> {
-        let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await;
+        let workspace_config = cx
+            .update(|cx| adapter.workspace_configuration(worktree_path, cx))
+            .await;
         let language_server = pending_server.task.await?;
 
         language_server
@@ -2964,11 +2971,14 @@ impl Project {
         language_server
             .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
                 let adapter = adapter.clone();
+                let worktree_path = worktree_path.to_path_buf();
                 move |params, mut cx| {
                     let adapter = adapter.clone();
+                    let worktree_path = worktree_path.clone();
                     async move {
-                        let workspace_config =
-                            cx.update(|cx| adapter.workspace_configuration(cx)).await;
+                        let workspace_config = cx
+                            .update(|cx| adapter.workspace_configuration(&worktree_path, cx))
+                            .await;
                         Ok(params
                             .items
                             .into_iter()
@@ -6523,9 +6533,15 @@ impl Project {
             })
     }
 
-    pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary {
+    pub fn diagnostic_summary(&self, include_ignored: bool, cx: &AppContext) -> DiagnosticSummary {
         let mut summary = DiagnosticSummary::default();
-        for (_, _, path_summary) in self.diagnostic_summaries(cx) {
+        for (_, _, path_summary) in
+            self.diagnostic_summaries(include_ignored, cx)
+                .filter(|(path, _, _)| {
+                    let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored);
+                    include_ignored || worktree == Some(false)
+                })
+        {
             summary.error_count += path_summary.error_count;
             summary.warning_count += path_summary.warning_count;
         }
@@ -6534,6 +6550,7 @@ impl Project {
 
     pub fn diagnostic_summaries<'a>(
         &'a self,
+        include_ignored: bool,
         cx: &'a AppContext,
     ) -> impl Iterator<Item = (ProjectPath, LanguageServerId, DiagnosticSummary)> + 'a {
         self.visible_worktrees(cx).flat_map(move |worktree| {
@@ -6544,6 +6561,10 @@ impl Project {
                 .map(move |(path, server_id, summary)| {
                     (ProjectPath { worktree_id, path }, server_id, summary)
                 })
+                .filter(move |(path, _, _)| {
+                    let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored);
+                    include_ignored || worktree == Some(false)
+                })
         })
     }
 

crates/project/src/project_tests.rs πŸ”—

@@ -806,7 +806,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
 }
 
 #[gpui::test]
-async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
+async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
     let fs = FakeFs::new(cx.background());
@@ -814,7 +814,12 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
         "/root",
         json!({
             "dir": {
+                ".git": {
+                    "HEAD": "ref: refs/heads/main",
+                },
+                ".gitignore": "b.rs",
                 "a.rs": "let a = 1;",
+                "b.rs": "let b = 2;",
             },
             "other.rs": "let b = c;"
         }),
@@ -822,6 +827,13 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
     .await;
 
     let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
+    let (worktree, _) = project
+        .update(cx, |project, cx| {
+            project.find_or_create_local_worktree("/root/dir", true, cx)
+        })
+        .await
+        .unwrap();
+    let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 
     let (worktree, _) = project
         .update(cx, |project, cx| {
@@ -829,12 +841,30 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
         })
         .await
         .unwrap();
-    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
+    let other_worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 
+    let server_id = LanguageServerId(0);
     project.update(cx, |project, cx| {
         project
             .update_diagnostics(
-                LanguageServerId(0),
+                server_id,
+                lsp::PublishDiagnosticsParams {
+                    uri: Url::from_file_path("/root/dir/b.rs").unwrap(),
+                    version: None,
+                    diagnostics: vec![lsp::Diagnostic {
+                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
+                        severity: Some(lsp::DiagnosticSeverity::ERROR),
+                        message: "unused variable 'b'".to_string(),
+                        ..Default::default()
+                    }],
+                },
+                &[],
+                cx,
+            )
+            .unwrap();
+        project
+            .update_diagnostics(
+                server_id,
                 lsp::PublishDiagnosticsParams {
                     uri: Url::from_file_path("/root/other.rs").unwrap(),
                     version: None,
@@ -851,11 +881,34 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
             .unwrap();
     });
 
-    let buffer = project
-        .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
+    let main_ignored_buffer = project
+        .update(cx, |project, cx| {
+            project.open_buffer((main_worktree_id, "b.rs"), cx)
+        })
         .await
         .unwrap();
-    buffer.read_with(cx, |buffer, _| {
+    main_ignored_buffer.read_with(cx, |buffer, _| {
+        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
+        assert_eq!(
+            chunks
+                .iter()
+                .map(|(s, d)| (s.as_str(), *d))
+                .collect::<Vec<_>>(),
+            &[
+                ("let ", None),
+                ("b", Some(DiagnosticSeverity::ERROR)),
+                (" = 2;", None),
+            ],
+            "Gigitnored buffers should still get in-buffer diagnostics",
+        );
+    });
+    let other_buffer = project
+        .update(cx, |project, cx| {
+            project.open_buffer((other_worktree_id, ""), cx)
+        })
+        .await
+        .unwrap();
+    other_buffer.read_with(cx, |buffer, _| {
         let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
         assert_eq!(
             chunks
@@ -866,13 +919,29 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
                 ("let b = ", None),
                 ("c", Some(DiagnosticSeverity::ERROR)),
                 (";", None),
-            ]
+            ],
+            "Buffers from hidden projects should still get in-buffer diagnostics"
         );
     });
 
     project.read_with(cx, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).next(), None);
-        assert_eq!(project.diagnostic_summary(cx).error_count, 0);
+        assert_eq!(project.diagnostic_summaries(false, cx).next(), None);
+        assert_eq!(
+            project.diagnostic_summaries(true, cx).collect::<Vec<_>>(),
+            vec![(
+                ProjectPath {
+                    worktree_id: main_worktree_id,
+                    path: Arc::from(Path::new("b.rs")),
+                },
+                server_id,
+                DiagnosticSummary {
+                    error_count: 1,
+                    warning_count: 0,
+                }
+            )]
+        );
+        assert_eq!(project.diagnostic_summary(false, cx).error_count, 0);
+        assert_eq!(project.diagnostic_summary(true, cx).error_count, 1);
     });
 }
 
@@ -1145,7 +1214,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
     });
     project.read_with(cx, |project, cx| {
         assert_eq!(
-            project.diagnostic_summary(cx),
+            project.diagnostic_summary(false, cx),
             DiagnosticSummary {
                 error_count: 1,
                 warning_count: 0,
@@ -1171,7 +1240,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
     });
     project.read_with(cx, |project, cx| {
         assert_eq!(
-            project.diagnostic_summary(cx),
+            project.diagnostic_summary(false, cx),
             DiagnosticSummary {
                 error_count: 0,
                 warning_count: 0,
@@ -1763,7 +1832,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
             .unwrap();
 
         assert_eq!(
-            project.diagnostic_summary(cx),
+            project.diagnostic_summary(false, cx),
             DiagnosticSummary {
                 error_count: 2,
                 warning_count: 0,

crates/project2/src/project2.rs πŸ”—

@@ -2677,8 +2677,9 @@ impl Project {
                 })?;
 
                 for (adapter, server) in servers {
-                    let workspace_config =
-                        cx.update(|cx| adapter.workspace_configuration(cx))?.await;
+                    let workspace_config = cx
+                        .update(|cx| adapter.workspace_configuration(server.root_path(), cx))?
+                        .await;
                     server
                         .notify::<lsp::notification::DidChangeConfiguration>(
                             lsp::DidChangeConfigurationParams {
@@ -2790,7 +2791,7 @@ impl Project {
             stderr_capture.clone(),
             language.clone(),
             adapter.clone(),
-            worktree_path,
+            Arc::clone(&worktree_path),
             ProjectLspAdapterDelegate::new(self, cx),
             cx,
         ) {
@@ -2822,6 +2823,7 @@ impl Project {
             cx.spawn(move |this, mut cx| async move {
                 let result = Self::setup_and_insert_language_server(
                     this.clone(),
+                    &worktree_path,
                     initialization_options,
                     pending_server,
                     adapter.clone(),
@@ -2942,6 +2944,7 @@ impl Project {
 
     async fn setup_and_insert_language_server(
         this: WeakModel<Self>,
+        worktree_path: &Path,
         initialization_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
         adapter: Arc<CachedLspAdapter>,
@@ -2954,6 +2957,7 @@ impl Project {
             this.clone(),
             initialization_options,
             pending_server,
+            worktree_path,
             adapter.clone(),
             server_id,
             cx,
@@ -2983,11 +2987,14 @@ impl Project {
         this: WeakModel<Self>,
         initialization_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
+        worktree_path: &Path,
         adapter: Arc<CachedLspAdapter>,
         server_id: LanguageServerId,
         cx: &mut AsyncAppContext,
     ) -> Result<Arc<LanguageServer>> {
-        let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx))?.await;
+        let workspace_config = cx
+            .update(|cx| adapter.workspace_configuration(worktree_path, cx))?
+            .await;
         let language_server = pending_server.task.await?;
 
         language_server
@@ -3016,11 +3023,14 @@ impl Project {
         language_server
             .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
                 let adapter = adapter.clone();
+                let worktree_path = worktree_path.to_path_buf();
                 move |params, cx| {
                     let adapter = adapter.clone();
+                    let worktree_path = worktree_path.clone();
                     async move {
-                        let workspace_config =
-                            cx.update(|cx| adapter.workspace_configuration(cx))?.await;
+                        let workspace_config = cx
+                            .update(|cx| adapter.workspace_configuration(&worktree_path, cx))?
+                            .await;
                         Ok(params
                             .items
                             .into_iter()
@@ -6596,9 +6606,15 @@ impl Project {
             })
     }
 
-    pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary {
+    pub fn diagnostic_summary(&self, include_ignored: bool, cx: &AppContext) -> DiagnosticSummary {
         let mut summary = DiagnosticSummary::default();
-        for (_, _, path_summary) in self.diagnostic_summaries(cx) {
+        for (_, _, path_summary) in
+            self.diagnostic_summaries(include_ignored, cx)
+                .filter(|(path, _, _)| {
+                    let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored);
+                    include_ignored || worktree == Some(false)
+                })
+        {
             summary.error_count += path_summary.error_count;
             summary.warning_count += path_summary.warning_count;
         }
@@ -6607,17 +6623,23 @@ impl Project {
 
     pub fn diagnostic_summaries<'a>(
         &'a self,
+        include_ignored: bool,
         cx: &'a AppContext,
     ) -> impl Iterator<Item = (ProjectPath, LanguageServerId, DiagnosticSummary)> + 'a {
-        self.visible_worktrees(cx).flat_map(move |worktree| {
-            let worktree = worktree.read(cx);
-            let worktree_id = worktree.id();
-            worktree
-                .diagnostic_summaries()
-                .map(move |(path, server_id, summary)| {
-                    (ProjectPath { worktree_id, path }, server_id, summary)
-                })
-        })
+        self.visible_worktrees(cx)
+            .flat_map(move |worktree| {
+                let worktree = worktree.read(cx);
+                let worktree_id = worktree.id();
+                worktree
+                    .diagnostic_summaries()
+                    .map(move |(path, server_id, summary)| {
+                        (ProjectPath { worktree_id, path }, server_id, summary)
+                    })
+            })
+            .filter(move |(path, _, _)| {
+                let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored);
+                include_ignored || worktree == Some(false)
+            })
     }
 
     pub fn disk_based_diagnostics_started(

crates/project2/src/project_tests.rs πŸ”—

@@ -823,7 +823,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
 }
 
 #[gpui::test]
-async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
+async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
     let fs = FakeFs::new(cx.executor());
@@ -831,7 +831,12 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
         "/root",
         json!({
             "dir": {
+                ".git": {
+                    "HEAD": "ref: refs/heads/main",
+                },
+                ".gitignore": "b.rs",
                 "a.rs": "let a = 1;",
+                "b.rs": "let b = 2;",
             },
             "other.rs": "let b = c;"
         }),
@@ -839,6 +844,13 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
     .await;
 
     let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
+    let (worktree, _) = project
+        .update(cx, |project, cx| {
+            project.find_or_create_local_worktree("/root/dir", true, cx)
+        })
+        .await
+        .unwrap();
+    let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 
     let (worktree, _) = project
         .update(cx, |project, cx| {
@@ -846,12 +858,30 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
         })
         .await
         .unwrap();
-    let worktree_id = worktree.update(cx, |tree, _| tree.id());
+    let other_worktree_id = worktree.update(cx, |tree, _| tree.id());
 
+    let server_id = LanguageServerId(0);
     project.update(cx, |project, cx| {
         project
             .update_diagnostics(
-                LanguageServerId(0),
+                server_id,
+                lsp::PublishDiagnosticsParams {
+                    uri: Url::from_file_path("/root/dir/b.rs").unwrap(),
+                    version: None,
+                    diagnostics: vec![lsp::Diagnostic {
+                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
+                        severity: Some(lsp::DiagnosticSeverity::ERROR),
+                        message: "unused variable 'b'".to_string(),
+                        ..Default::default()
+                    }],
+                },
+                &[],
+                cx,
+            )
+            .unwrap();
+        project
+            .update_diagnostics(
+                server_id,
                 lsp::PublishDiagnosticsParams {
                     uri: Url::from_file_path("/root/other.rs").unwrap(),
                     version: None,
@@ -868,11 +898,34 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
             .unwrap();
     });
 
-    let buffer = project
-        .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
+    let main_ignored_buffer = project
+        .update(cx, |project, cx| {
+            project.open_buffer((main_worktree_id, "b.rs"), cx)
+        })
         .await
         .unwrap();
-    buffer.update(cx, |buffer, _| {
+    main_ignored_buffer.update(cx, |buffer, _| {
+        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
+        assert_eq!(
+            chunks
+                .iter()
+                .map(|(s, d)| (s.as_str(), *d))
+                .collect::<Vec<_>>(),
+            &[
+                ("let ", None),
+                ("b", Some(DiagnosticSeverity::ERROR)),
+                (" = 2;", None),
+            ],
+            "Gigitnored buffers should still get in-buffer diagnostics",
+        );
+    });
+    let other_buffer = project
+        .update(cx, |project, cx| {
+            project.open_buffer((other_worktree_id, ""), cx)
+        })
+        .await
+        .unwrap();
+    other_buffer.update(cx, |buffer, _| {
         let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
         assert_eq!(
             chunks
@@ -883,13 +936,29 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
                 ("let b = ", None),
                 ("c", Some(DiagnosticSeverity::ERROR)),
                 (";", None),
-            ]
+            ],
+            "Buffers from hidden projects should still get in-buffer diagnostics"
         );
     });
 
     project.update(cx, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).next(), None);
-        assert_eq!(project.diagnostic_summary(cx).error_count, 0);
+        assert_eq!(project.diagnostic_summaries(false, cx).next(), None);
+        assert_eq!(
+            project.diagnostic_summaries(true, cx).collect::<Vec<_>>(),
+            vec![(
+                ProjectPath {
+                    worktree_id: main_worktree_id,
+                    path: Arc::from(Path::new("b.rs")),
+                },
+                server_id,
+                DiagnosticSummary {
+                    error_count: 1,
+                    warning_count: 0,
+                }
+            )]
+        );
+        assert_eq!(project.diagnostic_summary(false, cx).error_count, 0);
+        assert_eq!(project.diagnostic_summary(true, cx).error_count, 1);
     });
 }
 
@@ -1162,7 +1231,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
     });
     project.update(cx, |project, cx| {
         assert_eq!(
-            project.diagnostic_summary(cx),
+            project.diagnostic_summary(false, cx),
             DiagnosticSummary {
                 error_count: 1,
                 warning_count: 0,
@@ -1188,7 +1257,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
     });
     project.update(cx, |project, cx| {
         assert_eq!(
-            project.diagnostic_summary(cx),
+            project.diagnostic_summary(false, cx),
             DiagnosticSummary {
                 error_count: 0,
                 warning_count: 0,
@@ -1777,7 +1846,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
             .unwrap();
 
         assert_eq!(
-            project.diagnostic_summary(cx),
+            project.diagnostic_summary(false, cx),
             DiagnosticSummary {
                 error_count: 2,
                 warning_count: 0,

crates/project_panel/src/project_panel.rs πŸ”—

@@ -1627,9 +1627,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::{
@@ -29,8 +29,7 @@ use std::{
     path::Path,
     sync::Arc,
 };
-use theme::ActiveTheme as _;
-use ui::{v_stack, ContextMenu, IconElement, Label, ListItem};
+use ui::{prelude::*, v_stack, ContextMenu, IconElement, Label, ListItem};
 use unicase::UniCase;
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
@@ -1421,6 +1420,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 {
@@ -1434,7 +1449,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))
@@ -2845,7 +2860,7 @@ mod tests {
                 let worktree = worktree.read(cx);
                 if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
                     let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
-                    panel.selection = Some(Selection {
+                    panel.selection = Some(crate::Selection {
                         worktree_id: worktree.id(),
                         entry_id,
                     });

crates/search2/src/search.rs πŸ”—

@@ -4,7 +4,7 @@ use gpui::{actions, Action, AppContext, IntoElement};
 pub use mode::SearchMode;
 use project::search::SearchQuery;
 use ui::prelude::*;
-use ui::{ButtonStyle2, Icon, IconButton};
+use ui::{ButtonStyle, Icon, IconButton};
 //pub use project_search::{ProjectSearchBar, ProjectSearchView};
 // use theme::components::{
 //     action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle,
@@ -91,8 +91,8 @@ impl SearchOptions {
                     cx.dispatch_action(action.boxed_clone());
                 }
             })
-            .style(ButtonStyle2::Subtle)
-            .when(active, |button| button.style(ButtonStyle2::Filled))
+            .style(ButtonStyle::Subtle)
+            .when(active, |button| button.style(ButtonStyle::Filled))
     }
 }
 
@@ -103,8 +103,8 @@ fn toggle_replace_button(active: bool) -> impl IntoElement {
             cx.dispatch_action(Box::new(ToggleReplace));
             cx.notify();
         })
-        .style(ButtonStyle2::Subtle)
-        .when(active, |button| button.style(ButtonStyle2::Filled))
+        .style(ButtonStyle::Subtle)
+        .when(active, |button| button.style(ButtonStyle::Filled))
 }
 
 fn render_replace_button(

crates/storybook2/src/story_selector.rs πŸ”—

@@ -24,6 +24,7 @@ pub enum ComponentStory {
     Keybinding,
     Label,
     List,
+    ListHeader,
     ListItem,
     Scroll,
     Text,
@@ -46,6 +47,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/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, ParentElement, Render,
-    SharedString, View, ViewContext, VisualContext, WeakView,
+    actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, SharedString, View,
+    ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use settings::{update_settings_file, SettingsStore};
 use std::sync::Arc;
-use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings};
-use ui::ListItem;
+use theme::{Theme, ThemeRegistry, ThemeSettings};
+use ui::{prelude::*, ListItem};
 use util::ResultExt;
 use workspace::{ui::HighlightedLabel, Workspace};
 

crates/ui2/src/components/button/button.rs πŸ”—

@@ -1,7 +1,7 @@
 use gpui::AnyView;
 
 use crate::prelude::*;
-use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Label, LineHeightStyle};
+use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Label, LineHeightStyle};
 
 #[derive(IntoElement)]
 pub struct Button {
@@ -54,12 +54,12 @@ impl ButtonCommon for Button {
         self.base.id()
     }
 
-    fn style(mut self, style: ButtonStyle2) -> Self {
+    fn style(mut self, style: ButtonStyle) -> Self {
         self.base = self.base.style(style);
         self
     }
 
-    fn size(mut self, size: ButtonSize2) -> Self {
+    fn size(mut self, size: ButtonSize) -> Self {
         self.base = self.base.size(size);
         self
     }
@@ -79,7 +79,7 @@ impl RenderOnce for Button {
         } else if self.base.selected {
             Color::Selected
         } else {
-            Color::Default
+            self.label_color.unwrap_or_default()
         };
 
         self.base.child(

crates/ui2/src/components/button/button_like.rs πŸ”—

@@ -1,4 +1,4 @@
-use gpui::{rems, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
+use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
 use smallvec::SmallVec;
 
 use crate::h_stack;
@@ -6,13 +6,13 @@ use crate::prelude::*;
 
 pub trait ButtonCommon: Clickable + Disableable {
     fn id(&self) -> &ElementId;
-    fn style(self, style: ButtonStyle2) -> Self;
-    fn size(self, size: ButtonSize2) -> Self;
+    fn style(self, style: ButtonStyle) -> Self;
+    fn size(self, size: ButtonSize) -> Self;
     fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
 }
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
-pub enum ButtonStyle2 {
+pub enum ButtonStyle {
     #[default]
     Filled,
     // Tinted,
@@ -21,54 +21,57 @@ pub enum ButtonStyle2 {
 }
 
 #[derive(Debug, Clone)]
-pub struct ButtonStyle {
+pub(crate) struct ButtonLikeStyles {
     pub background: Hsla,
+    #[allow(unused)]
     pub border_color: Hsla,
+    #[allow(unused)]
     pub label_color: Hsla,
+    #[allow(unused)]
     pub icon_color: Hsla,
 }
 
-impl ButtonStyle2 {
-    pub fn enabled(self, cx: &mut WindowContext) -> ButtonStyle {
+impl ButtonStyle {
+    pub(crate) fn enabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
         match self {
-            ButtonStyle2::Filled => ButtonStyle {
+            ButtonStyle::Filled => ButtonLikeStyles {
                 background: cx.theme().colors().element_background,
-                border_color: gpui::transparent_black(),
+                border_color: transparent_black(),
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
-            ButtonStyle2::Subtle => ButtonStyle {
+            ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_background,
-                border_color: gpui::transparent_black(),
+                border_color: transparent_black(),
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
-            ButtonStyle2::Transparent => ButtonStyle {
-                background: gpui::transparent_black(),
-                border_color: gpui::transparent_black(),
+            ButtonStyle::Transparent => ButtonLikeStyles {
+                background: transparent_black(),
+                border_color: transparent_black(),
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
         }
     }
 
-    pub fn hovered(self, cx: &mut WindowContext) -> ButtonStyle {
+    pub(crate) fn hovered(self, cx: &mut WindowContext) -> ButtonLikeStyles {
         match self {
-            ButtonStyle2::Filled => ButtonStyle {
+            ButtonStyle::Filled => ButtonLikeStyles {
                 background: cx.theme().colors().element_hover,
-                border_color: gpui::transparent_black(),
+                border_color: transparent_black(),
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
-            ButtonStyle2::Subtle => ButtonStyle {
+            ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_hover,
-                border_color: gpui::transparent_black(),
+                border_color: transparent_black(),
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
-            ButtonStyle2::Transparent => ButtonStyle {
-                background: gpui::transparent_black(),
-                border_color: gpui::transparent_black(),
+            ButtonStyle::Transparent => ButtonLikeStyles {
+                background: transparent_black(),
+                border_color: transparent_black(),
                 // TODO: These are not great
                 label_color: Color::Muted.color(cx),
                 // TODO: These are not great
@@ -77,23 +80,23 @@ impl ButtonStyle2 {
         }
     }
 
-    pub fn active(self, cx: &mut WindowContext) -> ButtonStyle {
+    pub(crate) fn active(self, cx: &mut WindowContext) -> ButtonLikeStyles {
         match self {
-            ButtonStyle2::Filled => ButtonStyle {
+            ButtonStyle::Filled => ButtonLikeStyles {
                 background: cx.theme().colors().element_active,
-                border_color: gpui::transparent_black(),
+                border_color: transparent_black(),
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
-            ButtonStyle2::Subtle => ButtonStyle {
+            ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_active,
-                border_color: gpui::transparent_black(),
+                border_color: transparent_black(),
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
-            ButtonStyle2::Transparent => ButtonStyle {
-                background: gpui::transparent_black(),
-                border_color: gpui::transparent_black(),
+            ButtonStyle::Transparent => ButtonLikeStyles {
+                background: transparent_black(),
+                border_color: transparent_black(),
                 // TODO: These are not great
                 label_color: Color::Muted.color(cx),
                 // TODO: These are not great
@@ -102,22 +105,23 @@ impl ButtonStyle2 {
         }
     }
 
-    pub fn focused(self, cx: &mut WindowContext) -> ButtonStyle {
+    #[allow(unused)]
+    pub(crate) fn focused(self, cx: &mut WindowContext) -> ButtonLikeStyles {
         match self {
-            ButtonStyle2::Filled => ButtonStyle {
+            ButtonStyle::Filled => ButtonLikeStyles {
                 background: cx.theme().colors().element_background,
                 border_color: cx.theme().colors().border_focused,
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
-            ButtonStyle2::Subtle => ButtonStyle {
+            ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_background,
                 border_color: cx.theme().colors().border_focused,
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
-            ButtonStyle2::Transparent => ButtonStyle {
-                background: gpui::transparent_black(),
+            ButtonStyle::Transparent => ButtonLikeStyles {
+                background: transparent_black(),
                 border_color: cx.theme().colors().border_focused,
                 label_color: Color::Accent.color(cx),
                 icon_color: Color::Accent.color(cx),
@@ -125,23 +129,23 @@ impl ButtonStyle2 {
         }
     }
 
-    pub fn disabled(self, cx: &mut WindowContext) -> ButtonStyle {
+    pub(crate) fn disabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
         match self {
-            ButtonStyle2::Filled => ButtonStyle {
+            ButtonStyle::Filled => ButtonLikeStyles {
                 background: cx.theme().colors().element_disabled,
                 border_color: cx.theme().colors().border_disabled,
                 label_color: Color::Disabled.color(cx),
                 icon_color: Color::Disabled.color(cx),
             },
-            ButtonStyle2::Subtle => ButtonStyle {
+            ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_disabled,
                 border_color: cx.theme().colors().border_disabled,
                 label_color: Color::Disabled.color(cx),
                 icon_color: Color::Disabled.color(cx),
             },
-            ButtonStyle2::Transparent => ButtonStyle {
-                background: gpui::transparent_black(),
-                border_color: gpui::transparent_black(),
+            ButtonStyle::Transparent => ButtonLikeStyles {
+                background: transparent_black(),
+                border_color: transparent_black(),
                 label_color: Color::Disabled.color(cx),
                 icon_color: Color::Disabled.color(cx),
             },
@@ -150,19 +154,19 @@ impl ButtonStyle2 {
 }
 
 #[derive(Default, PartialEq, Clone, Copy)]
-pub enum ButtonSize2 {
+pub enum ButtonSize {
     #[default]
     Default,
     Compact,
     None,
 }
 
-impl ButtonSize2 {
+impl ButtonSize {
     fn height(self) -> Rems {
         match self {
-            ButtonSize2::Default => rems(22. / 16.),
-            ButtonSize2::Compact => rems(18. / 16.),
-            ButtonSize2::None => rems(16. / 16.),
+            ButtonSize::Default => rems(22. / 16.),
+            ButtonSize::Compact => rems(18. / 16.),
+            ButtonSize::None => rems(16. / 16.),
         }
     }
 }
@@ -170,10 +174,10 @@ impl ButtonSize2 {
 #[derive(IntoElement)]
 pub struct ButtonLike {
     id: ElementId,
-    pub(super) style: ButtonStyle2,
+    pub(super) style: ButtonStyle,
     pub(super) disabled: bool,
     pub(super) selected: bool,
-    size: ButtonSize2,
+    size: ButtonSize,
     tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
     on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
     children: SmallVec<[AnyElement; 2]>,
@@ -183,10 +187,10 @@ impl ButtonLike {
     pub fn new(id: impl Into<ElementId>) -> Self {
         Self {
             id: id.into(),
-            style: ButtonStyle2::default(),
+            style: ButtonStyle::default(),
             disabled: false,
             selected: false,
-            size: ButtonSize2::Default,
+            size: ButtonSize::Default,
             tooltip: None,
             children: SmallVec::new(),
             on_click: None,
@@ -220,12 +224,12 @@ impl ButtonCommon for ButtonLike {
         &self.id
     }
 
-    fn style(mut self, style: ButtonStyle2) -> Self {
+    fn style(mut self, style: ButtonStyle) -> Self {
         self.style = style;
         self
     }
 
-    fn size(mut self, size: ButtonSize2) -> Self {
+    fn size(mut self, size: ButtonSize) -> Self {
         self.size = size;
         self
     }

crates/ui2/src/components/button/icon_button.rs πŸ”—

@@ -1,7 +1,7 @@
 use gpui::{Action, AnyView};
 
 use crate::prelude::*;
-use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Icon, IconElement, IconSize};
+use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconElement, IconSize};
 
 #[derive(IntoElement)]
 pub struct IconButton {
@@ -65,12 +65,12 @@ impl ButtonCommon for IconButton {
         self.base.id()
     }
 
-    fn style(mut self, style: ButtonStyle2) -> Self {
+    fn style(mut self, style: ButtonStyle) -> Self {
         self.base = self.base.style(style);
         self
     }
 
-    fn size(mut self, size: ButtonSize2) -> Self {
+    fn size(mut self, size: ButtonSize) -> Self {
         self.base = self.base.size(size);
         self
     }

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,21 +43,19 @@ 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
     }
+}
 
-    pub fn selected(mut self, selected: bool) -> Self {
+impl Selectable for ListHeader {
+    fn selected(mut self, selected: bool) -> Self {
         self.selected = selected;
         self
     }
@@ -73,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()
@@ -118,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 πŸ”—

@@ -83,11 +83,6 @@ impl ListItem {
         self
     }
 
-    pub fn selected(mut self, selected: bool) -> Self {
-        self.selected = selected;
-        self
-    }
-
     pub fn left_child(mut self, left_content: impl IntoElement) -> Self {
         self.left_slot = Some(left_content.into_any_element());
         self
@@ -109,6 +104,13 @@ impl ListItem {
     }
 }
 
+impl Selectable for ListItem {
+    fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+}
+
 impl ParentElement for ListItem {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
         &mut self.children

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 πŸ”—

@@ -2,7 +2,7 @@ use gpui::{Div, Render};
 use story::Story;
 
 use crate::prelude::*;
-use crate::{Button, ButtonStyle2};
+use crate::{Button, ButtonStyle};
 
 pub struct ButtonStory;
 
@@ -14,9 +14,13 @@ 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("With `label_color`"))
+            .child(Button::new("filled_with_label_color", "Click me").color(Color::Created))
             .child(Story::label("Default (Subtle)"))
-            .child(Button::new("default_subtle", "Click me").style(ButtonStyle2::Subtle))
+            .child(Button::new("default_subtle", "Click me").style(ButtonStyle::Subtle))
             .child(Story::label("Default (Transparent)"))
-            .child(Button::new("default_transparent", "Click me").style(ButtonStyle2::Transparent))
+            .child(Button::new("default_transparent", "Click me").style(ButtonStyle::Transparent))
     }
 }

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/slot.rs πŸ”—

@@ -1,12 +0,0 @@
-use gpui::{ImageSource, SharedString};
-
-use crate::Icon;
-
-/// A slot utility that provides a way to to pass either
-/// an icon or an image to a component.
-#[derive(Debug, Clone)]
-pub enum GraphicSlot {
-    Icon(Icon),
-    Avatar(ImageSource),
-    PublicActor(SharedString),
-}

crates/ui2/src/ui2.rs πŸ”—

@@ -18,7 +18,6 @@ mod disableable;
 mod fixed;
 pub mod prelude;
 mod selectable;
-mod slot;
 mod styled_ext;
 mod styles;
 pub mod utils;
@@ -29,6 +28,5 @@ pub use disableable::*;
 pub use fixed::*;
 pub use prelude::*;
 pub use selectable::*;
-pub use slot::*;
 pub use styled_ext::*;
 pub use styles::*;

crates/welcome2/src/base_keymap_picker.rs πŸ”—

@@ -1,14 +1,14 @@
 use super::base_keymap_setting::BaseKeymap;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Task,
-    View, ViewContext, VisualContext, WeakView,
+    actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, Task, View,
+    ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use project::Fs;
 use settings::{update_settings_file, Settings};
 use std::sync::Arc;
-use ui::ListItem;
+use ui::{prelude::*, ListItem};
 use util::ResultExt;
 use workspace::{ui::HighlightedLabel, Workspace};
 

crates/workspace2/Cargo.toml πŸ”—

@@ -10,29 +10,29 @@ 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" }
+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" }
 
@@ -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/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>,
@@ -759,300 +759,310 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
     }
 }
 
-// #[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/pane.rs πŸ”—

@@ -12,9 +12,9 @@ use gpui::{
     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,
@@ -457,6 +457,28 @@ 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());
     }
@@ -1483,12 +1505,20 @@ impl Pane {
                             .child(
                                 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()),
                                 ),
                             ),
@@ -1935,18 +1965,14 @@ impl Render for Pane {
         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_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: &mut Pane, _: &SplitDown, cx| {
-                pane.split(SplitDirection::Down, 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)))
             //     cx.add_action(Pane::toggle_zoom);
             //     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
             //         pane.activate_item(action.0, true, true, cx);

crates/workspace2/src/pane_group.rs πŸ”—

@@ -1,7 +1,7 @@
 use crate::{AppState, FollowerState, Pane, Workspace};
 use anyhow::{anyhow, bail, Result};
 use collections::HashMap;
-use db2::sqlez::{
+use db::sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
     statement::Statement,
 };
@@ -9,7 +9,7 @@ use gpui::{
     point, size, AnyWeakView, Bounds, Div, 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::*;

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/workspace2/src/status_bar.rs πŸ”—

@@ -52,22 +52,13 @@ 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")),
-                            ),
+                        h_stack().gap_1().child(
+                            // TODO: Language picker
+                            div()
+                                .border()
+                                .border_color(gpui::red())
+                                .child(Button::new("status_buffer_language", "Rust")),
+                        ),
                     )
                     .child(
                         h_stack()
@@ -133,7 +124,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/workspace2.rs πŸ”—

@@ -16,7 +16,7 @@ mod workspace_settings;
 
 use anyhow::{anyhow, Context as _, Result};
 use async_trait::async_trait;
-use client2::{
+use client::{
     proto::{self, PeerId},
     Client, TypedEnvelope, User, UserStore,
 };
@@ -37,7 +37,7 @@ use gpui::{
 };
 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 +49,9 @@ 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 status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use std::{
@@ -62,7 +62,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 util::ResultExt;
@@ -301,7 +301,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 fs: Arc<dyn fs::Fs>,
     pub call_factory: CallFactory,
     pub build_window_options:
         fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
@@ -312,7 +312,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)]
@@ -388,22 +388,22 @@ 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 {
@@ -567,29 +567,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)
@@ -597,7 +597,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()))
                 }),
 
@@ -1450,7 +1450,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)));
                                 })
                             }
                         })
@@ -1812,8 +1812,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
     }
@@ -1988,7 +1987,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
@@ -2013,7 +2012,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
@@ -2592,8 +2591,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>) {
@@ -3673,7 +3671,7 @@ fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncA
 
     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.")
@@ -4554,960 +4552,950 @@ fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
     Some(size((width as f64).into(), (height as f64).into()))
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use crate::{
-//         dock::test::TestPanel,
-//         item::test::{TestItem, TestItemEvent, TestProjectItem},
-//     };
-//     use fs::FakeFs;
-//     use gpui::{executor::Deterministic, test::EmptyView, TestAppContext};
-//     use project::{Project, ProjectEntryId};
-//     use serde_json::json;
-//     use settings::SettingsStore;
-//     use std::{cell::RefCell, rc::Rc};
-
-//     #[gpui::test]
-//     async fn test_tab_disambiguation(cx: &mut TestAppContext) {
-//         init_test(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         let project = Project::test(fs, [], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-
-//         // Adding an item with no ambiguity renders the tab without detail.
-//         let item1 = window.build_view(cx, |_| {
-//             let mut item = TestItem::new();
-//             item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
-//             item
-//         });
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.add_item(Box::new(item1.clone()), cx);
-//         });
-//         item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
-
-//         // Adding an item that creates ambiguity increases the level of detail on
-//         // both tabs.
-//         let item2 = window.build_view(cx, |_| {
-//             let mut item = TestItem::new();
-//             item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
-//             item
-//         });
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.add_item(Box::new(item2.clone()), cx);
-//         });
-//         item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
-//         item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
-
-//         // Adding an item that creates ambiguity increases the level of detail only
-//         // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
-//         // we stop at the highest detail available.
-//         let item3 = window.build_view(cx, |_| {
-//             let mut item = TestItem::new();
-//             item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
-//             item
-//         });
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.add_item(Box::new(item3.clone()), cx);
-//         });
-//         item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
-//         item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
-//         item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
-//     }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::item::{
+        test::{TestItem, TestProjectItem},
+        ItemEvent,
+    };
+    use fs::FakeFs;
+    use gpui::TestAppContext;
+    use project::{Project, ProjectEntryId};
+    use serde_json::json;
+    use settings::SettingsStore;
+    use std::{cell::RefCell, rc::Rc};
+
+    #[gpui::test]
+    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        let project = Project::test(fs, [], cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
+
+        // Adding an item with no ambiguity renders the tab without detail.
+        let item1 = cx.build_view(|cx| {
+            let mut item = TestItem::new(cx);
+            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
+            item
+        });
+        workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(item1.clone()), cx);
+        });
+        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
+
+        // Adding an item that creates ambiguity increases the level of detail on
+        // both tabs.
+        let item2 = cx.build_view(|cx| {
+            let mut item = TestItem::new(cx);
+            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
+            item
+        });
+        workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(item2.clone()), cx);
+        });
+        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
+        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
+
+        // Adding an item that creates ambiguity increases the level of detail only
+        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
+        // we stop at the highest detail available.
+        let item3 = cx.build_view(|cx| {
+            let mut item = TestItem::new(cx);
+            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
+            item
+        });
+        workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(item3.clone()), cx);
+        });
+        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
+        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
+        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
+    }
 
-//     #[gpui::test]
-//     async fn test_tracking_active_path(cx: &mut TestAppContext) {
-//         init_test(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         fs.insert_tree(
-//             "/root1",
-//             json!({
-//                 "one.txt": "",
-//                 "two.txt": "",
-//             }),
-//         )
-//         .await;
-//         fs.insert_tree(
-//             "/root2",
-//             json!({
-//                 "three.txt": "",
-//             }),
-//         )
-//         .await;
+    #[gpui::test]
+    async fn test_tracking_active_path(cx: &mut TestAppContext) {
+        init_test(cx);
 
-//         let project = Project::test(fs, ["root1".as_ref()], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
-//         let worktree_id = project.read_with(cx, |project, cx| {
-//             project.worktrees().next().unwrap().read(cx).id()
-//         });
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                "one.txt": "",
+                "two.txt": "",
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            "/root2",
+            json!({
+                "three.txt": "",
+            }),
+        )
+        .await;
 
-//         let item1 = window.build_view(cx, |cx| {
-//             TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
-//         });
-//         let item2 = window.build_view(cx, |cx| {
-//             TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
-//         });
+        let project = Project::test(fs, ["root1".as_ref()], cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
+        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
+        let worktree_id = project.read_with(cx, |project, cx| {
+            project.worktrees().next().unwrap().read(cx).id()
+        });
 
-//         // Add an item to an empty pane
-//         workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
-//         project.read_with(cx, |project, cx| {
-//             assert_eq!(
-//                 project.active_entry(),
-//                 project
-//                     .entry_for_path(&(worktree_id, "one.txt").into(), cx)
-//                     .map(|e| e.id)
-//             );
-//         });
-//         assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β€” root1"));
-
-//         // Add a second item to a non-empty pane
-//         workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
-//         assert_eq!(window.current_title(cx).as_deref(), Some("two.txt β€” root1"));
-//         project.read_with(cx, |project, cx| {
-//             assert_eq!(
-//                 project.active_entry(),
-//                 project
-//                     .entry_for_path(&(worktree_id, "two.txt").into(), cx)
-//                     .map(|e| e.id)
-//             );
-//         });
+        let item1 = cx.build_view(|cx| {
+            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
+        });
+        let item2 = cx.build_view(|cx| {
+            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
+        });
 
-//         // Close the active item
-//         pane.update(cx, |pane, cx| {
-//             pane.close_active_item(&Default::default(), cx).unwrap()
-//         })
-//         .await
-//         .unwrap();
-//         assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β€” root1"));
-//         project.read_with(cx, |project, cx| {
-//             assert_eq!(
-//                 project.active_entry(),
-//                 project
-//                     .entry_for_path(&(worktree_id, "one.txt").into(), cx)
-//                     .map(|e| e.id)
-//             );
-//         });
+        // Add an item to an empty pane
+        workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
+        project.read_with(cx, |project, cx| {
+            assert_eq!(
+                project.active_entry(),
+                project
+                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
+                    .map(|e| e.id)
+            );
+        });
+        assert_eq!(cx.window_title().as_deref(), Some("one.txt β€” root1"));
+
+        // Add a second item to a non-empty pane
+        workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
+        assert_eq!(cx.window_title().as_deref(), Some("two.txt β€” root1"));
+        project.read_with(cx, |project, cx| {
+            assert_eq!(
+                project.active_entry(),
+                project
+                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
+                    .map(|e| e.id)
+            );
+        });
 
-//         // Add a project folder
-//         project
-//             .update(cx, |project, cx| {
-//                 project.find_or_create_local_worktree("/root2", true, cx)
-//             })
-//             .await
-//             .unwrap();
-//         assert_eq!(
-//             window.current_title(cx).as_deref(),
-//             Some("one.txt β€” root1, root2")
-//         );
-
-//         // Remove a project folder
-//         project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
-//         assert_eq!(window.current_title(cx).as_deref(), Some("one.txt β€” root2"));
-//     }
+        // Close the active item
+        pane.update(cx, |pane, cx| {
+            pane.close_active_item(&Default::default(), cx).unwrap()
+        })
+        .await
+        .unwrap();
+        assert_eq!(cx.window_title().as_deref(), Some("one.txt β€” root1"));
+        project.read_with(cx, |project, cx| {
+            assert_eq!(
+                project.active_entry(),
+                project
+                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
+                    .map(|e| e.id)
+            );
+        });
 
-//     #[gpui::test]
-//     async fn test_close_window(cx: &mut TestAppContext) {
-//         init_test(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         fs.insert_tree("/root", json!({ "one": "" })).await;
-
-//         let project = Project::test(fs, ["root".as_ref()], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-
-//         // When there are no dirty items, there's nothing to do.
-//         let item1 = window.build_view(cx, |_| TestItem::new());
-//         workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
-//         let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
-//         assert!(task.await.unwrap());
-
-//         // When there are dirty untitled items, prompt to save each one. If the user
-//         // cancels any prompt, then abort.
-//         let item2 = window.build_view(cx, |_| TestItem::new().with_dirty(true));
-//         let item3 = window.build_view(cx, |cx| {
-//             TestItem::new()
-//                 .with_dirty(true)
-//                 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
-//         });
-//         workspace.update(cx, |w, cx| {
-//             w.add_item(Box::new(item2.clone()), cx);
-//             w.add_item(Box::new(item3.clone()), cx);
-//         });
-//         let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
-//         cx.foreground().run_until_parked();
-//         window.simulate_prompt_answer(2, cx); // cancel save all
-//         cx.foreground().run_until_parked();
-//         window.simulate_prompt_answer(2, cx); // cancel save all
-//         cx.foreground().run_until_parked();
-//         assert!(!window.has_pending_prompt(cx));
-//         assert!(!task.await.unwrap());
-//     }
+        // Add a project folder
+        project
+            .update(cx, |project, cx| {
+                project.find_or_create_local_worktree("/root2", true, cx)
+            })
+            .await
+            .unwrap();
+        assert_eq!(cx.window_title().as_deref(), Some("one.txt β€” root1, root2"));
 
-//     #[gpui::test]
-//     async fn test_close_pane_items(cx: &mut TestAppContext) {
-//         init_test(cx);
+        // Remove a project folder
+        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
+        assert_eq!(cx.window_title().as_deref(), Some("one.txt β€” root2"));
+    }
 
-//         let fs = FakeFs::new(cx.background());
+    #[gpui::test]
+    async fn test_close_window(cx: &mut TestAppContext) {
+        init_test(cx);
 
-//         let project = Project::test(fs, None, cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-//         let workspace = window.root(cx);
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree("/root", json!({ "one": "" })).await;
 
-//         let item1 = window.build_view(cx, |cx| {
-//             TestItem::new()
-//                 .with_dirty(true)
-//                 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
-//         });
-//         let item2 = window.build_view(cx, |cx| {
-//             TestItem::new()
-//                 .with_dirty(true)
-//                 .with_conflict(true)
-//                 .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
-//         });
-//         let item3 = window.build_view(cx, |cx| {
-//             TestItem::new()
-//                 .with_dirty(true)
-//                 .with_conflict(true)
-//                 .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
-//         });
-//         let item4 = window.build_view(cx, |cx| {
-//             TestItem::new()
-//                 .with_dirty(true)
-//                 .with_project_items(&[TestProjectItem::new_untitled(cx)])
-//         });
-//         let pane = workspace.update(cx, |workspace, cx| {
-//             workspace.add_item(Box::new(item1.clone()), cx);
-//             workspace.add_item(Box::new(item2.clone()), cx);
-//             workspace.add_item(Box::new(item3.clone()), cx);
-//             workspace.add_item(Box::new(item4.clone()), cx);
-//             workspace.active_pane().clone()
-//         });
+        let project = Project::test(fs, ["root".as_ref()], cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
 
-//         let close_items = pane.update(cx, |pane, cx| {
-//             pane.activate_item(1, true, true, cx);
-//             assert_eq!(pane.active_item().unwrap().id(), item2.id());
-//             let item1_id = item1.id();
-//             let item3_id = item3.id();
-//             let item4_id = item4.id();
-//             pane.close_items(cx, SaveIntent::Close, move |id| {
-//                 [item1_id, item3_id, item4_id].contains(&id)
-//             })
-//         });
-//         cx.foreground().run_until_parked();
-
-//         assert!(window.has_pending_prompt(cx));
-//         // Ignore "Save all" prompt
-//         window.simulate_prompt_answer(2, cx);
-//         cx.foreground().run_until_parked();
-//         // There's a prompt to save item 1.
-//         pane.read_with(cx, |pane, _| {
-//             assert_eq!(pane.items_len(), 4);
-//             assert_eq!(pane.active_item().unwrap().id(), item1.id());
-//         });
-//         // Confirm saving item 1.
-//         window.simulate_prompt_answer(0, cx);
-//         cx.foreground().run_until_parked();
-
-//         // Item 1 is saved. There's a prompt to save item 3.
-//         pane.read_with(cx, |pane, cx| {
-//             assert_eq!(item1.read(cx).save_count, 1);
-//             assert_eq!(item1.read(cx).save_as_count, 0);
-//             assert_eq!(item1.read(cx).reload_count, 0);
-//             assert_eq!(pane.items_len(), 3);
-//             assert_eq!(pane.active_item().unwrap().id(), item3.id());
-//         });
-//         assert!(window.has_pending_prompt(cx));
-
-//         // Cancel saving item 3.
-//         window.simulate_prompt_answer(1, cx);
-//         cx.foreground().run_until_parked();
-
-//         // Item 3 is reloaded. There's a prompt to save item 4.
-//         pane.read_with(cx, |pane, cx| {
-//             assert_eq!(item3.read(cx).save_count, 0);
-//             assert_eq!(item3.read(cx).save_as_count, 0);
-//             assert_eq!(item3.read(cx).reload_count, 1);
-//             assert_eq!(pane.items_len(), 2);
-//             assert_eq!(pane.active_item().unwrap().id(), item4.id());
-//         });
-//         assert!(window.has_pending_prompt(cx));
-
-//         // Confirm saving item 4.
-//         window.simulate_prompt_answer(0, cx);
-//         cx.foreground().run_until_parked();
-
-//         // There's a prompt for a path for item 4.
-//         cx.simulate_new_path_selection(|_| Some(Default::default()));
-//         close_items.await.unwrap();
-
-//         // The requested items are closed.
-//         pane.read_with(cx, |pane, cx| {
-//             assert_eq!(item4.read(cx).save_count, 0);
-//             assert_eq!(item4.read(cx).save_as_count, 1);
-//             assert_eq!(item4.read(cx).reload_count, 0);
-//             assert_eq!(pane.items_len(), 1);
-//             assert_eq!(pane.active_item().unwrap().id(), item2.id());
-//         });
-//     }
+        // When there are no dirty items, there's nothing to do.
+        let item1 = cx.build_view(|cx| TestItem::new(cx));
+        workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
+        let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
+        assert!(task.await.unwrap());
 
-//     #[gpui::test]
-//     async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
-//         init_test(cx);
-
-//         let fs = FakeFs::new(cx.background());
-
-//         let project = Project::test(fs, [], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-//         let workspace = window.root(cx);
-
-//         // Create several workspace items with single project entries, and two
-//         // workspace items with multiple project entries.
-//         let single_entry_items = (0..=4)
-//             .map(|project_entry_id| {
-//                 window.build_view(cx, |cx| {
-//                     TestItem::new()
-//                         .with_dirty(true)
-//                         .with_project_items(&[TestProjectItem::new(
-//                             project_entry_id,
-//                             &format!("{project_entry_id}.txt"),
-//                             cx,
-//                         )])
-//                 })
-//             })
-//             .collect::<Vec<_>>();
-//         let item_2_3 = window.build_view(cx, |cx| {
-//             TestItem::new()
-//                 .with_dirty(true)
-//                 .with_singleton(false)
-//                 .with_project_items(&[
-//                     single_entry_items[2].read(cx).project_items[0].clone(),
-//                     single_entry_items[3].read(cx).project_items[0].clone(),
-//                 ])
-//         });
-//         let item_3_4 = window.build_view(cx, |cx| {
-//             TestItem::new()
-//                 .with_dirty(true)
-//                 .with_singleton(false)
-//                 .with_project_items(&[
-//                     single_entry_items[3].read(cx).project_items[0].clone(),
-//                     single_entry_items[4].read(cx).project_items[0].clone(),
-//                 ])
-//         });
+        // When there are dirty untitled items, prompt to save each one. If the user
+        // cancels any prompt, then abort.
+        let item2 = cx.build_view(|cx| TestItem::new(cx).with_dirty(true));
+        let item3 = cx.build_view(|cx| {
+            TestItem::new(cx)
+                .with_dirty(true)
+                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
+        });
+        workspace.update(cx, |w, cx| {
+            w.add_item(Box::new(item2.clone()), cx);
+            w.add_item(Box::new(item3.clone()), cx);
+        });
+        let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
+        cx.executor().run_until_parked();
+        cx.simulate_prompt_answer(2); // cancel save all
+        cx.executor().run_until_parked();
+        cx.simulate_prompt_answer(2); // cancel save all
+        cx.executor().run_until_parked();
+        assert!(!cx.has_pending_prompt());
+        assert!(!task.await.unwrap());
+    }
 
-//         // Create two panes that contain the following project entries:
-//         //   left pane:
-//         //     multi-entry items:   (2, 3)
-//         //     single-entry items:  0, 1, 2, 3, 4
-//         //   right pane:
-//         //     single-entry items:  1
-//         //     multi-entry items:   (3, 4)
-//         let left_pane = workspace.update(cx, |workspace, cx| {
-//             let left_pane = workspace.active_pane().clone();
-//             workspace.add_item(Box::new(item_2_3.clone()), cx);
-//             for item in single_entry_items {
-//                 workspace.add_item(Box::new(item), cx);
-//             }
-//             left_pane.update(cx, |pane, cx| {
-//                 pane.activate_item(2, true, true, cx);
-//             });
+    #[gpui::test]
+    async fn test_close_pane_items(cx: &mut TestAppContext) {
+        init_test(cx);
 
-//             workspace
-//                 .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
-//                 .unwrap();
+        let fs = FakeFs::new(cx.executor());
 
-//             left_pane
-//         });
+        let project = Project::test(fs, None, cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
 
-//         //Need to cause an effect flush in order to respect new focus
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.add_item(Box::new(item_3_4.clone()), cx);
-//             cx.focus(&left_pane);
-//         });
+        let item1 = cx.build_view(|cx| {
+            TestItem::new(cx)
+                .with_dirty(true)
+                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
+        });
+        let item2 = cx.build_view(|cx| {
+            TestItem::new(cx)
+                .with_dirty(true)
+                .with_conflict(true)
+                .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
+        });
+        let item3 = cx.build_view(|cx| {
+            TestItem::new(cx)
+                .with_dirty(true)
+                .with_conflict(true)
+                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
+        });
+        let item4 = cx.build_view(|cx| {
+            TestItem::new(cx)
+                .with_dirty(true)
+                .with_project_items(&[TestProjectItem::new_untitled(cx)])
+        });
+        let pane = workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(item1.clone()), cx);
+            workspace.add_item(Box::new(item2.clone()), cx);
+            workspace.add_item(Box::new(item3.clone()), cx);
+            workspace.add_item(Box::new(item4.clone()), cx);
+            workspace.active_pane().clone()
+        });
 
-//         // When closing all of the items in the left pane, we should be prompted twice:
-//         // once for project entry 0, and once for project entry 2. After those two
-//         // prompts, the task should complete.
+        let close_items = pane.update(cx, |pane, cx| {
+            pane.activate_item(1, true, true, cx);
+            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
+            let item1_id = item1.item_id();
+            let item3_id = item3.item_id();
+            let item4_id = item4.item_id();
+            pane.close_items(cx, SaveIntent::Close, move |id| {
+                [item1_id, item3_id, item4_id].contains(&id)
+            })
+        });
+        cx.executor().run_until_parked();
+
+        assert!(cx.has_pending_prompt());
+        // Ignore "Save all" prompt
+        cx.simulate_prompt_answer(2);
+        cx.executor().run_until_parked();
+        // There's a prompt to save item 1.
+        pane.update(cx, |pane, _| {
+            assert_eq!(pane.items_len(), 4);
+            assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
+        });
+        // Confirm saving item 1.
+        cx.simulate_prompt_answer(0);
+        cx.executor().run_until_parked();
+
+        // Item 1 is saved. There's a prompt to save item 3.
+        pane.update(cx, |pane, cx| {
+            assert_eq!(item1.read(cx).save_count, 1);
+            assert_eq!(item1.read(cx).save_as_count, 0);
+            assert_eq!(item1.read(cx).reload_count, 0);
+            assert_eq!(pane.items_len(), 3);
+            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
+        });
+        assert!(cx.has_pending_prompt());
+
+        // Cancel saving item 3.
+        cx.simulate_prompt_answer(1);
+        cx.executor().run_until_parked();
+
+        // Item 3 is reloaded. There's a prompt to save item 4.
+        pane.update(cx, |pane, cx| {
+            assert_eq!(item3.read(cx).save_count, 0);
+            assert_eq!(item3.read(cx).save_as_count, 0);
+            assert_eq!(item3.read(cx).reload_count, 1);
+            assert_eq!(pane.items_len(), 2);
+            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
+        });
+        assert!(cx.has_pending_prompt());
+
+        // Confirm saving item 4.
+        cx.simulate_prompt_answer(0);
+        cx.executor().run_until_parked();
+
+        // There's a prompt for a path for item 4.
+        cx.simulate_new_path_selection(|_| Some(Default::default()));
+        close_items.await.unwrap();
+
+        // The requested items are closed.
+        pane.update(cx, |pane, cx| {
+            assert_eq!(item4.read(cx).save_count, 0);
+            assert_eq!(item4.read(cx).save_as_count, 1);
+            assert_eq!(item4.read(cx).reload_count, 0);
+            assert_eq!(pane.items_len(), 1);
+            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
+        });
+    }
 
-//         let close = left_pane.update(cx, |pane, cx| {
-//             pane.close_items(cx, SaveIntent::Close, move |_| true)
-//         });
-//         cx.foreground().run_until_parked();
-//         // Discard "Save all" prompt
-//         window.simulate_prompt_answer(2, cx);
-
-//         cx.foreground().run_until_parked();
-//         left_pane.read_with(cx, |pane, cx| {
-//             assert_eq!(
-//                 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
-//                 &[ProjectEntryId::from_proto(0)]
-//             );
-//         });
-//         window.simulate_prompt_answer(0, cx);
+    #[gpui::test]
+    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        let project = Project::test(fs, [], cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+
+        // Create several workspace items with single project entries, and two
+        // workspace items with multiple project entries.
+        let single_entry_items = (0..=4)
+            .map(|project_entry_id| {
+                cx.build_view(|cx| {
+                    TestItem::new(cx)
+                        .with_dirty(true)
+                        .with_project_items(&[TestProjectItem::new(
+                            project_entry_id,
+                            &format!("{project_entry_id}.txt"),
+                            cx,
+                        )])
+                })
+            })
+            .collect::<Vec<_>>();
+        let item_2_3 = cx.build_view(|cx| {
+            TestItem::new(cx)
+                .with_dirty(true)
+                .with_singleton(false)
+                .with_project_items(&[
+                    single_entry_items[2].read(cx).project_items[0].clone(),
+                    single_entry_items[3].read(cx).project_items[0].clone(),
+                ])
+        });
+        let item_3_4 = cx.build_view(|cx| {
+            TestItem::new(cx)
+                .with_dirty(true)
+                .with_singleton(false)
+                .with_project_items(&[
+                    single_entry_items[3].read(cx).project_items[0].clone(),
+                    single_entry_items[4].read(cx).project_items[0].clone(),
+                ])
+        });
 
-//         cx.foreground().run_until_parked();
-//         left_pane.read_with(cx, |pane, cx| {
-//             assert_eq!(
-//                 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
-//                 &[ProjectEntryId::from_proto(2)]
-//             );
-//         });
-//         window.simulate_prompt_answer(0, cx);
+        // Create two panes that contain the following project entries:
+        //   left pane:
+        //     multi-entry items:   (2, 3)
+        //     single-entry items:  0, 1, 2, 3, 4
+        //   right pane:
+        //     single-entry items:  1
+        //     multi-entry items:   (3, 4)
+        let left_pane = workspace.update(cx, |workspace, cx| {
+            let left_pane = workspace.active_pane().clone();
+            workspace.add_item(Box::new(item_2_3.clone()), cx);
+            for item in single_entry_items {
+                workspace.add_item(Box::new(item), cx);
+            }
+            left_pane.update(cx, |pane, cx| {
+                pane.activate_item(2, true, true, cx);
+            });
 
-//         cx.foreground().run_until_parked();
-//         close.await.unwrap();
-//         left_pane.read_with(cx, |pane, _| {
-//             assert_eq!(pane.items_len(), 0);
-//         });
-//     }
+            let right_pane = workspace
+                .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
+                .unwrap();
 
-//     #[gpui::test]
-//     async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
-//         init_test(cx);
+            right_pane.update(cx, |pane, cx| {
+                pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
+            });
 
-//         let fs = FakeFs::new(cx.background());
+            left_pane
+        });
 
-//         let project = Project::test(fs, [], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-//         let workspace = window.root(cx);
-//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+        cx.focus_view(&left_pane);
 
-//         let item = window.build_view(cx, |cx| {
-//             TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
-//         });
-//         let item_id = item.id();
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.add_item(Box::new(item.clone()), cx);
-//         });
+        // When closing all of the items in the left pane, we should be prompted twice:
+        // once for project entry 0, and once for project entry 2. Project entries 1,
+        // 3, and 4 are all still open in the other paten. After those two
+        // prompts, the task should complete.
 
-//         // Autosave on window change.
-//         item.update(cx, |item, cx| {
-//             cx.update_global(|settings: &mut SettingsStore, cx| {
-//                 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-//                     settings.autosave = Some(AutosaveSetting::OnWindowChange);
-//                 })
-//             });
-//             item.is_dirty = true;
-//         });
+        let close = left_pane.update(cx, |pane, cx| {
+            pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
+        });
+        cx.executor().run_until_parked();
 
-//         // Deactivating the window saves the file.
-//         window.simulate_deactivation(cx);
-//         deterministic.run_until_parked();
-//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
-
-//         // Autosave on focus change.
-//         item.update(cx, |item, cx| {
-//             cx.focus_self();
-//             cx.update_global(|settings: &mut SettingsStore, cx| {
-//                 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-//                     settings.autosave = Some(AutosaveSetting::OnFocusChange);
-//                 })
-//             });
-//             item.is_dirty = true;
-//         });
+        // Discard "Save all" prompt
+        cx.simulate_prompt_answer(2);
 
-//         // Blurring the item saves the file.
-//         item.update(cx, |_, cx| cx.blur());
-//         deterministic.run_until_parked();
-//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
+        cx.executor().run_until_parked();
+        left_pane.update(cx, |pane, cx| {
+            assert_eq!(
+                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
+                &[ProjectEntryId::from_proto(0)]
+            );
+        });
+        cx.simulate_prompt_answer(0);
 
-//         // Deactivating the window still saves the file.
-//         window.simulate_activation(cx);
-//         item.update(cx, |item, cx| {
-//             cx.focus_self();
-//             item.is_dirty = true;
-//         });
-//         window.simulate_deactivation(cx);
+        cx.executor().run_until_parked();
+        left_pane.update(cx, |pane, cx| {
+            assert_eq!(
+                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
+                &[ProjectEntryId::from_proto(2)]
+            );
+        });
+        cx.simulate_prompt_answer(0);
 
-//         deterministic.run_until_parked();
-//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
+        cx.executor().run_until_parked();
+        close.await.unwrap();
+        left_pane.update(cx, |pane, _| {
+            assert_eq!(pane.items_len(), 0);
+        });
+    }
 
-//         // Autosave after delay.
-//         item.update(cx, |item, cx| {
-//             cx.update_global(|settings: &mut SettingsStore, cx| {
-//                 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-//                     settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
-//                 })
-//             });
-//             item.is_dirty = true;
-//             cx.emit(TestItemEvent::Edit);
-//         });
+    #[gpui::test]
+    async fn test_autosave(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
 
-//         // Delay hasn't fully expired, so the file is still dirty and unsaved.
-//         deterministic.advance_clock(Duration::from_millis(250));
-//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
+        let fs = FakeFs::new(cx.executor());
+        let project = Project::test(fs, [], cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
 
-//         // After delay expires, the file is saved.
-//         deterministic.advance_clock(Duration::from_millis(250));
-//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
+        let item = cx.build_view(|cx| {
+            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
+        });
+        let item_id = item.entity_id();
+        workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(item.clone()), cx);
+        });
 
-//         // Autosave on focus change, ensuring closing the tab counts as such.
-//         item.update(cx, |item, cx| {
-//             cx.update_global(|settings: &mut SettingsStore, cx| {
-//                 settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-//                     settings.autosave = Some(AutosaveSetting::OnFocusChange);
-//                 })
-//             });
-//             item.is_dirty = true;
-//         });
+        // Autosave on window change.
+        item.update(cx, |item, cx| {
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
+                })
+            });
+            item.is_dirty = true;
+        });
 
-//         pane.update(cx, |pane, cx| {
-//             pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
-//         })
-//         .await
-//         .unwrap();
-//         assert!(!window.has_pending_prompt(cx));
-//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
-
-//         // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.add_item(Box::new(item.clone()), cx);
-//         });
-//         item.update(cx, |item, cx| {
-//             item.project_items[0].update(cx, |item, _| {
-//                 item.entry_id = None;
-//             });
-//             item.is_dirty = true;
-//             cx.blur();
-//         });
-//         deterministic.run_until_parked();
-//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
+        // Deactivating the window saves the file.
+        cx.simulate_deactivation();
+        cx.executor().run_until_parked();
+        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
+
+        // Autosave on focus change.
+        item.update(cx, |item, cx| {
+            cx.focus_self();
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
+                })
+            });
+            item.is_dirty = true;
+        });
 
-//         // Ensure autosave is prevented for deleted files also when closing the buffer.
-//         let _close_items = pane.update(cx, |pane, cx| {
-//             pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
-//         });
-//         deterministic.run_until_parked();
-//         assert!(window.has_pending_prompt(cx));
-//         item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
-//     }
+        // Blurring the item saves the file.
+        item.update(cx, |_, cx| cx.blur());
+        cx.executor().run_until_parked();
+        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
 
-//     #[gpui::test]
-//     async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
-//         init_test(cx);
+        // Deactivating the window still saves the file.
+        cx.simulate_activation();
+        item.update(cx, |item, cx| {
+            cx.focus_self();
+            item.is_dirty = true;
+        });
+        cx.simulate_deactivation();
 
-//         let fs = FakeFs::new(cx.background());
+        cx.executor().run_until_parked();
+        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
 
-//         let project = Project::test(fs, [], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-//         let workspace = window.root(cx);
+        // Autosave after delay.
+        item.update(cx, |item, cx| {
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
+                })
+            });
+            item.is_dirty = true;
+            cx.emit(ItemEvent::Edit);
+        });
 
-//         let item = window.build_view(cx, |cx| {
-//             TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
-//         });
-//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
-//         let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
-//         let toolbar_notify_count = Rc::new(RefCell::new(0));
-
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.add_item(Box::new(item.clone()), cx);
-//             let toolbar_notification_count = toolbar_notify_count.clone();
-//             cx.observe(&toolbar, move |_, _, _| {
-//                 *toolbar_notification_count.borrow_mut() += 1
-//             })
-//             .detach();
-//         });
+        // Delay hasn't fully expired, so the file is still dirty and unsaved.
+        cx.executor().advance_clock(Duration::from_millis(250));
+        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
 
-//         pane.read_with(cx, |pane, _| {
-//             assert!(!pane.can_navigate_backward());
-//             assert!(!pane.can_navigate_forward());
-//         });
+        // After delay expires, the file is saved.
+        cx.executor().advance_clock(Duration::from_millis(250));
+        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
 
-//         item.update(cx, |item, cx| {
-//             item.set_state("one".to_string(), cx);
-//         });
+        // Autosave on focus change, ensuring closing the tab counts as such.
+        item.update(cx, |item, cx| {
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
+                })
+            });
+            item.is_dirty = true;
+        });
 
-//         // Toolbar must be notified to re-render the navigation buttons
-//         assert_eq!(*toolbar_notify_count.borrow(), 1);
+        pane.update(cx, |pane, cx| {
+            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
+        })
+        .await
+        .unwrap();
+        assert!(!cx.has_pending_prompt());
+        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
+
+        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
+        workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(item.clone()), cx);
+        });
+        item.update(cx, |item, cx| {
+            item.project_items[0].update(cx, |item, _| {
+                item.entry_id = None;
+            });
+            item.is_dirty = true;
+            cx.blur();
+        });
+        cx.executor().run_until_parked();
+        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
 
-//         pane.read_with(cx, |pane, _| {
-//             assert!(pane.can_navigate_backward());
-//             assert!(!pane.can_navigate_forward());
-//         });
+        // Ensure autosave is prevented for deleted files also when closing the buffer.
+        let _close_items = pane.update(cx, |pane, cx| {
+            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
+        });
+        cx.executor().run_until_parked();
+        assert!(cx.has_pending_prompt());
+        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
+    }
 
-//         workspace
-//             .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
-//             .await
-//             .unwrap();
+    #[gpui::test]
+    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
 
-//         assert_eq!(*toolbar_notify_count.borrow(), 3);
-//         pane.read_with(cx, |pane, _| {
-//             assert!(!pane.can_navigate_backward());
-//             assert!(pane.can_navigate_forward());
-//         });
-//     }
+        let fs = FakeFs::new(cx.executor());
 
-//     #[gpui::test]
-//     async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
-//         init_test(cx);
-//         let fs = FakeFs::new(cx.background());
+        let project = Project::test(fs, [], cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
 
-//         let project = Project::test(fs, [], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-//         let workspace = window.root(cx);
+        let item = cx.build_view(|cx| {
+            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
+        });
+        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
+        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
+        let toolbar_notify_count = Rc::new(RefCell::new(0));
+
+        workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(item.clone()), cx);
+            let toolbar_notification_count = toolbar_notify_count.clone();
+            cx.observe(&toolbar, move |_, _, _| {
+                *toolbar_notification_count.borrow_mut() += 1
+            })
+            .detach();
+        });
 
-//         let panel = workspace.update(cx, |workspace, cx| {
-//             let panel = cx.build_view(|_| TestPanel::new(DockPosition::Right));
-//             workspace.add_panel(panel.clone(), cx);
+        pane.update(cx, |pane, _| {
+            assert!(!pane.can_navigate_backward());
+            assert!(!pane.can_navigate_forward());
+        });
 
-//             workspace
-//                 .right_dock()
-//                 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
+        item.update(cx, |item, cx| {
+            item.set_state("one".to_string(), cx);
+        });
 
-//             panel
-//         });
+        // Toolbar must be notified to re-render the navigation buttons
+        assert_eq!(*toolbar_notify_count.borrow(), 1);
 
-//         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
-//         pane.update(cx, |pane, cx| {
-//             let item = cx.build_view(|_| TestItem::new());
-//             pane.add_item(Box::new(item), true, true, None, cx);
-//         });
+        pane.update(cx, |pane, _| {
+            assert!(pane.can_navigate_backward());
+            assert!(!pane.can_navigate_forward());
+        });
 
-//         // Transfer focus from center to panel
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.toggle_panel_focus::<TestPanel>(cx);
-//         });
+        workspace
+            .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
+            .await
+            .unwrap();
 
-//         workspace.read_with(cx, |workspace, cx| {
-//             assert!(workspace.right_dock().read(cx).is_open());
-//             assert!(!panel.is_zoomed(cx));
-//             assert!(panel.has_focus(cx));
-//         });
+        assert_eq!(*toolbar_notify_count.borrow(), 2);
+        pane.update(cx, |pane, _| {
+            assert!(!pane.can_navigate_backward());
+            assert!(pane.can_navigate_forward());
+        });
+    }
 
-//         // Transfer focus from panel to center
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.toggle_panel_focus::<TestPanel>(cx);
-//         });
+    //     #[gpui::test]
+    //     async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
+    //         init_test(cx);
+    //         let fs = FakeFs::new(cx.executor());
 
-//         workspace.read_with(cx, |workspace, cx| {
-//             assert!(workspace.right_dock().read(cx).is_open());
-//             assert!(!panel.is_zoomed(cx));
-//             assert!(!panel.has_focus(cx));
-//         });
+    //         let project = Project::test(fs, [], cx).await;
+    //         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+    //         let workspace = window.root(cx);
 
-//         // Close the dock
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.toggle_dock(DockPosition::Right, cx);
-//         });
+    //         let panel = workspace.update(cx, |workspace, cx| {
+    //             let panel = cx.build_view(|_| TestPanel::new(DockPosition::Right));
+    //             workspace.add_panel(panel.clone(), cx);
 
-//         workspace.read_with(cx, |workspace, cx| {
-//             assert!(!workspace.right_dock().read(cx).is_open());
-//             assert!(!panel.is_zoomed(cx));
-//             assert!(!panel.has_focus(cx));
-//         });
+    //             workspace
+    //                 .right_dock()
+    //                 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
 
-//         // Open the dock
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.toggle_dock(DockPosition::Right, cx);
-//         });
+    //             panel
+    //         });
 
-//         workspace.read_with(cx, |workspace, cx| {
-//             assert!(workspace.right_dock().read(cx).is_open());
-//             assert!(!panel.is_zoomed(cx));
-//             assert!(panel.has_focus(cx));
-//         });
+    //         let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
+    //         pane.update(cx, |pane, cx| {
+    //             let item = cx.build_view(|_| TestItem::new(cx));
+    //             pane.add_item(Box::new(item), true, true, None, cx);
+    //         });
 
-//         // Focus and zoom panel
-//         panel.update(cx, |panel, cx| {
-//             cx.focus_self();
-//             panel.set_zoomed(true, cx)
-//         });
+    //         // Transfer focus from center to panel
+    //         workspace.update(cx, |workspace, cx| {
+    //             workspace.toggle_panel_focus::<TestPanel>(cx);
+    //         });
 
-//         workspace.read_with(cx, |workspace, cx| {
-//             assert!(workspace.right_dock().read(cx).is_open());
-//             assert!(panel.is_zoomed(cx));
-//             assert!(panel.has_focus(cx));
-//         });
+    //         workspace.update(cx, |workspace, cx| {
+    //             assert!(workspace.right_dock().read(cx).is_open());
+    //             assert!(!panel.is_zoomed(cx));
+    //             assert!(panel.has_focus(cx));
+    //         });
 
-//         // Transfer focus to the center closes the dock
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.toggle_panel_focus::<TestPanel>(cx);
-//         });
+    //         // Transfer focus from panel to center
+    //         workspace.update(cx, |workspace, cx| {
+    //             workspace.toggle_panel_focus::<TestPanel>(cx);
+    //         });
 
-//         workspace.read_with(cx, |workspace, cx| {
-//             assert!(!workspace.right_dock().read(cx).is_open());
-//             assert!(panel.is_zoomed(cx));
-//             assert!(!panel.has_focus(cx));
-//         });
+    //         workspace.update(cx, |workspace, cx| {
+    //             assert!(workspace.right_dock().read(cx).is_open());
+    //             assert!(!panel.is_zoomed(cx));
+    //             assert!(!panel.has_focus(cx));
+    //         });
 
-//         // Transferring focus back to the panel keeps it zoomed
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.toggle_panel_focus::<TestPanel>(cx);
-//         });
+    //         // Close the dock
+    //         workspace.update(cx, |workspace, cx| {
+    //             workspace.toggle_dock(DockPosition::Right, cx);
+    //         });
 
-//         workspace.read_with(cx, |workspace, cx| {
-//             assert!(workspace.right_dock().read(cx).is_open());
-//             assert!(panel.is_zoomed(cx));
-//             assert!(panel.has_focus(cx));
-//         });
+    //         workspace.update(cx, |workspace, cx| {
+    //             assert!(!workspace.right_dock().read(cx).is_open());
+    //             assert!(!panel.is_zoomed(cx));
+    //             assert!(!panel.has_focus(cx));
+    //         });
 
-//         // Close the dock while it is zoomed
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.toggle_dock(DockPosition::Right, cx)
-//         });
+    //         // Open the dock
+    //         workspace.update(cx, |workspace, cx| {
+    //             workspace.toggle_dock(DockPosition::Right, cx);
+    //         });
 
-//         workspace.read_with(cx, |workspace, cx| {
-//             assert!(!workspace.right_dock().read(cx).is_open());
-//             assert!(panel.is_zoomed(cx));
-//             assert!(workspace.zoomed.is_none());
-//             assert!(!panel.has_focus(cx));
-//         });
+    //         workspace.update(cx, |workspace, cx| {
+    //             assert!(workspace.right_dock().read(cx).is_open());
+    //             assert!(!panel.is_zoomed(cx));
+    //             assert!(panel.has_focus(cx));
+    //         });
 
-//         // Opening the dock, when it's zoomed, retains focus
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.toggle_dock(DockPosition::Right, cx)
-//         });
+    //         // Focus and zoom panel
+    //         panel.update(cx, |panel, cx| {
+    //             cx.focus_self();
+    //             panel.set_zoomed(true, cx)
+    //         });
 
-//         workspace.read_with(cx, |workspace, cx| {
-//             assert!(workspace.right_dock().read(cx).is_open());
-//             assert!(panel.is_zoomed(cx));
-//             assert!(workspace.zoomed.is_some());
-//             assert!(panel.has_focus(cx));
-//         });
+    //         workspace.update(cx, |workspace, cx| {
+    //             assert!(workspace.right_dock().read(cx).is_open());
+    //             assert!(panel.is_zoomed(cx));
+    //             assert!(panel.has_focus(cx));
+    //         });
 
-//         // Unzoom and close the panel, zoom the active pane.
-//         panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.toggle_dock(DockPosition::Right, cx)
-//         });
-//         pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
+    //         // Transfer focus to the center closes the dock
+    //         workspace.update(cx, |workspace, cx| {
+    //             workspace.toggle_panel_focus::<TestPanel>(cx);
+    //         });
 
-//         // Opening a dock unzooms the pane.
-//         workspace.update(cx, |workspace, cx| {
-//             workspace.toggle_dock(DockPosition::Right, cx)
-//         });
-//         workspace.read_with(cx, |workspace, cx| {
-//             let pane = pane.read(cx);
-//             assert!(!pane.is_zoomed());
-//             assert!(!pane.has_focus());
-//             assert!(workspace.right_dock().read(cx).is_open());
-//             assert!(workspace.zoomed.is_none());
-//         });
-//     }
+    //         workspace.update(cx, |workspace, cx| {
+    //             assert!(!workspace.right_dock().read(cx).is_open());
+    //             assert!(panel.is_zoomed(cx));
+    //             assert!(!panel.has_focus(cx));
+    //         });
 
-//     #[gpui::test]
-//     async fn test_panels(cx: &mut gpui::TestAppContext) {
-//         init_test(cx);
-//         let fs = FakeFs::new(cx.background());
-
-//         let project = Project::test(fs, [], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-//         let workspace = window.root(cx);
-
-//         let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
-//             // Add panel_1 on the left, panel_2 on the right.
-//             let panel_1 = cx.build_view(|_| TestPanel::new(DockPosition::Left));
-//             workspace.add_panel(panel_1.clone(), cx);
-//             workspace
-//                 .left_dock()
-//                 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
-//             let panel_2 = cx.build_view(|_| TestPanel::new(DockPosition::Right));
-//             workspace.add_panel(panel_2.clone(), cx);
-//             workspace
-//                 .right_dock()
-//                 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
-
-//             let left_dock = workspace.left_dock();
-//             assert_eq!(
-//                 left_dock.read(cx).visible_panel().unwrap().id(),
-//                 panel_1.id()
-//             );
-//             assert_eq!(
-//                 left_dock.read(cx).active_panel_size(cx).unwrap(),
-//                 panel_1.size(cx)
-//             );
+    //         // Transferring focus back to the panel keeps it zoomed
+    //         workspace.update(cx, |workspace, cx| {
+    //             workspace.toggle_panel_focus::<TestPanel>(cx);
+    //         });
 
-//             left_dock.update(cx, |left_dock, cx| {
-//                 left_dock.resize_active_panel(Some(1337.), cx)
-//             });
-//             assert_eq!(
-//                 workspace
-//                     .right_dock()
-//                     .read(cx)
-//                     .visible_panel()
-//                     .unwrap()
-//                     .id(),
-//                 panel_2.id()
-//             );
+    //         workspace.update(cx, |workspace, cx| {
+    //             assert!(workspace.right_dock().read(cx).is_open());
+    //             assert!(panel.is_zoomed(cx));
+    //             assert!(panel.has_focus(cx));
+    //         });
 
-//             (panel_1, panel_2)
-//         });
+    //         // Close the dock while it is zoomed
+    //         workspace.update(cx, |workspace, cx| {
+    //             workspace.toggle_dock(DockPosition::Right, cx)
+    //         });
 
-//         // Move panel_1 to the right
-//         panel_1.update(cx, |panel_1, cx| {
-//             panel_1.set_position(DockPosition::Right, cx)
-//         });
+    //         workspace.update(cx, |workspace, cx| {
+    //             assert!(!workspace.right_dock().read(cx).is_open());
+    //             assert!(panel.is_zoomed(cx));
+    //             assert!(workspace.zoomed.is_none());
+    //             assert!(!panel.has_focus(cx));
+    //         });
 
-//         workspace.update(cx, |workspace, cx| {
-//             // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
-//             // Since it was the only panel on the left, the left dock should now be closed.
-//             assert!(!workspace.left_dock().read(cx).is_open());
-//             assert!(workspace.left_dock().read(cx).visible_panel().is_none());
-//             let right_dock = workspace.right_dock();
-//             assert_eq!(
-//                 right_dock.read(cx).visible_panel().unwrap().id(),
-//                 panel_1.id()
-//             );
-//             assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
+    //         // Opening the dock, when it's zoomed, retains focus
+    //         workspace.update(cx, |workspace, cx| {
+    //             workspace.toggle_dock(DockPosition::Right, cx)
+    //         });
 
-//             // Now we move panel_2Β to the left
-//             panel_2.set_position(DockPosition::Left, cx);
-//         });
+    //         workspace.update(cx, |workspace, cx| {
+    //             assert!(workspace.right_dock().read(cx).is_open());
+    //             assert!(panel.is_zoomed(cx));
+    //             assert!(workspace.zoomed.is_some());
+    //             assert!(panel.has_focus(cx));
+    //         });
 
-//         workspace.update(cx, |workspace, cx| {
-//             // Since panel_2 was not visible on the right, we don't open the left dock.
-//             assert!(!workspace.left_dock().read(cx).is_open());
-//             // And the right dock is unaffected in it's displaying of panel_1
-//             assert!(workspace.right_dock().read(cx).is_open());
-//             assert_eq!(
-//                 workspace
-//                     .right_dock()
-//                     .read(cx)
-//                     .visible_panel()
-//                     .unwrap()
-//                     .id(),
-//                 panel_1.id()
-//             );
-//         });
+    //         // Unzoom and close the panel, zoom the active pane.
+    //         panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
+    //         workspace.update(cx, |workspace, cx| {
+    //             workspace.toggle_dock(DockPosition::Right, cx)
+    //         });
+    //         pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
 
-//         // Move panel_1 back to the left
-//         panel_1.update(cx, |panel_1, cx| {
-//             panel_1.set_position(DockPosition::Left, cx)
-//         });
+    //         // Opening a dock unzooms the pane.
+    //         workspace.update(cx, |workspace, cx| {
+    //             workspace.toggle_dock(DockPosition::Right, cx)
+    //         });
+    //         workspace.update(cx, |workspace, cx| {
+    //             let pane = pane.read(cx);
+    //             assert!(!pane.is_zoomed());
+    //             assert!(!pane.has_focus());
+    //             assert!(workspace.right_dock().read(cx).is_open());
+    //             assert!(workspace.zoomed.is_none());
+    //         });
+    //     }
 
-//         workspace.update(cx, |workspace, cx| {
-//             // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
-//             let left_dock = workspace.left_dock();
-//             assert!(left_dock.read(cx).is_open());
-//             assert_eq!(
-//                 left_dock.read(cx).visible_panel().unwrap().id(),
-//                 panel_1.id()
-//             );
-//             assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
-//             // And right the dock should be closed as it no longer has any panels.
-//             assert!(!workspace.right_dock().read(cx).is_open());
+    //     #[gpui::test]
+    //     async fn test_panels(cx: &mut gpui::TestAppContext) {
+    //         init_test(cx);
+    //         let fs = FakeFs::new(cx.executor());
+
+    //         let project = Project::test(fs, [], cx).await;
+    //         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+    //         let workspace = window.root(cx);
+
+    //         let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
+    //             // Add panel_1 on the left, panel_2 on the right.
+    //             let panel_1 = cx.build_view(|_| TestPanel::new(DockPosition::Left));
+    //             workspace.add_panel(panel_1.clone(), cx);
+    //             workspace
+    //                 .left_dock()
+    //                 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
+    //             let panel_2 = cx.build_view(|_| TestPanel::new(DockPosition::Right));
+    //             workspace.add_panel(panel_2.clone(), cx);
+    //             workspace
+    //                 .right_dock()
+    //                 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
+
+    //             let left_dock = workspace.left_dock();
+    //             assert_eq!(
+    //                 left_dock.read(cx).visible_panel().unwrap().id(),
+    //                 panel_1.id()
+    //             );
+    //             assert_eq!(
+    //                 left_dock.read(cx).active_panel_size(cx).unwrap(),
+    //                 panel_1.size(cx)
+    //             );
+
+    //             left_dock.update(cx, |left_dock, cx| {
+    //                 left_dock.resize_active_panel(Some(1337.), cx)
+    //             });
+    //             assert_eq!(
+    //                 workspace
+    //                     .right_dock()
+    //                     .read(cx)
+    //                     .visible_panel()
+    //                     .unwrap()
+    //                     .id(),
+    //                 panel_2.id()
+    //             );
+
+    //             (panel_1, panel_2)
+    //         });
 
-//             // Now we move panel_1 to the bottom
-//             panel_1.set_position(DockPosition::Bottom, cx);
-//         });
+    //         // Move panel_1 to the right
+    //         panel_1.update(cx, |panel_1, cx| {
+    //             panel_1.set_position(DockPosition::Right, cx)
+    //         });
 
-//         workspace.update(cx, |workspace, cx| {
-//             // Since panel_1 was visible on the left, we close the left dock.
-//             assert!(!workspace.left_dock().read(cx).is_open());
-//             // The bottom dock is sized based on the panel's default size,
-//             // since the panel orientation changed from vertical to horizontal.
-//             let bottom_dock = workspace.bottom_dock();
-//             assert_eq!(
-//                 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
-//                 panel_1.size(cx),
-//             );
-//             // Close bottom dock and move panel_1 back to the left.
-//             bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
-//             panel_1.set_position(DockPosition::Left, cx);
-//         });
+    //         workspace.update(cx, |workspace, cx| {
+    //             // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
+    //             // Since it was the only panel on the left, the left dock should now be closed.
+    //             assert!(!workspace.left_dock().read(cx).is_open());
+    //             assert!(workspace.left_dock().read(cx).visible_panel().is_none());
+    //             let right_dock = workspace.right_dock();
+    //             assert_eq!(
+    //                 right_dock.read(cx).visible_panel().unwrap().id(),
+    //                 panel_1.id()
+    //             );
+    //             assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
+
+    //             // Now we move panel_2Β to the left
+    //             panel_2.set_position(DockPosition::Left, cx);
+    //         });
 
-//         // Emit activated event on panel 1
-//         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
+    //         workspace.update(cx, |workspace, cx| {
+    //             // Since panel_2 was not visible on the right, we don't open the left dock.
+    //             assert!(!workspace.left_dock().read(cx).is_open());
+    //             // And the right dock is unaffected in it's displaying of panel_1
+    //             assert!(workspace.right_dock().read(cx).is_open());
+    //             assert_eq!(
+    //                 workspace
+    //                     .right_dock()
+    //                     .read(cx)
+    //                     .visible_panel()
+    //                     .unwrap()
+    //                     .id(),
+    //                 panel_1.id()
+    //             );
+    //         });
 
-//         // Now the left dock is open and panel_1 is active and focused.
-//         workspace.read_with(cx, |workspace, cx| {
-//             let left_dock = workspace.left_dock();
-//             assert!(left_dock.read(cx).is_open());
-//             assert_eq!(
-//                 left_dock.read(cx).visible_panel().unwrap().id(),
-//                 panel_1.id()
-//             );
-//             assert!(panel_1.is_focused(cx));
-//         });
+    //         // Move panel_1 back to the left
+    //         panel_1.update(cx, |panel_1, cx| {
+    //             panel_1.set_position(DockPosition::Left, cx)
+    //         });
 
-//         // Emit closed event on panel 2, which is not active
-//         panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
+    //         workspace.update(cx, |workspace, cx| {
+    //             // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
+    //             let left_dock = workspace.left_dock();
+    //             assert!(left_dock.read(cx).is_open());
+    //             assert_eq!(
+    //                 left_dock.read(cx).visible_panel().unwrap().id(),
+    //                 panel_1.id()
+    //             );
+    //             assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
+    //             // And right the dock should be closed as it no longer has any panels.
+    //             assert!(!workspace.right_dock().read(cx).is_open());
+
+    //             // Now we move panel_1 to the bottom
+    //             panel_1.set_position(DockPosition::Bottom, cx);
+    //         });
 
-//         // Wo don't close the left dock, because panel_2 wasn't the active panel
-//         workspace.read_with(cx, |workspace, cx| {
-//             let left_dock = workspace.left_dock();
-//             assert!(left_dock.read(cx).is_open());
-//             assert_eq!(
-//                 left_dock.read(cx).visible_panel().unwrap().id(),
-//                 panel_1.id()
-//             );
-//         });
+    //         workspace.update(cx, |workspace, cx| {
+    //             // Since panel_1 was visible on the left, we close the left dock.
+    //             assert!(!workspace.left_dock().read(cx).is_open());
+    //             // The bottom dock is sized based on the panel's default size,
+    //             // since the panel orientation changed from vertical to horizontal.
+    //             let bottom_dock = workspace.bottom_dock();
+    //             assert_eq!(
+    //                 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
+    //                 panel_1.size(cx),
+    //             );
+    //             // Close bottom dock and move panel_1 back to the left.
+    //             bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
+    //             panel_1.set_position(DockPosition::Left, cx);
+    //         });
 
-//         // Emitting a ZoomIn event shows the panel as zoomed.
-//         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
-//         workspace.read_with(cx, |workspace, _| {
-//             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
-//             assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
-//         });
+    //         // Emit activated event on panel 1
+    //         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
+
+    //         // Now the left dock is open and panel_1 is active and focused.
+    //         workspace.update(cx, |workspace, cx| {
+    //             let left_dock = workspace.left_dock();
+    //             assert!(left_dock.read(cx).is_open());
+    //             assert_eq!(
+    //                 left_dock.read(cx).visible_panel().unwrap().id(),
+    //                 panel_1.id()
+    //             );
+    //             assert!(panel_1.is_focused(cx));
+    //         });
 
-//         // Move panel to another dock while it is zoomed
-//         panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
-//         workspace.read_with(cx, |workspace, _| {
-//             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
-//             assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
-//         });
+    //         // Emit closed event on panel 2, which is not active
+    //         panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
+
+    //         // Wo don't close the left dock, because panel_2 wasn't the active panel
+    //         workspace.update(cx, |workspace, cx| {
+    //             let left_dock = workspace.left_dock();
+    //             assert!(left_dock.read(cx).is_open());
+    //             assert_eq!(
+    //                 left_dock.read(cx).visible_panel().unwrap().id(),
+    //                 panel_1.id()
+    //             );
+    //         });
 
-//         // If focus is transferred to another view that's not a panel or another pane, we still show
-//         // the panel as zoomed.
-//         let focus_receiver = window.build_view(cx, |_| EmptyView);
-//         focus_receiver.update(cx, |_, cx| cx.focus_self());
-//         workspace.read_with(cx, |workspace, _| {
-//             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
-//             assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
-//         });
+    //         // Emitting a ZoomIn event shows the panel as zoomed.
+    //         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
+    //         workspace.update(cx, |workspace, _| {
+    //             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
+    //             assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
+    //         });
 
-//         // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
-//         workspace.update(cx, |_, cx| cx.focus_self());
-//         workspace.read_with(cx, |workspace, _| {
-//             assert_eq!(workspace.zoomed, None);
-//             assert_eq!(workspace.zoomed_position, None);
-//         });
+    //         // Move panel to another dock while it is zoomed
+    //         panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
+    //         workspace.update(cx, |workspace, _| {
+    //             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
+    //             assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
+    //         });
 
-//         // If focus is transferred again to another view that's not a panel or a pane, we won't
-//         // show the panel as zoomed because it wasn't zoomed before.
-//         focus_receiver.update(cx, |_, cx| cx.focus_self());
-//         workspace.read_with(cx, |workspace, _| {
-//             assert_eq!(workspace.zoomed, None);
-//             assert_eq!(workspace.zoomed_position, None);
-//         });
+    //         // If focus is transferred to another view that's not a panel or another pane, we still show
+    //         // the panel as zoomed.
+    //         let focus_receiver = cx.build_view(|_| EmptyView);
+    //         focus_receiver.update(cx, |_, cx| cx.focus_self());
+    //         workspace.update(cx, |workspace, _| {
+    //             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
+    //             assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
+    //         });
 
-//         // When focus is transferred back to the panel, it is zoomed again.
-//         panel_1.update(cx, |_, cx| cx.focus_self());
-//         workspace.read_with(cx, |workspace, _| {
-//             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
-//             assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
-//         });
+    //         // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
+    //         workspace.update(cx, |_, cx| cx.focus_self());
+    //         workspace.update(cx, |workspace, _| {
+    //             assert_eq!(workspace.zoomed, None);
+    //             assert_eq!(workspace.zoomed_position, None);
+    //         });
 
-//         // Emitting a ZoomOut event unzooms the panel.
-//         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
-//         workspace.read_with(cx, |workspace, _| {
-//             assert_eq!(workspace.zoomed, None);
-//             assert_eq!(workspace.zoomed_position, None);
-//         });
+    //         // If focus is transferred again to another view that's not a panel or a pane, we won't
+    //         // show the panel as zoomed because it wasn't zoomed before.
+    //         focus_receiver.update(cx, |_, cx| cx.focus_self());
+    //         workspace.update(cx, |workspace, _| {
+    //             assert_eq!(workspace.zoomed, None);
+    //             assert_eq!(workspace.zoomed_position, None);
+    //         });
 
-//         // Emit closed event on panel 1, which is active
-//         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
+    //         // When focus is transferred back to the panel, it is zoomed again.
+    //         panel_1.update(cx, |_, cx| cx.focus_self());
+    //         workspace.update(cx, |workspace, _| {
+    //             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
+    //             assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
+    //         });
 
-//         // Now the left dock is closed, because panel_1 was the active panel
-//         workspace.read_with(cx, |workspace, cx| {
-//             let right_dock = workspace.right_dock();
-//             assert!(!right_dock.read(cx).is_open());
-//         });
-//     }
+    //         // Emitting a ZoomOut event unzooms the panel.
+    //         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
+    //         workspace.update(cx, |workspace, _| {
+    //             assert_eq!(workspace.zoomed, None);
+    //             assert_eq!(workspace.zoomed_position, None);
+    //         });
 
-//     pub fn init_test(cx: &mut TestAppContext) {
-//         cx.foreground().forbid_parking();
-//         cx.update(|cx| {
-//             cx.set_global(SettingsStore::test(cx));
-//             theme::init((), cx);
-//             language::init(cx);
-//             crate::init_settings(cx);
-//             Project::init_settings(cx);
-//         });
-//     }
-// }
+    //         // Emit closed event on panel 1, which is active
+    //         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
+
+    //         // Now the left dock is closed, because panel_1 was the active panel
+    //         workspace.update(cx, |workspace, cx| {
+    //             let right_dock = workspace.right_dock();
+    //             assert!(!right_dock.read(cx).is_open());
+    //         });
+    //     }
+
+    pub fn init_test(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let settings_store = SettingsStore::test(cx);
+            cx.set_global(settings_store);
+            theme::init(theme::LoadThemes::JustBase, cx);
+            language::init(cx);
+            crate::init_settings(cx);
+            Project::init_settings(cx);
+        });
+    }
+}

crates/zed/src/languages/json.rs πŸ”—

@@ -105,6 +105,7 @@ impl LspAdapter for JsonLspAdapter {
 
     fn workspace_configuration(
         &self,
+        _workspace_root: &Path,
         cx: &mut AppContext,
     ) -> BoxFuture<'static, serde_json::Value> {
         let action_names = cx.all_action_names().collect::<Vec<_>>();

crates/zed/src/languages/php.rs πŸ”—

@@ -29,7 +29,6 @@ pub struct IntelephenseLspAdapter {
 impl IntelephenseLspAdapter {
     const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js";
 
-    #[allow(unused)]
     pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         Self { node }
     }

crates/zed/src/languages/tailwind.rs πŸ”—

@@ -107,7 +107,11 @@ impl LspAdapter for TailwindLspAdapter {
         }))
     }
 
-    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(
+        &self,
+        _workspace_root: &Path,
+        _: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
         future::ready(json!({
             "tailwindCSS": {
                 "emmetCompletions": true,

crates/zed/src/languages/typescript.rs πŸ”—

@@ -205,7 +205,6 @@ pub struct EsLintLspAdapter {
 impl EsLintLspAdapter {
     const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
 
-    #[allow(unused)]
     pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         EsLintLspAdapter { node }
     }
@@ -213,13 +212,23 @@ impl EsLintLspAdapter {
 
 #[async_trait]
 impl LspAdapter for EsLintLspAdapter {
-    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(
+        &self,
+        workspace_root: &Path,
+        _: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
         future::ready(json!({
             "": {
                 "validate": "on",
                 "rulesCustomizations": [],
                 "run": "onType",
                 "nodePath": null,
+                "workingDirectory": {"mode": "auto"},
+                "workspaceFolder": {
+                    "uri": workspace_root,
+                    "name": workspace_root.file_name()
+                        .unwrap_or_else(|| workspace_root.as_os_str()),
+                },
             }
         }))
         .boxed()

crates/zed/src/languages/yaml.rs πŸ”—

@@ -93,7 +93,11 @@ impl LspAdapter for YamlLspAdapter {
     ) -> Option<LanguageServerBinary> {
         get_cached_server_binary(container_dir, &*self.node).await
     }
-    fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(
+        &self,
+        _workspace_root: &Path,
+        cx: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
         let tab_size = all_language_settings(None, cx)
             .language(Some("YAML"))
             .tab_size;

crates/zed2/src/languages/json.rs πŸ”—

@@ -105,6 +105,7 @@ impl LspAdapter for JsonLspAdapter {
 
     fn workspace_configuration(
         &self,
+        _workspace_root: &Path,
         cx: &mut AppContext,
     ) -> BoxFuture<'static, serde_json::Value> {
         let action_names = cx.all_action_names();

crates/zed2/src/languages/php.rs πŸ”—

@@ -29,7 +29,6 @@ pub struct IntelephenseLspAdapter {
 impl IntelephenseLspAdapter {
     const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js";
 
-    #[allow(unused)]
     pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         Self { node }
     }

crates/zed2/src/languages/tailwind.rs πŸ”—

@@ -107,7 +107,11 @@ impl LspAdapter for TailwindLspAdapter {
         }))
     }
 
-    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(
+        &self,
+        _workspace_root: &Path,
+        _: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
         future::ready(json!({
             "tailwindCSS": {
                 "emmetCompletions": true,

crates/zed2/src/languages/typescript.rs πŸ”—

@@ -205,7 +205,6 @@ pub struct EsLintLspAdapter {
 impl EsLintLspAdapter {
     const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
 
-    #[allow(unused)]
     pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         EsLintLspAdapter { node }
     }
@@ -213,13 +212,23 @@ impl EsLintLspAdapter {
 
 #[async_trait]
 impl LspAdapter for EsLintLspAdapter {
-    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(
+        &self,
+        workspace_root: &Path,
+        _: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
         future::ready(json!({
             "": {
                 "validate": "on",
                 "rulesCustomizations": [],
                 "run": "onType",
                 "nodePath": null,
+                "workingDirectory": {"mode": "auto"},
+                "workspaceFolder": {
+                    "uri": workspace_root,
+                    "name": workspace_root.file_name()
+                        .unwrap_or_else(|| workspace_root.as_os_str()),
+                },
             }
         }))
         .boxed()

crates/zed2/src/languages/yaml.rs πŸ”—

@@ -93,7 +93,11 @@ impl LspAdapter for YamlLspAdapter {
     ) -> Option<LanguageServerBinary> {
         get_cached_server_binary(container_dir, &*self.node).await
     }
-    fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(
+        &self,
+        _workspace_root: &Path,
+        cx: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
         let tab_size = all_language_settings(None, cx)
             .language(Some("YAML"))
             .tab_size;

crates/zed2/src/main.rs πŸ”—

@@ -746,9 +746,9 @@ fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
 }
 
 #[cfg(not(debug_assertions))]
-async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
+async fn watch_languages(_: Arc<dyn fs::Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
     None
 }
 
 #[cfg(not(debug_assertions))]
-fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
+fn watch_file_types(_fs: Arc<dyn fs::Fs>, _cx: &mut AppContext) {}

crates/zed2/src/zed2.rs πŸ”—

@@ -147,7 +147,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
         //     let feedback_button = cx.add_view(|_| {
         //         feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
         //     });
-        //     let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
+        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);
@@ -156,7 +156,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
             // 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);