Re-add MultiWorkspace (#48800)

Release Notes:

- Added agent panel restoration. Now restarting your editor won't cause
your thread to be forgotten.

---------

Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
Co-authored-by: Eric Holk <eric@zed.dev>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Anthony Eid <anthony@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Cameron Mcloughlin <cameron.studdstreet@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-02-12 01:06:23 +00:00
1 parent 4d00186953
commit a67197b3e2
133 files changed
+9289 -4099

No files matched your search

Generated
+28 -1
View File
@@ -4942,6 +4942,7 @@ dependencies = [
"serde_json", "serde_json",
"settings", "settings",
"smol", "smol",
"theme",
"ui", "ui",
"util", "util",
"workspace", "workspace",
@@ -8481,7 +8482,6 @@ dependencies = [
"fuzzy", "fuzzy",
"gpui", "gpui",
"language", "language",
"platform_title_bar",
"project", "project",
"serde_json", "serde_json",
"serde_json_lenient", "serde_json_lenient",
@@ -12371,6 +12371,7 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
name = "platform_title_bar" name = "platform_title_bar"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"feature_flags",
"gpui", "gpui",
"settings", "settings",
"smallvec", "smallvec",
@@ -15361,6 +15362,30 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "sidebar"
version = "0.1.0"
dependencies = [
"acp_thread",
"agent_ui",
"db",
"editor",
"feature_flags",
"fs",
"fuzzy",
"gpui",
"picker",
"project",
"recent_projects",
"serde_json",
"settings",
"theme",
"ui",
"ui_input",
"util",
"workspace",
]
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.18" version = "0.3.18"
@@ -17240,6 +17265,7 @@ dependencies = [
"cloud_api_types", "cloud_api_types",
"collections", "collections",
"db", "db",
"feature_flags",
"git_ui", "git_ui",
"gpui", "gpui",
"http_client", "http_client",
@@ -21127,6 +21153,7 @@ dependencies = [
"settings_profile_selector", "settings_profile_selector",
"settings_ui", "settings_ui",
"shellexpand 2.1.2", "shellexpand 2.1.2",
"sidebar",
"smol", "smol",
"snippet_provider", "snippet_provider",
"snippets_ui", "snippets_ui",
+3
View File
@@ -155,6 +155,7 @@ members = [
"crates/schema_generator", "crates/schema_generator",
"crates/search", "crates/search",
"crates/session", "crates/session",
"crates/sidebar",
"crates/settings", "crates/settings",
"crates/settings_content", "crates/settings_content",
"crates/settings_json", "crates/settings_json",
@@ -396,6 +397,7 @@ rules_library = { path = "crates/rules_library" }
scheduler = { path = "crates/scheduler" } scheduler = { path = "crates/scheduler" }
search = { path = "crates/search" } search = { path = "crates/search" }
session = { path = "crates/session" } session = { path = "crates/session" }
sidebar = { path = "crates/sidebar" }
settings = { path = "crates/settings" } settings = { path = "crates/settings" }
settings_content = { path = "crates/settings_content" } settings_content = { path = "crates/settings_content" }
settings_json = { path = "crates/settings_json" } settings_json = { path = "crates/settings_json" }
@@ -855,6 +857,7 @@ refineable = { codegen-units = 1 }
release_channel = { codegen-units = 1 } release_channel = { codegen-units = 1 }
reqwest_client = { codegen-units = 1 } reqwest_client = { codegen-units = 1 }
session = { codegen-units = 1 } session = { codegen-units = 1 }
sidebar = { codegen-units = 1 }
snippet = { codegen-units = 1 } snippet = { codegen-units = 1 }
snippets_ui = { codegen-units = 1 } snippets_ui = { codegen-units = 1 }
story = { codegen-units = 1 } story = { codegen-units = 1 }
+5
View File
@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.2" width="7" height="12" rx="2" transform="matrix(-1 0 0 1 9 2)" fill="#C6CAD0"/>
<path d="M9 2V14" stroke="#C6CAD0" stroke-width="1.2"/>
<rect x="2" y="2" width="12" height="12" rx="2" stroke="#C6CAD0" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 344 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="7" height="12" rx="2" transform="matrix(-1 0 0 1 9 2)" fill="#C6CAD0"/>
<path d="M9 2V14" stroke="#C6CAD0" stroke-width="1.2"/>
<rect x="2" y="2" width="12" height="12" rx="2" stroke="#C6CAD0" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 330 B

+9
View File
@@ -603,6 +603,8 @@
"ctrl-alt-b": "workspace::ToggleRightDock", "ctrl-alt-b": "workspace::ToggleRightDock",
"ctrl-b": "workspace::ToggleLeftDock", "ctrl-b": "workspace::ToggleLeftDock",
"ctrl-j": "workspace::ToggleBottomDock", "ctrl-j": "workspace::ToggleBottomDock",
"ctrl-alt-j": "multi_workspace::ToggleWorkspaceSidebar",
"ctrl-alt-;": "multi_workspace::FocusWorkspaceSidebar",
"ctrl-alt-y": "workspace::ToggleAllDocks", "ctrl-alt-y": "workspace::ToggleAllDocks",
"ctrl-alt-0": "workspace::ResetActiveDockSize", "ctrl-alt-0": "workspace::ResetActiveDockSize",
// For 0px parameter, uses UI font size value. // For 0px parameter, uses UI font size value.
@@ -662,6 +664,13 @@
"ctrl-w": "workspace::CloseActiveDock", "ctrl-w": "workspace::CloseActiveDock",
}, },
}, },
{
"context": "WorkspaceSidebar",
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "multi_workspace::NewWorkspaceInWindow",
},
},
{ {
"context": "Workspace && debugger_running", "context": "Workspace && debugger_running",
"bindings": { "bindings": {
+9
View File
@@ -664,6 +664,8 @@
"cmd-alt-b": "workspace::ToggleRightDock", "cmd-alt-b": "workspace::ToggleRightDock",
"cmd-r": "workspace::ToggleRightDock", "cmd-r": "workspace::ToggleRightDock",
"cmd-j": "workspace::ToggleBottomDock", "cmd-j": "workspace::ToggleBottomDock",
"cmd-alt-j": "multi_workspace::ToggleWorkspaceSidebar",
"cmd-alt-;": "multi_workspace::FocusWorkspaceSidebar",
"alt-cmd-y": "workspace::ToggleAllDocks", "alt-cmd-y": "workspace::ToggleAllDocks",
// For 0px parameter, uses UI font size value. // For 0px parameter, uses UI font size value.
"ctrl-alt-0": "workspace::ResetActiveDockSize", "ctrl-alt-0": "workspace::ResetActiveDockSize",
@@ -723,6 +725,13 @@
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }], // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
}, },
}, },
{
"context": "WorkspaceSidebar",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "multi_workspace::NewWorkspaceInWindow",
},
},
{ {
"context": "Workspace && debugger_running", "context": "Workspace && debugger_running",
"use_key_equivalents": true, "use_key_equivalents": true,
+9
View File
@@ -598,6 +598,8 @@
"ctrl-alt-b": "workspace::ToggleRightDock", "ctrl-alt-b": "workspace::ToggleRightDock",
"ctrl-b": "workspace::ToggleLeftDock", "ctrl-b": "workspace::ToggleLeftDock",
"ctrl-j": "workspace::ToggleBottomDock", "ctrl-j": "workspace::ToggleBottomDock",
"ctrl-alt-j": "multi_workspace::ToggleWorkspaceSidebar",
"ctrl-alt-;": "multi_workspace::FocusWorkspaceSidebar",
"ctrl-shift-y": "workspace::ToggleAllDocks", "ctrl-shift-y": "workspace::ToggleAllDocks",
"alt-r": "workspace::ResetActiveDockSize", "alt-r": "workspace::ResetActiveDockSize",
// For 0px parameter, uses UI font size value. // For 0px parameter, uses UI font size value.
@@ -666,6 +668,13 @@
"f5": "debugger::Continue", "f5": "debugger::Continue",
}, },
}, },
{
"context": "WorkspaceSidebar",
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "multi_workspace::NewWorkspaceInWindow",
},
},
{ {
"context": "ApplicationMenu", "context": "ApplicationMenu",
"use_key_equivalents": true, "use_key_equivalents": true,
+1 -1
View File
@@ -5,7 +5,7 @@ mod mode_selector;
mod model_selector; mod model_selector;
mod model_selector_popover; mod model_selector_popover;
mod thread_history; mod thread_history;
mod thread_view; pub(crate) mod thread_view;
pub use mode_selector::ModeSelector; pub use mode_selector::ModeSelector;
pub use model_selector::AcpModelSelector; pub use model_selector::AcpModelSelector;
+4 -3
View File
@@ -419,7 +419,7 @@ mod tests {
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use util::path; use util::path;
use workspace::Workspace; use workspace::MultiWorkspace;
#[gpui::test] #[gpui::test]
async fn test_diff_sync(cx: &mut TestAppContext) { async fn test_diff_sync(cx: &mut TestAppContext) {
@@ -434,8 +434,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let tool_call = acp::ToolCall::new("tool", "Tool call") let tool_call = acp::ToolCall::new("tool", "Tool call")
.status(acp::ToolCallStatus::InProgress) .status(acp::ToolCallStatus::InProgress)
+68 -34
View File
@@ -815,8 +815,13 @@ impl MessageEditor {
} }
if self.prompt_capabilities.borrow().image if self.prompt_capabilities.borrow().image
&& let Some(task) = && let Some(task) = paste_images_as_context(
paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx) self.editor.clone(),
self.mention_set.clone(),
self.workspace.clone(),
window,
cx,
)
{ {
task.detach(); task.detach();
return; return;
@@ -1084,6 +1089,7 @@ impl MessageEditor {
let editor = self.editor.clone(); let editor = self.editor.clone();
let mention_set = self.mention_set.clone(); let mention_set = self.mention_set.clone();
let workspace = self.workspace.clone();
let paths_receiver = cx.prompt_for_paths(gpui::PathPromptOptions { let paths_receiver = cx.prompt_for_paths(gpui::PathPromptOptions {
files: true, files: true,
@@ -1134,7 +1140,14 @@ impl MessageEditor {
images.push(gpui::Image::from_bytes(format, content)); images.push(gpui::Image::from_bytes(format, content));
} }
crate::mention_set::insert_images_as_context(images, editor, mention_set, cx).await; crate::mention_set::insert_images_as_context(
images,
editor,
mention_set,
workspace,
cx,
)
.await;
Ok(()) Ok(())
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
@@ -1450,7 +1463,7 @@ mod tests {
use text::Point; use text::Point;
use ui::{App, Context, IntoElement, Render, SharedString, Window}; use ui::{App, Context, IntoElement, Render, SharedString, Window};
use util::{path, paths::PathStyle, rel_path::rel_path}; use util::{path, paths::PathStyle, rel_path::rel_path};
use workspace::{AppState, Item, Workspace}; use workspace::{AppState, Item, MultiWorkspace};
use crate::acp::{ use crate::acp::{
message_editor::{Mention, MessageEditor, parse_mention_links}, message_editor::{Mention, MessageEditor, parse_mention_links},
@@ -1558,8 +1571,9 @@ mod tests {
fs.insert_tree("/project", json!({"file": ""})).await; fs.insert_tree("/project", json!({"file": ""})).await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = None; let thread_store = None;
let history = cx let history = cx
@@ -1673,8 +1687,9 @@ mod tests {
// Start with no available commands - simulating Claude which doesn't support slash commands // Start with no available commands - simulating Claude which doesn't support slash commands
let available_commands = Rc::new(RefCell::new(vec![])); let available_commands = Rc::new(RefCell::new(vec![]));
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let history = cx let history = cx
.update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx))); .update(|window, cx| cx.new(|cx| crate::acp::AcpThreadHistory::new(None, window, cx)));
let workspace_handle = workspace.downgrade(); let workspace_handle = workspace.downgrade();
@@ -1822,10 +1837,13 @@ mod tests {
}); });
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window =
let workspace = window.root(cx).unwrap(); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let mut cx = VisualTestContext::from_window(*window, cx); let mut cx = VisualTestContext::from_window(window.into(), cx);
let thread_store = None; let thread_store = None;
let history = cx let history = cx
@@ -2014,8 +2032,11 @@ mod tests {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window =
let workspace = window.root(cx).unwrap(); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let worktree = project.update(cx, |project, cx| { let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>(); let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
@@ -2024,7 +2045,7 @@ mod tests {
}); });
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
let mut cx = VisualTestContext::from_window(*window, cx); let mut cx = VisualTestContext::from_window(window.into(), cx);
let paths = vec![ let paths = vec![
rel_path("a/one.txt"), rel_path("a/one.txt"),
@@ -2551,8 +2572,9 @@ mod tests {
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
let history = cx let history = cx
@@ -2651,8 +2673,9 @@ mod tests {
fs.insert_tree("/project", json!({"file": ""})).await; fs.insert_tree("/project", json!({"file": ""})).await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
let history = cx let history = cx
@@ -2732,8 +2755,9 @@ mod tests {
fs.insert_tree("/project", json!({"file": ""})).await; fs.insert_tree("/project", json!({"file": ""})).await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = None; let thread_store = None;
let history = cx let history = cx
@@ -2791,8 +2815,9 @@ mod tests {
fs.insert_tree("/project", json!({"file": ""})).await; fs.insert_tree("/project", json!({"file": ""})).await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = None; let thread_store = None;
let history = cx let history = cx
@@ -2845,8 +2870,9 @@ mod tests {
fs.insert_tree("/project", json!({"file": ""})).await; fs.insert_tree("/project", json!({"file": ""})).await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
let history = cx let history = cx
@@ -2900,8 +2926,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
let history = cx let history = cx
@@ -2964,8 +2991,9 @@ mod tests {
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = Some(cx.new(|cx| ThreadStore::new(cx))); let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
let history = cx let history = cx
@@ -3085,8 +3113,11 @@ mod tests {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window =
let workspace = window.root(cx).unwrap(); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let worktree = project.update(cx, |project, cx| { let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>(); let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
@@ -3095,7 +3126,7 @@ mod tests {
}); });
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
let mut cx = VisualTestContext::from_window(*window, cx); let mut cx = VisualTestContext::from_window(window.into(), cx);
// Open a regular editor with the created file, and select a portion of // Open a regular editor with the created file, and select a portion of
// the text that will be used for the selections that are meant to be // the text that will be used for the selections that are meant to be
@@ -3237,10 +3268,13 @@ mod tests {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window =
let workspace = window.root(cx).unwrap(); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let mut cx = VisualTestContext::from_window(*window, cx); let mut cx = VisualTestContext::from_window(window.into(), cx);
let thread_store = cx.new(|cx| ThreadStore::new(cx)); let thread_store = cx.new(|cx| ThreadStore::new(cx));
let history = cx let history = cx
+198 -37
View File
@@ -57,7 +57,9 @@ use ui::{
}; };
use util::defer; use util::defer;
use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, NewTerminal, Toast, Workspace, notifications::NotificationId}; use workspace::{
CollaboratorId, MultiWorkspace, NewTerminal, Toast, Workspace, notifications::NotificationId,
};
use zed_actions::agent::{Chat, ToggleModelSelector}; use zed_actions::agent::{Chat, ToggleModelSelector};
use zed_actions::assistant::OpenRulesLibrary; use zed_actions::assistant::OpenRulesLibrary;
@@ -2161,9 +2163,30 @@ impl AcpServerView {
self.show_notification(caption, icon, window, cx); self.show_notification(caption, icon, window, cx);
} }
fn agent_is_visible(&self, window: &Window, cx: &App) -> bool {
if window.is_window_active() {
let workspace_is_foreground = window
.root::<MultiWorkspace>()
.flatten()
.and_then(|mw| {
let mw = mw.read(cx);
self.workspace.upgrade().map(|ws| mw.workspace() == &ws)
})
.unwrap_or(true);
if workspace_is_foreground {
if let Some(workspace) = self.workspace.upgrade() {
return AgentPanel::is_visible(&workspace, cx);
}
}
}
false
}
fn play_notification_sound(&self, window: &Window, cx: &mut App) { fn play_notification_sound(&self, window: &Window, cx: &mut App) {
let settings = AgentSettings::get_global(cx); let settings = AgentSettings::get_global(cx);
if settings.play_sound_when_agent_done && !window.is_window_active() { if settings.play_sound_when_agent_done && !self.agent_is_visible(window, cx) {
Audio::play_sound(Sound::AgentDone, cx); Audio::play_sound(Sound::AgentDone, cx);
} }
} }
@@ -2181,14 +2204,7 @@ impl AcpServerView {
let settings = AgentSettings::get_global(cx); let settings = AgentSettings::get_global(cx);
let window_is_inactive = !window.is_window_active(); let should_notify = !self.agent_is_visible(window, cx);
let panel_is_hidden = self
.workspace
.upgrade()
.map(|workspace| AgentPanel::is_hidden(&workspace, cx))
.unwrap_or(true);
let should_notify = window_is_inactive || panel_is_hidden;
if !should_notify { if !should_notify {
return; return;
@@ -2251,19 +2267,22 @@ impl AcpServerView {
.push(cx.subscribe_in(&pop_up, window, { .push(cx.subscribe_in(&pop_up, window, {
|this, _, event, window, cx| match event { |this, _, event, window, cx| match event {
AgentNotificationEvent::Accepted => { AgentNotificationEvent::Accepted => {
let handle = window.window_handle(); let Some(handle) = window.window_handle().downcast::<MultiWorkspace>()
else {
log::error!("root view should be a MultiWorkspace");
return;
};
cx.activate(true); cx.activate(true);
let workspace_handle = this.workspace.clone(); let workspace_handle = this.workspace.clone();
// If there are multiple Zed windows, activate the correct one.
cx.defer(move |cx| { cx.defer(move |cx| {
handle handle
.update(cx, |_view, window, _cx| { .update(cx, |multi_workspace, window, cx| {
window.activate_window(); window.activate_window();
if let Some(workspace) = workspace_handle.upgrade() { if let Some(workspace) = workspace_handle.upgrade() {
workspace.update(_cx, |workspace, cx| { multi_workspace.activate(workspace.clone(), cx);
workspace.update(cx, |workspace, cx| {
workspace.focus_panel::<AgentPanel>(window, cx); workspace.focus_panel::<AgentPanel>(window, cx);
}); });
} }
@@ -2288,12 +2307,12 @@ impl AcpServerView {
.push({ .push({
let pop_up_weak = pop_up.downgrade(); let pop_up_weak = pop_up.downgrade();
cx.observe_window_activation(window, move |_, window, cx| { cx.observe_window_activation(window, move |this, window, cx| {
if window.is_window_active() if this.agent_is_visible(window, cx)
&& let Some(pop_up) = pop_up_weak.upgrade() && let Some(pop_up) = pop_up_weak.upgrade()
{ {
pop_up.update(cx, |_, cx| { pop_up.update(cx, |notification, cx| {
cx.emit(AgentNotificationEvent::Dismissed); notification.dismiss(cx);
}); });
} }
}) })
@@ -2545,6 +2564,7 @@ pub(crate) mod tests {
use action_log::ActionLog; use action_log::ActionLog;
use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext}; use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext};
use agent_client_protocol::SessionId; use agent_client_protocol::SessionId;
use assistant_text_thread::TextThreadStore;
use editor::MultiBufferOffset; use editor::MultiBufferOffset;
use fs::FakeFs; use fs::FakeFs;
use gpui::{EventEmitter, TestAppContext, VisualTestContext}; use gpui::{EventEmitter, TestAppContext, VisualTestContext};
@@ -2556,7 +2576,9 @@ pub(crate) mod tests {
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use workspace::Item; use workspace::{Item, MultiWorkspace};
use crate::agent_panel;
use super::*; use super::*;
@@ -2628,8 +2650,9 @@ pub(crate) mod tests {
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await; let project = Project::test(fs, [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx))); let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
// Create history without an initial session list - it will be set after connection // Create history without an initial session list - it will be set after connection
@@ -2700,8 +2723,9 @@ pub(crate) mod tests {
let session = AgentSessionInfo::new(SessionId::new("resume-session")); let session = AgentSessionInfo::new(SessionId::new("resume-session"));
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await; let project = Project::test(fs, [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx))); let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx))); let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
@@ -2747,8 +2771,9 @@ pub(crate) mod tests {
) )
.await; .await;
let project = Project::test(fs, [Path::new("/project")], cx).await; let project = Project::test(fs, [Path::new("/project")], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let connection = CwdCapturingConnection::new(); let connection = CwdCapturingConnection::new();
let captured_cwd = connection.captured_cwd.clone(); let captured_cwd = connection.captured_cwd.clone();
@@ -2798,8 +2823,9 @@ pub(crate) mod tests {
) )
.await; .await;
let project = Project::test(fs, [Path::new("/project")], cx).await; let project = Project::test(fs, [Path::new("/project")], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let connection = CwdCapturingConnection::new(); let connection = CwdCapturingConnection::new();
let captured_cwd = connection.captured_cwd.clone(); let captured_cwd = connection.captured_cwd.clone();
@@ -2849,8 +2875,9 @@ pub(crate) mod tests {
) )
.await; .await;
let project = Project::test(fs, [Path::new("/project")], cx).await; let project = Project::test(fs, [Path::new("/project")], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let connection = CwdCapturingConnection::new(); let connection = CwdCapturingConnection::new();
let captured_cwd = connection.captured_cwd.clone(); let captured_cwd = connection.captured_cwd.clone();
@@ -3011,6 +3038,137 @@ pub(crate) mod tests {
); );
} }
#[gpui::test]
async fn test_notification_when_workspace_is_background_in_multi_workspace(
cx: &mut TestAppContext,
) {
init_test(cx);
// Enable multi-workspace feature flag and init globals needed by AgentPanel
let fs = FakeFs::new(cx.executor());
cx.update(|cx| {
cx.update_flags(true, vec!["agent-v2".to_string()]);
agent::ThreadStore::init_global(cx);
language_model::LanguageModelRegistry::test(cx);
<dyn Fs>::set_global(fs.clone(), cx);
});
let project1 = Project::test(fs.clone(), [], cx).await;
// Create a MultiWorkspace window with one workspace
let multi_workspace_handle =
cx.add_window(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
// Get workspace 1 (the initial workspace)
let workspace1 = multi_workspace_handle
.read_with(cx, |mw, _cx| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
workspace1.update_in(cx, |workspace, window, cx| {
let text_thread_store =
cx.new(|cx| TextThreadStore::fake(workspace.project().clone(), cx));
let panel =
cx.new(|cx| crate::AgentPanel::new(workspace, text_thread_store, None, window, cx));
workspace.add_panel(panel, window, cx);
// Open the dock and activate the agent panel so it's visible
workspace.focus_panel::<crate::AgentPanel>(window, cx);
});
cx.run_until_parked();
cx.read(|cx| {
assert!(
crate::AgentPanel::is_visible(&workspace1, cx),
"AgentPanel should be visible in workspace1's dock"
);
});
// Set up thread view in workspace 1
let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
let agent = StubAgentServer::default_response();
let thread_view = cx.update(|window, cx| {
cx.new(|cx| {
AcpServerView::new(
Rc::new(agent),
None,
None,
workspace1.downgrade(),
project1.clone(),
Some(thread_store),
None,
history,
window,
cx,
)
})
});
cx.run_until_parked();
let message_editor = message_editor(&thread_view, cx);
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
// Create a second workspace and switch to it.
// This makes workspace1 the "background" workspace.
let project2 = Project::test(fs, [], cx).await;
multi_workspace_handle
.update(cx, |mw, window, cx| {
mw.test_add_workspace(project2, window, cx);
})
.unwrap();
cx.run_until_parked();
// Verify workspace1 is no longer the active workspace
multi_workspace_handle
.read_with(cx, |mw, _cx| {
assert_eq!(mw.active_workspace_index(), 1);
assert_ne!(mw.workspace(), &workspace1);
})
.unwrap();
// Window is active, agent panel is visible in workspace1, but workspace1
// is in the background. The notification should show because the user
// can't actually see the agent panel.
active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
cx.run_until_parked();
assert!(
cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some()),
"Expected notification when workspace is in background within MultiWorkspace"
);
// Also verify: clicking "View Panel" should switch to workspace1.
cx.windows()
.iter()
.find_map(|window| window.downcast::<AgentNotification>())
.unwrap()
.update(cx, |window, _, cx| window.accept(cx))
.unwrap();
cx.run_until_parked();
multi_workspace_handle
.read_with(cx, |mw, _cx| {
assert_eq!(
mw.workspace(),
&workspace1,
"Expected workspace1 to become the active workspace after accepting notification"
);
})
.unwrap();
}
#[gpui::test] #[gpui::test]
async fn test_notification_respects_never_setting(cx: &mut TestAppContext) { async fn test_notification_respects_never_setting(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
@@ -3103,8 +3261,9 @@ pub(crate) mod tests {
) -> (Entity<AcpServerView>, &mut VisualTestContext) { ) -> (Entity<AcpServerView>, &mut VisualTestContext) {
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await; let project = Project::test(fs, [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx))); let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx))); let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
@@ -3173,18 +3332,18 @@ pub(crate) mod tests {
} }
} }
struct StubAgentServer<C> { pub(crate) struct StubAgentServer<C> {
connection: C, connection: C,
} }
impl<C> StubAgentServer<C> { impl<C> StubAgentServer<C> {
fn new(connection: C) -> Self { pub(crate) fn new(connection: C) -> Self {
Self { connection } Self { connection }
} }
} }
impl StubAgentServer<StubAgentConnection> { impl StubAgentServer<StubAgentConnection> {
fn default_response() -> Self { pub(crate) fn default_response() -> Self {
let conn = StubAgentConnection::new(); let conn = StubAgentConnection::new();
conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Default response".into()), acp::ContentChunk::new("Default response".into()),
@@ -3580,6 +3739,7 @@ pub(crate) mod tests {
cx.set_global(settings_store); cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx); theme::init(theme::LoadThemes::JustBase, cx);
editor::init(cx); editor::init(cx);
agent_panel::init(cx);
release_channel::init(semver::Version::new(0, 0, 0), cx); release_channel::init(semver::Version::new(0, 0, 0), cx);
prompt_store::init(cx) prompt_store::init(cx)
}); });
@@ -3614,8 +3774,9 @@ pub(crate) mod tests {
) )
.await; .await;
let project = Project::test(fs, [Path::new("/project")], cx).await; let project = Project::test(fs, [Path::new("/project")], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx))); let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx))); let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
@@ -599,6 +599,7 @@ mod tests {
use project::Project; use project::Project;
use settings::SettingsStore; use settings::SettingsStore;
use util::path; use util::path;
use workspace::MultiWorkspace;
#[gpui::test] #[gpui::test]
async fn test_save_provider_invalid_inputs(cx: &mut TestAppContext) { async fn test_save_provider_invalid_inputs(cx: &mut TestAppContext) {
@@ -815,8 +816,9 @@ mod tests {
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx)); cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (_, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let _workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
cx cx
} }
+11 -8
View File
@@ -1352,10 +1352,10 @@ impl AgentDiff {
self.update_reviewing_editors(workspace, window, cx); self.update_reviewing_editors(workspace, window, cx);
} }
} }
AcpThreadEvent::Stopped AcpThreadEvent::Stopped => {
| AcpThreadEvent::Error self.update_reviewing_editors(workspace, window, cx);
| AcpThreadEvent::LoadError(_) }
| AcpThreadEvent::Refusal => { AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) | AcpThreadEvent::Refusal => {
self.update_reviewing_editors(workspace, window, cx); self.update_reviewing_editors(workspace, window, cx);
} }
AcpThreadEvent::TitleUpdated AcpThreadEvent::TitleUpdated
@@ -1734,6 +1734,7 @@ mod tests {
use settings::SettingsStore; use settings::SettingsStore;
use std::{path::Path, rc::Rc}; use std::{path::Path, rc::Rc};
use util::path; use util::path;
use workspace::MultiWorkspace;
#[gpui::test] #[gpui::test]
async fn test_multibuffer_agent_diff(cx: &mut TestAppContext) { async fn test_multibuffer_agent_diff(cx: &mut TestAppContext) {
@@ -1770,8 +1771,9 @@ mod tests {
let action_log = cx.read(|cx| thread.read(cx).action_log().clone()); let action_log = cx.read(|cx| thread.read(cx).action_log().clone());
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let agent_diff = cx.new_window_entity(|window, cx| { let agent_diff = cx.new_window_entity(|window, cx| {
AgentDiffPane::new(thread.clone(), workspace.downgrade(), window, cx) AgentDiffPane::new(thread.clone(), workspace.downgrade(), window, cx)
}); });
@@ -1929,8 +1931,9 @@ mod tests {
}) })
.unwrap(); .unwrap();
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
// Add the diff toolbar to the active pane // Add the diff toolbar to the active pane
let diff_toolbar = cx.new_window_entity(|_, cx| AgentDiffToolbar::new(cx)); let diff_toolbar = cx.new_window_entity(|_, cx| AgentDiffToolbar::new(cx));
+329 -33
View File
@@ -67,6 +67,7 @@ use ui::{
use util::ResultExt as _; use util::ResultExt as _;
use workspace::{ use workspace::{
CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace, CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
WorkspaceId,
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
}; };
use zed_actions::{ use zed_actions::{
@@ -81,10 +82,50 @@ const AGENT_PANEL_KEY: &str = "agent_panel";
const RECENTLY_UPDATED_MENU_LIMIT: usize = 6; const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
const DEFAULT_THREAD_TITLE: &str = "New Thread"; const DEFAULT_THREAD_TITLE: &str = "New Thread";
#[derive(Serialize, Deserialize, Debug)] fn read_serialized_panel(workspace_id: workspace::WorkspaceId) -> Option<SerializedAgentPanel> {
let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
let key = i64::from(workspace_id).to_string();
scope
.read(&key)
.log_err()
.flatten()
.and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
}
async fn save_serialized_panel(
workspace_id: workspace::WorkspaceId,
panel: SerializedAgentPanel,
) -> Result<()> {
let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
let key = i64::from(workspace_id).to_string();
scope.write(key, serde_json::to_string(&panel)?).await?;
Ok(())
}
/// Migration: reads the original single-panel format stored under the
/// `"agent_panel"` KVP key before per-workspace keying was introduced.
fn read_legacy_serialized_panel() -> Option<SerializedAgentPanel> {
KEY_VALUE_STORE
.read_kvp(AGENT_PANEL_KEY)
.log_err()
.flatten()
.and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct SerializedAgentPanel { struct SerializedAgentPanel {
width: Option<Pixels>, width: Option<Pixels>,
selected_agent: Option<AgentType>, selected_agent: Option<AgentType>,
#[serde(default)]
last_active_thread: Option<SerializedActiveThread>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct SerializedActiveThread {
session_id: String,
agent_type: AgentType,
title: Option<String>,
cwd: Option<std::path::PathBuf>,
} }
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
@@ -128,7 +169,9 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &NewTextThread, window, cx| { .register_action(|workspace, _: &NewTextThread, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx); workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| panel.new_text_thread(window, cx)); panel.update(cx, |panel, cx| {
panel.new_text_thread(window, cx);
});
} }
}) })
.register_action(|workspace, action: &NewExternalAgentThread, window, cx| { .register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
@@ -413,6 +456,8 @@ impl ActiveView {
pub struct AgentPanel { pub struct AgentPanel {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
/// Workspace id is used as a database key
workspace_id: Option<WorkspaceId>,
user_store: Entity<UserStore>, user_store: Entity<UserStore>,
project: Entity<Project>, project: Entity<Project>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@@ -428,6 +473,7 @@ pub struct AgentPanel {
focus_handle: FocusHandle, focus_handle: FocusHandle,
active_view: ActiveView, active_view: ActiveView,
previous_view: Option<ActiveView>, previous_view: Option<ActiveView>,
_active_view_observation: Option<Subscription>,
new_thread_menu_handle: PopoverMenuHandle<ContextMenu>, new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>, agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>, agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -444,19 +490,39 @@ pub struct AgentPanel {
} }
impl AgentPanel { impl AgentPanel {
fn serialize(&mut self, cx: &mut Context<Self>) { fn serialize(&mut self, cx: &mut App) {
let Some(workspace_id) = self.workspace_id else {
return;
};
let width = self.width; let width = self.width;
let selected_agent = self.selected_agent.clone(); let selected_agent = self.selected_agent.clone();
let last_active_thread = self.active_agent_thread(cx).map(|thread| {
let thread = thread.read(cx);
let title = thread.title();
SerializedActiveThread {
session_id: thread.session_id().0.to_string(),
agent_type: self.selected_agent.clone(),
title: if title.as_ref() != DEFAULT_THREAD_TITLE {
Some(title.to_string())
} else {
None
},
cwd: None,
}
});
self.pending_serialization = Some(cx.background_spawn(async move { self.pending_serialization = Some(cx.background_spawn(async move {
KEY_VALUE_STORE save_serialized_panel(
.write_kvp( workspace_id,
AGENT_PANEL_KEY.into(), SerializedAgentPanel {
serde_json::to_string(&SerializedAgentPanel { width,
width, selected_agent: Some(selected_agent),
selected_agent: Some(selected_agent), last_active_thread,
})?, },
) )
.await?; .await?;
anyhow::Ok(()) anyhow::Ok(())
})); }));
} }
@@ -472,16 +538,18 @@ impl AgentPanel {
Ok(prompt_store) => prompt_store.await.ok(), Ok(prompt_store) => prompt_store.await.ok(),
Err(_) => None, Err(_) => None,
}; };
let serialized_panel = if let Some(panel) = cx let workspace_id = workspace
.background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) }) .read_with(cx, |workspace, _| workspace.database_id())
.await .ok()
.log_err() .flatten();
.flatten()
{ let serialized_panel = cx
serde_json::from_str::<SerializedAgentPanel>(&panel).log_err() .background_spawn(async move {
} else { workspace_id
None .and_then(read_serialized_panel)
}; .or_else(read_legacy_serialized_panel)
})
.await;
let slash_commands = Arc::new(SlashCommandWorkingSet::default()); let slash_commands = Arc::new(SlashCommandWorkingSet::default());
let text_thread_store = workspace let text_thread_store = workspace
@@ -500,15 +568,30 @@ impl AgentPanel {
let panel = let panel =
cx.new(|cx| Self::new(workspace, text_thread_store, prompt_store, window, cx)); cx.new(|cx| Self::new(workspace, text_thread_store, prompt_store, window, cx));
if let Some(serialized_panel) = serialized_panel { if let Some(serialized_panel) = &serialized_panel {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width.map(|w| w.round()); panel.width = serialized_panel.width.map(|w| w.round());
if let Some(selected_agent) = serialized_panel.selected_agent { if let Some(selected_agent) = serialized_panel.selected_agent.clone() {
panel.selected_agent = selected_agent; panel.selected_agent = selected_agent;
} }
cx.notify(); cx.notify();
}); });
} }
if let Some(thread_info) = serialized_panel.and_then(|p| p.last_active_thread) {
let agent_type = thread_info.agent_type.clone();
let session_info = AgentSessionInfo {
session_id: acp::SessionId::new(thread_info.session_id),
cwd: thread_info.cwd,
title: thread_info.title.map(SharedString::from),
updated_at: None,
meta: None,
};
panel.update(cx, |panel, cx| {
panel.selected_agent = agent_type;
panel.load_agent_thread(session_info, window, cx);
});
}
panel panel
})?; })?;
@@ -516,7 +599,7 @@ impl AgentPanel {
}) })
} }
fn new( pub(crate) fn new(
workspace: &Workspace, workspace: &Workspace,
text_thread_store: Entity<assistant_text_thread::TextThreadStore>, text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
@@ -528,6 +611,7 @@ impl AgentPanel {
let project = workspace.project(); let project = workspace.project();
let language_registry = project.read(cx).languages().clone(); let language_registry = project.read(cx).languages().clone();
let client = workspace.client().clone(); let client = workspace.client().clone();
let workspace_id = workspace.database_id();
let workspace = workspace.weak_handle(); let workspace = workspace.weak_handle();
let context_server_registry = let context_server_registry =
@@ -633,6 +717,7 @@ impl AgentPanel {
}; };
let mut panel = Self { let mut panel = Self {
workspace_id,
active_view, active_view,
workspace, workspace,
user_store, user_store,
@@ -646,6 +731,7 @@ impl AgentPanel {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
context_server_registry, context_server_registry,
previous_view: None, previous_view: None,
_active_view_observation: None,
new_thread_menu_handle: PopoverMenuHandle::default(), new_thread_menu_handle: PopoverMenuHandle::default(),
agent_panel_menu_handle: PopoverMenuHandle::default(), agent_panel_menu_handle: PopoverMenuHandle::default(),
agent_navigation_menu_handle: PopoverMenuHandle::default(), agent_navigation_menu_handle: PopoverMenuHandle::default(),
@@ -714,7 +800,7 @@ impl AgentPanel {
&self.context_server_registry &self.context_server_registry
} }
pub fn is_hidden(workspace: &Entity<Workspace>, cx: &App) -> bool { pub fn is_visible(workspace: &Entity<Workspace>, cx: &App) -> bool {
let workspace_read = workspace.read(cx); let workspace_read = workspace.read(cx);
workspace_read workspace_read
@@ -722,15 +808,13 @@ impl AgentPanel {
.map(|panel| { .map(|panel| {
let panel_id = Entity::entity_id(&panel); let panel_id = Entity::entity_id(&panel);
let is_visible = workspace_read.all_docks().iter().any(|dock| { workspace_read.all_docks().iter().any(|dock| {
dock.read(cx) dock.read(cx)
.visible_panel() .visible_panel()
.is_some_and(|visible_panel| visible_panel.panel_id() == panel_id) .is_some_and(|visible_panel| visible_panel.panel_id() == panel_id)
}); })
!is_visible
}) })
.unwrap_or(true) .unwrap_or(false)
} }
pub(crate) fn active_thread_view(&self) -> Option<&Entity<AcpServerView>> { pub(crate) fn active_thread_view(&self) -> Option<&Entity<AcpServerView>> {
@@ -1023,6 +1107,7 @@ impl AgentPanel {
ActiveView::Configuration | ActiveView::History { .. } => { ActiveView::Configuration | ActiveView::History { .. } => {
if let Some(previous_view) = self.previous_view.take() { if let Some(previous_view) = self.previous_view.take() {
self.active_view = previous_view; self.active_view = previous_view;
cx.emit(AgentPanelEvent::ActiveViewChanged);
match &self.active_view { match &self.active_view {
ActiveView::AgentThread { thread_view } => { ActiveView::AgentThread { thread_view } => {
@@ -1419,7 +1504,7 @@ impl AgentPanel {
} }
} }
pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> { pub fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
match &self.active_view { match &self.active_view {
ActiveView::AgentThread { thread_view, .. } => thread_view ActiveView::AgentThread { thread_view, .. } => thread_view
.read(cx) .read(cx)
@@ -1475,9 +1560,21 @@ impl AgentPanel {
self.active_view = new_view; self.active_view = new_view;
} }
self._active_view_observation = match &self.active_view {
ActiveView::AgentThread { thread_view } => {
Some(cx.observe(thread_view, |this, _, cx| {
cx.emit(AgentPanelEvent::ActiveViewChanged);
this.serialize(cx);
cx.notify();
}))
}
_ => None,
};
if focus { if focus {
self.focus_handle(cx).focus(window, cx); self.focus_handle(cx).focus(window, cx);
} }
cx.emit(AgentPanelEvent::ActiveViewChanged);
} }
fn populate_recently_updated_menu_section( fn populate_recently_updated_menu_section(
@@ -1750,7 +1847,12 @@ fn agent_panel_dock_position(cx: &App) -> DockPosition {
AgentSettings::get_global(cx).dock.into() AgentSettings::get_global(cx).dock.into()
} }
pub enum AgentPanelEvent {
ActiveViewChanged,
}
impl EventEmitter<PanelEvent> for AgentPanel {} impl EventEmitter<PanelEvent> for AgentPanel {}
impl EventEmitter<AgentPanelEvent> for AgentPanel {}
impl Panel for AgentPanel { impl Panel for AgentPanel {
fn persistent_name() -> &'static str { fn persistent_name() -> &'static str {
@@ -3251,7 +3353,8 @@ impl Dismissable for TrialEndUpsell {
const KEY: &'static str = "dismissed-trial-end-upsell"; const KEY: &'static str = "dismissed-trial-end-upsell";
} }
#[cfg(feature = "test-support")] /// Test-only helper methods
#[cfg(any(test, feature = "test-support"))]
impl AgentPanel { impl AgentPanel {
/// Opens an external thread using an arbitrary AgentServer. /// Opens an external thread using an arbitrary AgentServer.
/// ///
@@ -3284,3 +3387,196 @@ impl AgentPanel {
self.active_thread_view() self.active_thread_view()
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::acp::thread_view::tests::{StubAgentServer, init_test};
use assistant_text_thread::TextThreadStore;
use feature_flags::FeatureFlagAppExt;
use fs::FakeFs;
use gpui::{TestAppContext, VisualTestContext};
use project::Project;
use workspace::MultiWorkspace;
#[gpui::test]
async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
cx.update_flags(true, vec!["agent-v2".to_string()]);
agent::ThreadStore::init_global(cx);
language_model::LanguageModelRegistry::test(cx);
});
// --- Create a MultiWorkspace window with two workspaces ---
let fs = FakeFs::new(cx.executor());
let project_a = Project::test(fs.clone(), [], cx).await;
let project_b = Project::test(fs, [], cx).await;
let multi_workspace =
cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
let workspace_a = multi_workspace
.read_with(cx, |multi_workspace, _cx| {
multi_workspace.workspace().clone()
})
.unwrap();
let workspace_b = multi_workspace
.update(cx, |multi_workspace, window, cx| {
multi_workspace.test_add_workspace(project_b.clone(), window, cx)
})
.unwrap();
workspace_a.update(cx, |workspace, _cx| {
workspace.set_random_database_id();
});
workspace_b.update(cx, |workspace, _cx| {
workspace.set_random_database_id();
});
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
// --- Set up workspace A: width=300, with an active thread ---
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_a.clone(), cx));
cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
});
panel_a.update(cx, |panel, _cx| {
panel.width = Some(px(300.0));
});
panel_a.update_in(cx, |panel, window, cx| {
panel.open_external_thread_with_server(
Rc::new(StubAgentServer::default_response()),
window,
cx,
);
});
cx.run_until_parked();
panel_a.read_with(cx, |panel, cx| {
assert!(
panel.active_agent_thread(cx).is_some(),
"workspace A should have an active thread after connection"
);
});
let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone());
// --- Set up workspace B: ClaudeCode, width=400, no active thread ---
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_b.clone(), cx));
cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
});
panel_b.update(cx, |panel, _cx| {
panel.width = Some(px(400.0));
panel.selected_agent = AgentType::ClaudeCode;
});
// --- Serialize both panels ---
panel_a.update(cx, |panel, cx| panel.serialize(cx));
panel_b.update(cx, |panel, cx| panel.serialize(cx));
cx.run_until_parked();
// --- Load fresh panels for each workspace and verify independent state ---
let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap());
let async_cx = cx.update(|window, cx| window.to_async(cx));
let loaded_a = AgentPanel::load(workspace_a.downgrade(), prompt_builder.clone(), async_cx)
.await
.expect("panel A load should succeed");
cx.run_until_parked();
let async_cx = cx.update(|window, cx| window.to_async(cx));
let loaded_b = AgentPanel::load(workspace_b.downgrade(), prompt_builder.clone(), async_cx)
.await
.expect("panel B load should succeed");
cx.run_until_parked();
// Workspace A should restore its thread, width, and agent type
loaded_a.read_with(cx, |panel, _cx| {
assert_eq!(
panel.width,
Some(px(300.0)),
"workspace A width should be restored"
);
assert_eq!(
panel.selected_agent, agent_type_a,
"workspace A agent type should be restored"
);
assert!(
panel.active_thread_view().is_some(),
"workspace A should have its active thread restored"
);
});
// Workspace B should restore its own width and agent type, with no thread
loaded_b.read_with(cx, |panel, _cx| {
assert_eq!(
panel.width,
Some(px(400.0)),
"workspace B width should be restored"
);
assert_eq!(
panel.selected_agent,
AgentType::ClaudeCode,
"workspace B agent type should be restored"
);
assert!(
panel.active_thread_view().is_none(),
"workspace B should have no active thread"
);
});
}
// Simple regression test
#[gpui::test]
async fn test_new_text_thread_action_handler(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
cx.update(|cx| {
cx.update_flags(true, vec!["agent-v2".to_string()]);
agent::ThreadStore::init_global(cx);
language_model::LanguageModelRegistry::test(cx);
let slash_command_registry =
assistant_slash_command::SlashCommandRegistry::default_global(cx);
slash_command_registry
.register_command(assistant_slash_commands::DefaultSlashCommand, false);
<dyn fs::Fs>::set_global(fs.clone(), cx);
});
let project = Project::test(fs.clone(), [], cx).await;
let multi_workspace =
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace_a = multi_workspace
.read_with(cx, |multi_workspace, _cx| {
multi_workspace.workspace().clone()
})
.unwrap();
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
workspace_a.update_in(cx, |workspace, window, cx| {
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
let panel =
cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
workspace.add_panel(panel, window, cx);
});
cx.run_until_parked();
workspace_a.update_in(cx, |_, window, cx| {
window.dispatch_action(NewTextThread.boxed_clone(), cx);
});
cx.run_until_parked();
}
}
+7 -1
View File
@@ -49,7 +49,7 @@ use std::any::TypeId;
use workspace::Workspace; use workspace::Workspace;
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal}; use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate}; pub use crate::agent_panel::{AgentPanel, AgentPanelEvent, ConcreteAssistantPanelDelegate};
use crate::agent_registry_ui::AgentRegistryPage; use crate::agent_registry_ui::AgentRegistryPage;
pub use crate::inline_assistant::InlineAssistant; pub use crate::inline_assistant::InlineAssistant;
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar}; pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
@@ -422,6 +422,12 @@ fn update_command_palette_filter(cx: &mut App) {
filter.hide_action_types(&[TypeId::of::<zed_actions::agent::ToggleAgentPane>()]); filter.hide_action_types(&[TypeId::of::<zed_actions::agent::ToggleAgentPane>()]);
} }
} }
if agent_v2_enabled {
filter.show_namespace("multi_workspace");
} else {
filter.hide_namespace("multi_workspace");
}
}); });
} }
+4 -3
View File
@@ -2354,7 +2354,7 @@ mod tests {
use project::Project; use project::Project;
use serde_json::json; use serde_json::json;
use util::{path, rel_path::rel_path}; use util::{path, rel_path::rel_path};
use workspace::AppState; use workspace::{AppState, MultiWorkspace};
let app_state = cx.update(|cx| { let app_state = cx.update(|cx| {
let state = AppState::test(cx); let state = AppState::test(cx);
@@ -2379,8 +2379,9 @@ mod tests {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| workspace::Workspace::test_new(project, window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let worktree_id = cx.read(|cx| { let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+8 -3
View File
@@ -417,8 +417,13 @@ impl<T: 'static> PromptEditor<T> {
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) { fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if inline_assistant_model_supports_images(cx) if inline_assistant_model_supports_images(cx)
&& let Some(task) = && let Some(task) = paste_images_as_context(
paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx) self.editor.clone(),
self.mention_set.clone(),
self.workspace.clone(),
window,
cx,
)
{ {
task.detach(); task.detach();
} }
@@ -438,7 +443,7 @@ impl<T: 'static> PromptEditor<T> {
self.mention_set self.mention_set
.update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot)); .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
if let Some(workspace) = window.root::<Workspace>().flatten() { if let Some(workspace) = Workspace::for_window(window, cx) {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
let is_via_ssh = workspace.project().read(cx).is_via_remote_server(); let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
+12 -5
View File
@@ -297,8 +297,9 @@ impl MentionSet {
self.mentions.insert(crease_id, (mention_uri, task.clone())); self.mentions.insert(crease_id, (mention_uri, task.clone()));
// Notify the user if we failed to load the mentioned context // Notify the user if we failed to load the mentioned context
cx.spawn_in(window, async move |this, cx| { let workspace = workspace.downgrade();
let result = task.await.notify_async_err(cx); cx.spawn(async move |this, mut cx| {
let result = task.await.notify_workspace_async_err(workspace, &mut cx);
drop(tx); drop(tx);
if result.is_none() { if result.is_none() {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
@@ -644,6 +645,7 @@ pub(crate) async fn insert_images_as_context(
images: Vec<gpui::Image>, images: Vec<gpui::Image>,
editor: Entity<Editor>, editor: Entity<Editor>,
mention_set: Entity<MentionSet>, mention_set: Entity<MentionSet>,
workspace: WeakEntity<Workspace>,
cx: &mut gpui::AsyncWindowContext, cx: &mut gpui::AsyncWindowContext,
) { ) {
if images.is_empty() { if images.is_empty() {
@@ -718,7 +720,11 @@ pub(crate) async fn insert_images_as_context(
mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone()) mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone())
}); });
if task.await.notify_async_err(cx).is_none() { if task
.await
.notify_workspace_async_err(workspace.clone(), cx)
.is_none()
{
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.edit([(start_anchor..end_anchor, "")], cx); editor.edit([(start_anchor..end_anchor, "")], cx);
}); });
@@ -732,11 +738,12 @@ pub(crate) async fn insert_images_as_context(
pub(crate) fn paste_images_as_context( pub(crate) fn paste_images_as_context(
editor: Entity<Editor>, editor: Entity<Editor>,
mention_set: Entity<MentionSet>, mention_set: Entity<MentionSet>,
workspace: WeakEntity<Workspace>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Option<Task<()>> { ) -> Option<Task<()>> {
let clipboard = cx.read_from_clipboard()?; let clipboard = cx.read_from_clipboard()?;
Some(window.spawn(cx, async move |cx| { Some(window.spawn(cx, async move |mut cx| {
use itertools::Itertools; use itertools::Itertools;
let (mut images, paths) = clipboard let (mut images, paths) = clipboard
.into_entries() .into_entries()
@@ -783,7 +790,7 @@ pub(crate) fn paste_images_as_context(
}) })
.ok(); .ok();
insert_images_as_context(images, editor, mention_set, cx).await; insert_images_as_context(images, editor, mention_set, workspace, &mut cx).await;
})) }))
} }
+21 -18
View File
@@ -3168,6 +3168,7 @@ mod tests {
use text::OffsetRangeExt; use text::OffsetRangeExt;
use unindent::Unindent; use unindent::Unindent;
use util::path; use util::path;
use workspace::MultiWorkspace;
#[gpui::test] #[gpui::test]
async fn test_copy_paste_whole_message(cx: &mut TestAppContext) { async fn test_copy_paste_whole_message(cx: &mut TestAppContext) {
@@ -3337,25 +3338,27 @@ mod tests {
let text_thread = create_text_thread_with_messages(messages, cx); let text_thread = create_text_thread_with_messages(messages, cx);
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window_handle =
let workspace = window.root(cx).unwrap(); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let mut cx = VisualTestContext::from_window(*window, cx); let workspace = window_handle
.read_with(cx, |mw, _| mw.workspace().clone())
let text_thread_editor = window
.update(&mut cx, |_, window, cx| {
cx.new(|cx| {
TextThreadEditor::for_text_thread(
text_thread.clone(),
fs,
workspace.downgrade(),
project,
None,
window,
cx,
)
})
})
.unwrap(); .unwrap();
let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
let weak_workspace = workspace.downgrade();
let text_thread_editor = workspace.update_in(&mut cx, |_, window, cx| {
cx.new(|cx| {
TextThreadEditor::for_text_thread(
text_thread.clone(),
fs,
weak_workspace,
project,
None,
window,
cx,
)
})
});
(text_thread, text_thread_editor, cx) (text_thread, text_thread_editor, cx)
} }
+14 -4
View File
@@ -75,6 +75,16 @@ pub enum AgentNotificationEvent {
impl EventEmitter<AgentNotificationEvent> for AgentNotification {} impl EventEmitter<AgentNotificationEvent> for AgentNotification {}
impl AgentNotification {
pub fn accept(&mut self, cx: &mut Context<Self>) {
cx.emit(AgentNotificationEvent::Accepted);
}
pub fn dismiss(&mut self, cx: &mut Context<Self>) {
cx.emit(AgentNotificationEvent::Dismissed);
}
}
impl Render for AgentNotification { impl Render for AgentNotification {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let ui_font = theme::setup_ui_font(window, cx); let ui_font = theme::setup_ui_font(window, cx);
@@ -174,14 +184,14 @@ impl Render for AgentNotification {
.style(ButtonStyle::Tinted(ui::TintColor::Accent)) .style(ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width() .full_width()
.on_click({ .on_click({
cx.listener(move |_this, _event, _, cx| { cx.listener(move |this, _event, _, cx| {
cx.emit(AgentNotificationEvent::Accepted); this.accept(cx);
}) })
}), }),
) )
.child(Button::new("dismiss", "Dismiss").full_width().on_click({ .child(Button::new("dismiss", "Dismiss").full_width().on_click({
cx.listener(move |_, _event, _, cx| { cx.listener(move |this, _event, _, cx| {
cx.emit(AgentNotificationEvent::Dismissed); this.dismiss(cx);
}) })
})), })),
) )
@@ -34,9 +34,11 @@ async fn test_channel_guests(
cx_a.executor().run_until_parked(); cx_a.executor().run_until_parked();
// Client B joins channel A as a guest // Client B joins channel A as a guest
cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx)) cx_b.update(|cx| {
.await workspace::join_channel(channel_id, client_b.app_state.clone(), None, None, cx)
.unwrap(); })
.await
.unwrap();
// b should be following a in the shared project. // b should be following a in the shared project.
// B is a guest, // B is a guest,
@@ -76,9 +78,11 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
.await; .await;
let project_a = client_a.build_test_project(cx_a).await; let project_a = client_a.build_test_project(cx_a).await;
cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx)) cx_a.update(|cx| {
.await workspace::join_channel(channel_id, client_a.app_state.clone(), None, None, cx)
.unwrap(); })
.await
.unwrap();
// Client A shares a project in the channel // Client A shares a project in the channel
active_call_a active_call_a
@@ -88,9 +92,11 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
cx_a.run_until_parked(); cx_a.run_until_parked();
// Client B joins channel A as a guest // Client B joins channel A as a guest
cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx)) cx_b.update(|cx| {
.await workspace::join_channel(channel_id, client_b.app_state.clone(), None, None, cx)
.unwrap(); })
.await
.unwrap();
cx_a.run_until_parked(); cx_a.run_until_parked();
// client B opens 1.txt as a guest // client B opens 1.txt as a guest
+37 -27
View File
@@ -19,7 +19,8 @@ use fs::Fs;
use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex}; use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
use git::repository::repo_path; use git::repository::repo_path;
use gpui::{ use gpui::{
App, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext, App, AppContext as _, Entity, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext,
VisualTestContext,
}; };
use indoc::indoc; use indoc::indoc;
use language::{FakeLspAdapter, language_settings::language_settings, rust_lang}; use language::{FakeLspAdapter, language_settings::language_settings, rust_lang};
@@ -52,7 +53,7 @@ use std::{
use text::Point; use text::Point;
use util::{path, rel_path::rel_path, uri}; use util::{path, rel_path::rel_path, uri};
use workspace::item::Item as _; use workspace::item::Item as _;
use workspace::{CloseIntent, Workspace}; use workspace::{CloseIntent, MultiWorkspace, Workspace};
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_host_disconnect( async fn test_host_disconnect(
@@ -96,34 +97,46 @@ async fn test_host_disconnect(
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer())); assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
let workspace_b = cx_b.add_window(|window, cx| { let window_b = cx_b.add_window(|window, cx| {
Workspace::new( let workspace = cx.new(|cx| {
None, Workspace::new(
project_b.clone(), None,
client_b.app_state.clone(), project_b.clone(),
window, client_b.app_state.clone(),
cx, window,
) cx,
)
});
MultiWorkspace::new(workspace, cx)
}); });
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b);
let workspace_b_view = workspace_b.root(cx_b).unwrap(); let workspace_b = window_b
.root(cx_b)
.unwrap()
.read_with(cx_b, |multi_workspace, _| {
multi_workspace.workspace().clone()
});
let editor_b = workspace_b let editor_b: Entity<Editor> = workspace_b
.update(cx_b, |workspace, window, cx| { .update_in(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, rel_path("b.txt")), None, true, window, cx) workspace.open_path((worktree_id, rel_path("b.txt")), None, true, window, cx)
}) })
.unwrap()
.await .await
.unwrap() .unwrap()
.downcast::<Editor>() .downcast::<Editor>()
.unwrap(); .unwrap();
//TODO: focus //TODO: focus
assert!(cx_b.update_window_entity(&editor_b, |editor, window, _| editor.is_focused(window))); assert!(
editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx)); cx_b.update_window_entity(&editor_b, |editor: &mut Editor, window, _| editor
.is_focused(window))
);
editor_b.update_in(cx_b, |editor: &mut Editor, window, cx| {
editor.insert("X", window, cx)
});
cx_b.update(|_, cx| { cx_b.update(|_, cx| {
assert!(workspace_b_view.read(cx).is_edited()); assert!(workspace_b.read(cx).is_edited());
}); });
// Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
@@ -141,19 +154,16 @@ async fn test_host_disconnect(
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer())); assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
// Ensure client B's edited state is reset and that the whole window is blurred. // Ensure client B's edited state is reset and that the whole window is blurred.
workspace_b workspace_b.update(cx_b, |workspace, cx| {
.update(cx_b, |workspace, _, cx| { assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some()); assert!(!workspace.is_edited());
assert!(!workspace.is_edited()); });
})
.unwrap();
// Ensure client B is not prompted to save edits when closing window after disconnecting. // Ensure client B is not prompted to save edits when closing window after disconnecting.
let can_close = workspace_b let can_close: bool = workspace_b
.update(cx_b, |workspace, window, cx| { .update_in(cx_b, |workspace, window, cx| {
workspace.prepare_to_close(CloseIntent::Quit, window, cx) workspace.prepare_to_close(CloseIntent::Quit, window, cx)
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
assert!(can_close); assert!(can_close);
@@ -17,7 +17,7 @@ use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use text::{Point, ToPoint}; use text::{Point, ToPoint};
use util::{path, rel_path::rel_path, test::sample_text}; use util::{path, rel_path::rel_path, test::sample_text};
use workspace::{CollaboratorId, SplitDirection, Workspace, item::ItemHandle as _}; use workspace::{CollaboratorId, MultiWorkspace, SplitDirection, Workspace, item::ItemHandle as _};
use super::TestClient; use super::TestClient;
@@ -1555,9 +1555,9 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
let mut cx_b2 = VisualTestContext::from_window(window_b_project_a, cx_b); let mut cx_b2 = VisualTestContext::from_window(window_b_project_a, cx_b);
let workspace_b_project_a = window_b_project_a let workspace_b_project_a = window_b_project_a
.downcast::<Workspace>() .downcast::<MultiWorkspace>()
.unwrap() .unwrap()
.root(cx_b) .read_with(cx_b, |mw, _| mw.workspace().clone())
.unwrap(); .unwrap();
// assert that b is following a in project a in w.rs // assert that b is following a in project a in w.rs
@@ -1657,9 +1657,9 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
.unwrap(); .unwrap();
let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a); let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a);
let workspace_a_project_b = window_a_project_b let workspace_a_project_b = window_a_project_b
.downcast::<Workspace>() .downcast::<MultiWorkspace>()
.unwrap() .unwrap()
.root(cx_a) .read_with(cx_a, |mw, _| mw.workspace().clone())
.unwrap(); .unwrap();
executor.run_until_parked(); executor.run_until_parked();
@@ -2144,7 +2144,7 @@ pub(crate) async fn join_channel(
client: &TestClient, client: &TestClient,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx)) cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, None, cx))
.await .await
} }
+20 -12
View File
@@ -3,11 +3,11 @@ use std::path::Path;
use call::ActiveCall; use call::ActiveCall;
use git::status::{FileStatus, StatusCode, TrackedStatus}; use git::status::{FileStatus, StatusCode, TrackedStatus};
use git_ui::project_diff::ProjectDiff; use git_ui::project_diff::ProjectDiff;
use gpui::{TestAppContext, VisualTestContext}; use gpui::{AppContext as _, TestAppContext, VisualTestContext};
use project::ProjectPath; use project::ProjectPath;
use serde_json::json; use serde_json::json;
use util::{path, rel_path::rel_path}; use util::{path, rel_path::rel_path};
use workspace::Workspace; use workspace::{MultiWorkspace, Workspace};
// //
use crate::TestServer; use crate::TestServer;
@@ -57,17 +57,25 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
cx_b.update(editor::init); cx_b.update(editor::init);
cx_b.update(git_ui::init); cx_b.update(git_ui::init);
let project_b = client_b.join_remote_project(project_id, cx_b).await; let project_b = client_b.join_remote_project(project_id, cx_b).await;
let workspace_b = cx_b.add_window(|window, cx| { let window_b = cx_b.add_window(|window, cx| {
Workspace::new( let workspace = cx.new(|cx| {
None, Workspace::new(
project_b.clone(), None,
client_b.app_state.clone(), project_b.clone(),
window, client_b.app_state.clone(),
cx, window,
) cx,
)
});
MultiWorkspace::new(workspace, cx)
}); });
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b);
let workspace_b = workspace_b.root(cx_b).unwrap(); let workspace_b = window_b
.root(cx_b)
.unwrap()
.read_with(cx_b, |multi_workspace, _| {
multi_workspace.workspace().clone()
});
cx_b.update(|window, cx| { cx_b.update(|window, cx| {
window window
@@ -8,7 +8,9 @@ use editor::{Editor, EditorMode, MultiBuffer};
use extension::ExtensionHostProxy; use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs as _, RemoveOptions}; use fs::{FakeFs, Fs as _, RemoveOptions};
use futures::StreamExt as _; use futures::StreamExt as _;
use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext}; use gpui::{
AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext as _,
};
use http_client::BlockedHttpClient; use http_client::BlockedHttpClient;
use language::{ use language::{
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
@@ -663,7 +665,7 @@ async fn test_remote_server_debugger(
let workspace_window = cx_a let workspace_window = cx_a
.window_handle() .window_handle()
.downcast::<workspace::Workspace>() .downcast::<workspace::MultiWorkspace>()
.unwrap(); .unwrap();
let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap(); let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
@@ -671,13 +673,16 @@ async fn test_remote_server_debugger(
debug_panel.update(cx_a, |debug_panel, cx| { debug_panel.update(cx_a, |debug_panel, cx| {
assert_eq!( assert_eq!(
debug_panel.active_session().unwrap().read(cx).session(cx), debug_panel.active_session().unwrap().read(cx).session(cx),
session session.clone()
) )
}); });
session.update(cx_a, |session, _| { session.update(
assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock")); cx_a,
}); |session: &mut project::debugger::session::Session, _| {
assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
},
);
let shutdown_session = workspace.update(cx_a, |workspace, cx| { let shutdown_session = workspace.update(cx_a, |workspace, cx| {
workspace.project().update(cx, |project, cx| { workspace.project().update(cx, |project, cx| {
@@ -772,7 +777,7 @@ async fn test_slow_adapter_startup_retries(
let workspace_window = cx_a let workspace_window = cx_a
.window_handle() .window_handle()
.downcast::<workspace::Workspace>() .downcast::<workspace::MultiWorkspace>()
.unwrap(); .unwrap();
let count = Arc::new(AtomicUsize::new(0)); let count = Arc::new(AtomicUsize::new(0));
@@ -804,7 +809,10 @@ async fn test_slow_adapter_startup_retries(
.unwrap(); .unwrap();
cx_a.run_until_parked(); cx_a.run_until_parked();
let client = session.update(cx_a, |session, _| session.adapter_client().unwrap()); let client = session.update(
cx_a,
|session: &mut project::debugger::session::Session, _| session.adapter_client().unwrap(),
);
client client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
reason: dap::StoppedEventReason::Pause, reason: dap::StoppedEventReason::Pause,
+42 -12
View File
@@ -45,7 +45,7 @@ use std::{
}, },
}; };
use util::path; use util::path;
use workspace::{Workspace, WorkspaceStore}; use workspace::{MultiWorkspace, Workspace, WorkspaceStore};
use livekit_client::test::TestServer as LivekitTestServer; use livekit_client::test::TestServer as LivekitTestServer;
@@ -827,7 +827,7 @@ impl TestClient {
channel_id: ChannelId, channel_id: ChannelId,
cx: &'a mut TestAppContext, cx: &'a mut TestAppContext,
) -> (Entity<Workspace>, &'a mut VisualTestContext) { ) -> (Entity<Workspace>, &'a mut VisualTestContext) {
cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx)) cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, None, cx))
.await .await
.unwrap(); .unwrap();
cx.run_until_parked(); cx.run_until_parked();
@@ -881,10 +881,19 @@ impl TestClient {
project: &Entity<Project>, project: &Entity<Project>,
cx: &'a mut TestAppContext, cx: &'a mut TestAppContext,
) -> (Entity<Workspace>, &'a mut VisualTestContext) { ) -> (Entity<Workspace>, &'a mut VisualTestContext) {
cx.add_window_view(|window, cx| { let app_state = self.app_state.clone();
let project = project.clone();
let window = cx.add_window(|window, cx| {
window.activate_window(); window.activate_window();
Workspace::new(None, project.clone(), self.app_state.clone(), window, cx) let workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
}) MultiWorkspace::new(workspace, cx)
});
let cx = VisualTestContext::from_window(*window, cx).into_mut();
cx.run_until_parked();
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
(workspace, cx)
} }
pub async fn build_test_workspace<'a>( pub async fn build_test_workspace<'a>(
@@ -892,19 +901,33 @@ impl TestClient {
cx: &'a mut TestAppContext, cx: &'a mut TestAppContext,
) -> (Entity<Workspace>, &'a mut VisualTestContext) { ) -> (Entity<Workspace>, &'a mut VisualTestContext) {
let project = self.build_test_project(cx).await; let project = self.build_test_project(cx).await;
cx.add_window_view(|window, cx| { let app_state = self.app_state.clone();
let window = cx.add_window(|window, cx| {
window.activate_window(); window.activate_window();
Workspace::new(None, project.clone(), self.app_state.clone(), window, cx) let workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
}) MultiWorkspace::new(workspace, cx)
});
let cx = VisualTestContext::from_window(*window, cx).into_mut();
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
(workspace, cx)
} }
pub fn active_workspace<'a>( pub fn active_workspace<'a>(
&'a self, &'a self,
cx: &'a mut TestAppContext, cx: &'a mut TestAppContext,
) -> (Entity<Workspace>, &'a mut VisualTestContext) { ) -> (Entity<Workspace>, &'a mut VisualTestContext) {
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap()); let window = cx.update(|cx| {
cx.active_window()
.unwrap()
.downcast::<MultiWorkspace>()
.unwrap()
});
let entity = window.root(cx).unwrap(); let entity = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = VisualTestContext::from_window(*window.deref(), cx).into_mut(); let cx = VisualTestContext::from_window(*window.deref(), cx).into_mut();
// it might be nice to try and cleanup these at the end of each test. // it might be nice to try and cleanup these at the end of each test.
(entity, cx) (entity, cx)
@@ -915,8 +938,15 @@ pub fn open_channel_notes(
channel_id: ChannelId, channel_id: ChannelId,
cx: &mut VisualTestContext, cx: &mut VisualTestContext,
) -> Task<anyhow::Result<Entity<ChannelView>>> { ) -> Task<anyhow::Result<Entity<ChannelView>>> {
let window = cx.update(|_, cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap()); let window = cx.update(|_, cx| {
let entity = window.root(cx).unwrap(); cx.active_window()
.unwrap()
.downcast::<MultiWorkspace>()
.unwrap()
});
let entity = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
cx.update(|window, cx| ChannelView::open(channel_id, None, entity.clone(), window, cx)) cx.update(|window, cx| ChannelView::open(channel_id, None, entity.clone(), window, cx))
} }
+17 -10
View File
@@ -36,7 +36,8 @@ use ui::{
}; };
use util::{ResultExt, TryFutureExt, maybe}; use util::{ResultExt, TryFutureExt, maybe};
use workspace::{ use workspace::{
CopyRoomId, Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace, CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes, ScreenShare,
ShareProject, Workspace,
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotifyResultExt}, notifications::{DetachAndPromptErr, NotifyResultExt},
}; };
@@ -120,6 +121,7 @@ pub fn init(cx: &mut App) {
if let Some(room) = ActiveCall::global(cx).read(cx).room() { if let Some(room) = ActiveCall::global(cx).read(cx).room() {
let romo_id_fut = room.read(cx).room_id(); let romo_id_fut = room.read(cx).room_id();
let workspace_handle = cx.weak_entity();
cx.spawn(async move |workspace, cx| { cx.spawn(async move |workspace, cx| {
let room_id = romo_id_fut.await.context("Failed to get livekit room")?; let room_id = romo_id_fut.await.context("Failed to get livekit room")?;
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
@@ -134,7 +136,7 @@ pub fn init(cx: &mut App) {
); );
}) })
}) })
.detach_and_notify_err(window, cx); .detach_and_notify_err(workspace_handle, window, cx);
} else { } else {
workspace.show_error(&"There’s no active call; join one first.", cx); workspace.show_error(&"There’s no active call; join one first.", cx);
} }
@@ -2189,12 +2191,13 @@ impl CollabPanel {
&["Remove", "Cancel"], &["Remove", "Cancel"],
cx, cx,
); );
cx.spawn_in(window, async move |this, cx| { let workspace = self.workspace.clone();
cx.spawn_in(window, async move |this, mut cx| {
if answer.await? == 0 { if answer.await? == 0 {
channel_store channel_store
.update(cx, |channels, _| channels.remove_channel(channel_id)) .update(cx, |channels, _| channels.remove_channel(channel_id))
.await .await
.notify_async_err(cx); .notify_workspace_async_err(workspace, &mut cx);
this.update_in(cx, |_, window, cx| cx.focus_self(window)) this.update_in(cx, |_, window, cx| cx.focus_self(window))
.ok(); .ok();
} }
@@ -2223,12 +2226,13 @@ impl CollabPanel {
&["Remove", "Cancel"], &["Remove", "Cancel"],
cx, cx,
); );
cx.spawn_in(window, async move |_, cx| { let workspace = self.workspace.clone();
cx.spawn_in(window, async move |_, mut cx| {
if answer.await? == 0 { if answer.await? == 0 {
user_store user_store
.update(cx, |store, cx| store.remove_contact(user_id, cx)) .update(cx, |store, cx| store.remove_contact(user_id, cx))
.await .await
.notify_async_err(cx); .notify_workspace_async_err(workspace, &mut cx);
} }
anyhow::Ok(()) anyhow::Ok(())
}) })
@@ -2279,13 +2283,15 @@ impl CollabPanel {
let Some(workspace) = self.workspace.upgrade() else { let Some(workspace) = self.workspace.upgrade() else {
return; return;
}; };
let Some(handle) = window.window_handle().downcast::<Workspace>() else {
let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() else {
return; return;
}; };
workspace::join_channel( workspace::join_channel(
channel_id, channel_id,
workspace.read(cx).app_state().clone(), workspace.read(cx).app_state().clone(),
Some(handle), Some(handle),
Some(self.workspace.clone()),
cx, cx,
) )
.detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None) .detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None)
@@ -2328,12 +2334,13 @@ impl CollabPanel {
.full_width() .full_width()
.on_click(cx.listener(|this, _, window, cx| { .on_click(cx.listener(|this, _, window, cx| {
let client = this.client.clone(); let client = this.client.clone();
cx.spawn_in(window, async move |_, cx| { let workspace = this.workspace.clone();
cx.spawn_in(window, async move |_, mut cx| {
client client
.connect(true, cx) .connect(true, &mut cx)
.await .await
.into_response() .into_response()
.notify_async_err(cx); .notify_workspace_async_err(workspace, &mut cx);
}) })
.detach() .detach()
})), })),
+28 -19
View File
@@ -723,7 +723,7 @@ mod tests {
use language::Point; use language::Point;
use project::Project; use project::Project;
use settings::KeymapFile; use settings::KeymapFile;
use workspace::{AppState, Workspace}; use workspace::{AppState, MultiWorkspace, Workspace};
#[test] #[test]
fn test_humanize_action_name() { fn test_humanize_action_name() {
@@ -777,8 +777,9 @@ mod tests {
.unwrap(); .unwrap();
let app_state = init_test(cx); let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let editor = cx.new_window_entity(|window, cx| { let editor = cx.new_window_entity(|window, cx| {
let mut editor = Editor::single_line(window, cx); let mut editor = Editor::single_line(window, cx);
@@ -848,8 +849,9 @@ mod tests {
async fn test_normalized_matches(cx: &mut TestAppContext) { async fn test_normalized_matches(cx: &mut TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let editor = cx.new_window_entity(|window, cx| { let editor = cx.new_window_entity(|window, cx| {
let mut editor = Editor::single_line(window, cx); let mut editor = Editor::single_line(window, cx);
@@ -884,8 +886,9 @@ mod tests {
async fn test_go_to_line(cx: &mut TestAppContext) { async fn test_go_to_line(cx: &mut TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
cx.simulate_keystrokes("cmd-n"); cx.simulate_keystrokes("cmd-n");
@@ -974,8 +977,9 @@ mod tests {
async fn test_history_navigation_basic(cx: &mut TestAppContext) { async fn test_history_navigation_basic(cx: &mut TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let palette = open_palette_with_history(&workspace, &["backspace", "select all"], cx); let palette = open_palette_with_history(&workspace, &["backspace", "select all"], cx);
@@ -1017,8 +1021,9 @@ mod tests {
async fn test_history_mode_exit_on_typing(cx: &mut TestAppContext) { async fn test_history_mode_exit_on_typing(cx: &mut TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let palette = open_palette_with_history(&workspace, &["backspace"], cx); let palette = open_palette_with_history(&workspace, &["backspace"], cx);
@@ -1041,8 +1046,9 @@ mod tests {
async fn test_history_navigation_with_suggestions(cx: &mut TestAppContext) { async fn test_history_navigation_with_suggestions(cx: &mut TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let palette = open_palette_with_history(&workspace, &["editor: close", "editor: open"], cx); let palette = open_palette_with_history(&workspace, &["editor: close", "editor: open"], cx);
@@ -1083,8 +1089,9 @@ mod tests {
async fn test_history_prefix_search(cx: &mut TestAppContext) { async fn test_history_prefix_search(cx: &mut TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let palette = open_palette_with_history( let palette = open_palette_with_history(
&workspace, &workspace,
@@ -1136,8 +1143,9 @@ mod tests {
async fn test_history_prefix_search_no_matches(cx: &mut TestAppContext) { async fn test_history_prefix_search_no_matches(cx: &mut TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let palette = let palette =
open_palette_with_history(&workspace, &["open file", "backspace", "select all"], cx); open_palette_with_history(&workspace, &["open file", "backspace", "select all"], cx);
@@ -1158,8 +1166,9 @@ mod tests {
async fn test_history_empty_prefix_searches_all(cx: &mut TestAppContext) { async fn test_history_empty_prefix_searches_all(cx: &mut TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let palette = open_palette_with_history(&workspace, &["alpha", "beta", "gamma"], cx); let palette = open_palette_with_history(&workspace, &["alpha", "beta", "gamma"], cx);
+2 -2
View File
@@ -35,7 +35,7 @@ pub fn initiate_sign_out(copilot: Entity<Copilot>, window: &mut Window, cx: &mut
cx.update(|window, cx| copilot_toast(Some("Signed out of Copilot"), window, cx)) cx.update(|window, cx| copilot_toast(Some("Signed out of Copilot"), window, cx))
} }
Err(err) => cx.update(|window, cx| { Err(err) => cx.update(|window, cx| {
if let Some(workspace) = window.root::<Workspace>().flatten() { if let Some(workspace) = Workspace::for_window(window, cx) {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
workspace.show_error(&err, cx); workspace.show_error(&err, cx);
}) })
@@ -82,7 +82,7 @@ fn open_copilot_code_verification_window(copilot: &Entity<Copilot>, window: &Win
fn copilot_toast(message: Option<&'static str>, window: &Window, cx: &mut App) { fn copilot_toast(message: Option<&'static str>, window: &Window, cx: &mut App) {
const NOTIFICATION_ID: NotificationId = NotificationId::unique::<CopilotStatusToast>(); const NOTIFICATION_ID: NotificationId = NotificationId::unique::<CopilotStatusToast>();
let Some(workspace) = window.root::<Workspace>().flatten() else { let Some(workspace) = Workspace::for_window(window, cx) else {
return; return;
}; };
+121 -6
View File
@@ -1,3 +1,4 @@
use anyhow::Context as _;
use gpui::App; use gpui::App;
use sqlez_macros::sql; use sqlez_macros::sql;
use util::ResultExt as _; use util::ResultExt as _;
@@ -13,12 +14,22 @@ pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnect
impl Domain for KeyValueStore { impl Domain for KeyValueStore {
const NAME: &str = stringify!(KeyValueStore); const NAME: &str = stringify!(KeyValueStore);
const MIGRATIONS: &[&str] = &[sql!( const MIGRATIONS: &[&str] = &[
CREATE TABLE IF NOT EXISTS kv_store( sql!(
key TEXT PRIMARY KEY, CREATE TABLE IF NOT EXISTS kv_store(
value TEXT NOT NULL key TEXT PRIMARY KEY,
) STRICT; value TEXT NOT NULL
)]; ) STRICT;
),
sql!(
CREATE TABLE IF NOT EXISTS scoped_kv_store(
namespace TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY(namespace, key)
) STRICT;
),
];
} }
crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []); crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
@@ -69,6 +80,64 @@ impl KeyValueStore {
DELETE FROM kv_store WHERE key = (?) DELETE FROM kv_store WHERE key = (?)
} }
} }
pub fn scoped<'a>(&'a self, namespace: &'a str) -> ScopedKeyValueStore<'a> {
ScopedKeyValueStore {
store: self,
namespace,
}
}
}
pub struct ScopedKeyValueStore<'a> {
store: &'a KeyValueStore,
namespace: &'a str,
}
impl ScopedKeyValueStore<'_> {
pub fn read(&self, key: &str) -> anyhow::Result<Option<String>> {
self.store.select_row_bound::<(&str, &str), String>(
"SELECT value FROM scoped_kv_store WHERE namespace = (?) AND key = (?)",
)?((self.namespace, key))
.context("Failed to read from scoped_kv_store")
}
pub async fn write(&self, key: String, value: String) -> anyhow::Result<()> {
let namespace = self.namespace.to_owned();
self.store
.write(move |connection| {
connection.exec_bound::<(&str, &str, &str)>(
"INSERT OR REPLACE INTO scoped_kv_store(namespace, key, value) VALUES ((?), (?), (?))",
)?((&namespace, &key, &value))
.context("Failed to write to scoped_kv_store")
})
.await
}
pub async fn delete(&self, key: String) -> anyhow::Result<()> {
let namespace = self.namespace.to_owned();
self.store
.write(move |connection| {
connection.exec_bound::<(&str, &str)>(
"DELETE FROM scoped_kv_store WHERE namespace = (?) AND key = (?)",
)?((&namespace, &key))
.context("Failed to delete from scoped_kv_store")
})
.await
}
pub async fn delete_all(&self) -> anyhow::Result<()> {
let namespace = self.namespace.to_owned();
self.store
.write(move |connection| {
connection
.exec_bound::<&str>("DELETE FROM scoped_kv_store WHERE namespace = (?)")?(
&namespace,
)
.context("Failed to delete_all from scoped_kv_store")
})
.await
}
} }
#[cfg(test)] #[cfg(test)]
@@ -99,6 +168,52 @@ mod tests {
db.delete_kvp("key-1".to_string()).await.unwrap(); db.delete_kvp("key-1".to_string()).await.unwrap();
assert_eq!(db.read_kvp("key-1").unwrap(), None); assert_eq!(db.read_kvp("key-1").unwrap(), None);
} }
#[gpui::test]
async fn test_scoped_kvp() {
let db = KeyValueStore::open_test_db("test_scoped_kvp").await;
let scope_a = db.scoped("namespace-a");
let scope_b = db.scoped("namespace-b");
// Reading a missing key returns None
assert_eq!(scope_a.read("key-1").unwrap(), None);
// Writing and reading back a key works
scope_a
.write("key-1".to_string(), "value-a1".to_string())
.await
.unwrap();
assert_eq!(scope_a.read("key-1").unwrap(), Some("value-a1".to_string()));
// Two namespaces with the same key don't collide
scope_b
.write("key-1".to_string(), "value-b1".to_string())
.await
.unwrap();
assert_eq!(scope_a.read("key-1").unwrap(), Some("value-a1".to_string()));
assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string()));
// delete removes a single key without affecting others in the namespace
scope_a
.write("key-2".to_string(), "value-a2".to_string())
.await
.unwrap();
scope_a.delete("key-1".to_string()).await.unwrap();
assert_eq!(scope_a.read("key-1").unwrap(), None);
assert_eq!(scope_a.read("key-2").unwrap(), Some("value-a2".to_string()));
assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string()));
// delete_all removes all keys in a namespace without affecting other namespaces
scope_a
.write("key-3".to_string(), "value-a3".to_string())
.await
.unwrap();
scope_a.delete_all().await.unwrap();
assert_eq!(scope_a.read("key-2").unwrap(), None);
assert_eq!(scope_a.read("key-3").unwrap(), None);
assert_eq!(scope_b.read("key-1").unwrap(), Some("value-b1".to_string()));
}
} }
pub struct GlobalKeyValueStore(ThreadSafeConnection); pub struct GlobalKeyValueStore(ThreadSafeConnection);
+40 -29
View File
@@ -8,7 +8,7 @@ use project::{Project, debugger::session::Session};
use settings::SettingsStore; use settings::SettingsStore;
use task::SharedTaskContext; use task::SharedTaskContext;
use terminal_view::terminal_panel::TerminalPanel; use terminal_view::terminal_panel::TerminalPanel;
use workspace::Workspace; use workspace::MultiWorkspace;
use crate::{debugger_panel::DebugPanel, session::DebugSession}; use crate::{debugger_panel::DebugPanel, session::DebugSession};
@@ -52,14 +52,16 @@ pub fn init_test(cx: &mut gpui::TestAppContext) {
pub async fn init_test_workspace( pub async fn init_test_workspace(
project: &Entity<Project>, project: &Entity<Project>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> WindowHandle<Workspace> { ) -> WindowHandle<MultiWorkspace> {
let workspace_handle = let workspace_handle =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let debugger_panel = workspace_handle let debugger_panel = workspace_handle
.update(cx, |_, window, cx| { .update(cx, |multi, window, cx| {
cx.spawn_in(window, async move |this, cx| { multi.workspace().update(cx, |_workspace, cx| {
DebugPanel::load(this, cx).await cx.spawn_in(window, async move |this, cx| {
DebugPanel::load(this, cx).await
})
}) })
}) })
.unwrap() .unwrap()
@@ -67,9 +69,10 @@ pub async fn init_test_workspace(
.expect("Failed to load debug panel"); .expect("Failed to load debug panel");
let terminal_panel = workspace_handle let terminal_panel = workspace_handle
.update(cx, |_, window, cx| { .update(cx, |multi, window, cx| {
cx.spawn_in(window, async |this, cx| { let weak_workspace = multi.workspace().downgrade();
TerminalPanel::load(this, cx.clone()).await cx.spawn_in(window, async move |_, cx| {
TerminalPanel::load(weak_workspace, cx.clone()).await
}) })
}) })
.unwrap() .unwrap()
@@ -77,9 +80,11 @@ pub async fn init_test_workspace(
.expect("Failed to load terminal panel"); .expect("Failed to load terminal panel");
workspace_handle workspace_handle
.update(cx, |workspace, window, cx| { .update(cx, |multi, window, cx| {
workspace.add_panel(debugger_panel, window, cx); multi.workspace().update(cx, |workspace, cx| {
workspace.add_panel(terminal_panel, window, cx); workspace.add_panel(debugger_panel, window, cx);
workspace.add_panel(terminal_panel, window, cx);
});
}) })
.unwrap(); .unwrap();
workspace_handle workspace_handle
@@ -87,39 +92,45 @@ pub async fn init_test_workspace(
#[track_caller] #[track_caller]
pub fn active_debug_session_panel( pub fn active_debug_session_panel(
workspace: WindowHandle<Workspace>, workspace: WindowHandle<MultiWorkspace>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> Entity<DebugSession> { ) -> Entity<DebugSession> {
workspace workspace
.update(cx, |workspace, _window, cx| { .update(cx, |multi, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); multi.workspace().update(cx, |workspace, cx| {
debug_panel let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
.update(cx, |this, _| this.active_session()) debug_panel
.unwrap() .update(cx, |this, _| this.active_session())
.unwrap()
})
}) })
.unwrap() .unwrap()
} }
pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>( pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
workspace: &WindowHandle<Workspace>, workspace: &WindowHandle<MultiWorkspace>,
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
config: DebugTaskDefinition, config: DebugTaskDefinition,
configure: T, configure: T,
) -> Result<Entity<Session>> { ) -> Result<Entity<Session>> {
let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure); let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure);
workspace.update(cx, |workspace, window, cx| { workspace.update(cx, |multi, window, cx| {
workspace.start_debug_session( multi.workspace().update(cx, |workspace, cx| {
config.to_scenario(), workspace.start_debug_session(
SharedTaskContext::default(), config.to_scenario(),
None, SharedTaskContext::default(),
None, None,
window, None,
cx, window,
) cx,
)
})
})?; })?;
cx.run_until_parked(); cx.run_until_parked();
let session = workspace.read_with(cx, |workspace, cx| { let session = workspace.read_with(cx, |workspace, cx| {
workspace workspace
.workspace()
.read(cx)
.panel::<DebugPanel>(cx) .panel::<DebugPanel>(cx)
.and_then(|panel| panel.read(cx).active_session()) .and_then(|panel| panel.read(cx).active_session())
.map(|session| session.read(cx).running_state().read(cx).session()) .map(|session| session.read(cx).running_state().read(cx).session())
@@ -131,7 +142,7 @@ pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
} }
pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>( pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
workspace: &WindowHandle<Workspace>, workspace: &WindowHandle<MultiWorkspace>,
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
configure: T, configure: T,
) -> Result<Entity<Session>> { ) -> Result<Entity<Session>> {
+31 -23
View File
@@ -60,7 +60,13 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
// assert we didn't show the attach modal // assert we didn't show the attach modal
workspace workspace
.update(cx, |workspace, _window, cx| { .update(cx, |workspace, _window, cx| {
assert!(workspace.active_modal::<AttachModal>(cx).is_none()); assert!(
workspace
.workspace()
.read(cx)
.active_modal::<AttachModal>(cx)
.is_none()
);
}) })
.unwrap(); .unwrap();
} }
@@ -97,9 +103,9 @@ async fn test_show_attach_modal_and_select_process(
}); });
}); });
let attach_modal = workspace let attach_modal = workspace
.update(cx, |workspace, window, cx| { .update(cx, |multi, window, cx| {
let workspace_handle = cx.weak_entity(); let workspace_handle = multi.workspace().downgrade();
workspace.toggle_modal(window, cx, |window, cx| { multi.toggle_modal(window, cx, |window, cx| {
AttachModal::with_processes( AttachModal::with_processes(
workspace_handle, workspace_handle,
vec![ vec![
@@ -133,7 +139,7 @@ async fn test_show_attach_modal_and_select_process(
) )
}); });
workspace.active_modal::<AttachModal>(cx).unwrap() multi.active_modal::<AttachModal>(cx).unwrap()
}) })
.unwrap(); .unwrap();
@@ -208,24 +214,26 @@ async fn test_attach_with_pick_pid_variable(executor: BackgroundExecutor, cx: &m
let pick_pid_placeholder = task::VariableName::PickProcessId.template_value(); let pick_pid_placeholder = task::VariableName::PickProcessId.template_value();
workspace workspace
.update(cx, |workspace, window, cx| { .update(cx, |multi, window, cx| {
workspace.start_debug_session( multi.workspace().update(cx, |workspace, cx| {
DebugTaskDefinition { workspace.start_debug_session(
adapter: FakeAdapter::ADAPTER_NAME.into(), DebugTaskDefinition {
label: "attach with picker".into(), adapter: FakeAdapter::ADAPTER_NAME.into(),
config: json!({ label: "attach with picker".into(),
"request": "attach", config: json!({
"process_id": pick_pid_placeholder, "request": "attach",
}), "process_id": pick_pid_placeholder,
tcp_connection: None, }),
} tcp_connection: None,
.to_scenario(), }
SharedTaskContext::default(), .to_scenario(),
None, SharedTaskContext::default(),
None, None,
window, None,
cx, window,
) cx,
);
})
}) })
.unwrap(); .unwrap();
@@ -145,15 +145,17 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
}; };
workspace workspace
.update(cx, |workspace, window, cx| { .update(cx, |multi, window, cx| {
workspace.start_debug_session( multi.workspace().update(cx, |workspace, cx| {
scenario, workspace.start_debug_session(
task_context.clone(), scenario,
None, task_context.clone(),
None, None,
window, None,
cx, window,
) cx,
);
})
}) })
.unwrap(); .unwrap();
@@ -182,8 +184,10 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace workspace
.update(cx, |workspace, window, cx| { .update(cx, |multi, window, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); multi.workspace().update(cx, |workspace, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
});
}) })
.unwrap(); .unwrap();
@@ -324,8 +328,10 @@ async fn test_debug_modal_subtitles_with_multiple_worktrees(
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace workspace
.update(cx, |workspace, window, cx| { .update(cx, |multi, window, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); multi.workspace().update(cx, |workspace, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
});
}) })
.unwrap(); .unwrap();
@@ -1113,8 +1113,8 @@ async fn test_stack_frame_filter_persistence(
let workspace = init_test_workspace(&project, cx).await; let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace workspace
.update(cx, |workspace, _, _| { .update(cx, |workspace, _, cx| {
workspace.set_random_database_id(); workspace.set_random_database_id(cx);
}) })
.unwrap(); .unwrap();
@@ -1211,7 +1211,7 @@ async fn test_stack_frame_filter_persistence(
cx.run_until_parked(); cx.run_until_parked();
let workspace_id = workspace let workspace_id = workspace
.update(cx, |workspace, _window, _cx| workspace.database_id()) .update(cx, |workspace, _window, cx| workspace.database_id(cx))
.ok() .ok()
.flatten() .flatten()
.expect("workspace id has to be some for this test to work properly"); .expect("workspace id has to be some for this test to work properly");
+2 -1
View File
@@ -24,11 +24,12 @@ worktree.workspace = true
workspace.workspace = true workspace.workspace = true
[dev-dependencies] [dev-dependencies]
fs.workspace = true fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] }
serde_json.workspace = true serde_json.workspace = true
settings = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] }
theme.workspace = true
workspace = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] }
worktree = { workspace = true, features = ["test-support"] } worktree = { workspace = true, features = ["test-support"] }
+89 -211
View File
@@ -2,19 +2,17 @@ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fmt::Display, fmt::Display,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc,
}; };
use gpui::AsyncWindowContext;
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use serde::Deserialize; use serde::Deserialize;
use settings::{DevContainerConnection, Settings as _}; use settings::DevContainerConnection;
use smol::{fs, process::Command}; use smol::{fs, process::Command};
use util::rel_path::RelPath; use util::rel_path::RelPath;
use workspace::Workspace; use workspace::Workspace;
use worktree::Snapshot; use worktree::Snapshot;
use crate::{DevContainerFeature, DevContainerSettings, DevContainerTemplate}; use crate::{DevContainerContext, DevContainerFeature, DevContainerTemplate};
/// Represents a discovered devcontainer configuration /// Represents a discovered devcontainer configuration
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -67,6 +65,31 @@ pub(crate) struct DevContainerConfigurationOutput {
configuration: DevContainerConfiguration, configuration: DevContainerConfiguration,
} }
pub(crate) struct DevContainerCli {
pub path: PathBuf,
node_runtime_path: Option<PathBuf>,
}
impl DevContainerCli {
fn command(&self, use_podman: bool) -> Command {
let mut command = if let Some(node_runtime_path) = &self.node_runtime_path {
let mut command = util::command::new_smol_command(
node_runtime_path.as_os_str().display().to_string(),
);
command.arg(self.path.display().to_string());
command
} else {
util::command::new_smol_command(self.path.display().to_string())
};
if use_podman {
command.arg("--docker-path");
command.arg("podman");
}
command
}
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum DevContainerError { pub enum DevContainerError {
DockerNotAvailable, DockerNotAvailable,
@@ -107,87 +130,23 @@ impl Display for DevContainerError {
} }
} }
pub(crate) async fn read_devcontainer_configuration_for_project(
cx: &mut AsyncWindowContext,
node_runtime: &NodeRuntime,
) -> Result<DevContainerConfigurationOutput, DevContainerError> {
let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?;
let Some(directory) = project_directory(cx) else {
return Err(DevContainerError::NotInValidProject);
};
devcontainer_read_configuration(
&path_to_devcontainer_cli,
found_in_path,
node_runtime,
&directory,
None,
use_podman(cx),
)
.await
}
pub(crate) async fn apply_dev_container_template(
template: &DevContainerTemplate,
options_selected: &HashMap<String, String>,
features_selected: &HashSet<DevContainerFeature>,
cx: &mut AsyncWindowContext,
node_runtime: &NodeRuntime,
) -> Result<DevContainerApply, DevContainerError> {
let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?;
let Some(directory) = project_directory(cx) else {
return Err(DevContainerError::NotInValidProject);
};
devcontainer_template_apply(
template,
options_selected,
features_selected,
&path_to_devcontainer_cli,
found_in_path,
node_runtime,
&directory,
false, // devcontainer template apply does not use --docker-path option
)
.await
}
fn use_podman(cx: &mut AsyncWindowContext) -> bool {
cx.update(|_, cx| DevContainerSettings::get_global(cx).use_podman)
.unwrap_or(false)
}
/// Finds all available devcontainer configurations in the project. /// Finds all available devcontainer configurations in the project.
/// ///
/// See [`find_configs_in_snapshot`] for the locations that are scanned. /// See [`find_configs_in_snapshot`] for the locations that are scanned.
pub fn find_devcontainer_configs(cx: &mut AsyncWindowContext) -> Vec<DevContainerConfig> { pub fn find_devcontainer_configs(workspace: &Workspace, cx: &gpui::App) -> Vec<DevContainerConfig> {
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else { let project = workspace.project().read(cx);
log::debug!("find_devcontainer_configs: No workspace found");
let worktree = project
.visible_worktrees(cx)
.find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
let Some(worktree) = worktree else {
log::debug!("find_devcontainer_configs: No worktree found");
return Vec::new(); return Vec::new();
}; };
let Ok(configs) = workspace.update(cx, |workspace, _, cx| { let worktree = worktree.read(cx);
let project = workspace.project().read(cx); find_configs_in_snapshot(worktree)
let worktree = project
.visible_worktrees(cx)
.find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
let Some(worktree) = worktree else {
log::debug!("find_devcontainer_configs: No worktree found");
return Vec::new();
};
let worktree = worktree.read(cx);
find_configs_in_snapshot(worktree)
}) else {
log::debug!("find_devcontainer_configs: Failed to update workspace");
return Vec::new();
};
configs
} }
/// Scans a worktree snapshot for devcontainer configurations. /// Scans a worktree snapshot for devcontainer configurations.
@@ -280,60 +239,36 @@ pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec<DevContainerConfig>
} }
pub async fn start_dev_container_with_config( pub async fn start_dev_container_with_config(
cx: &mut AsyncWindowContext, context: DevContainerContext,
node_runtime: NodeRuntime,
config: Option<DevContainerConfig>, config: Option<DevContainerConfig>,
) -> Result<(DevContainerConnection, String), DevContainerError> { ) -> Result<(DevContainerConnection, String), DevContainerError> {
let use_podman = use_podman(cx); check_for_docker(context.use_podman).await?;
check_for_docker(use_podman).await?; let cli = ensure_devcontainer_cli(&context.node_runtime).await?;
let config_path = config.map(|c| context.project_directory.join(&c.config_path));
let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?; match devcontainer_up(&context, &cli, config_path.as_deref()).await {
let Some(directory) = project_directory(cx) else {
return Err(DevContainerError::NotInValidProject);
};
let config_path = config.map(|c| directory.join(&c.config_path));
match devcontainer_up(
&path_to_devcontainer_cli,
found_in_path,
&node_runtime,
directory.clone(),
config_path.clone(),
use_podman,
)
.await
{
Ok(DevContainerUp { Ok(DevContainerUp {
container_id, container_id,
remote_workspace_folder, remote_workspace_folder,
remote_user, remote_user,
.. ..
}) => { }) => {
let project_name = match devcontainer_read_configuration( let project_name =
&path_to_devcontainer_cli, match read_devcontainer_configuration(&context, &cli, config_path.as_deref()).await
found_in_path, {
&node_runtime, Ok(DevContainerConfigurationOutput {
&directory, configuration:
config_path.as_ref(), DevContainerConfiguration {
use_podman, name: Some(project_name),
) },
.await }) => project_name,
{ _ => get_backup_project_name(&remote_workspace_folder, &container_id),
Ok(DevContainerConfigurationOutput { };
configuration:
DevContainerConfiguration {
name: Some(project_name),
},
}) => project_name,
_ => get_backup_project_name(&remote_workspace_folder, &container_id),
};
let connection = DevContainerConnection { let connection = DevContainerConnection {
name: project_name, name: project_name,
container_id: container_id, container_id,
use_podman, use_podman: context.use_podman,
remote_user, remote_user,
}; };
@@ -377,9 +312,9 @@ async fn check_for_docker(use_podman: bool) -> Result<(), DevContainerError> {
} }
} }
async fn ensure_devcontainer_cli( pub(crate) async fn ensure_devcontainer_cli(
node_runtime: &NodeRuntime, node_runtime: &NodeRuntime,
) -> Result<(PathBuf, bool), DevContainerError> { ) -> Result<DevContainerCli, DevContainerError> {
let mut command = util::command::new_smol_command(&dev_container_cli()); let mut command = util::command::new_smol_command(&dev_container_cli());
command.arg("--version"); command.arg("--version");
@@ -417,7 +352,10 @@ async fn ensure_devcontainer_cli(
Ok(output) => { Ok(output) => {
if output.status.success() { if output.status.success() {
log::info!("Found devcontainer CLI in Data dir"); log::info!("Found devcontainer CLI in Data dir");
return Ok((datadir_cli_path.clone(), false)); return Ok(DevContainerCli {
path: datadir_cli_path.clone(),
node_runtime_path: Some(node_runtime_path.clone()),
});
} else { } else {
log::error!( log::error!(
"Could not run devcontainer CLI from data_dir. Will try once more to install. Output: {:?}", "Could not run devcontainer CLI from data_dir. Will try once more to install. Output: {:?}",
@@ -457,32 +395,29 @@ async fn ensure_devcontainer_cli(
); );
Err(DevContainerError::DevContainerCliNotAvailable) Err(DevContainerError::DevContainerCliNotAvailable)
} else { } else {
Ok((datadir_cli_path, false)) Ok(DevContainerCli {
path: datadir_cli_path,
node_runtime_path: Some(node_runtime_path),
})
} }
} else { } else {
log::info!("Found devcontainer cli on $PATH, using it"); log::info!("Found devcontainer cli on $PATH, using it");
Ok((PathBuf::from(&dev_container_cli()), true)) Ok(DevContainerCli {
path: PathBuf::from(&dev_container_cli()),
node_runtime_path: None,
})
} }
} }
async fn devcontainer_up( async fn devcontainer_up(
path_to_cli: &PathBuf, context: &DevContainerContext,
found_in_path: bool, cli: &DevContainerCli,
node_runtime: &NodeRuntime, config_path: Option<&Path>,
path: Arc<Path>,
config_path: Option<PathBuf>,
use_podman: bool,
) -> Result<DevContainerUp, DevContainerError> { ) -> Result<DevContainerUp, DevContainerError> {
let Ok(node_runtime_path) = node_runtime.binary_path().await else { let mut command = cli.command(context.use_podman);
log::error!("Unable to find node runtime path");
return Err(DevContainerError::NodeRuntimeNotAvailable);
};
let mut command =
devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman);
command.arg("up"); command.arg("up");
command.arg("--workspace-folder"); command.arg("--workspace-folder");
command.arg(path.display().to_string()); command.arg(context.project_directory.display().to_string());
if let Some(config) = config_path { if let Some(config) = config_path {
command.arg("--config"); command.arg("--config");
@@ -515,24 +450,15 @@ async fn devcontainer_up(
} }
} }
async fn devcontainer_read_configuration( pub(crate) async fn read_devcontainer_configuration(
path_to_cli: &PathBuf, context: &DevContainerContext,
found_in_path: bool, cli: &DevContainerCli,
node_runtime: &NodeRuntime, config_path: Option<&Path>,
path: &Arc<Path>,
config_path: Option<&PathBuf>,
use_podman: bool,
) -> Result<DevContainerConfigurationOutput, DevContainerError> { ) -> Result<DevContainerConfigurationOutput, DevContainerError> {
let Ok(node_runtime_path) = node_runtime.binary_path().await else { let mut command = cli.command(context.use_podman);
log::error!("Unable to find node runtime path");
return Err(DevContainerError::NodeRuntimeNotAvailable);
};
let mut command =
devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman);
command.arg("read-configuration"); command.arg("read-configuration");
command.arg("--workspace-folder"); command.arg("--workspace-folder");
command.arg(path.display().to_string()); command.arg(context.project_directory.display().to_string());
if let Some(config) = config_path { if let Some(config) = config_path {
command.arg("--config"); command.arg("--config");
@@ -562,23 +488,14 @@ async fn devcontainer_read_configuration(
} }
} }
async fn devcontainer_template_apply( pub(crate) async fn apply_dev_container_template(
template: &DevContainerTemplate, template: &DevContainerTemplate,
template_options: &HashMap<String, String>, template_options: &HashMap<String, String>,
features_selected: &HashSet<DevContainerFeature>, features_selected: &HashSet<DevContainerFeature>,
path_to_cli: &PathBuf, context: &DevContainerContext,
found_in_path: bool, cli: &DevContainerCli,
node_runtime: &NodeRuntime,
path: &Arc<Path>,
use_podman: bool,
) -> Result<DevContainerApply, DevContainerError> { ) -> Result<DevContainerApply, DevContainerError> {
let Ok(node_runtime_path) = node_runtime.binary_path().await else { let mut command = cli.command(context.use_podman);
log::error!("Unable to find node runtime path");
return Err(DevContainerError::NodeRuntimeNotAvailable);
};
let mut command =
devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman);
let Ok(serialized_options) = serde_json::to_string(template_options) else { let Ok(serialized_options) = serde_json::to_string(template_options) else {
log::error!("Unable to serialize options for {:?}", template_options); log::error!("Unable to serialize options for {:?}", template_options);
@@ -588,7 +505,7 @@ async fn devcontainer_template_apply(
command.arg("templates"); command.arg("templates");
command.arg("apply"); command.arg("apply");
command.arg("--workspace-folder"); command.arg("--workspace-folder");
command.arg(path.display().to_string()); command.arg(context.project_directory.display().to_string());
command.arg("--template-id"); command.arg("--template-id");
command.arg(format!( command.arg(format!(
"{}/{}", "{}/{}",
@@ -652,28 +569,6 @@ fn parse_json_from_cli<T: serde::de::DeserializeOwned>(raw: &str) -> Result<T, D
}) })
} }
fn devcontainer_cli_command(
path_to_cli: &PathBuf,
found_in_path: bool,
node_runtime_path: &PathBuf,
use_podman: bool,
) -> Command {
let mut command = if found_in_path {
util::command::new_smol_command(path_to_cli.display().to_string())
} else {
let mut command =
util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string());
command.arg(path_to_cli.display().to_string());
command
};
if use_podman {
command.arg("--docker-path");
command.arg("podman");
}
command
}
fn get_backup_project_name(remote_workspace_folder: &str, container_id: &str) -> String { fn get_backup_project_name(remote_workspace_folder: &str, container_id: &str) -> String {
Path::new(remote_workspace_folder) Path::new(remote_workspace_folder)
.file_name() .file_name()
@@ -682,22 +577,6 @@ fn get_backup_project_name(remote_workspace_folder: &str, container_id: &str) ->
.unwrap_or_else(|| container_id.to_string()) .unwrap_or_else(|| container_id.to_string())
} }
fn project_directory(cx: &mut AsyncWindowContext) -> Option<Arc<Path>> {
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
return None;
};
match workspace.update(cx, |workspace, _, cx| {
workspace.project().read(cx).active_project_directory(cx)
}) {
Ok(dir) => dir,
Err(e) => {
log::error!("Error getting project directory from workspace: {:?}", e);
None
}
}
}
fn template_features_to_json(features_selected: &HashSet<DevContainerFeature>) -> String { fn template_features_to_json(features_selected: &HashSet<DevContainerFeature>) -> String {
let features_map = features_selected let features_map = features_selected
.iter() .iter()
@@ -725,6 +604,9 @@ fn template_features_to_json(features_selected: &HashSet<DevContainerFeature>) -
mod tests { mod tests {
use std::path::PathBuf; use std::path::PathBuf;
use crate::devcontainer_api::{
DevContainerConfig, DevContainerUp, find_configs_in_snapshot, parse_json_from_cli,
};
use fs::FakeFs; use fs::FakeFs;
use gpui::TestAppContext; use gpui::TestAppContext;
use project::Project; use project::Project;
@@ -732,10 +614,6 @@ mod tests {
use settings::SettingsStore; use settings::SettingsStore;
use util::path; use util::path;
use crate::devcontainer_api::{
DevContainerConfig, DevContainerUp, find_configs_in_snapshot, parse_json_from_cli,
};
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
+62 -19
View File
@@ -1,3 +1,5 @@
use std::path::Path;
use gpui::AppContext; use gpui::AppContext;
use gpui::Entity; use gpui::Entity;
use gpui::Task; use gpui::Task;
@@ -41,7 +43,8 @@ use http_client::{AsyncBody, HttpClient};
mod devcontainer_api; mod devcontainer_api;
use devcontainer_api::read_devcontainer_configuration_for_project; use devcontainer_api::ensure_devcontainer_cli;
use devcontainer_api::read_devcontainer_configuration;
use crate::devcontainer_api::DevContainerError; use crate::devcontainer_api::DevContainerError;
use crate::devcontainer_api::apply_dev_container_template; use crate::devcontainer_api::apply_dev_container_template;
@@ -51,11 +54,34 @@ pub use devcontainer_api::{
start_dev_container_with_config, start_dev_container_with_config,
}; };
pub struct DevContainerContext {
pub project_directory: Arc<Path>,
pub use_podman: bool,
pub node_runtime: node_runtime::NodeRuntime,
}
impl DevContainerContext {
pub fn from_workspace(workspace: &Workspace, cx: &App) -> Option<Self> {
let project_directory = workspace.project().read(cx).active_project_directory(cx)?;
let use_podman = DevContainerSettings::get_global(cx).use_podman;
let node_runtime = workspace.app_state().node_runtime.clone();
Some(Self {
project_directory,
use_podman,
node_runtime,
})
}
}
#[derive(RegisterSetting)] #[derive(RegisterSetting)]
struct DevContainerSettings { struct DevContainerSettings {
use_podman: bool, use_podman: bool,
} }
pub fn use_podman(cx: &App) -> bool {
DevContainerSettings::get_global(cx).use_podman
}
impl Settings for DevContainerSettings { impl Settings for DevContainerSettings {
fn from_settings(content: &settings::SettingsContent) -> Self { fn from_settings(content: &settings::SettingsContent) -> Self {
Self { Self {
@@ -1420,22 +1446,41 @@ fn dispatch_apply_templates(
cx: &mut Context<DevContainerModal>, cx: &mut Context<DevContainerModal>,
) { ) {
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
if let Some(tree_id) = workspace.update(cx, |workspace, cx| { let Some((tree_id, context)) = workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone(); let worktree = workspace
let worktree = project.read(cx).visible_worktrees(cx).find_map(|tree| { .project()
tree.read(cx) .read(cx)
.root_entry()? .visible_worktrees(cx)
.is_dir() .find_map(|tree| {
.then_some(tree.read(cx)) tree.read(cx)
}); .root_entry()?
worktree.map(|w| w.id()) .is_dir()
}) { .then_some(tree.read(cx))
let node_runtime = workspace.read_with(cx, |workspace, _| { });
workspace.app_state().node_runtime.clone() let tree_id = worktree.map(|w| w.id())?;
}); let context = DevContainerContext::from_workspace(workspace, cx)?;
Some((tree_id, context))
}) else {
return;
};
let Ok(cli) = ensure_devcontainer_cli(&context.node_runtime).await else {
this.update_in(cx, |this, window, cx| {
this.accept_message(
DevContainerMessage::FailedToWriteTemplate(
DevContainerError::DevContainerCliNotAvailable,
),
window,
cx,
);
})
.log_err();
return;
};
{
if check_for_existing if check_for_existing
&& read_devcontainer_configuration_for_project(cx, &node_runtime) && read_devcontainer_configuration(&context, &cli, None)
.await .await
.is_ok() .is_ok()
{ {
@@ -1454,8 +1499,8 @@ fn dispatch_apply_templates(
&template_entry.template, &template_entry.template,
&template_entry.options_selected, &template_entry.options_selected,
&template_entry.features_selected, &template_entry.features_selected,
cx, &context,
&node_runtime, &cli,
) )
.await .await
{ {
@@ -1497,8 +1542,6 @@ fn dispatch_apply_templates(
this.dismiss(&menu::Cancel, window, cx); this.dismiss(&menu::Cancel, window, cx);
}) })
.ok(); .ok();
} else {
return;
} }
}) })
.detach(); .detach();
+1 -1
View File
@@ -904,7 +904,7 @@ impl Render for BufferDiagnosticsEditor {
.style(ButtonStyle::Transparent) .style(ButtonStyle::Transparent)
.tooltip(Tooltip::text("Open File")) .tooltip(Tooltip::text("Open File"))
.on_click(cx.listener(|buffer_diagnostics, _, window, cx| { .on_click(cx.listener(|buffer_diagnostics, _, window, cx| {
if let Some(workspace) = window.root::<Workspace>().flatten() { if let Some(workspace) = Workspace::for_window(window, cx) {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
workspace workspace
.open_path( .open_path(
+37 -24
View File
@@ -28,6 +28,7 @@ use std::{
}; };
use unindent::Unindent as _; use unindent::Unindent as _;
use util::{RandomCharIter, path, post_inc, rel_path::rel_path}; use util::{RandomCharIter, path, post_inc, rel_path::rel_path};
use workspace::MultiWorkspace;
#[ctor::ctor] #[ctor::ctor]
fn init_logger() { fn init_logger() {
@@ -68,9 +69,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
let language_server_id = LanguageServerId(0); let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let workspace = window.root(cx).unwrap(); let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap(); let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
// Create some diagnostics // Create some diagnostics
@@ -344,9 +347,11 @@ async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
let server_id_2 = LanguageServerId(101); let server_id_2 = LanguageServerId(101);
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let workspace = window.root(cx).unwrap(); let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let diagnostics = window.build_entity(cx, |window, cx| { let diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
@@ -453,9 +458,11 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
let server_id_2 = LanguageServerId(101); let server_id_2 = LanguageServerId(101);
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let workspace = window.root(cx).unwrap(); let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let diagnostics = window.build_entity(cx, |window, cx| { let diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
@@ -663,9 +670,11 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let workspace = window.root(cx).unwrap(); let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let mutated_diagnostics = window.build_entity(cx, |window, cx| { let mutated_diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
@@ -836,9 +845,11 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let workspace = window.root(cx).unwrap(); let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let mutated_diagnostics = window.build_entity(cx, |window, cx| { let mutated_diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
@@ -1389,9 +1400,11 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
let language_server_id = LanguageServerId(0); let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let workspace = window.root(cx).unwrap(); let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let uri = lsp::Uri::from_file_path(path!("/root/main.js")).unwrap(); let uri = lsp::Uri::from_file_path(path!("/root/main.js")).unwrap();
// Create diagnostics with code fields // Create diagnostics with code fields
@@ -1618,8 +1631,8 @@ async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
.await; .await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let project_path = project::ProjectPath { let project_path = project::ProjectPath {
worktree_id: project.read_with(cx, |project, cx| { worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
@@ -1772,8 +1785,8 @@ async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
.await; .await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let project_path = project::ProjectPath { let project_path = project::ProjectPath {
worktree_id: project.read_with(cx, |project, cx| { worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
@@ -1901,8 +1914,8 @@ async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
.await; .await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let project_path = project::ProjectPath { let project_path = project::ProjectPath {
worktree_id: project.read_with(cx, |project, cx| { worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
@@ -119,7 +119,7 @@ impl Render for EditPredictionButton {
IconButton::new("copilot-error", icon) IconButton::new("copilot-error", icon)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.on_click(cx.listener(move |_, _, window, cx| { .on_click(cx.listener(move |_, _, window, cx| {
if let Some(workspace) = window.root::<Workspace>().flatten() { if let Some(workspace) = Workspace::for_window(window, cx) {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
let copilot = copilot.clone(); let copilot = copilot.clone();
workspace.show_toast( workspace.show_toast(
+73 -88
View File
@@ -415,14 +415,14 @@ mod tests {
}; };
use futures::StreamExt; use futures::StreamExt;
use gpui::{Rgba, TestAppContext, VisualTestContext}; use gpui::{Rgba, TestAppContext};
use language::FakeLspAdapter; use language::FakeLspAdapter;
use languages::rust_lang; use languages::rust_lang;
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use serde_json::json; use serde_json::json;
use util::{path, rel_path::rel_path}; use util::{path, rel_path::rel_path};
use workspace::{ use workspace::{
CloseActiveItem, MoveItemToPaneInDirection, OpenOptions, CloseActiveItem, MoveItemToPaneInDirection, MultiWorkspace, OpenOptions,
item::{Item as _, SaveOptions}, item::{Item as _, SaveOptions},
}; };
@@ -460,9 +460,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, [path!("/a").as_ref()], cx).await; let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
let workspace = let (multi_workspace, cx) =
cx.add_window(|window, cx| workspace::Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx); let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let language_registry = project.read_with(cx, |project, _| project.languages().clone()); let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang()); language_registry.add(rust_lang());
@@ -490,7 +490,7 @@ mod tests {
); );
let editor = workspace let editor = workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_abs_path( workspace.open_abs_path(
PathBuf::from(path!("/a/first.rs")), PathBuf::from(path!("/a/first.rs")),
OpenOptions::default(), OpenOptions::default(),
@@ -498,7 +498,6 @@ mod tests {
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap() .unwrap()
.downcast::<Editor>() .downcast::<Editor>()
@@ -579,53 +578,49 @@ mod tests {
}); });
// opening another file in a split should not influence the LSP query counter // opening another file in a split should not influence the LSP query counter
workspace workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { assert_eq!(
assert_eq!( workspace.panes().len(),
workspace.panes().len(), 1,
1, "Should have one pane with one editor"
"Should have one pane with one editor" );
); workspace.move_item_to_pane_in_direction(
workspace.move_item_to_pane_in_direction( &MoveItemToPaneInDirection {
&MoveItemToPaneInDirection { direction: workspace::SplitDirection::Right,
direction: workspace::SplitDirection::Right, focus: false,
focus: false, clone: true,
clone: true, },
}, window,
window, cx,
cx, );
); });
})
.unwrap();
cx.run_until_parked(); cx.run_until_parked();
workspace workspace.update_in(cx, |workspace, _, cx| {
.update(cx, |workspace, _, cx| { let panes = workspace.panes();
let panes = workspace.panes(); assert_eq!(panes.len(), 2, "Should have two panes after splitting");
assert_eq!(panes.len(), 2, "Should have two panes after splitting"); for pane in panes {
for pane in panes { let editor = pane
let editor = pane .read(cx)
.read(cx) .active_item()
.active_item() .and_then(|item| item.downcast::<Editor>())
.and_then(|item| item.downcast::<Editor>()) .expect("Should have opened an editor in each split");
.expect("Should have opened an editor in each split"); let editor_file = editor
let editor_file = editor .read(cx)
.read(cx) .buffer()
.buffer() .read(cx)
.read(cx) .as_singleton()
.as_singleton() .expect("test deals with singleton buffers")
.expect("test deals with singleton buffers") .read(cx)
.read(cx) .file()
.file() .expect("test buffese should have a file")
.expect("test buffese should have a file") .path();
.path(); assert_eq!(
assert_eq!( editor_file.as_ref(),
editor_file.as_ref(), rel_path("first.rs"),
rel_path("first.rs"), "Both editors should be opened for the same file"
"Both editors should be opened for the same file" )
) }
} });
})
.unwrap();
cx.executor().advance_clock(Duration::from_millis(500)); cx.executor().advance_clock(Duration::from_millis(500));
let save = editor.update_in(cx, |editor, window, cx| { let save = editor.update_in(cx, |editor, window, cx| {
@@ -652,54 +647,44 @@ mod tests {
); );
drop(editor); drop(editor);
let close = workspace let close = workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, cx| { pane.close_active_item(&CloseActiveItem::default(), window, cx)
pane.close_active_item(&CloseActiveItem::default(), window, cx)
})
}) })
.unwrap(); });
close.await.unwrap(); close.await.unwrap();
let close = workspace let close = workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, cx| { pane.close_active_item(&CloseActiveItem::default(), window, cx)
pane.close_active_item(&CloseActiveItem::default(), window, cx)
})
}) })
.unwrap(); });
close.await.unwrap(); close.await.unwrap();
assert_eq!( assert_eq!(
2, 2,
requests_made.load(atomic::Ordering::Acquire), requests_made.load(atomic::Ordering::Acquire),
"After saving and closing all editors, no extra requests should be made" "After saving and closing all editors, no extra requests should be made"
); );
workspace workspace.update_in(cx, |workspace, _, cx| {
.update(cx, |workspace, _, cx| { assert!(
assert!( workspace.active_item(cx).is_none(),
workspace.active_item(cx).is_none(), "Should close all editors"
"Should close all editors" )
) });
})
.unwrap();
workspace workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, cx| { pane.navigate_backward(&workspace::GoBack, window, cx);
pane.navigate_backward(&workspace::GoBack, window, cx);
})
}) })
.unwrap(); });
cx.executor().advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT); cx.executor().advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT);
cx.run_until_parked(); cx.run_until_parked();
let editor = workspace let editor = workspace.update_in(cx, |workspace, _, cx| {
.update(cx, |workspace, _, cx| { workspace
workspace .active_item(cx)
.active_item(cx) .expect("Should have reopened the editor again after navigating back")
.expect("Should have reopened the editor again after navigating back") .downcast::<Editor>()
.downcast::<Editor>() .expect("Should be an editor")
.expect("Should be an editor") });
})
.unwrap();
assert_eq!( assert_eq!(
2, 2,
+20 -2
View File
@@ -3114,6 +3114,24 @@ impl Editor {
self.workspace.as_ref()?.0.upgrade() self.workspace.as_ref()?.0.upgrade()
} }
/// Detaches a task and shows an error notification in the workspace if available,
/// otherwise just logs the error.
pub fn detach_and_notify_err<R, E>(
&self,
task: Task<Result<R, E>>,
window: &mut Window,
cx: &mut App,
) where
E: std::fmt::Debug + std::fmt::Display + 'static,
R: 'static,
{
if let Some(workspace) = self.workspace() {
task.detach_and_notify_err(workspace.downgrade(), window, cx);
} else {
task.detach_and_log_err(cx);
}
}
/// Returns the workspace serialization ID if this editor should be serialized. /// Returns the workspace serialization ID if this editor should be serialized.
fn workspace_serialization_id(&self, _cx: &App) -> Option<WorkspaceId> { fn workspace_serialization_id(&self, _cx: &App) -> Option<WorkspaceId> {
self.workspace self.workspace
@@ -11481,8 +11499,8 @@ impl Editor {
let Some(project) = self.project.clone() else { let Some(project) = self.project.clone() else {
return; return;
}; };
self.reload(project, window, cx) let task = self.reload(project, window, cx);
.detach_and_notify_err(window, cx); self.detach_and_notify_err(task, window, cx);
} }
pub fn restore_file( pub fn restore_file(
File diff suppressed because it is too large. Load diff
+10 -11
View File
@@ -99,7 +99,6 @@ use workspace::{
CollaboratorId, ItemHandle, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, CollaboratorId, ItemHandle, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel,
Workspace, Workspace,
item::{BreadcrumbText, Item, ItemBufferKind}, item::{BreadcrumbText, Item, ItemBufferKind},
notifications::NotifyTaskExt,
}; };
/// Determines what kinds of highlights should be applied to a lines background. /// Determines what kinds of highlights should be applied to a lines background.
@@ -541,21 +540,21 @@ impl EditorElement {
register_action(editor, window, |editor, action, window, cx| { register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.format(action, window, cx) { if let Some(task) = editor.format(action, window, cx) {
task.detach_and_notify_err(window, cx); editor.detach_and_notify_err(task, window, cx);
} else { } else {
cx.propagate(); cx.propagate();
} }
}); });
register_action(editor, window, |editor, action, window, cx| { register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.format_selections(action, window, cx) { if let Some(task) = editor.format_selections(action, window, cx) {
task.detach_and_notify_err(window, cx); editor.detach_and_notify_err(task, window, cx);
} else { } else {
cx.propagate(); cx.propagate();
} }
}); });
register_action(editor, window, |editor, action, window, cx| { register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.organize_imports(action, window, cx) { if let Some(task) = editor.organize_imports(action, window, cx) {
task.detach_and_notify_err(window, cx); editor.detach_and_notify_err(task, window, cx);
} else { } else {
cx.propagate(); cx.propagate();
} }
@@ -565,49 +564,49 @@ impl EditorElement {
register_action(editor, window, Editor::show_character_palette); register_action(editor, window, Editor::show_character_palette);
register_action(editor, window, |editor, action, window, cx| { register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_completion(action, window, cx) { if let Some(task) = editor.confirm_completion(action, window, cx) {
task.detach_and_notify_err(window, cx); editor.detach_and_notify_err(task, window, cx);
} else { } else {
cx.propagate(); cx.propagate();
} }
}); });
register_action(editor, window, |editor, action, window, cx| { register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_completion_replace(action, window, cx) { if let Some(task) = editor.confirm_completion_replace(action, window, cx) {
task.detach_and_notify_err(window, cx); editor.detach_and_notify_err(task, window, cx);
} else { } else {
cx.propagate(); cx.propagate();
} }
}); });
register_action(editor, window, |editor, action, window, cx| { register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_completion_insert(action, window, cx) { if let Some(task) = editor.confirm_completion_insert(action, window, cx) {
task.detach_and_notify_err(window, cx); editor.detach_and_notify_err(task, window, cx);
} else { } else {
cx.propagate(); cx.propagate();
} }
}); });
register_action(editor, window, |editor, action, window, cx| { register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.compose_completion(action, window, cx) { if let Some(task) = editor.compose_completion(action, window, cx) {
task.detach_and_notify_err(window, cx); editor.detach_and_notify_err(task, window, cx);
} else { } else {
cx.propagate(); cx.propagate();
} }
}); });
register_action(editor, window, |editor, action, window, cx| { register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_code_action(action, window, cx) { if let Some(task) = editor.confirm_code_action(action, window, cx) {
task.detach_and_notify_err(window, cx); editor.detach_and_notify_err(task, window, cx);
} else { } else {
cx.propagate(); cx.propagate();
} }
}); });
register_action(editor, window, |editor, action, window, cx| { register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.rename(action, window, cx) { if let Some(task) = editor.rename(action, window, cx) {
task.detach_and_notify_err(window, cx); editor.detach_and_notify_err(task, window, cx);
} else { } else {
cx.propagate(); cx.propagate();
} }
}); });
register_action(editor, window, |editor, action, window, cx| { register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_rename(action, window, cx) { if let Some(task) = editor.confirm_rename(action, window, cx) {
task.detach_and_notify_err(window, cx); editor.detach_and_notify_err(task, window, cx);
} else { } else {
cx.propagate(); cx.propagate();
} }
+1 -1
View File
@@ -719,7 +719,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) { pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) {
if let Ok(uri) = Url::parse(&link) if let Ok(uri) = Url::parse(&link)
&& uri.scheme() == "file" && uri.scheme() == "file"
&& let Some(workspace) = window.root::<Workspace>().flatten() && let Some(workspace) = Workspace::for_window(window, cx)
{ {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
let task = workspace.open_abs_path( let task = workspace.open_abs_path(
+25 -12
View File
@@ -2017,6 +2017,7 @@ fn restore_serialized_buffer_contents(
mod tests { mod tests {
use crate::editor_tests::init_test; use crate::editor_tests::init_test;
use fs::Fs; use fs::Fs;
use workspace::MultiWorkspace;
use super::*; use super::*;
use fs::MTime; use fs::MTime;
@@ -2071,8 +2072,10 @@ mod tests {
// Test case 1: Deserialize with path and contents // Test case 1: Deserialize with path and contents
{ {
let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); MultiWorkspace::test_new(project.clone(), window, cx)
});
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
let item_id = 1234 as ItemId; let item_id = 1234 as ItemId;
let mtime = fs let mtime = fs
@@ -2108,8 +2111,10 @@ mod tests {
// Test case 2: Deserialize with only path // Test case 2: Deserialize with only path
{ {
let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); MultiWorkspace::test_new(project.clone(), window, cx)
});
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
@@ -2146,8 +2151,10 @@ mod tests {
project.languages().add(languages::rust_lang()) project.languages().add(languages::rust_lang())
}); });
let (workspace, cx) = let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); MultiWorkspace::test_new(project.clone(), window, cx)
});
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
@@ -2182,8 +2189,10 @@ mod tests {
// Test case 4: Deserialize with path, content, and old mtime // Test case 4: Deserialize with path, content, and old mtime
{ {
let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); MultiWorkspace::test_new(project.clone(), window, cx)
});
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
@@ -2212,8 +2221,10 @@ mod tests {
// Test case 5: Deserialize with no path, no content, no language, and no old mtime (new, empty, unsaved buffer) // Test case 5: Deserialize with no path, no content, no language, and no old mtime (new, empty, unsaved buffer)
{ {
let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); MultiWorkspace::test_new(project.clone(), window, cx)
});
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
@@ -2252,8 +2263,10 @@ mod tests {
// Create an empty project with no worktrees // Create an empty project with no worktrees
let project = Project::test(fs.clone(), [], cx).await; let project = Project::test(fs.clone(), [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); MultiWorkspace::test_new(project.clone(), window, cx)
});
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
let item_id = 11000 as ItemId; let item_id = 11000 as ItemId;
+32 -37
View File
@@ -417,14 +417,12 @@ fn convert_token(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{ use std::{
ops::{Deref as _, Range}, ops::Range,
sync::atomic::{self, AtomicUsize}, sync::atomic::{self, AtomicUsize},
}; };
use futures::StreamExt as _; use futures::StreamExt as _;
use gpui::{ use gpui::{AppContext as _, Entity, Focusable as _, HighlightStyle, TestAppContext};
AppContext as _, Entity, Focusable as _, HighlightStyle, TestAppContext, VisualTestContext,
};
use language::{Language, LanguageConfig, LanguageMatcher}; use language::{Language, LanguageConfig, LanguageMatcher};
use languages::FakeLspAdapter; use languages::FakeLspAdapter;
use multi_buffer::{ use multi_buffer::{
@@ -434,7 +432,7 @@ mod tests {
use rope::Point; use rope::Point;
use serde_json::json; use serde_json::json;
use settings::{LanguageSettingsContent, SemanticTokenRules, SemanticTokens, SettingsStore}; use settings::{LanguageSettingsContent, SemanticTokenRules, SemanticTokens, SettingsStore};
use workspace::{Workspace, WorkspaceHandle as _}; use workspace::{MultiWorkspace, WorkspaceHandle as _};
use crate::{ use crate::{
Capability, Capability,
@@ -854,12 +852,11 @@ mod tests {
) )
.await; .await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let (multi_workspace, cx) =
let workspace = window.root(cx).unwrap(); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
project project
.update(&mut cx, |project, cx| { .update(cx, |project, cx| {
project.find_or_create_worktree(EditorLspTestContext::root_path(), true, cx) project.find_or_create_worktree(EditorLspTestContext::root_path(), true, cx)
}) })
.await .await
@@ -869,7 +866,7 @@ mod tests {
let toml_file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let toml_file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let toml_item = workspace let toml_item = workspace
.update_in(&mut cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_path(toml_file, None, true, window, cx) workspace.open_path(toml_file, None, true, window, cx)
}) })
.await .await
@@ -881,7 +878,7 @@ mod tests {
.expect("Opened test file wasn't an editor") .expect("Opened test file wasn't an editor")
}); });
editor.update_in(&mut cx, |editor, window, cx| { editor.update_in(cx, |editor, window, cx| {
let nav_history = workspace let nav_history = workspace
.read(cx) .read(cx)
.active_pane() .active_pane()
@@ -895,11 +892,11 @@ mod tests {
let _toml_server_2 = toml_server_2.next().await.unwrap(); let _toml_server_2 = toml_server_2.next().await.unwrap();
// Trigger semantic tokens. // Trigger semantic tokens.
editor.update_in(&mut cx, |editor, _, cx| { editor.update_in(cx, |editor, _, cx| {
editor.edit([(MultiBufferOffset(0)..MultiBufferOffset(1), "b")], cx); editor.edit([(MultiBufferOffset(0)..MultiBufferOffset(1), "b")], cx);
}); });
cx.executor().advance_clock(Duration::from_millis(200)); cx.executor().advance_clock(Duration::from_millis(200));
let task = editor.update_in(&mut cx, |e, _, _| e.semantic_token_state.take_update_task()); let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task());
cx.run_until_parked(); cx.run_until_parked();
task.await; task.await;
@@ -1074,12 +1071,11 @@ mod tests {
) )
.await; .await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let (multi_workspace, cx) =
let workspace = window.root(cx).unwrap(); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
project project
.update(&mut cx, |project, cx| { .update(cx, |project, cx| {
project.find_or_create_worktree(EditorLspTestContext::root_path(), true, cx) project.find_or_create_worktree(EditorLspTestContext::root_path(), true, cx)
}) })
.await .await
@@ -1089,7 +1085,7 @@ mod tests {
let toml_file = cx.read(|cx| workspace.file_project_paths(cx)[1].clone()); let toml_file = cx.read(|cx| workspace.file_project_paths(cx)[1].clone());
let rust_file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let rust_file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let (toml_item, rust_item) = workspace.update_in(&mut cx, |workspace, window, cx| { let (toml_item, rust_item) = workspace.update_in(cx, |workspace, window, cx| {
( (
workspace.open_path(toml_file, None, true, window, cx), workspace.open_path(toml_file, None, true, window, cx),
workspace.open_path(rust_file, None, true, window, cx), workspace.open_path(rust_file, None, true, window, cx),
@@ -1139,12 +1135,12 @@ mod tests {
multibuffer multibuffer
}); });
let editor = workspace.update_in(&mut cx, |workspace, window, cx| { let editor = workspace.update_in(cx, |workspace, window, cx| {
let editor = cx.new(|cx| build_editor_with_project(project, multibuffer, window, cx)); let editor = cx.new(|cx| build_editor_with_project(project, multibuffer, window, cx));
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
editor editor
}); });
editor.update_in(&mut cx, |editor, window, cx| { editor.update_in(cx, |editor, window, cx| {
let nav_history = workspace let nav_history = workspace
.read(cx) .read(cx)
.active_pane() .active_pane()
@@ -1159,7 +1155,7 @@ mod tests {
// Initial request. // Initial request.
cx.executor().advance_clock(Duration::from_millis(200)); cx.executor().advance_clock(Duration::from_millis(200));
let task = editor.update_in(&mut cx, |e, _, _| e.semantic_token_state.take_update_task()); let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task());
cx.run_until_parked(); cx.run_until_parked();
task.await; task.await;
assert_eq!(full_counter_toml.load(atomic::Ordering::Acquire), 1); assert_eq!(full_counter_toml.load(atomic::Ordering::Acquire), 1);
@@ -1174,8 +1170,8 @@ mod tests {
// Get the excerpt id for the TOML excerpt and expand it down by 2 lines. // Get the excerpt id for the TOML excerpt and expand it down by 2 lines.
let toml_excerpt_id = let toml_excerpt_id =
editor.read_with(&cx, |editor, cx| editor.buffer().read(cx).excerpt_ids()[0]); editor.read_with(cx, |editor, cx| editor.buffer().read(cx).excerpt_ids()[0]);
editor.update_in(&mut cx, |editor, _, cx| { editor.update_in(cx, |editor, _, cx| {
editor.buffer().update(cx, |buffer, cx| { editor.buffer().update(cx, |buffer, cx| {
buffer.expand_excerpts([toml_excerpt_id], 2, ExpandExcerptDirection::Down, cx); buffer.expand_excerpts([toml_excerpt_id], 2, ExpandExcerptDirection::Down, cx);
}); });
@@ -1183,7 +1179,7 @@ mod tests {
// Wait for semantic tokens to be re-fetched after expansion. // Wait for semantic tokens to be re-fetched after expansion.
cx.executor().advance_clock(Duration::from_millis(200)); cx.executor().advance_clock(Duration::from_millis(200));
let task = editor.update_in(&mut cx, |e, _, _| e.semantic_token_state.take_update_task()); let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task());
cx.run_until_parked(); cx.run_until_parked();
task.await; task.await;
@@ -1306,12 +1302,11 @@ mod tests {
) )
.await; .await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let (multi_workspace, cx) =
let workspace = window.root(cx).unwrap(); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
project project
.update(&mut cx, |project, cx| { .update(cx, |project, cx| {
project.find_or_create_worktree(EditorLspTestContext::root_path(), true, cx) project.find_or_create_worktree(EditorLspTestContext::root_path(), true, cx)
}) })
.await .await
@@ -1321,7 +1316,7 @@ mod tests {
let toml_file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let toml_file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let toml_item = workspace let toml_item = workspace
.update_in(&mut cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_path(toml_file, None, true, window, cx) workspace.open_path(toml_file, None, true, window, cx)
}) })
.await .await
@@ -1355,10 +1350,10 @@ mod tests {
multibuffer multibuffer
}); });
let editor = workspace.update_in(&mut cx, |_, window, cx| { let editor = workspace.update_in(cx, |_, window, cx| {
cx.new(|cx| build_editor_with_project(project, multibuffer, window, cx)) cx.new(|cx| build_editor_with_project(project, multibuffer, window, cx))
}); });
editor.update_in(&mut cx, |editor, window, cx| { editor.update_in(cx, |editor, window, cx| {
let nav_history = workspace let nav_history = workspace
.read(cx) .read(cx)
.active_pane() .active_pane()
@@ -1372,7 +1367,7 @@ mod tests {
// Initial request. // Initial request.
cx.executor().advance_clock(Duration::from_millis(200)); cx.executor().advance_clock(Duration::from_millis(200));
let task = editor.update_in(&mut cx, |e, _, _| e.semantic_token_state.take_update_task()); let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task());
cx.run_until_parked(); cx.run_until_parked();
task.await; task.await;
assert_eq!(full_counter_toml.load(atomic::Ordering::Acquire), 1); assert_eq!(full_counter_toml.load(atomic::Ordering::Acquire), 1);
@@ -1381,12 +1376,12 @@ mod tests {
// //
// Without debouncing, this grabs semantic tokens 4 times (twice for the // Without debouncing, this grabs semantic tokens 4 times (twice for the
// toml editor, and twice for the multibuffer). // toml editor, and twice for the multibuffer).
editor.update_in(&mut cx, |editor, _, cx| { editor.update_in(cx, |editor, _, cx| {
editor.edit([(MultiBufferOffset(0)..MultiBufferOffset(1), "b")], cx); editor.edit([(MultiBufferOffset(0)..MultiBufferOffset(1), "b")], cx);
editor.edit([(MultiBufferOffset(12)..MultiBufferOffset(13), "c")], cx); editor.edit([(MultiBufferOffset(12)..MultiBufferOffset(13), "c")], cx);
}); });
cx.executor().advance_clock(Duration::from_millis(200)); cx.executor().advance_clock(Duration::from_millis(200));
let task = editor.update_in(&mut cx, |e, _, _| e.semantic_token_state.take_update_task()); let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task());
cx.run_until_parked(); cx.run_until_parked();
task.await; task.await;
assert_eq!( assert_eq!(
+4 -3
View File
@@ -2087,7 +2087,7 @@ mod tests {
use rand::rngs::StdRng; use rand::rngs::StdRng;
use settings::{DiffViewStyle, SettingsStore}; use settings::{DiffViewStyle, SettingsStore};
use ui::{VisualContext as _, div, px}; use ui::{VisualContext as _, div, px};
use workspace::Workspace; use workspace::MultiWorkspace;
use crate::SplittableEditor; use crate::SplittableEditor;
use crate::display_map::{BlockPlacement, BlockProperties, BlockStyle}; use crate::display_map::{BlockPlacement, BlockProperties, BlockStyle};
@@ -2105,8 +2105,9 @@ mod tests {
crate::init(cx); crate::init(cx);
}); });
let project = Project::test(FakeFs::new(cx.executor()), [], cx).await; let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let rhs_multibuffer = cx.new(|cx| { let rhs_multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
multibuffer.set_all_diff_hunks_expanded(cx); multibuffer.set_all_diff_hunks_expanded(cx);
@@ -22,7 +22,7 @@ use language::{
use lsp::{notification, request}; use lsp::{notification, request};
use project::Project; use project::Project;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use workspace::{AppState, Workspace, WorkspaceHandle}; use workspace::{AppState, MultiWorkspace, Workspace, WorkspaceHandle};
use super::editor_test_context::{AssertionContextManager, EditorTestContext}; use super::editor_test_context::{AssertionContextManager, EditorTestContext};
@@ -95,7 +95,8 @@ impl EditorLspTestContext {
) )
.await; .await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window =
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap(); let workspace = window.root(cx).unwrap();
@@ -106,12 +107,20 @@ impl EditorLspTestContext {
}) })
.await .await
.unwrap(); .unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) cx.read(|cx| {
.await; workspace
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); .read(cx)
.workspace()
.read(cx)
.worktree_scans_complete(cx)
})
.await;
let file = cx.read(|cx| workspace.read(cx).workspace().file_project_paths(cx)[0].clone());
let item = workspace let item = workspace
.update_in(&mut cx, |workspace, window, cx| { .update_in(&mut cx, |workspace, window, cx| {
workspace.open_path(file, None, true, window, cx) workspace.workspace().update(cx, |workspace, cx| {
workspace.open_path(file, None, true, window, cx)
})
}) })
.await .await
.expect("Could not open test file"); .expect("Could not open test file");
@@ -121,6 +130,8 @@ impl EditorLspTestContext {
}); });
editor.update_in(&mut cx, |editor, window, cx| { editor.update_in(&mut cx, |editor, window, cx| {
let nav_history = workspace let nav_history = workspace
.read(cx)
.workspace()
.read(cx) .read(cx)
.active_pane() .active_pane()
.read(cx) .read(cx)
@@ -134,6 +145,8 @@ impl EditorLspTestContext {
// Ensure the language server is fully registered with the buffer // Ensure the language server is fully registered with the buffer
cx.executor().run_until_parked(); cx.executor().run_until_parked();
let workspace = cx.read(|cx| workspace.read(cx).workspace().clone());
Self { Self {
cx: EditorTestContext { cx: EditorTestContext {
cx, cx,
+4
View File
@@ -16,6 +16,10 @@ pub struct AgentV2FeatureFlag;
impl FeatureFlag for AgentV2FeatureFlag { impl FeatureFlag for AgentV2FeatureFlag {
const NAME: &'static str = "agent-v2"; const NAME: &'static str = "agent-v2";
fn enabled_for_staff() -> bool {
true
}
} }
pub struct AcpBetaFeatureFlag; pub struct AcpBetaFeatureFlag;
+5 -2
View File
@@ -1566,9 +1566,12 @@ impl PickerDelegate for FileFinderDelegate {
.unwrap_or(0) .unwrap_or(0)
.saturating_sub(1); .saturating_sub(1);
let finder = self.file_finder.clone(); let finder = self.file_finder.clone();
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |_, cx| { cx.spawn_in(window, async move |_, mut cx| {
let item = open_task.await.notify_async_err(cx)?; let item = open_task
.await
.notify_workspace_async_err(workspace, &mut cx)?;
if let Some(row) = row if let Some(row) = row
&& let Some(active_editor) = item.downcast::<Editor>() && let Some(active_editor) = item.downcast::<Editor>()
{ {
+104 -38
View File
@@ -9,7 +9,9 @@ use project::{FS_WATCH_LATENCY, RemoveOptions};
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use util::{path, rel_path::rel_path}; use util::{path, rel_path::rel_path};
use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace, open_paths}; use workspace::{
AppState, CloseActiveItem, MultiWorkspace, OpenOptions, ToggleFileFinder, Workspace, open_paths,
};
#[ctor::ctor] #[ctor::ctor]
fn init_logger() { fn init_logger() {
@@ -1109,7 +1111,9 @@ async fn test_history_items_uniqueness_for_multiple_worktree(cx: &mut TestAppCon
) )
.await; .await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let (worktree_id1, worktree_id2) = cx.read(|cx| { let (worktree_id1, worktree_id2) = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
(worktrees[0].read(cx).id(), worktrees[1].read(cx).id()) (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
@@ -1207,7 +1211,9 @@ async fn test_create_file_for_multiple_worktrees(cx: &mut TestAppContext) {
) )
.await; .await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let (_worktree_id1, worktree_id2) = cx.read(|cx| { let (_worktree_id1, worktree_id2) = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
(worktrees[0].read(cx).id(), worktrees[1].read(cx).id()) (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
@@ -1282,7 +1288,9 @@ async fn test_create_file_no_focused_with_multiple_worktrees(cx: &mut TestAppCon
) )
.await; .await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let (_worktree_id1, worktree_id2) = cx.read(|cx| { let (_worktree_id1, worktree_id2) = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
(worktrees[0].read(cx).id(), worktrees[1].read(cx).id()) (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
@@ -1334,7 +1342,9 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let worktree_id = cx.read(|cx| { let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
@@ -1423,7 +1433,9 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let worktree_id = cx.read(|cx| { let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1); assert_eq!(worktrees.len(), 1);
@@ -1565,7 +1577,9 @@ async fn test_history_match_positions(cx: &mut gpui::TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
workspace.update_in(cx, |_workspace, window, cx| window.focused(cx)); workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
@@ -1642,7 +1656,9 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
.detach(); .detach();
cx.background_executor.run_until_parked(); cx.background_executor.run_until_parked();
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let worktree_id = cx.read(|cx| { let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1,); assert_eq!(worktrees.len(), 1,);
@@ -1741,7 +1757,9 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
// generate some history to select from // generate some history to select from
open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
@@ -1797,7 +1815,9 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let worktree_id = cx.read(|cx| { let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1,); assert_eq!(worktrees.len(), 1,);
@@ -1903,7 +1923,9 @@ async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
// generate some history to select from // generate some history to select from
open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await; open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await; open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
@@ -1957,7 +1979,9 @@ async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppCon
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
// Open new buffer // Open new buffer
open_queried_buffer("1", 1, "1_qw", &workspace, cx).await; open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
@@ -1991,7 +2015,9 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await; open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await; open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
@@ -2099,7 +2125,9 @@ async fn test_setting_auto_select_first_and_select_active_file(cx: &mut TestAppC
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await; open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await; open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
@@ -2155,7 +2183,9 @@ async fn test_non_separate_history_items(cx: &mut TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await; open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await; open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
@@ -2250,7 +2280,9 @@ async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
open_queried_buffer("2", 1, "2.txt", &workspace, cx).await; open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
@@ -2308,7 +2340,9 @@ async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await; open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await; open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
@@ -2369,7 +2403,9 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
// generate some history to select from // generate some history to select from
open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
@@ -2414,7 +2450,9 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext)
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); // generate some history to select from
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
@@ -2462,8 +2500,9 @@ async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAp
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
// Initial state // Initial state
let picker = open_file_picker(&workspace, cx); let picker = open_file_picker(&workspace, cx);
@@ -2534,8 +2573,14 @@ async fn test_search_results_refreshed_on_standalone_file_creation(cx: &mut gpui
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (workspace, cx) = let window = cx.add_window({
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let project = project.clone();
|window, cx| MultiWorkspace::test_new(project, window, cx)
});
let cx = VisualTestContext::from_window(*window, cx).into_mut();
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
cx.update(|_, cx| { cx.update(|_, cx| {
open_paths( open_paths(
@@ -2589,8 +2634,9 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let worktree_1_id = project.update(cx, |project, cx| { let worktree_1_id = project.update(cx, |project, cx| {
let worktree = project.worktrees(cx).last().expect("worktree not found"); let worktree = project.worktrees(cx).last().expect("worktree not found");
worktree.read(cx).id() worktree.read(cx).id()
@@ -2680,7 +2726,9 @@ async fn test_history_items_uniqueness_for_multiple_worktree_open_all_files(
) )
.await; .await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let (worktree_id1, worktree_id2) = cx.read(|cx| { let (worktree_id1, worktree_id2) = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
(worktrees[0].read(cx).id(), worktrees[1].read(cx).id()) (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
@@ -2804,8 +2852,9 @@ async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpu
} }
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
// Initial state // Initial state
let picker = open_file_picker(&workspace, cx); let picker = open_file_picker(&workspace, cx);
@@ -2863,8 +2912,9 @@ async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list(
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
// Initial state // Initial state
let picker = open_file_picker(&workspace, cx); let picker = open_file_picker(&workspace, cx);
@@ -2902,7 +2952,9 @@ async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
@@ -2930,7 +2982,9 @@ async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext)
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
open_queried_buffer("2", 1, "2.txt", &workspace, cx).await; open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
@@ -2970,7 +3024,9 @@ async fn test_switches_between_release_norelease_modes_on_forward_nav(
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
open_queried_buffer("2", 1, "2.txt", &workspace, cx).await; open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
@@ -3026,7 +3082,9 @@ async fn test_switches_between_release_norelease_modes_on_backward_nav(
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
open_queried_buffer("2", 1, "2.txt", &workspace, cx).await; open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
@@ -3081,7 +3139,9 @@ async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::Test
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
@@ -3112,7 +3172,9 @@ async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
cx.dispatch_action(ToggleFileFinder::default()); cx.dispatch_action(ToggleFileFinder::default());
let picker = active_file_picker(&workspace, cx); let picker = active_file_picker(&workspace, cx);
@@ -3231,7 +3293,9 @@ fn build_find_picker(
Entity<Workspace>, Entity<Workspace>,
&mut VisualTestContext, &mut VisualTestContext,
) { ) {
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let picker = open_file_picker(&workspace, cx); let picker = open_file_picker(&workspace, cx);
(picker, workspace, cx) (picker, workspace, cx)
} }
@@ -3469,7 +3533,9 @@ async fn test_clear_navigation_history(cx: &mut TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
workspace.update_in(cx, |_workspace, window, cx| window.focused(cx)); workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
+10 -5
View File
@@ -1295,6 +1295,7 @@ mod tests {
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use util::path; use util::path;
use workspace::MultiWorkspace;
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| { cx.update(|cx| {
@@ -1347,13 +1348,17 @@ mod tests {
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await; let project = Project::test(fs, [], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let window_handle =
cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = window_handle
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let branch_list = workspace let branch_list = window_handle
.update(cx, |workspace, window, cx| { .update(cx, |_multi_workspace, window, cx| {
cx.new(|cx| { cx.new(|cx| {
let mut delegate = BranchListDelegate::new( let mut delegate = BranchListDelegate::new(
workspace.weak_handle(), workspace.downgrade(),
repository, repository,
BranchListStyle::Modal, BranchListStyle::Modal,
cx, cx,
@@ -1380,7 +1385,7 @@ mod tests {
}) })
.unwrap(); .unwrap();
let cx = VisualTestContext::from_window(*workspace, cx); let cx = VisualTestContext::from_window(window_handle.into(), cx);
(branch_list, cx) (branch_list, cx)
} }
+1 -1
View File
@@ -774,7 +774,7 @@ impl CommitView {
callback(repo, &sha, stash, commit_view_entity, workspace_weak, cx).await?; callback(repo, &sha, stash, commit_view_entity, workspace_weak, cx).await?;
anyhow::Ok(()) anyhow::Ok(())
}) })
.detach_and_notify_err(window, cx); .detach_and_notify_err(workspace.weak_handle(), window, cx);
} }
async fn close_commit_view( async fn close_commit_view(
+11 -10
View File
@@ -6,7 +6,7 @@ use editor::{Editor, EditorEvent, MultiBuffer};
use futures::{FutureExt, select_biased}; use futures::{FutureExt, select_biased};
use gpui::{ use gpui::{
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle, AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, Render, Task, Window, Focusable, IntoElement, Render, Task, WeakEntity, Window,
}; };
use language::{Buffer, LanguageRegistry}; use language::{Buffer, LanguageRegistry};
use project::Project; use project::Project;
@@ -40,11 +40,10 @@ impl FileDiffView {
pub fn open( pub fn open(
old_path: PathBuf, old_path: PathBuf,
new_path: PathBuf, new_path: PathBuf,
workspace: &Workspace, workspace: WeakEntity<Workspace>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Task<Result<Entity<Self>>> { ) -> Task<Result<Entity<Self>>> {
let workspace = workspace.weak_handle();
window.spawn(cx, async move |cx| { window.spawn(cx, async move |cx| {
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?; let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
let old_buffer = project let old_buffer = project
@@ -374,7 +373,7 @@ mod tests {
use std::path::PathBuf; use std::path::PathBuf;
use unindent::unindent; use unindent::unindent;
use util::path; use util::path;
use workspace::Workspace; use workspace::MultiWorkspace;
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| { cx.update(|cx| {
@@ -400,15 +399,16 @@ mod tests {
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let diff_view = workspace let diff_view = workspace
.update_in(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
FileDiffView::open( FileDiffView::open(
path!("/test/old_file.txt").into(), path!("/test/old_file.txt").into(),
path!("/test/new_file.txt").into(), path!("/test/new_file.txt").into(),
workspace, workspace.weak_handle(),
window, window,
cx, cx,
) )
@@ -534,15 +534,16 @@ mod tests {
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let diff_view = workspace let diff_view = workspace
.update_in(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
FileDiffView::open( FileDiffView::open(
PathBuf::from(path!("/test/old_file.txt")), PathBuf::from(path!("/test/old_file.txt")),
PathBuf::from(path!("/test/new_file.txt")), PathBuf::from(path!("/test/new_file.txt")),
workspace, workspace.weak_handle(),
window, window,
cx, cx,
) )
+61 -34
View File
@@ -1274,10 +1274,11 @@ impl GitPanel {
}) })
.ok()?; .ok()?;
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |_, mut cx| { cx.spawn_in(window, async move |_, mut cx| {
let item = open_task let item = open_task
.await .await
.notify_async_err(&mut cx) .notify_workspace_async_err(workspace, &mut cx)
.ok_or_else(|| anyhow::anyhow!("Failed to open file"))?; .ok_or_else(|| anyhow::anyhow!("Failed to open file"))?;
if let Some(active_editor) = item.downcast::<Editor>() { if let Some(active_editor) = item.downcast::<Editor>() {
if let Some(diff_task) = if let Some(diff_task) =
@@ -6262,6 +6263,8 @@ mod tests {
use util::path; use util::path;
use util::rel_path::rel_path; use util::rel_path::rel_path;
use workspace::MultiWorkspace;
use super::*; use super::*;
fn init_test(cx: &mut gpui::TestAppContext) { fn init_test(cx: &mut gpui::TestAppContext) {
@@ -6308,9 +6311,12 @@ mod tests {
let project = let project =
Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await; Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await;
let workspace = let window_handle =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx); let workspace = window_handle
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
cx.read(|cx| { cx.read(|cx| {
project project
@@ -6327,7 +6333,7 @@ mod tests {
cx.executor().run_until_parked(); cx.executor().run_until_parked();
let panel = workspace.update(cx, GitPanel::new).unwrap(); let panel = workspace.update_in(cx, GitPanel::new);
let handle = cx.update_window_entity(&panel, |panel, _, _| { let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
@@ -6429,9 +6435,12 @@ mod tests {
); );
let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await; let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;
let workspace = let window_handle =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx); let workspace = window_handle
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
cx.read(|cx| { cx.read(|cx| {
project project
@@ -6448,7 +6457,7 @@ mod tests {
cx.executor().run_until_parked(); cx.executor().run_until_parked();
let panel = workspace.update(cx, GitPanel::new).unwrap(); let panel = workspace.update_in(cx, GitPanel::new);
let handle = cx.update_window_entity(&panel, |panel, _, _| { let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
@@ -6621,9 +6630,12 @@ mod tests {
); );
let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await; let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;
let workspace = let window_handle =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx); let workspace = window_handle
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
cx.read(|cx| { cx.read(|cx| {
project project
@@ -6640,7 +6652,7 @@ mod tests {
cx.executor().run_until_parked(); cx.executor().run_until_parked();
let panel = workspace.update(cx, GitPanel::new).unwrap(); let panel = workspace.update_in(cx, GitPanel::new);
let handle = cx.update_window_entity(&panel, |panel, _, _| { let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
@@ -6832,11 +6844,14 @@ mod tests {
); );
let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await; let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;
let workspace = let window_handle =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx); let workspace = window_handle
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
let panel = workspace.update(cx, GitPanel::new).unwrap(); let panel = workspace.update_in(cx, GitPanel::new);
// Test: User has commit message, enables amend (saves message), then disables (restores message) // Test: User has commit message, enables amend (saves message), then disables (restores message)
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
@@ -6901,16 +6916,19 @@ mod tests {
); );
let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await; let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;
let workspace = let window_handle =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx); let workspace = window_handle
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
// Wait for the project scanning to finish so that `head_commit(cx)` is // Wait for the project scanning to finish so that `head_commit(cx)` is
// actually set, otherwise no head commit would be available from which // actually set, otherwise no head commit would be available from which
// to fetch the latest commit message from. // to fetch the latest commit message from.
cx.executor().run_until_parked(); cx.executor().run_until_parked();
let panel = workspace.update(cx, GitPanel::new).unwrap(); let panel = workspace.update_in(cx, GitPanel::new);
panel.read_with(cx, |panel, cx| { panel.read_with(cx, |panel, cx| {
assert!(panel.active_repository.is_some()); assert!(panel.active_repository.is_some());
assert!(panel.head_commit(cx).is_some()); assert!(panel.head_commit(cx).is_some());
@@ -6987,10 +7005,13 @@ mod tests {
); );
let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await; let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await;
let workspace = let window_handle =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx); let workspace = window_handle
let panel = workspace.update(cx, GitPanel::new).unwrap(); .read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
let panel = workspace.update_in(cx, GitPanel::new);
// Enable the `sort_by_path` setting and wait for entries to be updated, // Enable the `sort_by_path` setting and wait for entries to be updated,
// as there should no longer be separators between Tracked and Untracked // as there should no longer be separators between Tracked and Untracked
@@ -7016,7 +7037,7 @@ mod tests {
}); });
cx.run_until_parked(); cx.run_until_parked();
let _ = workspace.update(cx, |workspace, _window, cx| { workspace.update_in(cx, |workspace, _window, cx| {
let active_path = workspace let active_path = workspace
.item_of_type::<ProjectDiff>(cx) .item_of_type::<ProjectDiff>(cx)
.expect("ProjectDiff should exist") .expect("ProjectDiff should exist")
@@ -7060,9 +7081,12 @@ mod tests {
); );
let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await; let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await;
let workspace = let window_handle =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx); let workspace = window_handle
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
cx.read(|cx| { cx.read(|cx| {
project project
@@ -7087,7 +7111,7 @@ mod tests {
}); });
}); });
let panel = workspace.update(cx, GitPanel::new).unwrap(); let panel = workspace.update_in(cx, GitPanel::new);
let handle = cx.update_window_entity(&panel, |panel, _, _| { let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
@@ -7246,10 +7270,13 @@ mod tests {
); );
let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await; let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await;
let workspace = let window_handle =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx); let workspace = window_handle
let panel = workspace.update(cx, GitPanel::new).unwrap(); .read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
let panel = workspace.update_in(cx, GitPanel::new);
let handle = cx.update_window_entity(&panel, |panel, _, _| { let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
+34 -21
View File
@@ -124,6 +124,7 @@ impl ProjectDiff {
return; return;
} }
let workspace = cx.entity(); let workspace = cx.entity();
let workspace_weak = workspace.downgrade();
window window
.spawn(cx, async move |cx| { .spawn(cx, async move |cx| {
let this = cx let this = cx
@@ -138,7 +139,7 @@ impl ProjectDiff {
.ok(); .ok();
anyhow::Ok(()) anyhow::Ok(())
}) })
.detach_and_notify_err(window, cx); .detach_and_notify_err(workspace_weak, window, cx);
} }
pub fn deploy_at( pub fn deploy_at(
@@ -1851,6 +1852,8 @@ mod tests {
rel_path::{RelPath, rel_path}, rel_path::{RelPath, rel_path},
}; };
use workspace::MultiWorkspace;
use super::*; use super::*;
#[ctor::ctor] #[ctor::ctor]
@@ -1898,8 +1901,9 @@ mod tests {
&[("foo.txt", "foo\n".into())], &[("foo.txt", "foo\n".into())],
); );
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let diff = cx.new_window_entity(|window, cx| { let diff = cx.new_window_entity(|window, cx| {
ProjectDiff::new(project.clone(), workspace, window, cx) ProjectDiff::new(project.clone(), workspace, window, cx)
}); });
@@ -1946,8 +1950,9 @@ mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let diff = cx.new_window_entity(|window, cx| { let diff = cx.new_window_entity(|window, cx| {
ProjectDiff::new(project.clone(), workspace, window, cx) ProjectDiff::new(project.clone(), workspace, window, cx)
}); });
@@ -2016,8 +2021,9 @@ mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
fs.set_head_for_repo( fs.set_head_for_repo(
path!("/project/.git").as_ref(), path!("/project/.git").as_ref(),
&[("foo", "original\n".into())], &[("foo", "original\n".into())],
@@ -2146,8 +2152,9 @@ mod tests {
); );
let project = Project::test(fs, [Path::new(path!("/a"))], cx).await; let project = Project::test(fs, [Path::new(path!("/a"))], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
cx.run_until_parked(); cx.run_until_parked();
@@ -2260,8 +2267,9 @@ mod tests {
); );
let project = Project::test(fs, [Path::new(path!("/a"))], cx).await; let project = Project::test(fs, [Path::new(path!("/a"))], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
cx.run_until_parked(); cx.run_until_parked();
@@ -2315,8 +2323,9 @@ mod tests {
)], )],
); );
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let diff = cx.new_window_entity(|window, cx| { let diff = cx.new_window_entity(|window, cx| {
ProjectDiff::new(project.clone(), workspace, window, cx) ProjectDiff::new(project.clone(), workspace, window, cx)
}); });
@@ -2395,8 +2404,9 @@ mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let diff = cx.new_window_entity(|window, cx| { let diff = cx.new_window_entity(|window, cx| {
ProjectDiff::new(project.clone(), workspace, window, cx) ProjectDiff::new(project.clone(), workspace, window, cx)
}); });
@@ -2511,8 +2521,9 @@ mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let diff = cx let diff = cx
.update(|window, cx| { .update(|window, cx| {
ProjectDiff::new_with_default_branch(project.clone(), workspace, window, cx) ProjectDiff::new_with_default_branch(project.clone(), workspace, window, cx)
@@ -2608,8 +2619,9 @@ mod tests {
let worktree_id = project.read_with(cx, |project, cx| { let worktree_id = project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
}); });
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
cx.run_until_parked(); cx.run_until_parked();
let _editor = workspace let _editor = workspace
@@ -2693,8 +2705,9 @@ mod tests {
(worktrees[0].read(cx).id(), worktrees[1].read(cx).id()) (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
}); });
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
cx.run_until_parked(); cx.run_until_parked();
// Select project A via the dropdown override and open the diff. // Select project A via the dropdown override and open the diff.
+18 -18
View File
@@ -594,7 +594,7 @@ mod tests {
use picker::PickerDelegate; use picker::PickerDelegate;
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use settings::SettingsStore; use settings::SettingsStore;
use workspace::Workspace; use workspace::MultiWorkspace;
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| { cx.update(|cx| {
@@ -626,25 +626,27 @@ mod tests {
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await; let project = Project::test(fs, [], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let multi_workspace =
let cx = &mut VisualTestContext::from_window(*workspace, cx); cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
let cx = &mut VisualTestContext::from_window(*multi_workspace, cx);
let workspace = multi_workspace
.update(cx, |workspace, _, _| workspace.workspace().clone())
.unwrap();
let stash_entries = vec![ let stash_entries = vec![
stash_entry(0, "stash #0", Some("main")), stash_entry(0, "stash #0", Some("main")),
stash_entry(1, "stash #1", Some("develop")), stash_entry(1, "stash #1", Some("develop")),
]; ];
let stash_list = workspace let stash_list = workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { let weak_workspace = workspace.weak_handle();
let weak_workspace = workspace.weak_handle();
workspace.toggle_modal(window, cx, move |window, cx| { workspace.toggle_modal(window, cx, move |window, cx| {
StashList::new(None, weak_workspace, rems(34.), window, cx) StashList::new(None, weak_workspace, rems(34.), window, cx)
}); });
assert!(workspace.active_modal::<StashList>(cx).is_some()); assert!(workspace.active_modal::<StashList>(cx).is_some());
workspace.active_modal::<StashList>(cx).unwrap() workspace.active_modal::<StashList>(cx).unwrap()
}) });
.unwrap();
cx.run_until_parked(); cx.run_until_parked();
stash_list.update(cx, |stash_list, cx| { stash_list.update(cx, |stash_list, cx| {
@@ -667,10 +669,8 @@ mod tests {
stash_list.handle_show_stash(&Default::default(), window, cx); stash_list.handle_show_stash(&Default::default(), window, cx);
}); });
workspace workspace.update(cx, |workspace, cx| {
.update(cx, |workspace, _, cx| { assert!(workspace.active_modal::<StashList>(cx).is_none());
assert!(workspace.active_modal::<StashList>(cx).is_none()); });
})
.unwrap();
} }
} }
+4 -2
View File
@@ -450,6 +450,7 @@ mod tests {
use settings::SettingsStore; use settings::SettingsStore;
use unindent::unindent; use unindent::unindent;
use util::{path, test::marked_text_ranges}; use util::{path, test::marked_text_ranges};
use workspace::MultiWorkspace;
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| { cx.update(|cx| {
@@ -675,8 +676,9 @@ mod tests {
let project = Project::test(fs, [project_root.as_ref()], cx).await; let project = Project::test(fs, [project_root.as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let buffer = project let buffer = project
.update(cx, |project, cx| project.open_local_buffer(file_path, cx)) .update(cx, |project, cx| project.open_local_buffer(file_path, cx))
+26 -24
View File
@@ -4,8 +4,8 @@ use fuzzy::StringMatchCandidate;
use git::repository::Worktree as GitWorktree; use git::repository::Worktree as GitWorktree;
use gpui::{ use gpui::{
Action, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Focusable, InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement,
PathPromptOptions, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, PathPromptOptions, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window,
actions, rems, actions, rems,
}; };
@@ -20,7 +20,7 @@ use remote_connection::{RemoteConnectionModal, connect};
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*}; use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*};
use util::ResultExt; use util::ResultExt;
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr}; use workspace::{ModalView, MultiWorkspace, Workspace, notifications::DetachAndPromptErr};
actions!(git, [WorktreeFromDefault, WorktreeFromDefaultOnWindow]); actions!(git, [WorktreeFromDefault, WorktreeFromDefaultOnWindow]);
@@ -289,7 +289,6 @@ impl WorktreeListDelegate {
}; };
let branch = worktree_branch.to_string(); let branch = worktree_branch.to_string();
let window_handle = window.window_handle();
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
cx.spawn_in(window, async move |_, cx| { cx.spawn_in(window, async move |_, cx| {
let Some(paths) = worktree_path.await? else { let Some(paths) = worktree_path.await? else {
@@ -355,7 +354,7 @@ impl WorktreeListDelegate {
connection_options, connection_options,
vec![new_worktree_path], vec![new_worktree_path],
app_state, app_state,
window_handle, workspace.clone(),
replace_current_window, replace_current_window,
cx, cx,
) )
@@ -407,13 +406,12 @@ impl WorktreeListDelegate {
|e, _, _| Some(e.to_string()), |e, _, _| Some(e.to_string()),
); );
} else if let Some(connection_options) = connection_options { } else if let Some(connection_options) = connection_options {
let window_handle = window.window_handle();
cx.spawn_in(window, async move |_, cx| { cx.spawn_in(window, async move |_, cx| {
open_remote_worktree( open_remote_worktree(
connection_options, connection_options,
vec![path], vec![path],
app_state, app_state,
window_handle, workspace,
replace_current_window, replace_current_window,
cx, cx,
) )
@@ -441,15 +439,16 @@ async fn open_remote_worktree(
connection_options: RemoteConnectionOptions, connection_options: RemoteConnectionOptions,
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
app_state: Arc<workspace::AppState>, app_state: Arc<workspace::AppState>,
window: gpui::AnyWindowHandle, workspace: WeakEntity<Workspace>,
replace_current_window: bool, replace_current_window: bool,
cx: &mut AsyncApp, cx: &mut AsyncWindowContext,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let workspace_window = window let workspace_window = cx
.downcast::<Workspace>() .window_handle()
.downcast::<MultiWorkspace>()
.ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?; .ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?;
let connect_task = workspace_window.update(cx, |workspace, window, cx| { let connect_task = workspace.update_in(cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| { workspace.toggle_modal(window, cx, |window, cx| {
RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx) RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx)
}); });
@@ -473,17 +472,19 @@ async fn open_remote_worktree(
let session = connect_task.await; let session = connect_task.await;
workspace_window.update(cx, |workspace, _window, cx| { workspace
if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) { .update_in(cx, |workspace, _window, cx| {
prompt.update(cx, |prompt, cx| prompt.finished(cx)) if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
} prompt.update(cx, |prompt, cx| prompt.finished(cx))
})?; }
})
.ok();
let Some(Some(session)) = session else { let Some(Some(session)) = session else {
return Ok(()); return Ok(());
}; };
let new_project: Entity<project::Project> = cx.update(|cx| { let new_project: Entity<project::Project> = cx.update(|_, cx| {
project::Project::remote( project::Project::remote(
session, session,
app_state.client.clone(), app_state.client.clone(),
@@ -494,29 +495,30 @@ async fn open_remote_worktree(
true, true,
cx, cx,
) )
}); })?;
let window_to_use = if replace_current_window { let window_to_use = if replace_current_window {
workspace_window workspace_window
} else { } else {
let workspace_position = cx let workspace_position = cx
.update(|cx| { .update(|_, cx| {
workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx) workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
}) })?
.await .await
.context("fetching workspace position from db")?; .context("fetching workspace position from db")?;
let mut options = let mut options =
cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx)); cx.update(|_, cx| (app_state.build_window_options)(workspace_position.display, cx))?;
options.window_bounds = workspace_position.window_bounds; options.window_bounds = workspace_position.window_bounds;
cx.open_window(options, |window, cx| { cx.open_window(options, |window, cx| {
cx.new(|cx| { let workspace = cx.new(|cx| {
let mut workspace = let mut workspace =
Workspace::new(None, new_project.clone(), app_state.clone(), window, cx); Workspace::new(None, new_project.clone(), app_state.clone(), window, cx);
workspace.centered_layout = workspace_position.centered_layout; workspace.centered_layout = workspace_position.centered_layout;
workspace workspace
}) });
cx.new(|cx| MultiWorkspace::new(workspace, cx))
})? })?
}; };
+22 -15
View File
@@ -378,7 +378,7 @@ mod tests {
use serde_json::json; use serde_json::json;
use std::{num::NonZeroU32, sync::Arc, time::Duration}; use std::{num::NonZeroU32, sync::Arc, time::Duration};
use util::{path, rel_path::rel_path}; use util::{path, rel_path::rel_path};
use workspace::{AppState, Workspace}; use workspace::{AppState, MultiWorkspace, Workspace};
#[gpui::test] #[gpui::test]
async fn test_go_to_line_view_row_highlights(cx: &mut TestAppContext) { async fn test_go_to_line_view_row_highlights(cx: &mut TestAppContext) {
@@ -407,8 +407,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let worktree_id = workspace.update(cx, |workspace, cx| { let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| { workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
@@ -504,8 +505,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
workspace.update_in(cx, |workspace, window, cx| { workspace.update_in(cx, |workspace, window, cx| {
let cursor_position = cx.new(|_| CursorPosition::new(workspace)); let cursor_position = cx.new(|_| CursorPosition::new(workspace));
workspace.status_bar().update(cx, |status_bar, cx| { workspace.status_bar().update(cx, |status_bar, cx| {
@@ -589,8 +591,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
workspace.update_in(cx, |workspace, window, cx| { workspace.update_in(cx, |workspace, window, cx| {
let cursor_position = cx.new(|_| CursorPosition::new(workspace)); let cursor_position = cx.new(|_| CursorPosition::new(workspace));
workspace.status_bar().update(cx, |status_bar, cx| { workspace.status_bar().update(cx, |status_bar, cx| {
@@ -667,8 +670,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
workspace.update_in(cx, |workspace, window, cx| { workspace.update_in(cx, |workspace, window, cx| {
let cursor_position = cx.new(|_| CursorPosition::new(workspace)); let cursor_position = cx.new(|_| CursorPosition::new(workspace));
workspace.status_bar().update(cx, |status_bar, cx| { workspace.status_bar().update(cx, |status_bar, cx| {
@@ -843,8 +847,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let worktree_id = workspace.update(cx, |workspace, cx| { let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| { workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
@@ -900,8 +905,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let worktree_id = workspace.update(cx, |workspace, cx| { let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| { workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
@@ -955,8 +961,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let worktree_id = workspace.update(cx, |workspace, cx| { let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| { workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
+2
View File
@@ -265,6 +265,8 @@ pub enum IconName {
UserRoundPen, UserRoundPen,
Warning, Warning,
WholeWord, WholeWord,
WorkspaceNavClosed,
WorkspaceNavOpen,
XCircle, XCircle,
XCircleFilled, XCircleFilled,
ZedAgent, ZedAgent,
-1
View File
@@ -18,7 +18,6 @@ editor.workspace = true
fuzzy.workspace = true fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true language.workspace = true
platform_title_bar.workspace = true
project.workspace = true project.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde_json_lenient.workspace = true serde_json_lenient.workspace = true
+2 -3
View File
@@ -1,8 +1,7 @@
use anyhow::{Context as _, anyhow}; use anyhow::{Context as _, anyhow};
use gpui::{App, DivInspectorState, Inspector, InspectorElementId, IntoElement, Window}; use gpui::{App, DivInspectorState, Inspector, InspectorElementId, IntoElement, Window};
use platform_title_bar::PlatformTitleBar;
use std::{cell::OnceCell, path::Path, sync::Arc}; use std::{cell::OnceCell, path::Path, sync::Arc};
use ui::{Label, Tooltip, prelude::*}; use ui::{Label, Tooltip, prelude::*, utils::platform_title_bar_height};
use util::{ResultExt as _, command::new_smol_command}; use util::{ResultExt as _, command::new_smol_command};
use workspace::AppState; use workspace::AppState;
@@ -61,7 +60,7 @@ fn render_inspector(
let ui_font = theme::setup_ui_font(window, cx); let ui_font = theme::setup_ui_font(window, cx);
let colors = cx.theme().colors(); let colors = cx.theme().colors();
let inspector_id = inspector.active_element_id(); let inspector_id = inspector.active_element_id();
let toolbar_height = PlatformTitleBar::height(window); let toolbar_height = platform_title_bar_height(window);
v_flex() v_flex()
.size_full() .size_full()
+14 -11
View File
@@ -118,17 +118,20 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
})? })?
.await?; .await?;
new_workspace new_workspace
.update(cx, |workspace, window, cx| { .update(cx, |multi_workspace, window, cx| {
workspace.open_paths( let workspace = multi_workspace.workspace().clone();
vec![entry_path], workspace.update(cx, |workspace, cx| {
workspace::OpenOptions { workspace.open_paths(
visible: Some(OpenVisible::All), vec![entry_path],
..Default::default() workspace::OpenOptions {
}, visible: Some(OpenVisible::All),
None, ..Default::default()
window, },
cx, None,
) window,
cx,
)
})
})? })?
.await .await
} else { } else {
+1 -1
View File
@@ -1319,7 +1319,7 @@ impl KeymapEditor {
cx.spawn(async move |_, _| { cx.spawn(async move |_, _| {
remove_keybinding(to_remove, &fs, keyboard_mapper.as_ref()).await remove_keybinding(to_remove, &fs, keyboard_mapper.as_ref()).await
}) })
.detach_and_notify_err(window, cx); .detach_and_notify_err(self.workspace.clone(), window, cx);
} }
fn copy_context_to_clipboard( fn copy_context_to_clipboard(
@@ -674,7 +674,7 @@ mod tests {
use itertools::Itertools as _; use itertools::Itertools as _;
use project::Project; use project::Project;
use settings::SettingsStore; use settings::SettingsStore;
use workspace::Workspace; use workspace::MultiWorkspace;
pub struct KeystrokeInputTestHelper { pub struct KeystrokeInputTestHelper {
input: Entity<KeystrokeInput>, input: Entity<KeystrokeInput>,
@@ -1120,9 +1120,9 @@ mod tests {
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await; let project = Project::test(fs, [], cx).await;
let workspace = let window_handle =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = VisualTestContext::from_window(*workspace, cx); let cx = VisualTestContext::from_window(window_handle.into(), cx);
KeystrokeInputTestHelper::new(cx) KeystrokeInputTestHelper::new(cx)
} }
+12 -15
View File
@@ -8,7 +8,7 @@ use std::{
use gpui::{ use gpui::{
App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement, App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement,
ParentElement as _, Render, SerializedTaskTiming, SharedString, StatefulInteractiveElement, ParentElement as _, Render, SerializedTaskTiming, SharedString, StatefulInteractiveElement,
Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WindowBounds, WindowHandle, Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WeakEntity, WindowBounds,
WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list, WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list,
}; };
use util::ResultExt; use util::ResultExt;
@@ -22,13 +22,10 @@ use workspace::{
use zed_actions::OpenPerformanceProfiler; use zed_actions::OpenPerformanceProfiler;
pub fn init(startup_time: Instant, cx: &mut App) { pub fn init(startup_time: Instant, cx: &mut App) {
cx.observe_new(move |workspace: &mut workspace::Workspace, _, _| { cx.observe_new(move |workspace: &mut workspace::Workspace, _, cx| {
workspace.register_action(move |workspace, _: &OpenPerformanceProfiler, window, cx| { let workspace_handle = cx.entity().downgrade();
let window_handle = window workspace.register_action(move |_workspace, _: &OpenPerformanceProfiler, window, cx| {
.window_handle() open_performance_profiler(startup_time, workspace_handle.clone(), window, cx);
.downcast::<Workspace>()
.expect("Workspaces are root Windows");
open_performance_profiler(startup_time, workspace, window_handle, cx);
}); });
}) })
.detach(); .detach();
@@ -36,8 +33,8 @@ pub fn init(startup_time: Instant, cx: &mut App) {
fn open_performance_profiler( fn open_performance_profiler(
startup_time: Instant, startup_time: Instant,
_workspace: &mut workspace::Workspace, workspace_handle: WeakEntity<Workspace>,
workspace_handle: WindowHandle<Workspace>, _window: &mut gpui::Window,
cx: &mut App, cx: &mut App,
) { ) {
let existing_window = cx let existing_window = cx
@@ -48,7 +45,7 @@ fn open_performance_profiler(
if let Some(existing_window) = existing_window { if let Some(existing_window) = existing_window {
existing_window existing_window
.update(cx, |profiler_window, window, _cx| { .update(cx, |profiler_window, window, _cx| {
profiler_window.workspace = Some(workspace_handle); profiler_window.workspace = Some(workspace_handle.clone());
window.activate_window(); window.activate_window();
}) })
.log_err(); .log_err();
@@ -97,14 +94,14 @@ pub struct ProfilerWindow {
include_self_timings: ToggleState, include_self_timings: ToggleState,
autoscroll: bool, autoscroll: bool,
scroll_handle: UniformListScrollHandle, scroll_handle: UniformListScrollHandle,
workspace: Option<WindowHandle<Workspace>>, workspace: Option<WeakEntity<Workspace>>,
_refresh: Option<Task<()>>, _refresh: Option<Task<()>>,
} }
impl ProfilerWindow { impl ProfilerWindow {
pub fn new( pub fn new(
startup_time: Instant, startup_time: Instant,
workspace_handle: Option<WindowHandle<Workspace>>, workspace_handle: Option<WeakEntity<Workspace>>,
cx: &mut App, cx: &mut App,
) -> Entity<Self> { ) -> Entity<Self> {
let entity = cx.new(|cx| ProfilerWindow { let entity = cx.new(|cx| ProfilerWindow {
@@ -280,7 +277,7 @@ impl Render for ProfilerWindow {
Button::new("export-data", "Save") Button::new("export-data", "Save")
.style(ButtonStyle::Filled) .style(ButtonStyle::Filled)
.on_click(cx.listener(|this, _, _window, cx| { .on_click(cx.listener(|this, _, _window, cx| {
let Some(workspace) = this.workspace else { let Some(workspace) = this.workspace.as_ref() else {
return; return;
}; };
@@ -297,7 +294,7 @@ impl Render for ProfilerWindow {
.log_err() .log_err()
.flatten() .flatten()
.and_then(|p| p.parent().map(|p| p.to_owned())) .and_then(|p| p.parent().map(|p| p.to_owned()))
.unwrap_or_else(|| PathBuf::default()); .unwrap_or_else(PathBuf::default);
let path = cx.prompt_for_new_path( let path = cx.prompt_for_new_path(
&active_path, &active_path,
+6 -5
View File
@@ -238,15 +238,16 @@ impl Onboarding {
go_to_welcome_page(cx); go_to_welcome_page(cx);
} }
fn handle_sign_in(_: &SignIn, window: &mut Window, cx: &mut App) { fn handle_sign_in(&mut self, _: &SignIn, window: &mut Window, cx: &mut Context<Self>) {
let client = Client::global(cx); let client = Client::global(cx);
let workspace = self.workspace.clone();
window window
.spawn(cx, async move |cx| { .spawn(cx, async move |mut cx| {
client client
.sign_in_with_optional_connect(true, cx) .sign_in_with_optional_connect(true, &cx)
.await .await
.notify_async_err(cx); .notify_workspace_async_err(workspace, &mut cx);
}) })
.detach(); .detach();
} }
@@ -274,7 +275,7 @@ impl Render for Onboarding {
.size_full() .size_full()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.on_action(Self::on_finish) .on_action(Self::on_finish)
.on_action(Self::handle_sign_in) .on_action(cx.listener(Self::handle_sign_in))
.on_action(Self::handle_open_account) .on_action(Self::handle_open_account)
.on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| { .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| {
window.focus_next(cx); window.focus_next(cx);
@@ -6,7 +6,7 @@ use project::Project;
use serde_json::json; use serde_json::json;
use ui::rems; use ui::rems;
use util::path; use util::path;
use workspace::{AppState, Workspace}; use workspace::{AppState, MultiWorkspace};
use crate::OpenPathDelegate; use crate::OpenPathDelegate;
@@ -426,7 +426,9 @@ fn build_open_path_prompt(
let (tx, _) = futures::channel::oneshot::channel(); let (tx, _) = futures::channel::oneshot::channel();
let lister = project::DirectoryLister::Project(project.clone()); let lister = project::DirectoryLister::Project(project.clone());
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
( (
workspace.update_in(cx, |_, window, cx| { workspace.update_in(cx, |_, window, cx| {
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, cx); let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, cx);
+9 -6
View File
@@ -20,7 +20,7 @@ use settings::Settings;
use theme::{ActiveTheme, ThemeSettings}; use theme::{ActiveTheme, ThemeSettings};
use ui::{ListItem, ListItemSpacing, prelude::*}; use ui::{ListItem, ListItemSpacing, prelude::*};
use util::ResultExt; use util::ResultExt;
use workspace::{DismissDecision, ModalView, Workspace}; use workspace::{DismissDecision, ModalView};
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.observe_new(OutlineView::register).detach(); cx.observe_new(OutlineView::register).detach();
@@ -41,7 +41,7 @@ pub fn toggle(
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) { ) {
let Some(workspace) = window.root::<Workspace>().flatten() else { let Some(workspace) = editor.read(cx).workspace() else {
return; return;
}; };
if workspace.read(cx).active_modal::<OutlineView>(cx).is_some() { if workspace.read(cx).active_modal::<OutlineView>(cx).is_some() {
@@ -453,7 +453,7 @@ mod tests {
use settings::SettingsStore; use settings::SettingsStore;
use smol::stream::StreamExt as _; use smol::stream::StreamExt as _;
use util::{path, rel_path::rel_path}; use util::{path, rel_path::rel_path};
use workspace::{AppState, Workspace}; use workspace::{AppState, MultiWorkspace, Workspace};
#[gpui::test] #[gpui::test]
async fn test_outline_view_row_highlights(cx: &mut TestAppContext) { async fn test_outline_view_row_highlights(cx: &mut TestAppContext) {
@@ -481,7 +481,9 @@ mod tests {
}); });
let (workspace, cx) = let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = cx.read(|cx| workspace.read(cx).workspace().clone());
let worktree_id = workspace.update(cx, |workspace, cx| { let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| { workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
@@ -736,8 +738,9 @@ mod tests {
}, },
); );
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = cx.read(|cx| multi_workspace.read(cx).workspace().clone());
let worktree_id = workspace.update(cx, |workspace, cx| { let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| { workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
+138 -162
View File
@@ -5387,7 +5387,7 @@ mod tests {
use serde_json::json; use serde_json::json;
use smol::stream::StreamExt as _; use smol::stream::StreamExt as _;
use util::path; use util::path;
use workspace::{OpenOptions, OpenVisible, ToolbarItemView}; use workspace::{MultiWorkspace, OpenOptions, OpenVisible, ToolbarItemView};
use super::*; use super::*;
@@ -5402,33 +5402,29 @@ mod tests {
populate_with_test_ra_project(&fs, root).await; populate_with_test_ra_project(&fs, root).await;
let project = Project::test(fs.clone(), [Path::new(root)], cx).await; let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
project.read_with(cx, |project, _| project.languages().add(rust_lang())); project.read_with(cx, |project, _| project.languages().add(rust_lang()));
let workspace = add_outline_panel(&project, cx).await; let (window, workspace) = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let outline_panel = outline_panel(&workspace, cx); let outline_panel = outline_panel(&workspace, cx);
outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.update_in(cx, |outline_panel, window, cx| {
outline_panel.set_active(true, window, cx) outline_panel.set_active(true, window, cx)
}); });
workspace workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { ProjectSearchView::deploy_search(
ProjectSearchView::deploy_search( workspace,
workspace, &workspace::DeploySearch::default(),
&workspace::DeploySearch::default(), window,
window, cx,
cx, )
) });
}) let search_view = workspace.update_in(cx, |workspace, _window, cx| {
.unwrap(); workspace
let search_view = workspace .active_pane()
.update(cx, |workspace, _, cx| { .read(cx)
workspace .items()
.active_pane() .find_map(|item| item.downcast::<ProjectSearchView>())
.read(cx) .expect("Project search view expected to appear after new search event trigger")
.items() });
.find_map(|item| item.downcast::<ProjectSearchView>())
.expect("Project search view expected to appear after new search event trigger")
})
.unwrap();
let query = "param_names_for_lifetime_elision_hints"; let query = "param_names_for_lifetime_elision_hints";
perform_project_search(&search_view, query, cx); perform_project_search(&search_view, query, cx);
@@ -5635,33 +5631,29 @@ mod tests {
populate_with_test_ra_project(&fs, root).await; populate_with_test_ra_project(&fs, root).await;
let project = Project::test(fs.clone(), [Path::new(root)], cx).await; let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
project.read_with(cx, |project, _| project.languages().add(rust_lang())); project.read_with(cx, |project, _| project.languages().add(rust_lang()));
let workspace = add_outline_panel(&project, cx).await; let (window, workspace) = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let outline_panel = outline_panel(&workspace, cx); let outline_panel = outline_panel(&workspace, cx);
outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.update_in(cx, |outline_panel, window, cx| {
outline_panel.set_active(true, window, cx) outline_panel.set_active(true, window, cx)
}); });
workspace workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { ProjectSearchView::deploy_search(
ProjectSearchView::deploy_search( workspace,
workspace, &workspace::DeploySearch::default(),
&workspace::DeploySearch::default(), window,
window, cx,
cx, )
) });
}) let search_view = workspace.update_in(cx, |workspace, _window, cx| {
.unwrap(); workspace
let search_view = workspace .active_pane()
.update(cx, |workspace, _, cx| { .read(cx)
workspace .items()
.active_pane() .find_map(|item| item.downcast::<ProjectSearchView>())
.read(cx) .expect("Project search view expected to appear after new search event trigger")
.items() });
.find_map(|item| item.downcast::<ProjectSearchView>())
.expect("Project search view expected to appear after new search event trigger")
})
.unwrap();
let query = "param_names_for_lifetime_elision_hints"; let query = "param_names_for_lifetime_elision_hints";
perform_project_search(&search_view, query, cx); perform_project_search(&search_view, query, cx);
@@ -5772,33 +5764,29 @@ mod tests {
populate_with_test_ra_project(&fs, root).await; populate_with_test_ra_project(&fs, root).await;
let project = Project::test(fs.clone(), [Path::new(root)], cx).await; let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
project.read_with(cx, |project, _| project.languages().add(rust_lang())); project.read_with(cx, |project, _| project.languages().add(rust_lang()));
let workspace = add_outline_panel(&project, cx).await; let (window, workspace) = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let outline_panel = outline_panel(&workspace, cx); let outline_panel = outline_panel(&workspace, cx);
outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.update_in(cx, |outline_panel, window, cx| {
outline_panel.set_active(true, window, cx) outline_panel.set_active(true, window, cx)
}); });
workspace workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { ProjectSearchView::deploy_search(
ProjectSearchView::deploy_search( workspace,
workspace, &workspace::DeploySearch::default(),
&workspace::DeploySearch::default(), window,
window, cx,
cx, )
) });
}) let search_view = workspace.update_in(cx, |workspace, _window, cx| {
.unwrap(); workspace
let search_view = workspace .active_pane()
.update(cx, |workspace, _, cx| { .read(cx)
workspace .items()
.active_pane() .find_map(|item| item.downcast::<ProjectSearchView>())
.read(cx) .expect("Project search view expected to appear after new search event trigger")
.items() });
.find_map(|item| item.downcast::<ProjectSearchView>())
.expect("Project search view expected to appear after new search event trigger")
})
.unwrap();
let query = "param_names_for_lifetime_elision_hints"; let query = "param_names_for_lifetime_elision_hints";
perform_project_search(&search_view, query, cx); perform_project_search(&search_view, query, cx);
@@ -5998,15 +5986,15 @@ outline: fn hints_lifetimes_named <==== selected"
) )
.await; .await;
let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await; let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await;
let workspace = add_outline_panel(&project, cx).await; let (window, workspace) = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let outline_panel = outline_panel(&workspace, cx); let outline_panel = outline_panel(&workspace, cx);
outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.update_in(cx, |outline_panel, window, cx| {
outline_panel.set_active(true, window, cx) outline_panel.set_active(true, window, cx)
}); });
let items = workspace let items = workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_paths( workspace.open_paths(
vec![PathBuf::from(path!("/root/two"))], vec![PathBuf::from(path!("/root/two"))],
OpenOptions { OpenOptions {
@@ -6018,7 +6006,6 @@ outline: fn hints_lifetimes_named <==== selected"
cx, cx,
) )
}) })
.unwrap()
.await; .await;
assert_eq!(items.len(), 1, "Were opening another worktree directory"); assert_eq!(items.len(), 1, "Were opening another worktree directory");
assert!( assert!(
@@ -6026,26 +6013,22 @@ outline: fn hints_lifetimes_named <==== selected"
"Directory should be opened successfully" "Directory should be opened successfully"
); );
workspace workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { ProjectSearchView::deploy_search(
ProjectSearchView::deploy_search( workspace,
workspace, &workspace::DeploySearch::default(),
&workspace::DeploySearch::default(), window,
window, cx,
cx, )
) });
}) let search_view = workspace.update_in(cx, |workspace, _window, cx| {
.unwrap(); workspace
let search_view = workspace .active_pane()
.update(cx, |workspace, _, cx| { .read(cx)
workspace .items()
.active_pane() .find_map(|item| item.downcast::<ProjectSearchView>())
.read(cx) .expect("Project search view expected to appear after new search event trigger")
.items() });
.find_map(|item| item.downcast::<ProjectSearchView>())
.expect("Project search view expected to appear after new search event trigger")
})
.unwrap();
let query = "aaa"; let query = "aaa";
perform_project_search(&search_view, query, cx); perform_project_search(&search_view, query, cx);
@@ -6183,8 +6166,8 @@ struct OutlineEntryExcerpt {
.await; .await;
let project = Project::test(fs.clone(), [Path::new(root)], cx).await; let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
project.read_with(cx, |project, _| project.languages().add(rust_lang())); project.read_with(cx, |project, _| project.languages().add(rust_lang()));
let workspace = add_outline_panel(&project, cx).await; let (window, workspace) = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let outline_panel = outline_panel(&workspace, cx); let outline_panel = outline_panel(&workspace, cx);
cx.update(|window, cx| { cx.update(|window, cx| {
outline_panel.update(cx, |outline_panel, cx| { outline_panel.update(cx, |outline_panel, cx| {
@@ -6193,7 +6176,7 @@ struct OutlineEntryExcerpt {
}); });
let _editor = workspace let _editor = workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_abs_path( workspace.open_abs_path(
PathBuf::from(path!("/root/src/lib.rs")), PathBuf::from(path!("/root/src/lib.rs")),
OpenOptions { OpenOptions {
@@ -6204,7 +6187,6 @@ struct OutlineEntryExcerpt {
cx, cx,
) )
}) })
.unwrap()
.await .await
.expect("Failed to open Rust source file") .expect("Failed to open Rust source file")
.downcast::<Editor>() .downcast::<Editor>()
@@ -6545,33 +6527,29 @@ outline: struct OutlineEntryExcerpt
) )
.await; .await;
let project = Project::test(fs.clone(), [Path::new(root)], cx).await; let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
let workspace = add_outline_panel(&project, cx).await; let (window, workspace) = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let outline_panel = outline_panel(&workspace, cx); let outline_panel = outline_panel(&workspace, cx);
outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.update_in(cx, |outline_panel, window, cx| {
outline_panel.set_active(true, window, cx) outline_panel.set_active(true, window, cx)
}); });
workspace workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { ProjectSearchView::deploy_search(
ProjectSearchView::deploy_search( workspace,
workspace, &workspace::DeploySearch::default(),
&workspace::DeploySearch::default(), window,
window, cx,
cx, )
) });
}) let search_view = workspace.update_in(cx, |workspace, _window, cx| {
.unwrap(); workspace
let search_view = workspace .active_pane()
.update(cx, |workspace, _, cx| { .read(cx)
workspace .items()
.active_pane() .find_map(|item| item.downcast::<ProjectSearchView>())
.read(cx) .expect("Project search view expected to appear after new search event trigger")
.items() });
.find_map(|item| item.downcast::<ProjectSearchView>())
.expect("Project search view expected to appear after new search event trigger")
})
.unwrap();
let query = "static"; let query = "static";
perform_project_search(&search_view, query, cx); perform_project_search(&search_view, query, cx);
@@ -6806,13 +6784,18 @@ outline: struct OutlineEntryExcerpt
async fn add_outline_panel( async fn add_outline_panel(
project: &Entity<Project>, project: &Entity<Project>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> WindowHandle<Workspace> { ) -> (WindowHandle<MultiWorkspace>, Entity<Workspace>) {
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window =
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let workspace_weak = workspace.downgrade();
let outline_panel = window let outline_panel = window
.update(cx, |_, window, cx| { .update(cx, |_, window, cx| {
cx.spawn_in(window, async |this, cx| { cx.spawn_in(window, async move |_this, cx| {
OutlinePanel::load(this, cx.clone()).await OutlinePanel::load(workspace_weak, cx.clone()).await
}) })
}) })
.unwrap() .unwrap()
@@ -6820,24 +6803,24 @@ outline: struct OutlineEntryExcerpt
.expect("Failed to load outline panel"); .expect("Failed to load outline panel");
window window
.update(cx, |workspace, window, cx| { .update(cx, |multi_workspace, window, cx| {
workspace.add_panel(outline_panel, window, cx); multi_workspace.workspace().update(cx, |workspace, cx| {
workspace.add_panel(outline_panel, window, cx);
});
}) })
.unwrap(); .unwrap();
window (window, workspace)
} }
fn outline_panel( fn outline_panel(
workspace: &WindowHandle<Workspace>, workspace: &Entity<Workspace>,
cx: &mut TestAppContext, cx: &mut VisualTestContext,
) -> Entity<OutlinePanel> { ) -> Entity<OutlinePanel> {
workspace workspace.update_in(cx, |workspace, _window, cx| {
.update(cx, |workspace, _, cx| { workspace
workspace .panel::<OutlinePanel>(cx)
.panel::<OutlinePanel>(cx) .expect("no outline panel")
.expect("no outline panel") })
})
.unwrap()
} }
fn display_entries( fn display_entries(
@@ -7196,8 +7179,8 @@ outline: struct OutlineEntryExcerpt
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
project.read_with(cx, |project, _| project.languages().add(rust_lang())); project.read_with(cx, |project, _| project.languages().add(rust_lang()));
let workspace = add_outline_panel(&project, cx).await; let (window, workspace) = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let outline_panel = outline_panel(&workspace, cx); let outline_panel = outline_panel(&workspace, cx);
outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.update_in(cx, |outline_panel, window, cx| {
@@ -7205,7 +7188,7 @@ outline: struct OutlineEntryExcerpt
}); });
workspace workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_abs_path( workspace.open_abs_path(
PathBuf::from("/test/src/lib.rs"), PathBuf::from("/test/src/lib.rs"),
OpenOptions { OpenOptions {
@@ -7216,7 +7199,6 @@ outline: struct OutlineEntryExcerpt
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
@@ -7452,8 +7434,8 @@ outline: fn main"
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
project.read_with(cx, |project, _| project.languages().add(rust_lang())); project.read_with(cx, |project, _| project.languages().add(rust_lang()));
let workspace = add_outline_panel(&project, cx).await; let (window, workspace) = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let outline_panel = outline_panel(&workspace, cx); let outline_panel = outline_panel(&workspace, cx);
outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.update_in(cx, |outline_panel, window, cx| {
@@ -7461,7 +7443,7 @@ outline: fn main"
}); });
let _editor = workspace let _editor = workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_abs_path( workspace.open_abs_path(
PathBuf::from("/test/src/main.rs"), PathBuf::from("/test/src/main.rs"),
OpenOptions { OpenOptions {
@@ -7472,7 +7454,6 @@ outline: fn main"
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
@@ -7666,8 +7647,8 @@ outline: fn main"
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
project.read_with(cx, |project, _| project.languages().add(rust_lang())); project.read_with(cx, |project, _| project.languages().add(rust_lang()));
let workspace = add_outline_panel(&project, cx).await; let (window, workspace) = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let outline_panel = outline_panel(&workspace, cx); let outline_panel = outline_panel(&workspace, cx);
outline_panel.update_in(cx, |outline_panel, window, cx| { outline_panel.update_in(cx, |outline_panel, window, cx| {
@@ -7675,7 +7656,7 @@ outline: fn main"
}); });
workspace workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_abs_path( workspace.open_abs_path(
PathBuf::from("/test/src/lib.rs"), PathBuf::from("/test/src/lib.rs"),
OpenOptions { OpenOptions {
@@ -7686,7 +7667,6 @@ outline: fn main"
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
@@ -7841,11 +7821,11 @@ outline: fn main"
.await; .await;
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let workspace = add_outline_panel(&project, cx).await; let (window, workspace) = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let editor = workspace let editor = workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_abs_path( workspace.open_abs_path(
PathBuf::from("/test/foo.txt"), PathBuf::from("/test/foo.txt"),
OpenOptions { OpenOptions {
@@ -7856,22 +7836,19 @@ outline: fn main"
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap() .unwrap()
.downcast::<Editor>() .downcast::<Editor>()
.unwrap(); .unwrap();
let search_bar = workspace let search_bar = workspace.update_in(cx, |_, window, cx| {
.update(cx, |_, window, cx| { cx.new(|cx| {
cx.new(|cx| { let mut search_bar = BufferSearchBar::new(None, window, cx);
let mut search_bar = BufferSearchBar::new(None, window, cx); search_bar.set_active_pane_item(Some(&editor), window, cx);
search_bar.set_active_pane_item(Some(&editor), window, cx); search_bar.show(window, cx);
search_bar.show(window, cx); search_bar
search_bar
})
}) })
.unwrap(); });
let outline_panel = outline_panel(&workspace, cx); let outline_panel = outline_panel(&workspace, cx);
@@ -8008,8 +7985,8 @@ search: | Field | Meaning « »|"
}, },
); );
let workspace = add_outline_panel(&project, cx).await; let (window, workspace) = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(window.into(), cx);
let outline_panel = outline_panel(&workspace, cx); let outline_panel = outline_panel(&workspace, cx);
cx.update(|window, cx| { cx.update(|window, cx| {
outline_panel.update(cx, |outline_panel, cx| { outline_panel.update(cx, |outline_panel, cx| {
@@ -8018,7 +7995,7 @@ search: | Field | Meaning « »|"
}); });
let _editor = workspace let _editor = workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_abs_path( workspace.open_abs_path(
PathBuf::from(path!("/root/src/lib.rs")), PathBuf::from(path!("/root/src/lib.rs")),
OpenOptions { OpenOptions {
@@ -8029,7 +8006,6 @@ search: | Field | Meaning « »|"
cx, cx,
) )
}) })
.unwrap()
.await .await
.expect("Failed to open Rust source file") .expect("Failed to open Rust source file")
.downcast::<Editor>() .downcast::<Editor>()
+1
View File
@@ -13,6 +13,7 @@ path = "src/platform_title_bar.rs"
doctest = false doctest = false
[dependencies] [dependencies]
feature_flags.workspace = true
gpui.workspace = true gpui.workspace = true
settings.workspace = true settings.workspace = true
smallvec.workspace = true smallvec.workspace = true
@@ -1,16 +1,21 @@
mod platforms; mod platforms;
mod system_window_tabs; mod system_window_tabs;
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
use gpui::{ use gpui::{
AnyElement, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, MouseButton, AnyElement, App, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement,
ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px, MouseButton, ParentElement, StatefulInteractiveElement, Styled, Window, WindowControlArea, div,
px,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use std::mem; use std::mem;
use ui::prelude::*; use ui::{
prelude::*,
utils::{TRAFFIC_LIGHT_PADDING, platform_title_bar_height},
};
use crate::{ use crate::{
platforms::{platform_linux, platform_mac, platform_windows}, platforms::{platform_linux, platform_windows},
system_window_tabs::SystemWindowTabs, system_window_tabs::SystemWindowTabs,
}; };
@@ -24,6 +29,8 @@ pub struct PlatformTitleBar {
children: SmallVec<[AnyElement; 2]>, children: SmallVec<[AnyElement; 2]>,
should_move: bool, should_move: bool,
system_window_tabs: Entity<SystemWindowTabs>, system_window_tabs: Entity<SystemWindowTabs>,
workspace_sidebar_open: bool,
sidebar_has_notifications: bool,
} }
impl PlatformTitleBar { impl PlatformTitleBar {
@@ -37,20 +44,11 @@ impl PlatformTitleBar {
children: SmallVec::new(), children: SmallVec::new(),
should_move: false, should_move: false,
system_window_tabs, system_window_tabs,
workspace_sidebar_open: false,
sidebar_has_notifications: false,
} }
} }
#[cfg(not(target_os = "windows"))]
pub fn height(window: &mut Window) -> Pixels {
(1.75 * window.rem_size()).max(px(34.))
}
#[cfg(target_os = "windows")]
pub fn height(_window: &mut Window) -> Pixels {
// todo(windows) instead of hard coded size report the actual size to the Windows platform API
px(32.)
}
pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla { pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
if cfg!(any(target_os = "linux", target_os = "freebsd")) { if cfg!(any(target_os = "linux", target_os = "freebsd")) {
if window.is_window_active() && !self.should_move { if window.is_window_active() && !self.should_move {
@@ -73,17 +71,46 @@ impl PlatformTitleBar {
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
SystemWindowTabs::init(cx); SystemWindowTabs::init(cx);
} }
pub fn is_workspace_sidebar_open(&self) -> bool {
self.workspace_sidebar_open
}
pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context<Self>) {
self.workspace_sidebar_open = open;
cx.notify();
}
pub fn sidebar_has_notifications(&self) -> bool {
self.sidebar_has_notifications
}
pub fn set_sidebar_has_notifications(
&mut self,
has_notifications: bool,
cx: &mut Context<Self>,
) {
self.sidebar_has_notifications = has_notifications;
cx.notify();
}
pub fn is_multi_workspace_enabled(cx: &App) -> bool {
cx.has_flag::<AgentV2FeatureFlag>()
}
} }
impl Render for PlatformTitleBar { impl Render for PlatformTitleBar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let supported_controls = window.window_controls(); let supported_controls = window.window_controls();
let decorations = window.window_decorations(); let decorations = window.window_decorations();
let height = Self::height(window); let height = platform_title_bar_height(window);
let titlebar_color = self.title_bar_color(window, cx); let titlebar_color = self.title_bar_color(window, cx);
let close_action = Box::new(workspace::CloseWindow); let close_action = Box::new(workspace::CloseWindow);
let children = mem::take(&mut self.children); let children = mem::take(&mut self.children);
let is_multiworkspace_sidebar_open =
PlatformTitleBar::is_multi_workspace_enabled(cx) && self.is_workspace_sidebar_open();
let title_bar = h_flex() let title_bar = h_flex()
.window_control_area(WindowControlArea::Drag) .window_control_area(WindowControlArea::Drag)
.w_full() .w_full()
@@ -132,8 +159,10 @@ impl Render for PlatformTitleBar {
.map(|this| { .map(|this| {
if window.is_fullscreen() { if window.is_fullscreen() {
this.pl_2() this.pl_2()
} else if self.platform_style == PlatformStyle::Mac { } else if self.platform_style == PlatformStyle::Mac
this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING)) && !is_multiworkspace_sidebar_open
{
this.pl(px(TRAFFIC_LIGHT_PADDING))
} else { } else {
this.pl_2() this.pl_2()
} }
@@ -144,9 +173,10 @@ impl Render for PlatformTitleBar {
.when(!(tiling.top || tiling.right), |el| { .when(!(tiling.top || tiling.right), |el| {
el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
}) })
.when(!(tiling.top || tiling.left), |el| { .when(
el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) !(tiling.top || tiling.left) && !is_multiworkspace_sidebar_open,
}) |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
)
// this border is to avoid a transparent gap in the rounded corners // this border is to avoid a transparent gap in the rounded corners
.mt(px(-1.)) .mt(px(-1.))
.mb(px(-1.)) .mb(px(-1.))
@@ -1,3 +1,2 @@
pub mod platform_linux; pub mod platform_linux;
pub mod platform_mac;
pub mod platform_windows; pub mod platform_windows;
@@ -1,10 +0,0 @@
// Use pixels here instead of a rem-based size because the macOS traffic
// lights are a static size, and don't scale with the rest of the UI.
//
// Magic number: There is one extra pixel of padding on the left side due to
// the 1px border around the window on macOS apps.
#[cfg(macos_sdk_26)]
pub const TRAFFIC_LIGHT_PADDING: f32 = 78.;
#[cfg(not(macos_sdk_26))]
pub const TRAFFIC_LIGHT_PADDING: f32 = 71.;
+16 -7
View File
@@ -772,7 +772,11 @@ impl ProjectPanel {
{ {
match project_panel.confirm_edit(false, window, cx) { match project_panel.confirm_edit(false, window, cx) {
Some(task) => { Some(task) => {
task.detach_and_notify_err(window, cx); task.detach_and_notify_err(
project_panel.workspace.clone(),
window,
cx,
);
} }
None => { None => {
project_panel.discard_edit_state(window, cx); project_panel.discard_edit_state(window, cx);
@@ -1648,7 +1652,7 @@ impl ProjectPanel {
fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) { fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
if let Some(task) = self.confirm_edit(true, window, cx) { if let Some(task) = self.confirm_edit(true, window, cx) {
task.detach_and_notify_err(window, cx); task.detach_and_notify_err(self.workspace.clone(), window, cx);
} }
} }
@@ -3033,20 +3037,25 @@ impl ProjectPanel {
} }
let item_count = paste_tasks.len(); let item_count = paste_tasks.len();
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |project_panel, cx| { cx.spawn_in(window, async move |project_panel, mut cx| {
let mut last_succeed = None; let mut last_succeed = None;
for task in paste_tasks { for task in paste_tasks {
match task { match task {
PasteTask::Rename(task) => { PasteTask::Rename(task) => {
if let Some(CreatedEntry::Included(entry)) = if let Some(CreatedEntry::Included(entry)) = task
task.await.notify_async_err(cx) .await
.notify_workspace_async_err(workspace.clone(), &mut cx)
{ {
last_succeed = Some(entry); last_succeed = Some(entry);
} }
} }
PasteTask::Copy(task) => { PasteTask::Copy(task) => {
if let Some(Some(entry)) = task.await.notify_async_err(cx) { if let Some(Some(entry)) = task
.await
.notify_workspace_async_err(workspace.clone(), &mut cx)
{
last_succeed = Some(entry); last_succeed = Some(entry);
} }
} }
@@ -3388,7 +3397,7 @@ impl ProjectPanel {
if let Some((file_path1, file_path2)) = selected_files { if let Some((file_path1, file_path2)) = selected_files {
self.workspace self.workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
FileDiffView::open(file_path1, file_path2, workspace, window, cx) FileDiffView::open(file_path1, file_path2, workspace.weak_handle(), window, cx)
.detach_and_log_err(cx); .detach_and_log_err(cx);
}) })
.ok(); .ok();
File diff suppressed because it is too large. Load diff
@@ -318,6 +318,7 @@ mod tests {
use settings::SettingsStore; use settings::SettingsStore;
use std::{path::Path, sync::Arc}; use std::{path::Path, sync::Arc};
use util::path; use util::path;
use workspace::MultiWorkspace;
#[gpui::test] #[gpui::test]
async fn test_project_symbols(cx: &mut TestAppContext) { async fn test_project_symbols(cx: &mut TestAppContext) {
@@ -409,8 +410,9 @@ mod tests {
}, },
); );
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
// Create the project symbols view. // Create the project symbols view.
let symbols = cx.new_window_entity(|window, cx| { let symbols = cx.new_window_entity(|window, cx| {
+2
View File
@@ -23,6 +23,7 @@ db.workspace = true
dev_container.workspace = true dev_container.workspace = true
editor.workspace = true editor.workspace = true
extension_host.workspace = true extension_host.workspace = true
fs.workspace = true
futures.workspace = true futures.workspace = true
fuzzy.workspace = true fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
@@ -66,6 +67,7 @@ language = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] }
release_channel.workspace = true release_channel.workspace = true
remote = { workspace = true, features = ["test-support"] } remote = { workspace = true, features = ["test-support"] }
remote_connection = { workspace = true, features = ["test-support"] }
remote_server.workspace = true remote_server.workspace = true
serde_json.workspace = true serde_json.workspace = true
settings = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] }
@@ -7,7 +7,9 @@ use ui::{
HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal, HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal,
ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, Window, div, h_flex, rems, ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, Window, div, h_flex, rems,
}; };
use workspace::{ModalView, OpenOptions, Workspace, notifications::DetachAndPromptErr}; use workspace::{
ModalView, MultiWorkspace, OpenOptions, Workspace, notifications::DetachAndPromptErr,
};
use crate::open_remote_project; use crate::open_remote_project;
@@ -109,7 +111,7 @@ impl DisconnectedOverlay {
return; return;
}; };
let Some(window_handle) = window.window_handle().downcast::<Workspace>() else { let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
return; return;
}; };
+125 -89
View File
@@ -4,7 +4,9 @@ mod remote_connections;
mod remote_servers; mod remote_servers;
mod ssh_config; mod ssh_config;
use std::path::PathBuf; use std::{path::PathBuf, sync::Arc};
use fs::Fs;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
mod wsl_picker; mod wsl_picker;
@@ -27,11 +29,11 @@ use picker::{
pub use remote_connections::RemoteSettings; pub use remote_connections::RemoteSettings;
pub use remote_servers::RemoteServerProjects; pub use remote_servers::RemoteServerProjects;
use settings::Settings; use settings::Settings;
use std::{path::Path, sync::Arc}; use std::path::Path;
use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container}; use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container};
use util::{ResultExt, paths::PathExt}; use util::{ResultExt, paths::PathExt};
use workspace::{ use workspace::{
CloseIntent, HistoryManager, ModalView, OpenOptions, PathList, SerializedWorkspaceLocation, HistoryManager, ModalView, MultiWorkspace, OpenOptions, PathList, SerializedWorkspaceLocation,
WORKSPACE_DB, Workspace, WorkspaceId, notifications::DetachAndPromptErr, WORKSPACE_DB, Workspace, WorkspaceId, notifications::DetachAndPromptErr,
with_active_or_new_workspace, with_active_or_new_workspace,
}; };
@@ -48,9 +50,10 @@ pub struct RecentProjectEntry {
pub async fn get_recent_projects( pub async fn get_recent_projects(
current_workspace_id: Option<WorkspaceId>, current_workspace_id: Option<WorkspaceId>,
limit: Option<usize>, limit: Option<usize>,
fs: Arc<dyn fs::Fs>,
) -> Vec<RecentProjectEntry> { ) -> Vec<RecentProjectEntry> {
let workspaces = WORKSPACE_DB let workspaces = WORKSPACE_DB
.recent_workspaces_on_disk() .recent_workspaces_on_disk(fs.as_ref())
.await .await
.unwrap_or_default(); .unwrap_or_default();
@@ -176,7 +179,7 @@ pub fn init(cx: &mut App) {
let fs = workspace.project().read(cx).fs().clone(); let fs = workspace.project().read(cx).fs().clone();
add_wsl_distro(fs, &open_wsl.distro, cx); add_wsl_distro(fs, &open_wsl.distro, cx);
let open_options = OpenOptions { let open_options = OpenOptions {
replace_window: window.window_handle().downcast::<Workspace>(), replace_window: window.window_handle().downcast::<MultiWorkspace>(),
..Default::default() ..Default::default()
}; };
@@ -232,10 +235,8 @@ pub fn init(cx: &mut App) {
cx.on_action(|_: &OpenDevContainer, cx| { cx.on_action(|_: &OpenDevContainer, cx| {
with_active_or_new_workspace(cx, move |workspace, window, cx| { with_active_or_new_workspace(cx, move |workspace, window, cx| {
let is_local = workspace.project().read(cx).is_local(); if !workspace.project().read(cx).is_local() {
cx.spawn_in(window, async move |_, cx| {
cx.spawn_in(window, async move |_, cx| {
if !is_local {
cx.prompt( cx.prompt(
gpui::PromptLevel::Critical, gpui::PromptLevel::Critical,
"Cannot open Dev Container from remote project", "Cannot open Dev Container from remote project",
@@ -244,21 +245,16 @@ pub fn init(cx: &mut App) {
) )
.await .await
.ok(); .ok();
return;
}
cx.update(|_, cx| {
with_active_or_new_workspace(cx, move |workspace, window, cx| {
let fs = workspace.project().read(cx).fs().clone();
let handle = cx.entity().downgrade();
workspace.toggle_modal(window, cx, |window, cx| {
RemoteServerProjects::new_dev_container(fs, window, handle, cx)
});
});
}) })
.log_err(); .detach();
}) return;
.detach(); }
let fs = workspace.project().read(cx).fs().clone();
let handle = cx.entity().downgrade();
workspace.toggle_modal(window, cx, |window, cx| {
RemoteServerProjects::new_dev_container(fs, window, handle, cx)
});
}); });
}); });
@@ -334,6 +330,7 @@ impl ModalView for RecentProjects {}
impl RecentProjects { impl RecentProjects {
fn new( fn new(
delegate: RecentProjectsDelegate, delegate: RecentProjectsDelegate,
fs: Option<Arc<dyn Fs>>,
rem_width: f32, rem_width: f32,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@@ -350,8 +347,9 @@ impl RecentProjects {
// We do not want to block the UI on a potentially lengthy call to DB, so we're gonna swap // We do not want to block the UI on a potentially lengthy call to DB, so we're gonna swap
// out workspace locations once the future runs to completion. // out workspace locations once the future runs to completion.
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let Some(fs) = fs else { return };
let workspaces = WORKSPACE_DB let workspaces = WORKSPACE_DB
.recent_workspaces_on_disk() .recent_workspaces_on_disk(fs.as_ref())
.await .await
.log_err() .log_err()
.unwrap_or_default(); .unwrap_or_default();
@@ -361,7 +359,7 @@ impl RecentProjects {
picker.update_matches(picker.query(cx), window, cx) picker.update_matches(picker.query(cx), window, cx)
}) })
}) })
.ok() .ok();
}) })
.detach(); .detach();
Self { Self {
@@ -379,10 +377,11 @@ impl RecentProjects {
cx: &mut Context<Workspace>, cx: &mut Context<Workspace>,
) { ) {
let weak = cx.entity().downgrade(); let weak = cx.entity().downgrade();
let fs = Some(workspace.app_state().fs.clone());
workspace.toggle_modal(window, cx, |window, cx| { workspace.toggle_modal(window, cx, |window, cx| {
let delegate = RecentProjectsDelegate::new(weak, create_new_window, true, focus_handle); let delegate = RecentProjectsDelegate::new(weak, create_new_window, true, focus_handle);
Self::new(delegate, 34., window, cx) Self::new(delegate, fs, 34., window, cx)
}) })
} }
@@ -393,10 +392,13 @@ impl RecentProjects {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Entity<Self> { ) -> Entity<Self> {
let fs = workspace
.upgrade()
.map(|ws| ws.read(cx).app_state().fs.clone());
cx.new(|cx| { cx.new(|cx| {
let delegate = let delegate =
RecentProjectsDelegate::new(workspace, create_new_window, true, focus_handle); RecentProjectsDelegate::new(workspace, create_new_window, true, focus_handle);
let list = Self::new(delegate, 34., window, cx); let list = Self::new(delegate, fs, 34., window, cx);
list.picker.focus_handle(cx).focus(window, cx); list.picker.focus_handle(cx).focus(window, cx);
list list
}) })
@@ -580,27 +582,21 @@ impl PickerDelegate for RecentProjectsDelegate {
SerializedWorkspaceLocation::Local => { SerializedWorkspaceLocation::Local => {
let paths = candidate_workspace_paths.paths().to_vec(); let paths = candidate_workspace_paths.paths().to_vec();
if replace_current_window { if replace_current_window {
cx.spawn_in(window, async move |workspace, cx| { if let Some(handle) =
let continue_replacing = workspace window.window_handle().downcast::<MultiWorkspace>()
.update_in(cx, |workspace, window, cx| { {
workspace.prepare_to_close( cx.defer(move |cx| {
CloseIntent::ReplaceWindow, if let Some(task) = handle
window, .update(cx, |multi_workspace, window, cx| {
cx, multi_workspace.open_project(paths, window, cx)
) })
})? .log_err()
.await?; {
if continue_replacing { task.detach_and_log_err(cx);
workspace }
.update_in(cx, |workspace, window, cx| { });
workspace }
.open_workspace_for_paths(true, paths, window, cx) return;
})?
.await
} else {
Ok(())
}
})
} else { } else {
workspace.open_workspace_for_paths(false, paths, window, cx) workspace.open_workspace_for_paths(false, paths, window, cx)
} }
@@ -609,7 +605,7 @@ impl PickerDelegate for RecentProjectsDelegate {
let app_state = workspace.app_state().clone(); let app_state = workspace.app_state().clone();
let replace_window = if replace_current_window { let replace_window = if replace_current_window {
window.window_handle().downcast::<Workspace>() window.window_handle().downcast::<MultiWorkspace>()
} else { } else {
None None
}; };
@@ -884,10 +880,18 @@ impl RecentProjectsDelegate {
) { ) {
if let Some(selected_match) = self.matches.get(ix) { if let Some(selected_match) = self.matches.get(ix) {
let (workspace_id, _, _) = self.workspaces[selected_match.candidate_id]; let (workspace_id, _, _) = self.workspaces[selected_match.candidate_id];
let fs = self
.workspace
.upgrade()
.map(|ws| ws.read(cx).app_state().fs.clone());
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await; WORKSPACE_DB
.delete_workspace_by_id(workspace_id)
.await
.log_err();
let Some(fs) = fs else { return };
let workspaces = WORKSPACE_DB let workspaces = WORKSPACE_DB
.recent_workspaces_on_disk() .recent_workspaces_on_disk(fs.as_ref())
.await .await
.unwrap_or_default(); .unwrap_or_default();
this.update_in(cx, move |picker, window, cx| { this.update_in(cx, move |picker, window, cx| {
@@ -904,6 +908,7 @@ impl RecentProjectsDelegate {
.update(cx, |this, cx| this.delete_history(workspace_id, cx)); .update(cx, |this, cx| this.delete_history(workspace_id, cx));
} }
}) })
.ok();
}) })
.detach(); .detach();
} }
@@ -951,7 +956,7 @@ mod tests {
use super::*; use super::*;
#[gpui::test] #[gpui::test]
async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) { async fn test_dirty_workspace_survives_when_opening_recent_project(cx: &mut TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
cx.update(|cx| { cx.update(|cx| {
@@ -975,6 +980,11 @@ mod tests {
}), }),
) )
.await; .await;
app_state
.fs
.as_fake()
.insert_tree(path!("/test/path"), json!({}))
.await;
cx.update(|cx| { cx.update(|cx| {
open_paths( open_paths(
&[PathBuf::from(path!("/dir/main.ts"))], &[PathBuf::from(path!("/dir/main.ts"))],
@@ -987,31 +997,40 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1); assert_eq!(cx.update(|cx| cx.windows().len()), 1);
let workspace = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap()); let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
workspace multi_workspace
.update(cx, |workspace, _, _| assert!(!workspace.is_edited())) .update(cx, |multi_workspace, _, cx| {
assert!(!multi_workspace.workspace().read(cx).is_edited())
})
.unwrap(); .unwrap();
let editor = workspace let editor = multi_workspace
.read_with(cx, |workspace, cx| { .read_with(cx, |multi_workspace, cx| {
workspace multi_workspace
.workspace()
.read(cx)
.active_item(cx) .active_item(cx)
.unwrap() .unwrap()
.downcast::<Editor>() .downcast::<Editor>()
.unwrap() .unwrap()
}) })
.unwrap(); .unwrap();
workspace multi_workspace
.update(cx, |_, window, cx| { .update(cx, |_, window, cx| {
editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx)); editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
}) })
.unwrap(); .unwrap();
workspace multi_workspace
.update(cx, |workspace, _, _| assert!(workspace.is_edited(), "After inserting more text into the editor without saving, we should have a dirty project")) .update(cx, |multi_workspace, _, cx| {
assert!(
multi_workspace.workspace().read(cx).is_edited(),
"After inserting more text into the editor without saving, we should have a dirty project"
)
})
.unwrap(); .unwrap();
let recent_projects_picker = open_recent_projects(&workspace, cx); let recent_projects_picker = open_recent_projects(&multi_workspace, cx);
workspace multi_workspace
.update(cx, |_, _, cx| { .update(cx, |_, _, cx| {
recent_projects_picker.update(cx, |picker, cx| { recent_projects_picker.update(cx, |picker, cx| {
assert_eq!(picker.query(cx), ""); assert_eq!(picker.query(cx), "");
@@ -1035,47 +1054,64 @@ mod tests {
!cx.has_pending_prompt(), !cx.has_pending_prompt(),
"Should have no pending prompt on dirty project before opening the new recent project" "Should have no pending prompt on dirty project before opening the new recent project"
); );
cx.dispatch_action(*workspace, menu::Confirm); let dirty_workspace = multi_workspace
workspace .read_with(cx, |multi_workspace, _cx| {
.update(cx, |workspace, _, cx| { multi_workspace.workspace().clone()
assert!(
workspace.active_modal::<RecentProjects>(cx).is_none(),
"Should remove the modal after selecting new recent project"
)
}) })
.unwrap(); .unwrap();
assert!(
cx.has_pending_prompt(), cx.dispatch_action(*multi_workspace, menu::Confirm);
"Dirty workspace should prompt before opening the new recent project" cx.run_until_parked();
);
cx.simulate_prompt_answer("Cancel"); multi_workspace
.update(cx, |multi_workspace, _, cx| {
assert!(
multi_workspace
.workspace()
.read(cx)
.active_modal::<RecentProjects>(cx)
.is_none(),
"Should remove the modal after selecting new recent project"
);
assert!(
multi_workspace.workspaces().len() >= 2,
"Should have at least 2 workspaces: the dirty one and the newly opened one"
);
assert!(
multi_workspace.workspaces().contains(&dirty_workspace),
"The original dirty workspace should still be present"
);
assert!(
dirty_workspace.read(cx).is_edited(),
"The original workspace should still be dirty"
);
})
.unwrap();
assert!( assert!(
!cx.has_pending_prompt(), !cx.has_pending_prompt(),
"Should have no pending prompt after cancelling" "No save prompt in multi-workspace mode — dirty workspace survives in background"
); );
workspace
.update(cx, |workspace, _, _| {
assert!(
workspace.is_edited(),
"Should be in the same dirty project after cancelling"
)
})
.unwrap();
} }
fn open_recent_projects( fn open_recent_projects(
workspace: &WindowHandle<Workspace>, multi_workspace: &WindowHandle<MultiWorkspace>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> Entity<Picker<RecentProjectsDelegate>> { ) -> Entity<Picker<RecentProjectsDelegate>> {
cx.dispatch_action( cx.dispatch_action(
(*workspace).into(), (*multi_workspace).into(),
OpenRecent { OpenRecent {
create_new_window: false, create_new_window: false,
}, },
); );
workspace multi_workspace
.update(cx, |workspace, _, cx| { .update(cx, |multi_workspace, _, cx| {
workspace multi_workspace
.workspace()
.read(cx)
.active_modal::<RecentProjects>(cx) .active_modal::<RecentProjects>(cx)
.unwrap() .unwrap()
.read(cx) .read(cx)
@@ -19,7 +19,7 @@ use remote::{
pub use settings::SshConnection; pub use settings::SshConnection;
use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection}; use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection};
use util::paths::PathWithPosition; use util::paths::PathWithPosition;
use workspace::{AppState, Workspace}; use workspace::{AppState, MultiWorkspace, Workspace};
pub use remote_connection::{ pub use remote_connection::{
RemoteClientDelegate, RemoteConnectionModal, RemoteConnectionPrompt, SshConnectionHeader, RemoteClientDelegate, RemoteConnectionModal, RemoteConnectionPrompt, SshConnectionHeader,
@@ -131,8 +131,11 @@ pub async fn open_remote_project(
cx: &mut AsyncApp, cx: &mut AsyncApp,
) -> Result<()> { ) -> Result<()> {
let created_new_window = open_options.replace_window.is_none(); let created_new_window = open_options.replace_window.is_none();
let window = if let Some(window) = open_options.replace_window { let (window, initial_workspace) = if let Some(window) = open_options.replace_window {
window let workspace = window.update(cx, |multi_workspace, _, _| {
multi_workspace.workspace().clone()
})?;
(window, workspace)
} else { } else {
let workspace_position = cx let workspace_position = cx
.update(|cx| { .update(|cx| {
@@ -145,7 +148,7 @@ pub async fn open_remote_project(
cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx)); cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx));
options.window_bounds = workspace_position.window_bounds; options.window_bounds = workspace_position.window_bounds;
cx.open_window(options, |window, cx| { let window = cx.open_window(options, |window, cx| {
let project = project::Project::local( let project = project::Project::local(
app_state.client.clone(), app_state.client.clone(),
app_state.node_runtime.clone(), app_state.node_runtime.clone(),
@@ -159,12 +162,17 @@ pub async fn open_remote_project(
}, },
cx, cx,
); );
cx.new(|cx| { let workspace = cx.new(|cx| {
let mut workspace = Workspace::new(None, project, app_state.clone(), window, cx); let mut workspace = Workspace::new(None, project, app_state.clone(), window, cx);
workspace.centered_layout = workspace_position.centered_layout; workspace.centered_layout = workspace_position.centered_layout;
workspace workspace
}) });
})? cx.new(|cx| MultiWorkspace::new(workspace, cx))
})?;
let workspace = window.update(cx, |multi_workspace, _, _cx| {
multi_workspace.workspace().clone()
})?;
(window, workspace)
}; };
loop { loop {
@@ -172,35 +180,38 @@ pub async fn open_remote_project(
let delegate = window.update(cx, { let delegate = window.update(cx, {
let paths = paths.clone(); let paths = paths.clone();
let connection_options = connection_options.clone(); let connection_options = connection_options.clone();
move |workspace, window, cx| { let initial_workspace = initial_workspace.clone();
move |_multi_workspace: &mut MultiWorkspace, window, cx| {
window.activate_window(); window.activate_window();
workspace.hide_modal(window, cx); initial_workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(window, cx, |window, cx| { workspace.hide_modal(window, cx);
RemoteConnectionModal::new(&connection_options, paths, window, cx) workspace.toggle_modal(window, cx, |window, cx| {
}); RemoteConnectionModal::new(&connection_options, paths, window, cx)
});
let ui = workspace let ui = workspace
.active_modal::<RemoteConnectionModal>(cx)? .active_modal::<RemoteConnectionModal>(cx)?
.read(cx) .read(cx)
.prompt .prompt
.clone(); .clone();
ui.update(cx, |ui, _cx| { ui.update(cx, |ui, _cx| {
ui.set_cancellation_tx(cancel_tx); ui.set_cancellation_tx(cancel_tx);
}); });
Some(Arc::new(RemoteClientDelegate::new( Some(Arc::new(RemoteClientDelegate::new(
window.window_handle(), window.window_handle(),
ui.downgrade(), ui.downgrade(),
if let RemoteConnectionOptions::Ssh(options) = &connection_options { if let RemoteConnectionOptions::Ssh(options) = &connection_options {
options options
.password .password
.as_deref() .as_deref()
.and_then(|pw| EncryptedPassword::try_from(pw).ok()) .and_then(|pw| EncryptedPassword::try_from(pw).ok())
} else { } else {
None None
}, },
))) )))
})
} }
})?; })?;
@@ -209,13 +220,11 @@ pub async fn open_remote_project(
let connection = remote::connect(connection_options.clone(), delegate.clone(), cx); let connection = remote::connect(connection_options.clone(), delegate.clone(), cx);
let connection = select! { let connection = select! {
_ = cancel_rx => { _ = cancel_rx => {
window initial_workspace.update(cx, |workspace, cx| {
.update(cx, |workspace, _, cx| { if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) { ui.update(cx, |modal, cx| modal.finished(cx))
ui.update(cx, |modal, cx| modal.finished(cx)) }
} });
})
.ok();
break; break;
}, },
@@ -224,13 +233,11 @@ pub async fn open_remote_project(
let remote_connection = match connection { let remote_connection = match connection {
Ok(connection) => connection, Ok(connection) => connection,
Err(e) => { Err(e) => {
window initial_workspace.update(cx, |workspace, cx| {
.update(cx, |workspace, _, cx| { if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) { ui.update(cx, |modal, cx| modal.finished(cx))
ui.update(cx, |modal, cx| modal.finished(cx)) }
} });
})
.ok();
log::error!("Failed to open project: {e:#}"); log::error!("Failed to open project: {e:#}");
let response = window let response = window
.update(cx, |_, window, cx| { .update(cx, |_, window, cx| {
@@ -284,13 +291,11 @@ pub async fn open_remote_project(
}) })
.await; .await;
window initial_workspace.update(cx, |workspace, cx| {
.update(cx, |workspace, _, cx| { if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) { ui.update(cx, |modal, cx| modal.finished(cx))
ui.update(cx, |modal, cx| modal.finished(cx)) }
} });
})
.ok();
match opened_items { match opened_items {
Err(e) => { Err(e) => {
@@ -320,20 +325,20 @@ pub async fn open_remote_project(
continue; continue;
} }
window if created_new_window {
.update(cx, |workspace, window, cx| { window
if created_new_window { .update(cx, |_, window, _| window.remove_window())
window.remove_window(); .ok();
} }
trusted_worktrees::track_worktree_trust( initial_workspace.update(cx, |workspace, cx| {
workspace.project().read(cx).worktree_store(), trusted_worktrees::track_worktree_trust(
None, workspace.project().read(cx).worktree_store(),
None, None,
None, None,
cx, None,
); cx,
}) );
.ok(); });
} }
Ok(items) => { Ok(items) => {
@@ -366,14 +371,20 @@ pub async fn open_remote_project(
break; break;
} }
// Register the remote client with extensions. We use `multi_workspace.workspace()` here
// (not `initial_workspace`) because `open_remote_project_inner` activated the new remote
// workspace, so the active workspace is now the one with the remote project.
window window
.update(cx, |workspace, _, cx| { .update(cx, |multi_workspace: &mut MultiWorkspace, _, cx| {
if let Some(client) = workspace.project().read(cx).remote_client() { let workspace = multi_workspace.workspace().clone();
if let Some(extension_store) = ExtensionStore::try_global(cx) { workspace.update(cx, |workspace, cx| {
extension_store if let Some(client) = workspace.project().read(cx).remote_client() {
.update(cx, |store, cx| store.register_remote_client(client, cx)); if let Some(extension_store) = ExtensionStore::try_global(cx) {
extension_store
.update(cx, |store, cx| store.register_remote_client(client, cx));
}
} }
} });
}) })
.ok(); .ok();
Ok(()) Ok(())
@@ -500,12 +511,16 @@ mod tests {
let windows = cx.update(|cx| cx.windows().len()); let windows = cx.update(|cx| cx.windows().len());
assert_eq!(windows, 1, "Should have opened a window"); assert_eq!(windows, 1, "Should have opened a window");
let workspace_handle = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap()); let multi_workspace_handle =
cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
workspace_handle multi_workspace_handle
.update(cx, |workspace, _, cx| { .update(cx, |multi_workspace, _, cx| {
let project = workspace.project().read(cx); let workspace = multi_workspace.workspace().clone();
assert!(project.is_remote(), "Project should be a remote project"); workspace.update(cx, |workspace, cx| {
let project = workspace.project().read(cx);
assert!(project.is_remote(), "Project should be a remote project");
});
}) })
.unwrap(); .unwrap();
} }
+57 -62
View File
@@ -6,7 +6,8 @@ use crate::{
ssh_config::parse_ssh_config_hosts, ssh_config::parse_ssh_config_hosts,
}; };
use dev_container::{ use dev_container::{
DevContainerConfig, find_devcontainer_configs, start_dev_container_with_config, DevContainerConfig, DevContainerContext, find_devcontainer_configs,
start_dev_container_with_config,
}; };
use editor::Editor; use editor::Editor;
@@ -51,7 +52,7 @@ use util::{
rel_path::RelPath, rel_path::RelPath,
}; };
use workspace::{ use workspace::{
ModalView, OpenLog, OpenOptions, Toast, Workspace, ModalView, MultiWorkspace, OpenLog, OpenOptions, Toast, Workspace,
notifications::{DetachAndPromptErr, NotificationId}, notifications::{DetachAndPromptErr, NotificationId},
open_remote_project_with_existing_connection, open_remote_project_with_existing_connection,
}; };
@@ -478,10 +479,11 @@ impl ProjectPicker {
.log_err()?; .log_err()?;
let window = cx let window = cx
.open_window(options, |window, cx| { .open_window(options, |window, cx| {
cx.new(|cx| { let workspace = cx.new(|cx| {
telemetry::event!("SSH Project Created"); telemetry::event!("SSH Project Created");
Workspace::new(None, project.clone(), app_state.clone(), window, cx) Workspace::new(None, project.clone(), app_state.clone(), window, cx)
}) });
cx.new(|cx| MultiWorkspace::new(workspace, cx))
}) })
.log_err()?; .log_err()?;
@@ -808,11 +810,18 @@ impl RemoteServerProjects {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let this = Self::new_inner( let configs = workspace
Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new( .read_with(cx, |workspace, cx| find_devcontainer_configs(workspace, cx))
DevContainerCreationProgress::Creating, .unwrap_or_default();
cx,
)), let initial_mode = if configs.len() > 1 {
DevContainerCreationProgress::SelectingConfig
} else {
DevContainerCreationProgress::Creating
};
let mut this = Self::new_inner(
Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(initial_mode, cx)),
false, false,
fs, fs,
window, window,
@@ -820,35 +829,15 @@ impl RemoteServerProjects {
cx, cx,
); );
// Spawn a task to scan for configs and then start the container if configs.len() > 1 {
cx.spawn_in(window, async move |entity, cx| { let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
let configs = find_devcontainer_configs(cx); this.dev_container_picker =
Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
entity } else {
.update_in(cx, |this, window, cx| { let config = configs.into_iter().next();
if configs.len() > 1 { this.open_dev_container(config, window, cx);
// Multiple configs found - show selection UI this.view_in_progress_dev_container(window, cx);
let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity()); }
this.dev_container_picker = Some(
cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)),
);
let state = CreateRemoteDevContainer::new(
DevContainerCreationProgress::SelectingConfig,
cx,
);
this.mode = Mode::CreateRemoteDevContainer(state);
cx.notify();
} else {
// Single or no config - proceed with opening
let config = configs.into_iter().next();
this.open_dev_container(config, window, cx);
this.view_in_progress_dev_container(window, cx);
}
})
.log_err();
})
.detach();
this this
} }
@@ -1551,7 +1540,9 @@ impl RemoteServerProjects {
let replace_window = match (create_new_window, secondary_confirm) { let replace_window = match (create_new_window, secondary_confirm) {
(true, false) | (false, true) => None, (true, false) | (false, true) => None,
(true, true) | (false, false) => window.window_handle().downcast::<Workspace>(), (true, true) | (false, false) => {
window.window_handle().downcast::<MultiWorkspace>()
}
}; };
cx.spawn_in(window, async move |_, cx| { cx.spawn_in(window, async move |_, cx| {
@@ -1803,25 +1794,25 @@ impl RemoteServerProjects {
} }
fn init_dev_container_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn init_dev_container_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
cx.spawn_in(window, async move |entity, cx| { let configs = self
let configs = find_devcontainer_configs(cx); .workspace
.read_with(cx, |workspace, cx| find_devcontainer_configs(workspace, cx))
.unwrap_or_default();
entity if configs.len() > 1 {
.update_in(cx, |this, window, cx| { let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity()); self.dev_container_picker =
this.dev_container_picker = Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
let state = CreateRemoteDevContainer::new( let state =
DevContainerCreationProgress::SelectingConfig, CreateRemoteDevContainer::new(DevContainerCreationProgress::SelectingConfig, cx);
cx, self.mode = Mode::CreateRemoteDevContainer(state);
); cx.notify();
this.mode = Mode::CreateRemoteDevContainer(state); } else {
cx.notify(); let config = configs.into_iter().next();
}) self.open_dev_container(config, window, cx);
.log_err(); self.view_in_progress_dev_container(window, cx);
}) }
.detach();
} }
fn open_dev_container( fn open_dev_container(
@@ -1830,21 +1821,25 @@ impl RemoteServerProjects {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let Some(app_state) = self let Some((app_state, context)) = self
.workspace .workspace
.read_with(cx, |workspace, _| workspace.app_state().clone()) .read_with(cx, |workspace, cx| {
let app_state = workspace.app_state().clone();
let context = DevContainerContext::from_workspace(workspace, cx)?;
Some((app_state, context))
})
.log_err() .log_err()
.flatten()
else { else {
log::error!("No active project directory for Dev Container");
return; return;
}; };
let replace_window = window.window_handle().downcast::<Workspace>(); let replace_window = window.window_handle().downcast::<MultiWorkspace>();
cx.spawn_in(window, async move |entity, cx| { cx.spawn_in(window, async move |entity, cx| {
let (connection, starting_dir) = let (connection, starting_dir) =
match start_dev_container_with_config(cx, app_state.node_runtime.clone(), config) match start_dev_container_with_config(context, config).await {
.await
{
Ok((c, s)) => (Connection::DevContainer(c), s), Ok((c, s)) => (Connection::DevContainer(c), s),
Err(e) => { Err(e) => {
log::error!("Failed to start dev container: {:?}", e); log::error!("Failed to start dev container: {:?}", e);
+2 -2
View File
@@ -8,7 +8,7 @@ use ui::{
Render, Styled, StyledExt, Toggleable, Window, div, h_flex, rems, v_flex, Render, Styled, StyledExt, Toggleable, Window, div, h_flex, rems, v_flex,
}; };
use util::ResultExt as _; use util::ResultExt as _;
use workspace::{ModalView, Workspace}; use workspace::{ModalView, MultiWorkspace};
use crate::open_remote_project; use crate::open_remote_project;
@@ -249,7 +249,7 @@ impl WslOpenModal {
false => !secondary, false => !secondary,
}; };
let replace_window = match replace_current_window { let replace_window = match replace_current_window {
true => window.window_handle().downcast::<Workspace>(), true => window.window_handle().downcast::<MultiWorkspace>(),
false => None, false => None,
}; };
+4 -2
View File
@@ -779,8 +779,10 @@ mod tests {
let fs = project::FakeFs::new(cx.background_executor.clone()); let fs = project::FakeFs::new(cx.background_executor.clone());
let project = project::Project::test(fs, [] as [&Path; 0], cx).await; let project = project::Project::test(fs, [] as [&Path; 0], cx).await;
let window = let window =
cx.add_window(|window, cx| workspace::Workspace::test_new(project, window, cx)); cx.add_window(|window, cx| workspace::MultiWorkspace::test_new(project, window, cx));
let workspace = window.root(cx).expect("workspace should exist"); let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let weak_workspace = workspace.downgrade(); let weak_workspace = workspace.downgrade();
let visual_cx = gpui::VisualTestContext::from_window(window.into(), cx); let visual_cx = gpui::VisualTestContext::from_window(window.into(), cx);
(visual_cx, weak_workspace) (visual_cx, weak_workspace)
+11 -8
View File
@@ -4,8 +4,8 @@ use editor::{CompletionProvider, SelectionEffects};
use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab}; use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab};
use gpui::{ use gpui::{
App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, PromptLevel, App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, PromptLevel,
Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, Subscription, Task, TextStyle, Tiling, TitlebarOptions, WindowBounds, WindowHandle,
actions, point, size, transparent_black, WindowOptions, actions, point, size, transparent_black,
}; };
use language::{Buffer, LanguageRegistry, language_settings::SoftWrap}; use language::{Buffer, LanguageRegistry, language_settings::SoftWrap};
use language_model::{ use language_model::{
@@ -24,7 +24,7 @@ use theme::ThemeSettings;
use ui::{Divider, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*}; use ui::{Divider, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
use ui_input::ErasedEditor; use ui_input::ErasedEditor;
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
use workspace::{Workspace, WorkspaceSettings, client_side_decorations}; use workspace::{MultiWorkspace, Workspace, WorkspaceSettings, client_side_decorations};
use zed_actions::assistant::InlineAssist; use zed_actions::assistant::InlineAssist;
use prompt_store::*; use prompt_store::*;
@@ -968,12 +968,14 @@ impl RulesLibrary {
.assist(rule_editor, initial_prompt, window, cx); .assist(rule_editor, initial_prompt, window, cx);
} else { } else {
for window in cx.windows() { for window in cx.windows() {
if let Some(workspace) = window.downcast::<Workspace>() { if let Some(multi_workspace) = window.downcast::<MultiWorkspace>() {
let panel = workspace let panel = multi_workspace
.update(cx, |workspace, window, cx| { .update(cx, |multi_workspace, window, cx| {
window.activate_window(); window.activate_window();
self.inline_assist_delegate multi_workspace.workspace().update(cx, |workspace, cx| {
.focus_agent_panel(workspace, window, cx) self.inline_assist_delegate
.focus_agent_panel(workspace, window, cx)
})
}) })
.ok(); .ok();
if panel == Some(true) { if panel == Some(true) {
@@ -1427,6 +1429,7 @@ impl Render for RulesLibrary {
), ),
window, window,
cx, cx,
Tiling::default(),
) )
} }
} }
+242 -256
View File
@@ -2495,7 +2495,6 @@ pub fn perform_project_search(
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use std::{ use std::{
ops::Deref as _,
path::PathBuf, path::PathBuf,
sync::{ sync::{
Arc, Arc,
@@ -2516,7 +2515,7 @@ pub mod tests {
}; };
use util::{path, paths::PathStyle, rel_path::rel_path}; use util::{path, paths::PathStyle, rel_path::rel_path};
use util_macros::perf; use util_macros::perf;
use workspace::DeploySearch; use workspace::{DeploySearch, MultiWorkspace};
#[perf] #[perf]
#[gpui::test] #[gpui::test]
@@ -2632,8 +2631,11 @@ pub mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window =
let workspace = window.root(cx).unwrap(); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx)); let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx));
let search_view = cx.add_window(|window, cx| { let search_view = cx.add_window(|window, cx| {
ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None)
@@ -2791,14 +2793,16 @@ pub mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = window; let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
let active_item = cx.read(|cx| { let active_item = cx.read(|cx| {
workspace workspace
.read(cx) .read(cx)
.unwrap()
.active_pane() .active_pane()
.read(cx) .read(cx)
.active_item() .active_item()
@@ -2809,27 +2813,24 @@ pub mod tests {
"Expected no search panel to be active" "Expected no search panel to be active"
); );
window workspace.update_in(cx, move |workspace, window, cx| {
.update(cx, move |workspace, window, cx| { assert_eq!(workspace.panes().len(), 1);
assert_eq!(workspace.panes().len(), 1); workspace.panes()[0].update(cx, |pane, cx| {
workspace.panes()[0].update(cx, |pane, cx| { pane.toolbar()
pane.toolbar() .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
.update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) });
});
ProjectSearchView::deploy_search( ProjectSearchView::deploy_search(
workspace, workspace,
&workspace::DeploySearch::find(), &workspace::DeploySearch::find(),
window, window,
cx, cx,
) )
}) });
.unwrap();
let Some(search_view) = cx.read(|cx| { let Some(search_view) = cx.read(|cx| {
workspace workspace
.read(cx) .read(cx)
.unwrap()
.active_pane() .active_pane()
.read(cx) .read(cx)
.active_item() .active_item()
@@ -2969,16 +2970,14 @@ pub mod tests {
}); });
}).unwrap(); }).unwrap();
workspace workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { ProjectSearchView::deploy_search(
ProjectSearchView::deploy_search( workspace,
workspace, &workspace::DeploySearch::find(),
&workspace::DeploySearch::find(), window,
window, cx,
cx, )
) });
})
.unwrap();
window.update(cx, |_, window, cx| { window.update(cx, |_, window, cx| {
search_view.update(cx, |search_view, cx| { search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row"); assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
@@ -3032,30 +3031,30 @@ pub mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = window; let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
window workspace.update_in(cx, move |workspace, window, cx| {
.update(cx, move |workspace, window, cx| { workspace.panes()[0].update(cx, |pane, cx| {
workspace.panes()[0].update(cx, |pane, cx| { pane.toolbar()
pane.toolbar() .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
.update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) });
});
ProjectSearchView::deploy_search( ProjectSearchView::deploy_search(
workspace, workspace,
&workspace::DeploySearch::find(), &workspace::DeploySearch::find(),
window, window,
cx, cx,
) )
}) });
.unwrap();
let Some(search_view) = cx.read(|cx| { let Some(search_view) = cx.read(|cx| {
workspace workspace
.read(cx) .read(cx)
.unwrap()
.active_pane() .active_pane()
.read(cx) .read(cx)
.active_item() .active_item()
@@ -3153,14 +3152,16 @@ pub mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = window; let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
let active_item = cx.read(|cx| { let active_item = cx.read(|cx| {
workspace workspace
.read(cx) .read(cx)
.unwrap()
.active_pane() .active_pane()
.read(cx) .read(cx)
.active_item() .active_item()
@@ -3171,22 +3172,19 @@ pub mod tests {
"Expected no search panel to be active" "Expected no search panel to be active"
); );
window workspace.update_in(cx, move |workspace, window, cx| {
.update(cx, move |workspace, window, cx| { assert_eq!(workspace.panes().len(), 1);
assert_eq!(workspace.panes().len(), 1); workspace.panes()[0].update(cx, |pane, cx| {
workspace.panes()[0].update(cx, |pane, cx| { pane.toolbar()
pane.toolbar() .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
.update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) });
});
ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
}) });
.unwrap();
let Some(search_view) = cx.read(|cx| { let Some(search_view) = cx.read(|cx| {
workspace workspace
.read(cx) .read(cx)
.unwrap()
.active_pane() .active_pane()
.read(cx) .read(cx)
.active_item() .active_item()
@@ -3326,16 +3324,13 @@ pub mod tests {
}); });
}).unwrap(); }).unwrap();
workspace workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) });
})
.unwrap();
cx.background_executor.run_until_parked(); cx.background_executor.run_until_parked();
let Some(search_view_2) = cx.read(|cx| { let Some(search_view_2) = cx.read(|cx| {
workspace workspace
.read(cx) .read(cx)
.unwrap()
.active_pane() .active_pane()
.read(cx) .read(cx)
.active_item() .active_item()
@@ -3456,8 +3451,11 @@ pub mod tests {
let worktree_id = project.read_with(cx, |project, cx| { let worktree_id = project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
}); });
let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = window.root(cx).unwrap(); let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
let active_item = cx.read(|cx| { let active_item = cx.read(|cx| {
@@ -3473,17 +3471,15 @@ pub mod tests {
"Expected no search panel to be active" "Expected no search panel to be active"
); );
window workspace.update_in(cx, move |workspace, window, cx| {
.update(cx, move |workspace, window, cx| { assert_eq!(workspace.panes().len(), 1);
assert_eq!(workspace.panes().len(), 1); workspace.panes()[0].update(cx, move |pane, cx| {
workspace.panes()[0].update(cx, move |pane, cx| { pane.toolbar()
pane.toolbar() .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
.update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) });
}); });
})
.unwrap();
let a_dir_entry = cx.update(|cx| { let a_dir_entry = cx.update(|_, cx| {
workspace workspace
.read(cx) .read(cx)
.project() .project()
@@ -3493,11 +3489,9 @@ pub mod tests {
.clone() .clone()
}); });
assert!(a_dir_entry.is_dir()); assert!(a_dir_entry.is_dir());
window workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry.path, window, cx)
ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry.path, window, cx) });
})
.unwrap();
let Some(search_view) = cx.read(|cx| { let Some(search_view) = cx.read(|cx| {
workspace workspace
@@ -3576,24 +3570,25 @@ pub mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = window.root(cx).unwrap(); let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
window workspace.update_in(cx, {
.update(cx, { let search_bar = search_bar.clone();
let search_bar = search_bar.clone(); |workspace, window, cx| {
|workspace, window, cx| { assert_eq!(workspace.panes().len(), 1);
assert_eq!(workspace.panes().len(), 1); workspace.panes()[0].update(cx, |pane, cx| {
workspace.panes()[0].update(cx, |pane, cx| { pane.toolbar()
pane.toolbar() .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
.update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) });
});
ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
} }
}) });
.unwrap();
let search_view = cx.read(|cx| { let search_view = cx.read(|cx| {
workspace workspace
@@ -3908,21 +3903,22 @@ pub mod tests {
this.worktrees(cx).next().unwrap().read(cx).id() this.worktrees(cx).next().unwrap().read(cx).id()
}); });
let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = window.root(cx).unwrap(); let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
let panes: Vec<_> = window
.update(cx, |this, _, _| this.panes().to_owned())
.unwrap(); .unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let panes: Vec<_> = workspace.update_in(cx, |this, _, _| this.panes().to_owned());
let search_bar_1 = window.build_entity(cx, |_, _| ProjectSearchBar::new()); let search_bar_1 = window.build_entity(cx, |_, _| ProjectSearchBar::new());
let search_bar_2 = window.build_entity(cx, |_, _| ProjectSearchBar::new()); let search_bar_2 = window.build_entity(cx, |_, _| ProjectSearchBar::new());
assert_eq!(panes.len(), 1); assert_eq!(panes.len(), 1);
let first_pane = panes.first().cloned().unwrap(); let first_pane = panes.first().cloned().unwrap();
assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0); assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 0);
window workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_path( workspace.open_path(
(worktree_id, rel_path("one.rs")), (worktree_id, rel_path("one.rs")),
Some(first_pane.downgrade()), Some(first_pane.downgrade()),
@@ -3931,25 +3927,22 @@ pub mod tests {
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 1);
// Add a project search item to the first pane // Add a project search item to the first pane
window workspace.update_in(cx, {
.update(cx, { let search_bar = search_bar_1.clone();
let search_bar = search_bar_1.clone(); |workspace, window, cx| {
|workspace, window, cx| { first_pane.update(cx, |pane, cx| {
first_pane.update(cx, |pane, cx| { pane.toolbar()
pane.toolbar() .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
.update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) });
});
ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
} }
}) });
.unwrap();
let search_view_1 = cx.read(|cx| { let search_view_1 = cx.read(|cx| {
workspace workspace
.read(cx) .read(cx)
@@ -3958,8 +3951,8 @@ pub mod tests {
.expect("Search view expected to appear after new search event trigger") .expect("Search view expected to appear after new search event trigger")
}); });
let second_pane = window let second_pane = workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.split_and_clone( workspace.split_and_clone(
first_pane.clone(), first_pane.clone(),
workspace::SplitDirection::Right, workspace::SplitDirection::Right,
@@ -3967,30 +3960,27 @@ pub mod tests {
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); assert_eq!(cx.update(|_, cx| second_pane.read(cx).items_len()), 1);
assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); assert_eq!(cx.update(|_, cx| second_pane.read(cx).items_len()), 1);
assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 2); assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 2);
// Add a project search item to the second pane // Add a project search item to the second pane
window workspace.update_in(cx, {
.update(cx, { let search_bar = search_bar_2.clone();
let search_bar = search_bar_2.clone(); let pane = second_pane.clone();
let pane = second_pane.clone(); move |workspace, window, cx| {
move |workspace, window, cx| { assert_eq!(workspace.panes().len(), 2);
assert_eq!(workspace.panes().len(), 2); pane.update(cx, |pane, cx| {
pane.update(cx, |pane, cx| { pane.toolbar()
pane.toolbar() .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
.update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) });
});
ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
} }
}) });
.unwrap();
let search_view_2 = cx.read(|cx| { let search_view_2 = cx.read(|cx| {
workspace workspace
@@ -4001,8 +3991,8 @@ pub mod tests {
}); });
cx.run_until_parked(); cx.run_until_parked();
assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 2); assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 2);
assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2); assert_eq!(cx.update(|_, cx| second_pane.read(cx).items_len()), 2);
let update_search_view = let update_search_view =
|search_view: &Entity<ProjectSearchView>, query: &str, cx: &mut TestAppContext| { |search_view: &Entity<ProjectSearchView>, query: &str, cx: &mut TestAppContext| {
@@ -4133,15 +4123,17 @@ pub mod tests {
let worktree_id = project.update(cx, |this, cx| { let worktree_id = project.update(cx, |this, cx| {
this.worktrees(cx).next().unwrap().read(cx).id() this.worktrees(cx).next().unwrap().read(cx).id()
}); });
let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
let panes: Vec<_> = window let workspace = window
.update(cx, |this, _, _| this.panes().to_owned()) .read_with(cx, |mw, _| mw.workspace().clone())
.unwrap(); .unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let panes: Vec<_> = workspace.update_in(cx, |this, _, _| this.panes().to_owned());
assert_eq!(panes.len(), 1); assert_eq!(panes.len(), 1);
let first_pane = panes.first().cloned().unwrap(); let first_pane = panes.first().cloned().unwrap();
assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0); assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 0);
window workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_path( workspace.open_path(
(worktree_id, rel_path("one.rs")), (worktree_id, rel_path("one.rs")),
Some(first_pane.downgrade()), Some(first_pane.downgrade()),
@@ -4150,12 +4142,11 @@ pub mod tests {
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 1);
let second_pane = window let second_pane = workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.split_and_clone( workspace.split_and_clone(
first_pane.clone(), first_pane.clone(),
workspace::SplitDirection::Right, workspace::SplitDirection::Right,
@@ -4163,10 +4154,9 @@ pub mod tests {
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap(); .unwrap();
assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); assert_eq!(cx.update(|_, cx| second_pane.read(cx).items_len()), 1);
assert!( assert!(
window window
.update(cx, |_, window, cx| second_pane .update(cx, |_, window, cx| second_pane
@@ -4175,76 +4165,66 @@ pub mod tests {
.unwrap() .unwrap()
); );
let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
window workspace.update_in(cx, {
.update(cx, { let search_bar = search_bar.clone();
let search_bar = search_bar.clone(); let pane = first_pane.clone();
let pane = first_pane.clone(); move |workspace, window, cx| {
move |workspace, window, cx| { assert_eq!(workspace.panes().len(), 2);
assert_eq!(workspace.panes().len(), 2); pane.update(cx, move |pane, cx| {
pane.update(cx, move |pane, cx| { pane.toolbar()
pane.toolbar() .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
.update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) });
}); }
} });
})
.unwrap();
// Add a project search item to the second pane // Add a project search item to the second pane
window workspace.update_in(cx, {
.update(cx, { |workspace, window, cx| {
|workspace, window, cx| { assert_eq!(workspace.panes().len(), 2);
assert_eq!(workspace.panes().len(), 2); second_pane.update(cx, |pane, cx| {
second_pane.update(cx, |pane, cx| { pane.toolbar()
pane.toolbar() .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx))
.update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) });
});
ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx)
} }
}) });
.unwrap();
cx.run_until_parked(); cx.run_until_parked();
assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2); assert_eq!(cx.update(|_, cx| second_pane.read(cx).items_len()), 2);
assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); assert_eq!(cx.update(|_, cx| first_pane.read(cx).items_len()), 1);
// Focus the first pane // Focus the first pane
window workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { assert_eq!(workspace.active_pane(), &second_pane);
assert_eq!(workspace.active_pane(), &second_pane); second_pane.update(cx, |this, cx| {
second_pane.update(cx, |this, cx| { assert_eq!(this.active_item_index(), 1);
assert_eq!(this.active_item_index(), 1); this.activate_previous_item(&Default::default(), window, cx);
this.activate_previous_item(&Default::default(), window, cx); assert_eq!(this.active_item_index(), 0);
assert_eq!(this.active_item_index(), 0); });
}); workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx);
workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx); });
}) workspace.update_in(cx, |workspace, _, cx| {
.unwrap(); assert_eq!(workspace.active_pane(), &first_pane);
window assert_eq!(first_pane.read(cx).items_len(), 1);
.update(cx, |workspace, _, cx| { assert_eq!(second_pane.read(cx).items_len(), 2);
assert_eq!(workspace.active_pane(), &first_pane); });
assert_eq!(first_pane.read(cx).items_len(), 1);
assert_eq!(second_pane.read(cx).items_len(), 2);
})
.unwrap();
// Deploy a new search // Deploy a new search
cx.dispatch_action(window.into(), DeploySearch::find()); cx.dispatch_action(DeploySearch::find());
// Both panes should now have a project search in them // Both panes should now have a project search in them
window workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { assert_eq!(workspace.active_pane(), &first_pane);
assert_eq!(workspace.active_pane(), &first_pane); first_pane.read_with(cx, |this, _| {
first_pane.read_with(cx, |this, _| { assert_eq!(this.active_item_index(), 1);
assert_eq!(this.active_item_index(), 1); assert_eq!(this.items_len(), 2);
assert_eq!(this.items_len(), 2); });
}); second_pane.update(cx, |this, cx| {
second_pane.update(cx, |this, cx| { assert!(!cx.focus_handle().contains_focused(window, cx));
assert!(!cx.focus_handle().contains_focused(window, cx)); assert_eq!(this.items_len(), 2);
assert_eq!(this.items_len(), 2); });
}); });
})
.unwrap();
// Focus the second pane's non-search item // Focus the second pane's non-search item
window window
@@ -4256,7 +4236,7 @@ pub mod tests {
.unwrap(); .unwrap();
// Deploy a new search // Deploy a new search
cx.dispatch_action(window.into(), DeploySearch::find()); cx.dispatch_action(DeploySearch::find());
// The project search view should now be focused in the second pane // The project search view should now be focused in the second pane
// And the number of items should be unchanged. // And the number of items should be unchanged.
@@ -4310,8 +4290,11 @@ pub mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window =
let workspace = window.root(cx).unwrap(); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let search = cx.new(|cx| ProjectSearch::new(project, cx)); let search = cx.new(|cx| ProjectSearch::new(project, cx));
let search_view = cx.add_window(|window, cx| { let search_view = cx.add_window(|window, cx| {
ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None)
@@ -4374,9 +4357,12 @@ pub mod tests {
let worktree_id = project.update(cx, |this, cx| { let worktree_id = project.update(cx, |this, cx| {
this.worktrees(cx).next().unwrap().read(cx).id() this.worktrees(cx).next().unwrap().read(cx).id()
}); });
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window =
let workspace = window.root(cx).unwrap(); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let mut cx = VisualTestContext::from_window(*window.deref(), cx); let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let mut cx = VisualTestContext::from_window(window.into(), cx);
let editor = workspace let editor = workspace
.update_in(&mut cx, |workspace, window, cx| { .update_in(&mut cx, |workspace, window, cx| {
@@ -4398,9 +4384,7 @@ pub mod tests {
search_bar search_bar
}); });
let panes: Vec<_> = window let panes: Vec<_> = workspace.update_in(&mut cx, |this, _, _| this.panes().to_owned());
.update(&mut cx, |this, _, _| this.panes().to_owned())
.unwrap();
assert_eq!(panes.len(), 1); assert_eq!(panes.len(), 1);
let pane = panes.first().cloned().unwrap(); let pane = panes.first().cloned().unwrap();
pane.update_in(&mut cx, |pane, window, cx| { pane.update_in(&mut cx, |pane, window, cx| {
@@ -4450,7 +4434,12 @@ pub mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window =
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
struct EmptyModalView { struct EmptyModalView {
focus_handle: gpui::FocusHandle, focus_handle: gpui::FocusHandle,
@@ -4468,34 +4457,28 @@ pub mod tests {
} }
impl workspace::ModalView for EmptyModalView {} impl workspace::ModalView for EmptyModalView {}
window workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { workspace.toggle_modal(window, cx, |_, cx| EmptyModalView {
workspace.toggle_modal(window, cx, |_, cx| EmptyModalView { focus_handle: cx.focus_handle(),
focus_handle: cx.focus_handle(), });
}); assert!(workspace.has_active_modal(window, cx));
assert!(workspace.has_active_modal(window, cx)); });
})
.unwrap();
cx.dispatch_action(window.into(), Deploy::find()); cx.dispatch_action(Deploy::find());
window workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { assert!(!workspace.has_active_modal(window, cx));
assert!(!workspace.has_active_modal(window, cx)); workspace.toggle_modal(window, cx, |_, cx| EmptyModalView {
workspace.toggle_modal(window, cx, |_, cx| EmptyModalView { focus_handle: cx.focus_handle(),
focus_handle: cx.focus_handle(), });
}); assert!(workspace.has_active_modal(window, cx));
assert!(workspace.has_active_modal(window, cx)); });
})
.unwrap();
cx.dispatch_action(window.into(), DeploySearch::find()); cx.dispatch_action(DeploySearch::find());
window workspace.update_in(cx, |workspace, window, cx| {
.update(cx, |workspace, window, cx| { assert!(!workspace.has_active_modal(window, cx));
assert!(!workspace.has_active_modal(window, cx)); });
})
.unwrap();
} }
#[perf] #[perf]
@@ -4562,8 +4545,12 @@ pub mod tests {
}, },
); );
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let window =
let workspace = window.root(cx).unwrap(); cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx)); let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx));
let search_view = cx.add_window(|window, cx| { let search_view = cx.add_window(|window, cx| {
ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None)
@@ -4609,8 +4596,8 @@ pub mod tests {
"We did drop the previous buffer when cleared the old project search results, hence another query was made", "We did drop the previous buffer when cleared the old project search results, hence another query was made",
); );
let singleton_editor = window let singleton_editor = workspace
.update(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_abs_path( workspace.open_abs_path(
PathBuf::from(path!("/dir/main.rs")), PathBuf::from(path!("/dir/main.rs")),
workspace::OpenOptions::default(), workspace::OpenOptions::default(),
@@ -4618,7 +4605,6 @@ pub mod tests {
cx, cx,
) )
}) })
.unwrap()
.await .await
.unwrap() .unwrap()
.downcast::<Editor>() .downcast::<Editor>()
+1 -1
View File
@@ -191,7 +191,7 @@ pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) {
struct NotifType(); struct NotifType();
let notification_id = NotificationId::unique::<NotifType>(); let notification_id = NotificationId::unique::<NotifType>();
let Some(workspace) = window.root::<Workspace>().flatten() else { let Some(workspace) = Workspace::for_window(window, cx) else {
return; return;
}; };
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
+14
View File
@@ -47,6 +47,15 @@ impl Session {
} }
} }
#[cfg(any(test, feature = "test-support"))]
pub fn test_with_old_session(old_session_id: String) -> Self {
Self {
session_id: uuid::Uuid::new_v4().to_string(),
old_session_id: Some(old_session_id),
old_window_ids: None,
}
}
pub fn id(&self) -> &str { pub fn id(&self) -> &str {
&self.session_id &self.session_id
} }
@@ -109,6 +118,11 @@ impl AppSession {
self.session.old_session_id.as_deref() self.session.old_session_id.as_deref()
} }
#[cfg(any(test, feature = "test-support"))]
pub fn replace_session_for_test(&mut self, session: Session) {
self.session = session;
}
pub fn last_session_window_stack(&self) -> Option<Vec<WindowId>> { pub fn last_session_window_stack(&self) -> Option<Vec<WindowId>> {
self.session.old_window_ids.clone() self.session.old_window_ids.clone()
} }
@@ -287,7 +287,7 @@ mod tests {
use serde_json::json; use serde_json::json;
use settings::Settings; use settings::Settings;
use theme::{self, ThemeSettings}; use theme::{self, ThemeSettings};
use workspace::{self, AppState}; use workspace::{self, AppState, MultiWorkspace};
use zed_actions::settings_profile_selector; use zed_actions::settings_profile_selector;
async fn init_test( async fn init_test(
@@ -320,8 +320,11 @@ mod tests {
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, ["/test".as_ref()], cx).await; let project = Project::test(fs, ["/test".as_ref()], cx).await;
let (workspace, cx) = let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = VisualTestContext::from_window(*window, cx).into_mut();
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
cx.update(|_, cx| { cx.update(|_, cx| {
assert!(!cx.has_global::<ActiveSettingsProfileName>()); assert!(!cx.has_global::<ActiveSettingsProfileName>());
+154 -135
View File
@@ -9,8 +9,9 @@ use fuzzy::StringMatchCandidate;
use gpui::{ use gpui::{
Action, App, AsyncApp, ClipboardItem, DEFAULT_ADDITIONAL_WINDOW_SIZE, Div, Entity, FocusHandle, Action, App, AsyncApp, ClipboardItem, DEFAULT_ADDITIONAL_WINDOW_SIZE, Div, Entity, FocusHandle,
Focusable, Global, KeyContext, ListState, ReadGlobal as _, ScrollHandle, Stateful, Focusable, Global, KeyContext, ListState, ReadGlobal as _, ScrollHandle, Stateful,
Subscription, Task, TitlebarOptions, UniformListScrollHandle, WeakEntity, Window, WindowBounds, Subscription, Task, Tiling, TitlebarOptions, UniformListScrollHandle, WeakEntity, Window,
WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, uniform_list, WindowBounds, WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px,
uniform_list,
}; };
use language::Buffer; use language::Buffer;
@@ -40,7 +41,9 @@ use ui::{
}; };
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath}; use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decorations}; use workspace::{
AppState, MultiWorkspace, OpenOptions, OpenVisible, Workspace, client_side_decorations,
};
use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt}; use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt};
use crate::components::{ use crate::components::{
@@ -394,7 +397,7 @@ pub fn init(cx: &mut App) {
|workspace, OpenSettingsAt { path }: &OpenSettingsAt, window, cx| { |workspace, OpenSettingsAt { path }: &OpenSettingsAt, window, cx| {
let window_handle = window let window_handle = window
.window_handle() .window_handle()
.downcast::<Workspace>() .downcast::<MultiWorkspace>()
.expect("Workspaces are root Windows"); .expect("Workspaces are root Windows");
open_settings_editor(workspace, Some(&path), false, window_handle, cx); open_settings_editor(workspace, Some(&path), false, window_handle, cx);
}, },
@@ -402,14 +405,14 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &OpenSettings, window, cx| { .register_action(|workspace, _: &OpenSettings, window, cx| {
let window_handle = window let window_handle = window
.window_handle() .window_handle()
.downcast::<Workspace>() .downcast::<MultiWorkspace>()
.expect("Workspaces are root Windows"); .expect("Workspaces are root Windows");
open_settings_editor(workspace, None, false, window_handle, cx); open_settings_editor(workspace, None, false, window_handle, cx);
}) })
.register_action(|workspace, _: &OpenProjectSettings, window, cx| { .register_action(|workspace, _: &OpenProjectSettings, window, cx| {
let window_handle = window let window_handle = window
.window_handle() .window_handle()
.downcast::<Workspace>() .downcast::<MultiWorkspace>()
.expect("Workspaces are root Windows"); .expect("Workspaces are root Windows");
open_settings_editor(workspace, None, true, window_handle, cx); open_settings_editor(workspace, None, true, window_handle, cx);
}); });
@@ -549,7 +552,7 @@ pub fn open_settings_editor(
_workspace: &mut Workspace, _workspace: &mut Workspace,
path: Option<&str>, path: Option<&str>,
open_project_settings: bool, open_project_settings: bool,
workspace_handle: WindowHandle<Workspace>, workspace_handle: WindowHandle<MultiWorkspace>,
cx: &mut App, cx: &mut App,
) { ) {
telemetry::event!("Settings Viewed"); telemetry::event!("Settings Viewed");
@@ -717,7 +720,7 @@ fn active_language_mut() -> Option<std::sync::RwLockWriteGuard<'static, Option<S
pub struct SettingsWindow { pub struct SettingsWindow {
title_bar: Option<Entity<PlatformTitleBar>>, title_bar: Option<Entity<PlatformTitleBar>>,
original_window: Option<WindowHandle<Workspace>>, original_window: Option<WindowHandle<MultiWorkspace>>,
files: Vec<(SettingsUiFile, FocusHandle)>, files: Vec<(SettingsUiFile, FocusHandle)>,
worktree_root_dirs: HashMap<WorktreeId, String>, worktree_root_dirs: HashMap<WorktreeId, String>,
current_file: SettingsUiFile, current_file: SettingsUiFile,
@@ -1450,7 +1453,7 @@ impl SettingsUiFile {
impl SettingsWindow { impl SettingsWindow {
fn new( fn new(
original_window: Option<WindowHandle<Workspace>>, original_window: Option<WindowHandle<MultiWorkspace>>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
@@ -1521,34 +1524,21 @@ impl SettingsWindow {
.detach(); .detach();
if let Some(app_state) = AppState::global(cx).upgrade() { if let Some(app_state) = AppState::global(cx).upgrade() {
for project in app_state let workspaces: Vec<Entity<Workspace>> = app_state
.workspace_store .workspace_store
.read(cx) .read(cx)
.workspaces() .workspaces()
.iter() .filter_map(|weak| weak.upgrade())
.filter_map(|space| { .collect();
space
.read(cx) for workspace in workspaces {
.ok() let project = workspace.read(cx).project().clone();
.map(|workspace| workspace.project().clone())
})
.collect::<Vec<_>>()
{
cx.observe_release_in(&project, window, |this, _, window, cx| { cx.observe_release_in(&project, window, |this, _, window, cx| {
this.fetch_files(window, cx) this.fetch_files(window, cx)
}) })
.detach(); .detach();
cx.subscribe_in(&project, window, Self::handle_project_event) cx.subscribe_in(&project, window, Self::handle_project_event)
.detach(); .detach();
}
for workspace in app_state
.workspace_store
.read(cx)
.workspaces()
.iter()
.filter_map(|space| space.entity(cx).ok())
{
cx.observe_release_in(&workspace, window, |this, _, window, cx| { cx.observe_release_in(&workspace, window, |this, _, window, cx| {
this.fetch_files(window, cx) this.fetch_files(window, cx)
}) })
@@ -3324,56 +3314,19 @@ impl SettingsWindow {
return; return;
}; };
original_window original_window
.update(cx, |workspace, window, cx| { .update(cx, |multi_workspace, window, cx| {
workspace multi_workspace
.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| { .workspace()
let project = workspace.project().clone(); .clone()
.update(cx, |workspace, cx| {
cx.spawn_in(window, async move |workspace, cx| { workspace
let (config_dir, settings_file) = .with_local_or_wsl_workspace(
project.update(cx, |project, cx| { window,
( cx,
project.try_windows_path_to_wsl( open_user_settings_in_workspace,
paths::config_dir().as_path(), )
cx, .detach();
), });
project.try_windows_path_to_wsl(
paths::settings_file().as_path(),
cx,
),
)
});
let config_dir = config_dir.await?;
let settings_file = settings_file.await?;
project
.update(cx, |project, cx| {
project.find_or_create_worktree(&config_dir, false, cx)
})
.await
.ok();
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_paths(
vec![settings_file],
OpenOptions {
visible: Some(OpenVisible::None),
..Default::default()
},
None,
window,
cx,
)
})?
.await;
workspace.update_in(cx, |_, window, cx| {
window.activate_window();
cx.notify();
})
})
.detach();
})
.detach();
}) })
.ok(); .ok();
@@ -3385,22 +3338,22 @@ impl SettingsWindow {
return; return;
}; };
let Some((worktree, corresponding_workspace)) = app_state let Some((workspace_window, worktree, corresponding_workspace)) = app_state
.workspace_store .workspace_store
.read(cx) .read(cx)
.workspaces() .workspaces_with_windows()
.iter() .filter_map(|(window_handle, weak)| {
.find_map(|workspace| { let workspace = weak.upgrade()?;
let window = window_handle.downcast::<MultiWorkspace>()?;
Some((window, workspace))
})
.find_map(|(window, workspace): (_, Entity<Workspace>)| {
workspace workspace
.read_with(cx, |workspace, cx| { .read(cx)
workspace .project()
.project() .read(cx)
.read(cx) .worktree_for_id(*worktree_id, cx)
.worktree_for_id(*worktree_id, cx) .map(|worktree| (window, worktree, workspace))
})
.ok()
.flatten()
.zip(Some(*workspace))
}) })
else { else {
log::error!( log::error!(
@@ -3428,14 +3381,15 @@ impl SettingsWindow {
// TODO: move zed::open_local_file() APIs to this crate, and // TODO: move zed::open_local_file() APIs to this crate, and
// re-implement the "initial_contents" behavior // re-implement the "initial_contents" behavior
corresponding_workspace let workspace_weak = corresponding_workspace.downgrade();
workspace_window
.update(cx, |_, window, cx| { .update(cx, |_, window, cx| {
cx.spawn_in(window, async move |workspace, cx| { cx.spawn_in(window, async move |_, cx| {
if let Some(create_task) = create_task { if let Some(create_task) = create_task {
create_task.await.ok()?; create_task.await.ok()?;
}; };
workspace workspace_weak
.update_in(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
workspace.open_path( workspace.open_path(
(worktree_id, settings_path.clone()), (worktree_id, settings_path.clone()),
@@ -3449,7 +3403,7 @@ impl SettingsWindow {
.await .await
.log_err()?; .log_err()?;
workspace workspace_weak
.update_in(cx, |_, window, cx| { .update_in(cx, |_, window, cx| {
window.activate_window(); window.activate_window();
cx.notify(); cx.notify();
@@ -3753,12 +3707,13 @@ impl Render for SettingsWindow {
), ),
window, window,
cx, cx,
Tiling::default(),
) )
} }
} }
fn all_projects( fn all_projects(
window: Option<&WindowHandle<Workspace>>, window: Option<&WindowHandle<MultiWorkspace>>,
cx: &App, cx: &App,
) -> impl Iterator<Item = Entity<Project>> { ) -> impl Iterator<Item = Entity<Project>> {
let mut seen_project_ids = std::collections::HashSet::new(); let mut seen_project_ids = std::collections::HashSet::new();
@@ -3769,10 +3724,19 @@ fn all_projects(
.workspace_store .workspace_store
.read(cx) .read(cx)
.workspaces() .workspaces()
.iter() .filter_map(|weak| weak.upgrade())
.filter_map(|workspace| Some(workspace.read(cx).ok()?.project().clone())) .map(|workspace: Entity<Workspace>| workspace.read(cx).project().clone())
.chain( .chain(
window.and_then(|workspace| Some(workspace.read(cx).ok()?.project().clone())), window
.and_then(|handle| handle.read(cx).ok())
.into_iter()
.flat_map(|multi_workspace| {
multi_workspace
.workspaces()
.iter()
.map(|workspace| workspace.read(cx).project().clone())
.collect::<Vec<_>>()
}),
) )
.filter(move |project| seen_project_ids.insert(project.entity_id())) .filter(move |project| seen_project_ids.insert(project.entity_id()))
}) })
@@ -3780,6 +3744,51 @@ fn all_projects(
.flatten() .flatten()
} }
fn open_user_settings_in_workspace(
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let project = workspace.project().clone();
cx.spawn_in(window, async move |workspace, cx| {
let (config_dir, settings_file) = project.update(cx, |project, cx| {
(
project.try_windows_path_to_wsl(paths::config_dir().as_path(), cx),
project.try_windows_path_to_wsl(paths::settings_file().as_path(), cx),
)
});
let config_dir = config_dir.await?;
let settings_file = settings_file.await?;
project
.update(cx, |project, cx| {
project.find_or_create_worktree(&config_dir, false, cx)
})
.await
.ok();
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_paths(
vec![settings_file],
OpenOptions {
visible: Some(OpenVisible::None),
..Default::default()
},
None,
window,
cx,
)
})?
.await;
workspace.update_in(cx, |_, window, cx| {
window.activate_window();
cx.notify();
})
})
.detach();
}
fn update_settings_file( fn update_settings_file(
file: SettingsUiFile, file: SettingsUiFile,
file_name: Option<&'static str>, file_name: Option<&'static str>,
@@ -4762,29 +4771,33 @@ pub mod test {
.await .await
.expect("Failed to create worktree_c"); .expect("Failed to create worktree_c");
let (_workspace1, cx) = cx.add_window_view(|window, cx| { let (_multi_workspace1, cx) = cx.add_window_view(|window, cx| {
Workspace::new( let workspace = cx.new(|cx| {
Default::default(), Workspace::new(
project1.clone(), Default::default(),
app_state.clone(), project1.clone(),
window, app_state.clone(),
cx, window,
) cx,
)
});
MultiWorkspace::new(workspace, cx)
}); });
let _workspace1_handle = cx.window_handle().downcast::<Workspace>().unwrap(); let (_multi_workspace2, cx) = cx.add_window_view(|window, cx| {
let workspace = cx.new(|cx| {
let (_workspace2, cx) = cx.add_window_view(|window, cx| { Workspace::new(
Workspace::new( Default::default(),
Default::default(), project2.clone(),
project2.clone(), app_state.clone(),
app_state.clone(), window,
window, cx,
cx, )
) });
MultiWorkspace::new(workspace, cx)
}); });
let workspace2_handle = cx.window_handle().downcast::<Workspace>().unwrap(); let workspace2_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
cx.run_until_parked(); cx.run_until_parked();
@@ -4903,17 +4916,20 @@ pub mod test {
.await .await
.expect("Failed to create worktree_a"); .expect("Failed to create worktree_a");
let (_workspace1, cx) = cx.add_window_view(|window, cx| { let (_multi_workspace1, cx) = cx.add_window_view(|window, cx| {
Workspace::new( let workspace = cx.new(|cx| {
Default::default(), Workspace::new(
project1.clone(), Default::default(),
app_state.clone(), project1.clone(),
window, app_state.clone(),
cx, window,
) cx,
)
});
MultiWorkspace::new(workspace, cx)
}); });
let workspace1_handle = cx.window_handle().downcast::<Workspace>().unwrap(); let workspace1_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
cx.run_until_parked(); cx.run_until_parked();
@@ -4950,14 +4966,17 @@ pub mod test {
.await .await
.expect("Failed to create worktree_b"); .expect("Failed to create worktree_b");
let (_workspace2, cx) = cx.add_window_view(|window, cx| { let (_multi_workspace2, cx) = cx.add_window_view(|window, cx| {
Workspace::new( let workspace = cx.new(|cx| {
Default::default(), Workspace::new(
project2.clone(), Default::default(),
app_state.clone(), project2.clone(),
window, app_state.clone(),
cx, window,
) cx,
)
});
MultiWorkspace::new(workspace, cx)
}); });
cx.run_until_parked(); cx.run_until_parked();
+43
View File
@@ -0,0 +1,43 @@
[package]
name = "sidebar"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/sidebar.rs"
[features]
default = []
test-support = []
[dependencies]
acp_thread.workspace = true
agent_ui.workspace = true
db.workspace = true
fs.workspace = true
fuzzy.workspace = true
serde_json.workspace = true
gpui.workspace = true
picker.workspace = true
project.workspace = true
recent_projects.workspace = true
theme.workspace = true
ui.workspace = true
ui_input.workspace = true
util.workspace = true
workspace.workspace = true
[dev-dependencies]
editor.workspace = true
feature_flags.workspace = true
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
recent_projects = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
+1
View File
@@ -0,0 +1 @@
../../LICENSE-GPL
File diff suppressed because it is too large. Load diff
+31 -21
View File
@@ -5,7 +5,7 @@ use menu::SelectPrevious;
use project::{Project, ProjectPath}; use project::{Project, ProjectPath};
use serde_json::json; use serde_json::json;
use util::{path, rel_path::rel_path}; use util::{path, rel_path::rel_path};
use workspace::{ActivatePreviousItem, AppState, Workspace, item::test::TestItem}; use workspace::{ActivatePreviousItem, AppState, MultiWorkspace, Workspace, item::test::TestItem};
#[ctor::ctor] #[ctor::ctor]
fn init_logger() { fn init_logger() {
@@ -33,8 +33,9 @@ async fn test_open_with_prev_tab_selected_and_cycle_on_toggle_action(
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let tab_1 = open_buffer("1.txt", &workspace, cx).await; let tab_1 = open_buffer("1.txt", &workspace, cx).await;
let tab_2 = open_buffer("2.txt", &workspace, cx).await; let tab_2 = open_buffer("2.txt", &workspace, cx).await;
@@ -89,8 +90,9 @@ async fn test_open_with_last_tab_selected(cx: &mut gpui::TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let tab_1 = open_buffer("1.txt", &workspace, cx).await; let tab_1 = open_buffer("1.txt", &workspace, cx).await;
let tab_2 = open_buffer("2.txt", &workspace, cx).await; let tab_2 = open_buffer("2.txt", &workspace, cx).await;
@@ -123,8 +125,9 @@ async fn test_open_item_on_modifiers_release(cx: &mut gpui::TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let tab_1 = open_buffer("1.txt", &workspace, cx).await; let tab_1 = open_buffer("1.txt", &workspace, cx).await;
let tab_2 = open_buffer("2.txt", &workspace, cx).await; let tab_2 = open_buffer("2.txt", &workspace, cx).await;
@@ -151,8 +154,9 @@ async fn test_open_on_empty_pane(cx: &mut gpui::TestAppContext) {
app_state.fs.as_fake().insert_tree("/root", json!({})).await; app_state.fs.as_fake().insert_tree("/root", json!({})).await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
cx.simulate_modifiers_change(Modifiers::control()); cx.simulate_modifiers_change(Modifiers::control());
let tab_switcher = open_tab_switcher(false, &workspace, cx); let tab_switcher = open_tab_switcher(false, &workspace, cx);
@@ -174,8 +178,9 @@ async fn test_open_with_single_item(cx: &mut gpui::TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let tab = open_buffer("1.txt", &workspace, cx).await; let tab = open_buffer("1.txt", &workspace, cx).await;
@@ -204,8 +209,9 @@ async fn test_close_selected_item(cx: &mut gpui::TestAppContext) {
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let tab_1 = open_buffer("1.txt", &workspace, cx).await; let tab_1 = open_buffer("1.txt", &workspace, cx).await;
let tab_3 = open_buffer("3.txt", &workspace, cx).await; let tab_3 = open_buffer("3.txt", &workspace, cx).await;
@@ -369,8 +375,9 @@ async fn test_open_in_active_pane_deduplicates_files_by_path(cx: &mut gpui::Test
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_buffer("1.txt", &workspace, cx).await; open_buffer("1.txt", &workspace, cx).await;
open_buffer("2.txt", &workspace, cx).await; open_buffer("2.txt", &workspace, cx).await;
@@ -406,8 +413,9 @@ async fn test_open_in_active_pane_clones_files_to_current_pane(cx: &mut gpui::Te
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_buffer("1.txt", &workspace, cx).await; open_buffer("1.txt", &workspace, cx).await;
@@ -453,8 +461,9 @@ async fn test_open_in_active_pane_clones_files_to_current_pane(cx: &mut gpui::Te
async fn test_open_in_active_pane_moves_terminals_to_current_pane(cx: &mut gpui::TestAppContext) { async fn test_open_in_active_pane_moves_terminals_to_current_pane(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let test_item = cx.new(|cx| TestItem::new(cx).with_label("terminal")); let test_item = cx.new(|cx| TestItem::new(cx).with_label("terminal"));
workspace.update_in(cx, |workspace, window, cx| { workspace.update_in(cx, |workspace, window, cx| {
@@ -506,8 +515,9 @@ async fn test_open_in_active_pane_closes_file_in_all_panes(cx: &mut gpui::TestAp
.await; .await;
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
open_buffer("1.txt", &workspace, cx).await; open_buffer("1.txt", &workspace, cx).await;
+10 -7
View File
@@ -754,7 +754,7 @@ mod tests {
use serde_json::json; use serde_json::json;
use task::TaskTemplates; use task::TaskTemplates;
use util::path; use util::path;
use workspace::{CloseInactiveTabsAndPanes, OpenOptions, OpenVisible}; use workspace::{CloseInactiveTabsAndPanes, MultiWorkspace, OpenOptions, OpenVisible};
use crate::{modal::Spawn, tests::init_test}; use crate::{modal::Spawn, tests::init_test};
@@ -787,8 +787,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let tasks_picker = open_spawn_tasks(&workspace, cx); let tasks_picker = open_spawn_tasks(&workspace, cx);
assert_eq!( assert_eq!(
@@ -960,8 +961,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let tasks_picker = open_spawn_tasks(&workspace, cx); let tasks_picker = open_spawn_tasks(&workspace, cx);
assert_eq!( assert_eq!(
@@ -1115,8 +1117,9 @@ mod tests {
))), ))),
)); ));
}); });
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let _ts_file_1 = workspace let _ts_file_1 = workspace
.update_in(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
+4 -3
View File
@@ -400,7 +400,7 @@ mod tests {
use task::{TaskContext, TaskVariables, VariableName}; use task::{TaskContext, TaskVariables, VariableName};
use ui::VisualContext; use ui::VisualContext;
use util::{path, rel_path::rel_path}; use util::{path, rel_path::rel_path};
use workspace::{AppState, Workspace}; use workspace::{AppState, MultiWorkspace};
use crate::task_contexts; use crate::task_contexts;
@@ -474,8 +474,9 @@ mod tests {
let worktree_id = project.update(cx, |project, cx| { let worktree_id = project.update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
}); });
let (workspace, cx) = let (multi_workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let buffer1 = workspace let buffer1 = workspace
.update(cx, |this, cx| { .update(cx, |this, cx| {
+22 -18
View File
@@ -1847,6 +1847,7 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use project::FakeFs; use project::FakeFs;
use settings::SettingsStore; use settings::SettingsStore;
use workspace::MultiWorkspace;
#[test] #[test]
fn test_prepare_empty_task() { fn test_prepare_empty_task() {
@@ -1878,13 +1879,14 @@ mod tests {
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await; let project = Project::test(fs, [], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let window_handle =
cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
let (window_handle, terminal_panel) = workspace let terminal_panel = window_handle
.update(cx, |workspace, window, cx| { .update(cx, |multi_workspace, window, cx| {
let window_handle = window.window_handle(); multi_workspace.workspace().update(cx, |workspace, cx| {
let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); cx.new(|cx| TerminalPanel::new(workspace, window, cx))
(window_handle, terminal_panel) })
}) })
.unwrap(); .unwrap();
@@ -1963,13 +1965,14 @@ mod tests {
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await; let project = Project::test(fs, [], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let window_handle =
cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
let (window_handle, terminal_panel) = workspace let terminal_panel = window_handle
.update(cx, |workspace, window, cx| { .update(cx, |multi_workspace, window, cx| {
let window_handle = window.window_handle(); multi_workspace.workspace().update(cx, |workspace, cx| {
let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); cx.new(|cx| TerminalPanel::new(workspace, window, cx))
(window_handle, terminal_panel) })
}) })
.unwrap(); .unwrap();
@@ -2006,13 +2009,14 @@ mod tests {
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await; let project = Project::test(fs, [], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let window_handle =
cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
let (window_handle, terminal_panel) = workspace let terminal_panel = window_handle
.update(cx, |workspace, window, cx| { .update(cx, |multi_workspace, window, cx| {
let window_handle = window.window_handle(); multi_workspace.workspace().update(cx, |workspace, cx| {
let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); cx.new(|cx| TerminalPanel::new(workspace, window, cx))
(window_handle, terminal_panel) })
}) })
.unwrap(); .unwrap();
@@ -523,7 +523,7 @@ mod tests {
terminal_settings::{AlternateScroll, CursorShape}, terminal_settings::{AlternateScroll, CursorShape},
}; };
use util::path; use util::path;
use workspace::AppState; use workspace::{AppState, MultiWorkspace};
async fn init_test( async fn init_test(
app_cx: &mut TestAppContext, app_cx: &mut TestAppContext,
@@ -552,8 +552,9 @@ mod tests {
) )
.await; .await;
let (workspace, _cx) = let (multi_workspace, cx) = app_cx
app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); .add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let terminal = app_cx.new(|cx| { let terminal = app_cx.new(|cx| {
TerminalBuilder::new_display_only( TerminalBuilder::new_display_only(
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff. Show more