Revert "New multi workspace (#47795)" (#48776)

Preparing this just in case.

Release Notes:

- N/A
This commit is contained in:
Finn Evers authored and GitHub committed 2026-02-09 15:37:16 +01:00
1 parent bc2490773b
commit e2d5c71e09
101 files changed
+1990 -6733

No files matched your search

Generated
+1 -30
View File
@@ -4942,7 +4942,6 @@ checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
name = "dev_container"
version = "0.1.0"
dependencies = [
"fs",
"futures 0.3.31",
"gpui",
"http 1.3.1",
@@ -4952,12 +4951,10 @@ dependencies = [
"node_runtime",
"paths",
"picker",
"project",
"serde",
"serde_json",
"settings",
"smol",
"theme",
"ui",
"util",
"workspace",
@@ -8495,6 +8492,7 @@ dependencies = [
"fuzzy",
"gpui",
"language",
"platform_title_bar",
"project",
"serde_json",
"serde_json_lenient",
@@ -12382,7 +12380,6 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
name = "platform_title_bar"
version = "0.1.0"
dependencies = [
"feature_flags",
"gpui",
"settings",
"smallvec",
@@ -15349,30 +15346,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "signal-hook"
version = "0.3.18"
@@ -17252,7 +17225,6 @@ dependencies = [
"cloud_api_types",
"collections",
"db",
"feature_flags",
"git_ui",
"gpui",
"http_client",
@@ -21138,7 +21110,6 @@ dependencies = [
"settings_profile_selector",
"settings_ui",
"shellexpand 2.1.2",
"sidebar",
"smol",
"snippet_provider",
"snippets_ui",
-3
View File
@@ -155,7 +155,6 @@ members = [
"crates/schema_generator",
"crates/search",
"crates/session",
"crates/sidebar",
"crates/settings",
"crates/settings_content",
"crates/settings_json",
@@ -397,7 +396,6 @@ rules_library = { path = "crates/rules_library" }
scheduler = { path = "crates/scheduler" }
search = { path = "crates/search" }
session = { path = "crates/session" }
sidebar = { path = "crates/sidebar" }
settings = { path = "crates/settings" }
settings_content = { path = "crates/settings_content" }
settings_json = { path = "crates/settings_json" }
@@ -857,7 +855,6 @@ refineable = { codegen-units = 1 }
release_channel = { codegen-units = 1 }
reqwest_client = { codegen-units = 1 }
session = { codegen-units = 1 }
sidebar = { codegen-units = 1 }
snippet = { codegen-units = 1 }
snippets_ui = { codegen-units = 1 }
story = { codegen-units = 1 }
-5
View File
@@ -1,5 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 344 B

-5
View File
@@ -1,5 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 330 B

-8
View File
@@ -596,7 +596,6 @@
"ctrl-alt-b": "workspace::ToggleRightDock",
"ctrl-b": "workspace::ToggleLeftDock",
"ctrl-j": "workspace::ToggleBottomDock",
"ctrl-alt-j": "multi_workspace::ToggleWorkspaceSidebar",
"ctrl-alt-y": "workspace::ToggleAllDocks",
"ctrl-alt-0": "workspace::ResetActiveDockSize",
// For 0px parameter, uses UI font size value.
@@ -656,13 +655,6 @@
"ctrl-w": "workspace::CloseActiveDock",
},
},
{
"context": "WorkspaceSidebar",
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "multi_workspace::NewWorkspaceInWindow",
},
},
{
"context": "Workspace && debugger_running",
"bindings": {
-8
View File
@@ -657,7 +657,6 @@
"cmd-alt-b": "workspace::ToggleRightDock",
"cmd-r": "workspace::ToggleRightDock",
"cmd-j": "workspace::ToggleBottomDock",
"cmd-alt-j": "multi_workspace::ToggleWorkspaceSidebar",
"alt-cmd-y": "workspace::ToggleAllDocks",
// For 0px parameter, uses UI font size value.
"ctrl-alt-0": "workspace::ResetActiveDockSize",
@@ -717,13 +716,6 @@
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
},
},
{
"context": "WorkspaceSidebar",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "multi_workspace::NewWorkspaceInWindow",
},
},
{
"context": "Workspace && debugger_running",
"use_key_equivalents": true,
-8
View File
@@ -591,7 +591,6 @@
"ctrl-alt-b": "workspace::ToggleRightDock",
"ctrl-b": "workspace::ToggleLeftDock",
"ctrl-j": "workspace::ToggleBottomDock",
"ctrl-alt-j": "multi_workspace::ToggleWorkspaceSidebar",
"ctrl-shift-y": "workspace::ToggleAllDocks",
"alt-r": "workspace::ResetActiveDockSize",
// For 0px parameter, uses UI font size value.
@@ -660,13 +659,6 @@
"f5": "debugger::Continue",
},
},
{
"context": "WorkspaceSidebar",
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "multi_workspace::NewWorkspaceInWindow",
},
},
{
"context": "ApplicationMenu",
"use_key_equivalents": true,
+1 -1
View File
@@ -5,7 +5,7 @@ mod mode_selector;
mod model_selector;
mod model_selector_popover;
mod thread_history;
pub(crate) mod thread_view;
mod thread_view;
pub use mode_selector::ModeSelector;
pub use model_selector::AcpModelSelector;
+3 -16
View File
@@ -815,13 +815,8 @@ impl MessageEditor {
}
if self.prompt_capabilities.borrow().image
&& let Some(task) = paste_images_as_context(
self.editor.clone(),
self.mention_set.clone(),
self.workspace.clone(),
window,
cx,
)
&& let Some(task) =
paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
{
task.detach();
return;
@@ -1089,7 +1084,6 @@ impl MessageEditor {
let editor = self.editor.clone();
let mention_set = self.mention_set.clone();
let workspace = self.workspace.clone();
let paths_receiver = cx.prompt_for_paths(gpui::PathPromptOptions {
files: true,
@@ -1140,14 +1134,7 @@ impl MessageEditor {
images.push(gpui::Image::from_bytes(format, content));
}
crate::mention_set::insert_images_as_context(
images,
editor,
mention_set,
workspace,
cx,
)
.await;
crate::mention_set::insert_images_as_context(images, editor, mention_set, cx).await;
Ok(())
})
.detach_and_log_err(cx);
+23 -175
View File
@@ -57,9 +57,7 @@ use ui::{
};
use util::defer;
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{
CollaboratorId, MultiWorkspace, NewTerminal, Toast, Workspace, notifications::NotificationId,
};
use workspace::{CollaboratorId, NewTerminal, Toast, Workspace, notifications::NotificationId};
use zed_actions::agent::{Chat, ToggleModelSelector};
use zed_actions::assistant::OpenRulesLibrary;
@@ -1988,30 +1986,9 @@ impl AcpServerView {
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) {
let settings = AgentSettings::get_global(cx);
if settings.play_sound_when_agent_done && !self.agent_is_visible(window, cx) {
if settings.play_sound_when_agent_done && !window.is_window_active() {
Audio::play_sound(Sound::AgentDone, cx);
}
}
@@ -2029,7 +2006,14 @@ impl AcpServerView {
let settings = AgentSettings::get_global(cx);
let should_notify = !self.agent_is_visible(window, cx);
let window_is_inactive = !window.is_window_active();
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 {
return;
@@ -2092,22 +2076,19 @@ impl AcpServerView {
.push(cx.subscribe_in(&pop_up, window, {
|this, _, event, window, cx| match event {
AgentNotificationEvent::Accepted => {
let Some(handle) = window.window_handle().downcast::<MultiWorkspace>()
else {
log::error!("root view should be a MultiWorkspace");
return;
};
let handle = window.window_handle();
cx.activate(true);
let workspace_handle = this.workspace.clone();
// If there are multiple Zed windows, activate the correct one.
cx.defer(move |cx| {
handle
.update(cx, |multi_workspace, window, cx| {
.update(cx, |_view, window, _cx| {
window.activate_window();
if let Some(workspace) = workspace_handle.upgrade() {
multi_workspace.activate(workspace.clone(), cx);
workspace.update(cx, |workspace, cx| {
workspace.update(_cx, |workspace, cx| {
workspace.focus_panel::<AgentPanel>(window, cx);
});
}
@@ -2132,12 +2113,12 @@ impl AcpServerView {
.push({
let pop_up_weak = pop_up.downgrade();
cx.observe_window_activation(window, move |this, window, cx| {
if this.agent_is_visible(window, cx)
cx.observe_window_activation(window, move |_, window, cx| {
if window.is_window_active()
&& let Some(pop_up) = pop_up_weak.upgrade()
{
pop_up.update(cx, |notification, cx| {
notification.dismiss(cx);
pop_up.update(cx, |_, cx| {
cx.emit(AgentNotificationEvent::Dismissed);
});
}
})
@@ -2388,7 +2369,6 @@ pub(crate) mod tests {
use action_log::ActionLog;
use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext};
use agent_client_protocol::SessionId;
use assistant_text_thread::TextThreadStore;
use editor::MultiBufferOffset;
use fs::FakeFs;
use gpui::{EventEmitter, TestAppContext, VisualTestContext};
@@ -2398,7 +2378,7 @@ pub(crate) mod tests {
use std::any::Any;
use std::path::Path;
use std::rc::Rc;
use workspace::{Item, MultiWorkspace};
use workspace::Item;
use super::*;
@@ -2698,138 +2678,6 @@ 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| {
let workspace2 = cx.new(|cx| Workspace::test_new(project2, window, cx));
mw.activate(workspace2, 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]
async fn test_notification_respects_never_setting(cx: &mut TestAppContext) {
init_test(cx);
@@ -2992,18 +2840,18 @@ pub(crate) mod tests {
}
}
pub(crate) struct StubAgentServer<C> {
struct StubAgentServer<C> {
connection: C,
}
impl<C> StubAgentServer<C> {
pub(crate) fn new(connection: C) -> Self {
fn new(connection: C) -> Self {
Self { connection }
}
}
impl StubAgentServer<StubAgentConnection> {
pub(crate) fn default_response() -> Self {
fn default_response() -> Self {
let conn = StubAgentConnection::new();
conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Default response".into()),
+4 -4
View File
@@ -1352,10 +1352,10 @@ impl AgentDiff {
self.update_reviewing_editors(workspace, window, cx);
}
}
AcpThreadEvent::Stopped => {
self.update_reviewing_editors(workspace, window, cx);
}
AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) | AcpThreadEvent::Refusal => {
AcpThreadEvent::Stopped
| AcpThreadEvent::Error
| AcpThreadEvent::LoadError(_)
| AcpThreadEvent::Refusal => {
self.update_reviewing_editors(workspace, window, cx);
}
AcpThreadEvent::TitleUpdated
+32 -288
View File
@@ -81,50 +81,10 @@ const AGENT_PANEL_KEY: &str = "agent_panel";
const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
const DEFAULT_THREAD_TITLE: &str = "New Thread";
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)]
#[derive(Serialize, Deserialize, Debug)]
struct SerializedAgentPanel {
width: Option<Pixels>,
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) {
@@ -468,7 +428,6 @@ pub struct AgentPanel {
focus_handle: FocusHandle,
active_view: ActiveView,
previous_view: Option<ActiveView>,
_active_view_observation: Option<Subscription>,
new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -485,45 +444,19 @@ pub struct AgentPanel {
}
impl AgentPanel {
fn serialize(&mut self, cx: &mut App) {
let workspace_id = self
.workspace
.read_with(cx, |workspace, _| workspace.database_id())
.ok()
.flatten();
let Some(workspace_id) = workspace_id else {
return;
};
fn serialize(&mut self, cx: &mut Context<Self>) {
let width = self.width;
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 {
save_serialized_panel(
workspace_id,
SerializedAgentPanel {
width,
selected_agent: Some(selected_agent),
last_active_thread,
},
)
.await?;
KEY_VALUE_STORE
.write_kvp(
AGENT_PANEL_KEY.into(),
serde_json::to_string(&SerializedAgentPanel {
width,
selected_agent: Some(selected_agent),
})?,
)
.await?;
anyhow::Ok(())
}));
}
@@ -539,18 +472,16 @@ impl AgentPanel {
Ok(prompt_store) => prompt_store.await.ok(),
Err(_) => None,
};
let workspace_id = workspace
.read_with(cx, |workspace, _| workspace.database_id())
.ok()
.flatten();
let serialized_panel = cx
.background_spawn(async move {
workspace_id
.and_then(read_serialized_panel)
.or_else(read_legacy_serialized_panel)
})
.await;
let serialized_panel = if let Some(panel) = cx
.background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
.await
.log_err()
.flatten()
{
serde_json::from_str::<SerializedAgentPanel>(&panel).log_err()
} else {
None
};
let slash_commands = Arc::new(SlashCommandWorkingSet::default());
let text_thread_store = workspace
@@ -569,30 +500,15 @@ impl AgentPanel {
let panel =
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.width = serialized_panel.width.map(|w| w.round());
if let Some(selected_agent) = serialized_panel.selected_agent.clone() {
if let Some(selected_agent) = serialized_panel.selected_agent {
panel.selected_agent = selected_agent;
}
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
})?;
@@ -600,7 +516,7 @@ impl AgentPanel {
})
}
pub(crate) fn new(
fn new(
workspace: &Workspace,
text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
@@ -730,7 +646,6 @@ impl AgentPanel {
focus_handle: cx.focus_handle(),
context_server_registry,
previous_view: None,
_active_view_observation: None,
new_thread_menu_handle: PopoverMenuHandle::default(),
agent_panel_menu_handle: PopoverMenuHandle::default(),
agent_navigation_menu_handle: PopoverMenuHandle::default(),
@@ -799,7 +714,7 @@ impl AgentPanel {
&self.context_server_registry
}
pub fn is_visible(workspace: &Entity<Workspace>, cx: &App) -> bool {
pub fn is_hidden(workspace: &Entity<Workspace>, cx: &App) -> bool {
let workspace_read = workspace.read(cx);
workspace_read
@@ -807,13 +722,15 @@ impl AgentPanel {
.map(|panel| {
let panel_id = Entity::entity_id(&panel);
workspace_read.all_docks().iter().any(|dock| {
let is_visible = workspace_read.all_docks().iter().any(|dock| {
dock.read(cx)
.visible_panel()
.is_some_and(|visible_panel| visible_panel.panel_id() == panel_id)
})
});
!is_visible
})
.unwrap_or(false)
.unwrap_or(true)
}
pub(crate) fn active_thread_view(&self) -> Option<&Entity<AcpServerView>> {
@@ -1106,7 +1023,6 @@ impl AgentPanel {
ActiveView::Configuration | ActiveView::History { .. } => {
if let Some(previous_view) = self.previous_view.take() {
self.active_view = previous_view;
cx.emit(AgentPanelEvent::ActiveViewChanged);
match &self.active_view {
ActiveView::AgentThread { thread_view } => {
@@ -1503,7 +1419,7 @@ impl AgentPanel {
}
}
pub fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
match &self.active_view {
ActiveView::AgentThread { thread_view, .. } => thread_view
.read(cx)
@@ -1559,21 +1475,9 @@ impl AgentPanel {
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 {
self.focus_handle(cx).focus(window, cx);
}
cx.emit(AgentPanelEvent::ActiveViewChanged);
}
fn populate_recently_updated_menu_section(
@@ -1846,12 +1750,7 @@ fn agent_panel_dock_position(cx: &App) -> DockPosition {
AgentSettings::get_global(cx).dock.into()
}
pub enum AgentPanelEvent {
ActiveViewChanged,
}
impl EventEmitter<PanelEvent> for AgentPanel {}
impl EventEmitter<AgentPanelEvent> for AgentPanel {}
impl Panel for AgentPanel {
fn persistent_name() -> &'static str {
@@ -1894,14 +1793,7 @@ impl Panel for AgentPanel {
DockPosition::Left | DockPosition::Right => self.width = size,
DockPosition::Bottom => self.height = size,
}
let this = cx.weak_entity();
cx.defer(move |cx| {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
this.serialize(cx);
});
}
});
self.serialize(cx);
cx.notify();
}
@@ -3392,151 +3284,3 @@ impl AgentPanel {
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, Workspace};
#[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| {
let workspace = cx.new(|cx| Workspace::test_new(project_b.clone(), window, cx));
multi_workspace.activate(workspace.clone(), cx);
workspace
})
.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"
);
});
}
}
+1 -7
View File
@@ -49,7 +49,7 @@ use std::any::TypeId;
use workspace::Workspace;
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
pub use crate::agent_panel::{AgentPanel, AgentPanelEvent, ConcreteAssistantPanelDelegate};
pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
use crate::agent_registry_ui::AgentRegistryPage;
pub use crate::inline_assistant::InlineAssistant;
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
@@ -422,12 +422,6 @@ fn update_command_palette_filter(cx: &mut App) {
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");
}
});
}
+3 -8
View File
@@ -417,13 +417,8 @@ impl<T: 'static> PromptEditor<T> {
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if inline_assistant_model_supports_images(cx)
&& let Some(task) = paste_images_as_context(
self.editor.clone(),
self.mention_set.clone(),
self.workspace.clone(),
window,
cx,
)
&& let Some(task) =
paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
{
task.detach();
}
@@ -443,7 +438,7 @@ impl<T: 'static> PromptEditor<T> {
self.mention_set
.update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
if let Some(workspace) = Workspace::for_window(window, cx) {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
+5 -12
View File
@@ -297,9 +297,8 @@ impl MentionSet {
self.mentions.insert(crease_id, (mention_uri, task.clone()));
// Notify the user if we failed to load the mentioned context
let workspace = workspace.downgrade();
cx.spawn(async move |this, mut cx| {
let result = task.await.notify_workspace_async_err(workspace, &mut cx);
cx.spawn_in(window, async move |this, cx| {
let result = task.await.notify_async_err(cx);
drop(tx);
if result.is_none() {
this.update(cx, |this, cx| {
@@ -645,7 +644,6 @@ pub(crate) async fn insert_images_as_context(
images: Vec<gpui::Image>,
editor: Entity<Editor>,
mention_set: Entity<MentionSet>,
workspace: WeakEntity<Workspace>,
cx: &mut gpui::AsyncWindowContext,
) {
if images.is_empty() {
@@ -720,11 +718,7 @@ pub(crate) async fn insert_images_as_context(
mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone())
});
if task
.await
.notify_workspace_async_err(workspace.clone(), cx)
.is_none()
{
if task.await.notify_async_err(cx).is_none() {
editor.update(cx, |editor, cx| {
editor.edit([(start_anchor..end_anchor, "")], cx);
});
@@ -738,12 +732,11 @@ pub(crate) async fn insert_images_as_context(
pub(crate) fn paste_images_as_context(
editor: Entity<Editor>,
mention_set: Entity<MentionSet>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Option<Task<()>> {
let clipboard = cx.read_from_clipboard()?;
Some(window.spawn(cx, async move |mut cx| {
Some(window.spawn(cx, async move |cx| {
use itertools::Itertools;
let (mut images, paths) = clipboard
.into_entries()
@@ -790,7 +783,7 @@ pub(crate) fn paste_images_as_context(
})
.ok();
insert_images_as_context(images, editor, mention_set, workspace, &mut cx).await;
insert_images_as_context(images, editor, mention_set, cx).await;
}))
}
+4 -14
View File
@@ -75,16 +75,6 @@ pub enum AgentNotificationEvent {
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 {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let ui_font = theme::setup_ui_font(window, cx);
@@ -184,14 +174,14 @@ impl Render for AgentNotification {
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width()
.on_click({
cx.listener(move |this, _event, _, cx| {
this.accept(cx);
cx.listener(move |_this, _event, _, cx| {
cx.emit(AgentNotificationEvent::Accepted);
})
}),
)
.child(Button::new("dismiss", "Dismiss").full_width().on_click({
cx.listener(move |this, _event, _, cx| {
this.dismiss(cx);
cx.listener(move |_, _event, _, cx| {
cx.emit(AgentNotificationEvent::Dismissed);
})
})),
)
@@ -34,11 +34,9 @@ async fn test_channel_guests(
cx_a.executor().run_until_parked();
// Client B joins channel A as a guest
cx_b.update(|cx| {
workspace::join_channel(channel_id, client_b.app_state.clone(), None, None, cx)
})
.await
.unwrap();
cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
.await
.unwrap();
// b should be following a in the shared project.
// B is a guest,
@@ -78,11 +76,9 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
.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, None, cx)
})
.await
.unwrap();
cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx))
.await
.unwrap();
// Client A shares a project in the channel
active_call_a
@@ -92,11 +88,9 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
cx_a.run_until_parked();
// Client B joins channel A as a guest
cx_b.update(|cx| {
workspace::join_channel(channel_id, client_b.app_state.clone(), None, None, cx)
})
.await
.unwrap();
cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
.await
.unwrap();
cx_a.run_until_parked();
// client B opens 1.txt as a guest
+27 -37
View File
@@ -19,8 +19,7 @@ use fs::Fs;
use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
use git::repository::repo_path;
use gpui::{
App, AppContext as _, Entity, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext,
VisualTestContext,
App, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext,
};
use indoc::indoc;
use language::{FakeLspAdapter, language_settings::language_settings, rust_lang};
@@ -52,7 +51,7 @@ use std::{
};
use text::Point;
use util::{path, rel_path::rel_path, uri};
use workspace::{CloseIntent, MultiWorkspace, Workspace};
use workspace::{CloseIntent, Workspace};
#[gpui::test(iterations = 10)]
async fn test_host_disconnect(
@@ -96,46 +95,34 @@ async fn test_host_disconnect(
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
let window_b = cx_b.add_window(|window, cx| {
let workspace = cx.new(|cx| {
Workspace::new(
None,
project_b.clone(),
client_b.app_state.clone(),
window,
cx,
)
});
MultiWorkspace::new(workspace, cx)
let workspace_b = cx_b.add_window(|window, cx| {
Workspace::new(
None,
project_b.clone(),
client_b.app_state.clone(),
window,
cx,
)
});
let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b);
let workspace_b = window_b
.root(cx_b)
.unwrap()
.read_with(cx_b, |multi_workspace, _| {
multi_workspace.workspace().clone()
});
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
let workspace_b_view = workspace_b.root(cx_b).unwrap();
let editor_b: Entity<Editor> = workspace_b
.update_in(cx_b, |workspace, window, cx| {
let editor_b = workspace_b
.update(cx_b, |workspace, window, cx| {
workspace.open_path((worktree_id, rel_path("b.txt")), None, true, window, cx)
})
.unwrap()
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
//TODO: focus
assert!(
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)
});
assert!(cx_b.update_window_entity(&editor_b, |editor, window, _| editor.is_focused(window)));
editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
cx_b.update(|_, cx| {
assert!(workspace_b.read(cx).is_edited());
assert!(workspace_b_view.read(cx).is_edited());
});
// Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
@@ -153,16 +140,19 @@ async fn test_host_disconnect(
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.
workspace_b.update(cx_b, |workspace, cx| {
assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
assert!(!workspace.is_edited());
});
workspace_b
.update(cx_b, |workspace, _, cx| {
assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
assert!(!workspace.is_edited());
})
.unwrap();
// Ensure client B is not prompted to save edits when closing window after disconnecting.
let can_close: bool = workspace_b
.update_in(cx_b, |workspace, window, cx| {
let can_close = workspace_b
.update(cx_b, |workspace, window, cx| {
workspace.prepare_to_close(CloseIntent::Quit, window, cx)
})
.unwrap()
.await
.unwrap();
assert!(can_close);
@@ -17,7 +17,7 @@ use serde_json::json;
use settings::SettingsStore;
use text::{Point, ToPoint};
use util::{path, rel_path::rel_path, test::sample_text};
use workspace::{CollaboratorId, MultiWorkspace, SplitDirection, Workspace, item::ItemHandle as _};
use workspace::{CollaboratorId, SplitDirection, Workspace, item::ItemHandle as _};
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 workspace_b_project_a = window_b_project_a
.downcast::<MultiWorkspace>()
.downcast::<Workspace>()
.unwrap()
.read_with(cx_b, |mw, _| mw.workspace().clone())
.root(cx_b)
.unwrap();
// 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();
let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a);
let workspace_a_project_b = window_a_project_b
.downcast::<MultiWorkspace>()
.downcast::<Workspace>()
.unwrap()
.read_with(cx_a, |mw, _| mw.workspace().clone())
.root(cx_a)
.unwrap();
executor.run_until_parked();
@@ -2144,7 +2144,7 @@ pub(crate) async fn join_channel(
client: &TestClient,
cx: &mut TestAppContext,
) -> anyhow::Result<()> {
cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, None, cx))
cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
.await
}
+12 -20
View File
@@ -3,11 +3,11 @@ use std::path::Path;
use call::ActiveCall;
use git::status::{FileStatus, StatusCode, TrackedStatus};
use git_ui::project_diff::ProjectDiff;
use gpui::{AppContext as _, TestAppContext, VisualTestContext};
use gpui::{TestAppContext, VisualTestContext};
use project::ProjectPath;
use serde_json::json;
use util::{path, rel_path::rel_path};
use workspace::{MultiWorkspace, Workspace};
use workspace::Workspace;
//
use crate::TestServer;
@@ -57,25 +57,17 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
cx_b.update(editor::init);
cx_b.update(git_ui::init);
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let window_b = cx_b.add_window(|window, cx| {
let workspace = cx.new(|cx| {
Workspace::new(
None,
project_b.clone(),
client_b.app_state.clone(),
window,
cx,
)
});
MultiWorkspace::new(workspace, cx)
let workspace_b = cx_b.add_window(|window, cx| {
Workspace::new(
None,
project_b.clone(),
client_b.app_state.clone(),
window,
cx,
)
});
let cx_b = &mut VisualTestContext::from_window(*window_b, cx_b);
let workspace_b = window_b
.root(cx_b)
.unwrap()
.read_with(cx_b, |multi_workspace, _| {
multi_workspace.workspace().clone()
});
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
let workspace_b = workspace_b.root(cx_b).unwrap();
cx_b.update(|window, cx| {
window
@@ -8,9 +8,7 @@ use editor::{Editor, EditorMode, MultiBuffer};
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs as _, RemoveOptions};
use futures::StreamExt as _;
use gpui::{
AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext as _,
};
use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext};
use http_client::BlockedHttpClient;
use language::{
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
@@ -665,7 +663,7 @@ async fn test_remote_server_debugger(
let workspace_window = cx_a
.window_handle()
.downcast::<workspace::MultiWorkspace>()
.downcast::<workspace::Workspace>()
.unwrap();
let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
@@ -673,16 +671,13 @@ async fn test_remote_server_debugger(
debug_panel.update(cx_a, |debug_panel, cx| {
assert_eq!(
debug_panel.active_session().unwrap().read(cx).session(cx),
session.clone()
session
)
});
session.update(
cx_a,
|session: &mut project::debugger::session::Session, _| {
assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
},
);
session.update(cx_a, |session, _| {
assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
});
let shutdown_session = workspace.update(cx_a, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
@@ -777,7 +772,7 @@ async fn test_slow_adapter_startup_retries(
let workspace_window = cx_a
.window_handle()
.downcast::<workspace::MultiWorkspace>()
.downcast::<workspace::Workspace>()
.unwrap();
let count = Arc::new(AtomicUsize::new(0));
@@ -809,10 +804,7 @@ async fn test_slow_adapter_startup_retries(
.unwrap();
cx_a.run_until_parked();
let client = session.update(
cx_a,
|session: &mut project::debugger::session::Session, _| session.adapter_client().unwrap(),
);
let client = session.update(cx_a, |session, _| session.adapter_client().unwrap());
client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
reason: dap::StoppedEventReason::Pause,
+12 -42
View File
@@ -45,7 +45,7 @@ use std::{
},
};
use util::path;
use workspace::{MultiWorkspace, Workspace, WorkspaceStore};
use workspace::{Workspace, WorkspaceStore};
use livekit_client::test::TestServer as LivekitTestServer;
@@ -843,7 +843,7 @@ impl TestClient {
channel_id: ChannelId,
cx: &'a mut TestAppContext,
) -> (Entity<Workspace>, &'a mut VisualTestContext) {
cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, None, cx))
cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx))
.await
.unwrap();
cx.run_until_parked();
@@ -897,19 +897,10 @@ impl TestClient {
project: &Entity<Project>,
cx: &'a mut TestAppContext,
) -> (Entity<Workspace>, &'a mut VisualTestContext) {
let app_state = self.app_state.clone();
let project = project.clone();
let window = cx.add_window(|window, cx| {
cx.add_window_view(|window, cx| {
window.activate_window();
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)
Workspace::new(None, project.clone(), self.app_state.clone(), window, cx)
})
}
pub async fn build_test_workspace<'a>(
@@ -917,33 +908,19 @@ impl TestClient {
cx: &'a mut TestAppContext,
) -> (Entity<Workspace>, &'a mut VisualTestContext) {
let project = self.build_test_project(cx).await;
let app_state = self.app_state.clone();
let window = cx.add_window(|window, cx| {
cx.add_window_view(|window, cx| {
window.activate_window();
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)
Workspace::new(None, project.clone(), self.app_state.clone(), window, cx)
})
}
pub fn active_workspace<'a>(
&'a self,
cx: &'a mut TestAppContext,
) -> (Entity<Workspace>, &'a mut VisualTestContext) {
let window = cx.update(|cx| {
cx.active_window()
.unwrap()
.downcast::<MultiWorkspace>()
.unwrap()
});
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
let entity = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let entity = window.root(cx).unwrap();
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.
(entity, cx)
@@ -954,15 +931,8 @@ pub fn open_channel_notes(
channel_id: ChannelId,
cx: &mut VisualTestContext,
) -> Task<anyhow::Result<Entity<ChannelView>>> {
let window = cx.update(|_, cx| {
cx.active_window()
.unwrap()
.downcast::<MultiWorkspace>()
.unwrap()
});
let entity = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let window = cx.update(|_, cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
let entity = window.root(cx).unwrap();
cx.update(|window, cx| ChannelView::open(channel_id, None, entity.clone(), window, cx))
}
+10 -17
View File
@@ -36,8 +36,7 @@ use ui::{
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::{
CopyRoomId, Deafen, LeaveCall, MultiWorkspace, Mute, OpenChannelNotes, ScreenShare,
ShareProject, Workspace,
CopyRoomId, Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace,
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotifyResultExt},
};
@@ -121,7 +120,6 @@ pub fn init(cx: &mut App) {
if let Some(room) = ActiveCall::global(cx).read(cx).room() {
let romo_id_fut = room.read(cx).room_id();
let workspace_handle = cx.weak_entity();
cx.spawn(async move |workspace, cx| {
let room_id = romo_id_fut.await.context("Failed to get livekit room")?;
workspace.update(cx, |workspace, cx| {
@@ -136,7 +134,7 @@ pub fn init(cx: &mut App) {
);
})
})
.detach_and_notify_err(workspace_handle, window, cx);
.detach_and_notify_err(window, cx);
} else {
workspace.show_error(&"There’s no active call; join one first.", cx);
}
@@ -2179,13 +2177,12 @@ impl CollabPanel {
&["Remove", "Cancel"],
cx,
);
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |this, mut cx| {
cx.spawn_in(window, async move |this, cx| {
if answer.await? == 0 {
channel_store
.update(cx, |channels, _| channels.remove_channel(channel_id))
.await
.notify_workspace_async_err(workspace, &mut cx);
.notify_async_err(cx);
this.update_in(cx, |_, window, cx| cx.focus_self(window))
.ok();
}
@@ -2214,13 +2211,12 @@ impl CollabPanel {
&["Remove", "Cancel"],
cx,
);
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |_, mut cx| {
cx.spawn_in(window, async move |_, cx| {
if answer.await? == 0 {
user_store
.update(cx, |store, cx| store.remove_contact(user_id, cx))
.await
.notify_workspace_async_err(workspace, &mut cx);
.notify_async_err(cx);
}
anyhow::Ok(())
})
@@ -2271,15 +2267,13 @@ impl CollabPanel {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() else {
let Some(handle) = window.window_handle().downcast::<Workspace>() else {
return;
};
workspace::join_channel(
channel_id,
workspace.read(cx).app_state().clone(),
Some(handle),
Some(self.workspace.clone()),
cx,
)
.detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None)
@@ -2322,13 +2316,12 @@ impl CollabPanel {
.full_width()
.on_click(cx.listener(|this, _, window, cx| {
let client = this.client.clone();
let workspace = this.workspace.clone();
cx.spawn_in(window, async move |_, mut cx| {
cx.spawn_in(window, async move |_, cx| {
client
.connect(true, &mut cx)
.connect(true, cx)
.await
.into_response()
.notify_workspace_async_err(workspace, &mut cx);
.notify_async_err(cx);
})
.detach()
})),
+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))
}
Err(err) => cx.update(|window, cx| {
if let Some(workspace) = Workspace::for_window(window, cx) {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, 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) {
const NOTIFICATION_ID: NotificationId = NotificationId::unique::<CopilotStatusToast>();
let Some(workspace) = Workspace::for_window(window, cx) else {
let Some(workspace) = window.root::<Workspace>().flatten() else {
return;
};
+6 -121
View File
@@ -1,4 +1,3 @@
use anyhow::Context as _;
use gpui::App;
use sqlez_macros::sql;
use util::ResultExt as _;
@@ -14,22 +13,12 @@ pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnect
impl Domain for KeyValueStore {
const NAME: &str = stringify!(KeyValueStore);
const MIGRATIONS: &[&str] = &[
sql!(
CREATE TABLE IF NOT EXISTS kv_store(
key TEXT PRIMARY KEY,
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;
),
];
const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE IF NOT EXISTS kv_store(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
)];
}
crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
@@ -80,64 +69,6 @@ impl KeyValueStore {
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)]
@@ -168,52 +99,6 @@ mod tests {
db.delete_kvp("key-1".to_string()).await.unwrap();
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);
+29 -40
View File
@@ -8,7 +8,7 @@ use project::{Project, debugger::session::Session};
use settings::SettingsStore;
use task::SharedTaskContext;
use terminal_view::terminal_panel::TerminalPanel;
use workspace::MultiWorkspace;
use workspace::Workspace;
use crate::{debugger_panel::DebugPanel, session::DebugSession};
@@ -52,16 +52,14 @@ pub fn init_test(cx: &mut gpui::TestAppContext) {
pub async fn init_test_workspace(
project: &Entity<Project>,
cx: &mut TestAppContext,
) -> WindowHandle<MultiWorkspace> {
) -> WindowHandle<Workspace> {
let workspace_handle =
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let debugger_panel = workspace_handle
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |_workspace, cx| {
cx.spawn_in(window, async move |this, cx| {
DebugPanel::load(this, cx).await
})
.update(cx, |_, window, cx| {
cx.spawn_in(window, async move |this, cx| {
DebugPanel::load(this, cx).await
})
})
.unwrap()
@@ -69,10 +67,9 @@ pub async fn init_test_workspace(
.expect("Failed to load debug panel");
let terminal_panel = workspace_handle
.update(cx, |multi, window, cx| {
let weak_workspace = multi.workspace().downgrade();
cx.spawn_in(window, async move |_, cx| {
TerminalPanel::load(weak_workspace, cx.clone()).await
.update(cx, |_, window, cx| {
cx.spawn_in(window, async |this, cx| {
TerminalPanel::load(this, cx.clone()).await
})
})
.unwrap()
@@ -80,11 +77,9 @@ pub async fn init_test_workspace(
.expect("Failed to load terminal panel");
workspace_handle
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
workspace.add_panel(debugger_panel, window, cx);
workspace.add_panel(terminal_panel, window, cx);
});
.update(cx, |workspace, window, cx| {
workspace.add_panel(debugger_panel, window, cx);
workspace.add_panel(terminal_panel, window, cx);
})
.unwrap();
workspace_handle
@@ -92,45 +87,39 @@ pub async fn init_test_workspace(
#[track_caller]
pub fn active_debug_session_panel(
workspace: WindowHandle<MultiWorkspace>,
workspace: WindowHandle<Workspace>,
cx: &mut TestAppContext,
) -> Entity<DebugSession> {
workspace
.update(cx, |multi, _window, cx| {
multi.workspace().update(cx, |workspace, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
debug_panel
.update(cx, |this, _| this.active_session())
.unwrap()
})
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
debug_panel
.update(cx, |this, _| this.active_session())
.unwrap()
})
.unwrap()
}
pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
workspace: &WindowHandle<MultiWorkspace>,
workspace: &WindowHandle<Workspace>,
cx: &mut gpui::TestAppContext,
config: DebugTaskDefinition,
configure: T,
) -> Result<Entity<Session>> {
let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure);
workspace.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
workspace.start_debug_session(
config.to_scenario(),
SharedTaskContext::default(),
None,
None,
window,
cx,
)
})
workspace.update(cx, |workspace, window, cx| {
workspace.start_debug_session(
config.to_scenario(),
SharedTaskContext::default(),
None,
None,
window,
cx,
)
})?;
cx.run_until_parked();
let session = workspace.read_with(cx, |workspace, cx| {
workspace
.workspace()
.read(cx)
.panel::<DebugPanel>(cx)
.and_then(|panel| panel.read(cx).active_session())
.map(|session| session.read(cx).running_state().read(cx).session())
@@ -142,7 +131,7 @@ pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
}
pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
workspace: &WindowHandle<MultiWorkspace>,
workspace: &WindowHandle<Workspace>,
cx: &mut gpui::TestAppContext,
configure: T,
) -> Result<Entity<Session>> {
+23 -31
View File
@@ -60,13 +60,7 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
// assert we didn't show the attach modal
workspace
.update(cx, |workspace, _window, cx| {
assert!(
workspace
.workspace()
.read(cx)
.active_modal::<AttachModal>(cx)
.is_none()
);
assert!(workspace.active_modal::<AttachModal>(cx).is_none());
})
.unwrap();
}
@@ -103,9 +97,9 @@ async fn test_show_attach_modal_and_select_process(
});
});
let attach_modal = workspace
.update(cx, |multi, window, cx| {
let workspace_handle = multi.workspace().downgrade();
multi.toggle_modal(window, cx, |window, cx| {
.update(cx, |workspace, window, cx| {
let workspace_handle = cx.weak_entity();
workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::with_processes(
workspace_handle,
vec![
@@ -139,7 +133,7 @@ async fn test_show_attach_modal_and_select_process(
)
});
multi.active_modal::<AttachModal>(cx).unwrap()
workspace.active_modal::<AttachModal>(cx).unwrap()
})
.unwrap();
@@ -214,26 +208,24 @@ async fn test_attach_with_pick_pid_variable(executor: BackgroundExecutor, cx: &m
let pick_pid_placeholder = task::VariableName::PickProcessId.template_value();
workspace
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
workspace.start_debug_session(
DebugTaskDefinition {
adapter: FakeAdapter::ADAPTER_NAME.into(),
label: "attach with picker".into(),
config: json!({
"request": "attach",
"process_id": pick_pid_placeholder,
}),
tcp_connection: None,
}
.to_scenario(),
SharedTaskContext::default(),
None,
None,
window,
cx,
);
})
.update(cx, |workspace, window, cx| {
workspace.start_debug_session(
DebugTaskDefinition {
adapter: FakeAdapter::ADAPTER_NAME.into(),
label: "attach with picker".into(),
config: json!({
"request": "attach",
"process_id": pick_pid_placeholder,
}),
tcp_connection: None,
}
.to_scenario(),
SharedTaskContext::default(),
None,
None,
window,
cx,
)
})
.unwrap();
@@ -145,17 +145,15 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
};
workspace
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
workspace.start_debug_session(
scenario,
task_context.clone(),
None,
None,
window,
cx,
);
})
.update(cx, |workspace, window, cx| {
workspace.start_debug_session(
scenario,
task_context.clone(),
None,
None,
window,
cx,
)
})
.unwrap();
@@ -184,10 +182,8 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
});
.update(cx, |workspace, window, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
})
.unwrap();
@@ -328,10 +324,8 @@ async fn test_debug_modal_subtitles_with_multiple_worktrees(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
});
.update(cx, |workspace, window, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
})
.unwrap();
@@ -1113,8 +1113,8 @@ async fn test_stack_frame_filter_persistence(
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |workspace, _, cx| {
workspace.set_random_database_id(cx);
.update(cx, |workspace, _, _| {
workspace.set_random_database_id();
})
.unwrap();
@@ -1211,7 +1211,7 @@ async fn test_stack_frame_filter_persistence(
cx.run_until_parked();
let workspace_id = workspace
.update(cx, |workspace, _window, cx| workspace.database_id(cx))
.update(cx, |workspace, _window, _cx| workspace.database_id())
.ok()
.flatten()
.expect("workspace id has to be some for this test to work properly");
-5
View File
@@ -23,12 +23,7 @@ util.workspace = true
workspace.workspace = true
[dev-dependencies]
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
theme.workspace = true
workspace = { workspace = true, features = ["test-support"] }
[lints]
workspace = true
+266 -298
View File
@@ -2,16 +2,18 @@ use std::{
collections::{HashMap, HashSet},
fmt::Display,
path::{Path, PathBuf},
sync::Arc,
};
use gpui::AsyncWindowContext;
use node_runtime::NodeRuntime;
use serde::Deserialize;
use settings::DevContainerConnection;
use settings::{DevContainerConnection, Settings as _};
use smol::{fs, process::Command};
use util::rel_path::RelPath;
use workspace::Workspace;
use crate::{DevContainerContext, DevContainerFeature, DevContainerTemplate};
use crate::{DevContainerFeature, DevContainerSettings, DevContainerTemplate};
/// Represents a discovered devcontainer configuration
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -57,31 +59,6 @@ pub(crate) struct DevContainerConfigurationOutput {
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)]
pub enum DevContainerError {
DockerNotAvailable,
@@ -122,6 +99,58 @@ 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.
///
/// This function scans for:
@@ -129,124 +158,160 @@ impl Display for DevContainerError {
/// 2. `.devcontainer/<subfolder>/devcontainer.json` (named configurations)
///
/// Returns a list of found configurations, or an empty list if none are found.
pub fn find_devcontainer_configs(workspace: &Workspace, cx: &gpui::App) -> Vec<DevContainerConfig> {
let project = workspace.project().read(cx);
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");
pub fn find_devcontainer_configs(cx: &mut AsyncWindowContext) -> Vec<DevContainerConfig> {
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
log::debug!("find_devcontainer_configs: No workspace found");
return Vec::new();
};
let worktree = worktree.read(cx);
let mut configs = Vec::new();
let Ok(configs) = workspace.update(cx, |workspace, _, cx| {
let project = workspace.project().read(cx);
let devcontainer_path = RelPath::unix(".devcontainer").expect("valid path");
let worktree = project
.visible_worktrees(cx)
.find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
let Some(devcontainer_entry) = worktree.entry_for_path(devcontainer_path) else {
log::debug!("find_devcontainer_configs: .devcontainer directory not found in worktree");
return Vec::new();
};
let Some(worktree) = worktree else {
log::debug!("find_devcontainer_configs: No worktree found");
return Vec::new();
};
if !devcontainer_entry.is_dir() {
log::debug!("find_devcontainer_configs: .devcontainer is not a directory");
return Vec::new();
}
let worktree = worktree.read(cx);
let mut configs = Vec::new();
log::debug!("find_devcontainer_configs: Scanning .devcontainer directory");
let devcontainer_json_path =
RelPath::unix(".devcontainer/devcontainer.json").expect("valid path");
for entry in worktree.child_entries(devcontainer_path) {
log::debug!(
"find_devcontainer_configs: Found entry: {:?}, is_file: {}, is_dir: {}",
entry.path.as_unix_str(),
entry.is_file(),
entry.is_dir()
);
let devcontainer_path = RelPath::unix(".devcontainer").expect("valid path");
if entry.is_file() && entry.path.as_ref() == devcontainer_json_path {
log::debug!("find_devcontainer_configs: Found default devcontainer.json");
configs.push(DevContainerConfig::default_config());
} else if entry.is_dir() {
let subfolder_name = entry
.path
.file_name()
.map(|n| n.to_string())
.unwrap_or_default();
let Some(devcontainer_entry) = worktree.entry_for_path(devcontainer_path) else {
log::debug!("find_devcontainer_configs: .devcontainer directory not found in worktree");
return Vec::new();
};
let config_json_path = format!("{}/devcontainer.json", entry.path.as_unix_str());
if let Ok(rel_config_path) = RelPath::unix(&config_json_path) {
if worktree.entry_for_path(rel_config_path).is_some() {
log::debug!(
"find_devcontainer_configs: Found config in subfolder: {}",
subfolder_name
);
configs.push(DevContainerConfig {
name: subfolder_name,
config_path: PathBuf::from(&config_json_path),
});
} else {
log::debug!(
"find_devcontainer_configs: Subfolder {} has no devcontainer.json",
subfolder_name
);
if !devcontainer_entry.is_dir() {
log::debug!("find_devcontainer_configs: .devcontainer is not a directory");
return Vec::new();
}
log::debug!("find_devcontainer_configs: Scanning .devcontainer directory");
let devcontainer_json_path =
RelPath::unix(".devcontainer/devcontainer.json").expect("valid path");
for entry in worktree.child_entries(devcontainer_path) {
log::debug!(
"find_devcontainer_configs: Found entry: {:?}, is_file: {}, is_dir: {}",
entry.path.as_unix_str(),
entry.is_file(),
entry.is_dir()
);
if entry.is_file() && entry.path.as_ref() == devcontainer_json_path {
log::debug!("find_devcontainer_configs: Found default devcontainer.json");
configs.push(DevContainerConfig::default_config());
} else if entry.is_dir() {
let subfolder_name = entry
.path
.file_name()
.map(|n| n.to_string())
.unwrap_or_default();
let config_json_path = format!("{}/devcontainer.json", entry.path.as_unix_str());
if let Ok(rel_config_path) = RelPath::unix(&config_json_path) {
if worktree.entry_for_path(rel_config_path).is_some() {
log::debug!(
"find_devcontainer_configs: Found config in subfolder: {}",
subfolder_name
);
configs.push(DevContainerConfig {
name: subfolder_name,
config_path: PathBuf::from(&config_json_path),
});
} else {
log::debug!(
"find_devcontainer_configs: Subfolder {} has no devcontainer.json",
subfolder_name
);
}
}
}
}
}
log::info!(
"find_devcontainer_configs: Found {} configurations",
configs.len()
);
log::info!(
"find_devcontainer_configs: Found {} configurations",
configs.len()
);
configs.sort_by(|a, b| {
if a.name == "default" {
std::cmp::Ordering::Less
} else if b.name == "default" {
std::cmp::Ordering::Greater
} else {
a.name.cmp(&b.name)
}
});
configs.sort_by(|a, b| {
if a.name == "default" {
std::cmp::Ordering::Less
} else if b.name == "default" {
std::cmp::Ordering::Greater
} else {
a.name.cmp(&b.name)
}
});
configs
}) else {
log::debug!("find_devcontainer_configs: Failed to update workspace");
return Vec::new();
};
configs
}
pub async fn start_dev_container_with_config(
context: DevContainerContext,
cx: &mut AsyncWindowContext,
node_runtime: NodeRuntime,
config: Option<DevContainerConfig>,
) -> Result<(DevContainerConnection, String), DevContainerError> {
check_for_docker(context.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 use_podman = use_podman(cx);
check_for_docker(use_podman).await?;
match devcontainer_up(&context, &cli, config_path.as_deref()).await {
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);
};
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 {
container_id,
remote_workspace_folder,
remote_user,
..
}) => {
let project_name =
match read_devcontainer_configuration(&context, &cli, config_path.as_deref()).await
{
Ok(DevContainerConfigurationOutput {
configuration:
DevContainerConfiguration {
name: Some(project_name),
},
}) => project_name,
_ => get_backup_project_name(&remote_workspace_folder, &container_id),
};
let project_name = match devcontainer_read_configuration(
&path_to_devcontainer_cli,
found_in_path,
&node_runtime,
&directory,
config_path.as_ref(),
use_podman,
)
.await
{
Ok(DevContainerConfigurationOutput {
configuration:
DevContainerConfiguration {
name: Some(project_name),
},
}) => project_name,
_ => get_backup_project_name(&remote_workspace_folder, &container_id),
};
let connection = DevContainerConnection {
name: project_name,
container_id,
use_podman: context.use_podman,
container_id: container_id,
use_podman,
remote_user,
};
@@ -290,9 +355,9 @@ async fn check_for_docker(use_podman: bool) -> Result<(), DevContainerError> {
}
}
pub(crate) async fn ensure_devcontainer_cli(
async fn ensure_devcontainer_cli(
node_runtime: &NodeRuntime,
) -> Result<DevContainerCli, DevContainerError> {
) -> Result<(PathBuf, bool), DevContainerError> {
let mut command = util::command::new_smol_command(&dev_container_cli());
command.arg("--version");
@@ -330,10 +395,7 @@ pub(crate) async fn ensure_devcontainer_cli(
Ok(output) => {
if output.status.success() {
log::info!("Found devcontainer CLI in Data dir");
return Ok(DevContainerCli {
path: datadir_cli_path.clone(),
node_runtime_path: Some(node_runtime_path.clone()),
});
return Ok((datadir_cli_path.clone(), false));
} else {
log::error!(
"Could not run devcontainer CLI from data_dir. Will try once more to install. Output: {:?}",
@@ -373,29 +435,32 @@ pub(crate) async fn ensure_devcontainer_cli(
);
Err(DevContainerError::DevContainerCliNotAvailable)
} else {
Ok(DevContainerCli {
path: datadir_cli_path,
node_runtime_path: Some(node_runtime_path),
})
Ok((datadir_cli_path, false))
}
} else {
log::info!("Found devcontainer cli on $PATH, using it");
Ok(DevContainerCli {
path: PathBuf::from(&dev_container_cli()),
node_runtime_path: None,
})
Ok((PathBuf::from(&dev_container_cli()), true))
}
}
async fn devcontainer_up(
context: &DevContainerContext,
cli: &DevContainerCli,
config_path: Option<&Path>,
path_to_cli: &PathBuf,
found_in_path: bool,
node_runtime: &NodeRuntime,
path: Arc<Path>,
config_path: Option<PathBuf>,
use_podman: bool,
) -> Result<DevContainerUp, DevContainerError> {
let mut command = cli.command(context.use_podman);
let Ok(node_runtime_path) = node_runtime.binary_path().await else {
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("--workspace-folder");
command.arg(context.project_directory.display().to_string());
command.arg(path.display().to_string());
if let Some(config) = config_path {
command.arg("--config");
@@ -428,15 +493,24 @@ async fn devcontainer_up(
}
}
pub(crate) async fn read_devcontainer_configuration(
context: &DevContainerContext,
cli: &DevContainerCli,
config_path: Option<&Path>,
async fn devcontainer_read_configuration(
path_to_cli: &PathBuf,
found_in_path: bool,
node_runtime: &NodeRuntime,
path: &Arc<Path>,
config_path: Option<&PathBuf>,
use_podman: bool,
) -> Result<DevContainerConfigurationOutput, DevContainerError> {
let mut command = cli.command(context.use_podman);
let Ok(node_runtime_path) = node_runtime.binary_path().await else {
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("--workspace-folder");
command.arg(context.project_directory.display().to_string());
command.arg(path.display().to_string());
if let Some(config) = config_path {
command.arg("--config");
@@ -466,14 +540,23 @@ pub(crate) async fn read_devcontainer_configuration(
}
}
pub(crate) async fn apply_dev_container_template(
async fn devcontainer_template_apply(
template: &DevContainerTemplate,
template_options: &HashMap<String, String>,
features_selected: &HashSet<DevContainerFeature>,
context: &DevContainerContext,
cli: &DevContainerCli,
path_to_cli: &PathBuf,
found_in_path: bool,
node_runtime: &NodeRuntime,
path: &Arc<Path>,
use_podman: bool,
) -> Result<DevContainerApply, DevContainerError> {
let mut command = cli.command(context.use_podman);
let Ok(node_runtime_path) = node_runtime.binary_path().await else {
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 {
log::error!("Unable to serialize options for {:?}", template_options);
@@ -483,7 +566,7 @@ pub(crate) async fn apply_dev_container_template(
command.arg("templates");
command.arg("apply");
command.arg("--workspace-folder");
command.arg(context.project_directory.display().to_string());
command.arg(path.display().to_string());
command.arg("--template-id");
command.arg(format!(
"{}/{}",
@@ -547,6 +630,28 @@ 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 {
Path::new(remote_workspace_folder)
.file_name()
@@ -555,6 +660,22 @@ fn get_backup_project_name(remote_workspace_folder: &str, container_id: &str) ->
.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 {
let features_map = features_selected
.iter()
@@ -580,160 +701,7 @@ fn template_features_to_json(features_selected: &HashSet<DevContainerFeature>) -
#[cfg(test)]
mod tests {
use crate::devcontainer_api::{DevContainerUp, find_devcontainer_configs, parse_json_from_cli};
use fs::FakeFs;
use gpui::TestAppContext;
use project::Project;
use serde_json::json;
use settings::SettingsStore;
use workspace::Workspace;
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
});
}
#[gpui::test]
async fn test_find_devcontainer_configs_no_devcontainer_dir(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/project",
json!({
"src": { "main.rs": "fn main() {}" }
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx));
assert!(
configs.is_empty(),
"Expected no configs when .devcontainer dir is absent, got: {configs:?}"
);
}
#[gpui::test]
async fn test_find_devcontainer_configs_single_default(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/project",
json!({
".devcontainer": {
"devcontainer.json": r#"{"image": "ubuntu"}"#
}
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx));
assert_eq!(
configs.len(),
1,
"Expected exactly one config, got: {configs:?}"
);
assert_eq!(configs[0].name, "default");
assert_eq!(
configs[0].config_path.to_str().unwrap(),
".devcontainer/devcontainer.json"
);
}
#[gpui::test]
async fn test_find_devcontainer_configs_multiple_subfolders(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/project",
json!({
".devcontainer": {
"python": { "devcontainer.json": r#"{}"# },
"node": { "devcontainer.json": r#"{}"# }
}
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx));
assert_eq!(configs.len(), 2, "Expected two configs, got: {configs:?}");
assert_eq!(configs[0].name, "node");
assert_eq!(configs[1].name, "python");
}
#[gpui::test]
async fn test_find_devcontainer_configs_default_plus_subfolders(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/project",
json!({
".devcontainer": {
"devcontainer.json": r#"{}"#,
"python": { "devcontainer.json": r#"{}"# },
"node": { "devcontainer.json": r#"{}"# }
}
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx));
assert_eq!(configs.len(), 3, "Expected three configs, got: {configs:?}");
assert_eq!(
configs[0].name, "default",
"Default config should be sorted first"
);
assert_eq!(configs[1].name, "node");
assert_eq!(configs[2].name, "python");
}
#[gpui::test]
async fn test_find_devcontainer_configs_subfolder_without_json(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/project",
json!({
".devcontainer": {
"devcontainer.json": r#"{}"#,
"has_config": { "devcontainer.json": r#"{}"# },
"no_config": { "README.md": "not a devcontainer" }
}
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
let configs = cx.read(|cx| find_devcontainer_configs(workspace.read(cx), cx));
assert_eq!(
configs.len(),
2,
"Subfolder without devcontainer.json should be skipped, got: {configs:?}"
);
assert_eq!(configs[0].name, "default");
assert_eq!(configs[1].name, "has_config");
}
use crate::devcontainer_api::{DevContainerUp, parse_json_from_cli};
#[test]
fn should_parse_from_devcontainer_json() {
+19 -62
View File
@@ -1,5 +1,3 @@
use std::path::Path;
use gpui::AppContext;
use gpui::Entity;
use gpui::Task;
@@ -43,8 +41,7 @@ use http_client::{AsyncBody, HttpClient};
mod devcontainer_api;
use devcontainer_api::ensure_devcontainer_cli;
use devcontainer_api::read_devcontainer_configuration;
use devcontainer_api::read_devcontainer_configuration_for_project;
use crate::devcontainer_api::DevContainerError;
use crate::devcontainer_api::apply_dev_container_template;
@@ -53,34 +50,11 @@ pub use devcontainer_api::{
DevContainerConfig, find_devcontainer_configs, 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)]
struct DevContainerSettings {
use_podman: bool,
}
pub fn use_podman(cx: &App) -> bool {
DevContainerSettings::get_global(cx).use_podman
}
impl Settings for DevContainerSettings {
fn from_settings(content: &settings::SettingsContent) -> Self {
Self {
@@ -1445,41 +1419,22 @@ fn dispatch_apply_templates(
cx: &mut Context<DevContainerModal>,
) {
cx.spawn_in(window, async move |this, cx| {
let Some((tree_id, context)) = workspace.update(cx, |workspace, cx| {
let worktree = workspace
.project()
.read(cx)
.visible_worktrees(cx)
.find_map(|tree| {
tree.read(cx)
.root_entry()?
.is_dir()
.then_some(tree.read(cx))
});
let tree_id = worktree.map(|w| w.id())?;
let context = DevContainerContext::from_workspace(workspace, cx)?;
Some((tree_id, context))
}) else {
return;
};
if let Some(tree_id) = workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone();
let worktree = project.read(cx).visible_worktrees(cx).find_map(|tree| {
tree.read(cx)
.root_entry()?
.is_dir()
.then_some(tree.read(cx))
});
worktree.map(|w| w.id())
}) {
let node_runtime = workspace.read_with(cx, |workspace, _| {
workspace.app_state().node_runtime.clone()
});
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
&& read_devcontainer_configuration(&context, &cli, None)
&& read_devcontainer_configuration_for_project(cx, &node_runtime)
.await
.is_ok()
{
@@ -1498,8 +1453,8 @@ fn dispatch_apply_templates(
&template_entry.template,
&template_entry.options_selected,
&template_entry.features_selected,
&context,
&cli,
cx,
&node_runtime,
)
.await
{
@@ -1541,6 +1496,8 @@ fn dispatch_apply_templates(
this.dismiss(&menu::Cancel, window, cx);
})
.ok();
} else {
return;
}
})
.detach();
+1 -1
View File
@@ -904,7 +904,7 @@ impl Render for BufferDiagnosticsEditor {
.style(ButtonStyle::Transparent)
.tooltip(Tooltip::text("Open File"))
.on_click(cx.listener(|buffer_diagnostics, _, window, cx| {
if let Some(workspace) = Workspace::for_window(window, cx) {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
workspace
.open_path(
@@ -119,7 +119,7 @@ impl Render for EditPredictionButton {
IconButton::new("copilot-error", icon)
.icon_size(IconSize::Small)
.on_click(cx.listener(move |_, _, window, cx| {
if let Some(workspace) = Workspace::for_window(window, cx) {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
let copilot = copilot.clone();
workspace.show_toast(
+2 -20
View File
@@ -3105,24 +3105,6 @@ impl Editor {
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.
fn workspace_serialization_id(&self, _cx: &App) -> Option<WorkspaceId> {
self.workspace
@@ -11479,8 +11461,8 @@ impl Editor {
let Some(project) = self.project.clone() else {
return;
};
let task = self.reload(project, window, cx);
self.detach_and_notify_err(task, window, cx);
self.reload(project, window, cx)
.detach_and_notify_err(window, cx);
}
pub fn restore_file(
+11 -10
View File
@@ -99,6 +99,7 @@ use workspace::{
CollaboratorId, ItemHandle, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel,
Workspace,
item::{BreadcrumbText, Item, ItemBufferKind},
notifications::NotifyTaskExt,
};
/// Determines what kinds of highlights should be applied to a lines background.
@@ -540,21 +541,21 @@ impl EditorElement {
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.format(action, window, cx) {
editor.detach_and_notify_err(task, window, cx);
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.format_selections(action, window, cx) {
editor.detach_and_notify_err(task, window, cx);
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.organize_imports(action, window, cx) {
editor.detach_and_notify_err(task, window, cx);
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
@@ -564,49 +565,49 @@ impl EditorElement {
register_action(editor, window, Editor::show_character_palette);
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_completion(action, window, cx) {
editor.detach_and_notify_err(task, window, cx);
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_completion_replace(action, window, cx) {
editor.detach_and_notify_err(task, window, cx);
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_completion_insert(action, window, cx) {
editor.detach_and_notify_err(task, window, cx);
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.compose_completion(action, window, cx) {
editor.detach_and_notify_err(task, window, cx);
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_code_action(action, window, cx) {
editor.detach_and_notify_err(task, window, cx);
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.rename(action, window, cx) {
editor.detach_and_notify_err(task, window, cx);
task.detach_and_notify_err(window, cx);
} else {
cx.propagate();
}
});
register_action(editor, window, |editor, action, window, cx| {
if let Some(task) = editor.confirm_rename(action, window, cx) {
editor.detach_and_notify_err(task, window, cx);
task.detach_and_notify_err(window, cx);
} else {
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) {
if let Ok(uri) = Url::parse(&link)
&& uri.scheme() == "file"
&& let Some(workspace) = Workspace::for_window(window, cx)
&& let Some(workspace) = window.root::<Workspace>().flatten()
{
workspace.update(cx, |workspace, cx| {
let task = workspace.open_abs_path(
@@ -22,7 +22,7 @@ use language::{
use lsp::{notification, request};
use project::Project;
use smol::stream::StreamExt;
use workspace::{AppState, MultiWorkspace, Workspace, WorkspaceHandle};
use workspace::{AppState, Workspace, WorkspaceHandle};
use super::editor_test_context::{AssertionContextManager, EditorTestContext};
@@ -95,8 +95,7 @@ impl EditorLspTestContext {
)
.await;
let window =
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
@@ -107,20 +106,12 @@ impl EditorLspTestContext {
})
.await
.unwrap();
cx.read(|cx| {
workspace
.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());
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.workspace().update(cx, |workspace, cx| {
workspace.open_path(file, None, true, window, cx)
})
workspace.open_path(file, None, true, window, cx)
})
.await
.expect("Could not open test file");
@@ -130,8 +121,6 @@ impl EditorLspTestContext {
});
editor.update_in(&mut cx, |editor, window, cx| {
let nav_history = workspace
.read(cx)
.workspace()
.read(cx)
.active_pane()
.read(cx)
@@ -145,8 +134,6 @@ impl EditorLspTestContext {
// Ensure the language server is fully registered with the buffer
cx.executor().run_until_parked();
let workspace = cx.read(|cx| workspace.read(cx).workspace().clone());
Self {
cx: EditorTestContext {
cx,
-4
View File
@@ -16,10 +16,6 @@ pub struct AgentV2FeatureFlag;
impl FeatureFlag for AgentV2FeatureFlag {
const NAME: &'static str = "agent-v2";
fn enabled_for_staff() -> bool {
true
}
}
pub struct AcpBetaFeatureFlag;
+2 -5
View File
@@ -1566,12 +1566,9 @@ impl PickerDelegate for FileFinderDelegate {
.unwrap_or(0)
.saturating_sub(1);
let finder = self.file_finder.clone();
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |_, mut cx| {
let item = open_task
.await
.notify_workspace_async_err(workspace, &mut cx)?;
cx.spawn_in(window, async move |_, cx| {
let item = open_task.await.notify_async_err(cx)?;
if let Some(row) = row
&& let Some(active_editor) = item.downcast::<Editor>()
{
+3 -11
View File
@@ -9,9 +9,7 @@ use project::{FS_WATCH_LATENCY, RemoveOptions};
use serde_json::json;
use settings::SettingsStore;
use util::{path, rel_path::rel_path};
use workspace::{
AppState, CloseActiveItem, MultiWorkspace, OpenOptions, ToggleFileFinder, Workspace, open_paths,
};
use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace, open_paths};
#[ctor::ctor]
fn init_logger() {
@@ -2536,14 +2534,8 @@ async fn test_search_results_refreshed_on_standalone_file_creation(cx: &mut gpui
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let window = cx.add_window({
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();
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
cx.update(|_, cx| {
open_paths(
+1 -1
View File
@@ -774,7 +774,7 @@ impl CommitView {
callback(repo, &sha, stash, commit_view_entity, workspace_weak, cx).await?;
anyhow::Ok(())
})
.detach_and_notify_err(workspace.weak_handle(), window, cx);
.detach_and_notify_err(window, cx);
}
async fn close_commit_view(
+5 -4
View File
@@ -6,7 +6,7 @@ use editor::{Editor, EditorEvent, MultiBuffer};
use futures::{FutureExt, select_biased};
use gpui::{
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, Render, Task, WeakEntity, Window,
Focusable, IntoElement, Render, Task, Window,
};
use language::{Buffer, LanguageRegistry};
use project::Project;
@@ -39,10 +39,11 @@ impl FileDiffView {
pub fn open(
old_path: PathBuf,
new_path: PathBuf,
workspace: WeakEntity<Workspace>,
workspace: &Workspace,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Entity<Self>>> {
let workspace = workspace.weak_handle();
window.spawn(cx, async move |cx| {
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
let old_buffer = project
@@ -405,7 +406,7 @@ mod tests {
FileDiffView::open(
path!("/test/old_file.txt").into(),
path!("/test/new_file.txt").into(),
workspace.weak_handle(),
workspace,
window,
cx,
)
@@ -539,7 +540,7 @@ mod tests {
FileDiffView::open(
PathBuf::from(path!("/test/old_file.txt")),
PathBuf::from(path!("/test/new_file.txt")),
workspace.weak_handle(),
workspace,
window,
cx,
)
+1 -2
View File
@@ -1274,11 +1274,10 @@ impl GitPanel {
})
.ok()?;
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |_, mut cx| {
let item = open_task
.await
.notify_workspace_async_err(workspace, &mut cx)
.notify_async_err(&mut cx)
.ok_or_else(|| anyhow::anyhow!("Failed to open file"))?;
if let Some(active_editor) = item.downcast::<Editor>() {
if let Some(diff_task) =
+1 -2
View File
@@ -124,7 +124,6 @@ impl ProjectDiff {
return;
}
let workspace = cx.entity();
let workspace_weak = workspace.downgrade();
window
.spawn(cx, async move |cx| {
let this = cx
@@ -139,7 +138,7 @@ impl ProjectDiff {
.ok();
anyhow::Ok(())
})
.detach_and_notify_err(workspace_weak, window, cx);
.detach_and_notify_err(window, cx);
}
pub fn deploy_at(
+24 -26
View File
@@ -4,8 +4,8 @@ use fuzzy::StringMatchCandidate;
use git::repository::Worktree as GitWorktree;
use gpui::{
Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
Focusable, InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement,
Action, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement,
PathPromptOptions, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window,
actions, rems,
};
@@ -20,7 +20,7 @@ use remote_connection::{RemoteConnectionModal, connect};
use std::{path::PathBuf, sync::Arc};
use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*};
use util::ResultExt;
use workspace::{ModalView, MultiWorkspace, Workspace, notifications::DetachAndPromptErr};
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr};
actions!(git, [WorktreeFromDefault, WorktreeFromDefaultOnWindow]);
@@ -289,6 +289,7 @@ impl WorktreeListDelegate {
};
let branch = worktree_branch.to_string();
let window_handle = window.window_handle();
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |_, cx| {
let Some(paths) = worktree_path.await? else {
@@ -354,7 +355,7 @@ impl WorktreeListDelegate {
connection_options,
vec![new_worktree_path],
app_state,
workspace.clone(),
window_handle,
replace_current_window,
cx,
)
@@ -406,12 +407,13 @@ impl WorktreeListDelegate {
|e, _, _| Some(e.to_string()),
);
} else if let Some(connection_options) = connection_options {
let window_handle = window.window_handle();
cx.spawn_in(window, async move |_, cx| {
open_remote_worktree(
connection_options,
vec![path],
app_state,
workspace,
window_handle,
replace_current_window,
cx,
)
@@ -439,16 +441,15 @@ async fn open_remote_worktree(
connection_options: RemoteConnectionOptions,
paths: Vec<PathBuf>,
app_state: Arc<workspace::AppState>,
workspace: WeakEntity<Workspace>,
window: gpui::AnyWindowHandle,
replace_current_window: bool,
cx: &mut AsyncWindowContext,
cx: &mut AsyncApp,
) -> anyhow::Result<()> {
let workspace_window = cx
.window_handle()
.downcast::<MultiWorkspace>()
let workspace_window = window
.downcast::<Workspace>()
.ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?;
let connect_task = workspace.update_in(cx, |workspace, window, cx| {
let connect_task = workspace_window.update(cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx)
});
@@ -472,19 +473,17 @@ async fn open_remote_worktree(
let session = connect_task.await;
workspace
.update_in(cx, |workspace, _window, cx| {
if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
prompt.update(cx, |prompt, cx| prompt.finished(cx))
}
})
.ok();
workspace_window.update(cx, |workspace, _window, cx| {
if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
prompt.update(cx, |prompt, cx| prompt.finished(cx))
}
})?;
let Some(Some(session)) = session else {
return Ok(());
};
let new_project: Entity<project::Project> = cx.update(|_, cx| {
let new_project: Entity<project::Project> = cx.update(|cx| {
project::Project::remote(
session,
app_state.client.clone(),
@@ -495,30 +494,29 @@ async fn open_remote_worktree(
true,
cx,
)
})?;
});
let window_to_use = if replace_current_window {
workspace_window
} else {
let workspace_position = cx
.update(|_, cx| {
.update(|cx| {
workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
})?
})
.await
.context("fetching workspace position from db")?;
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;
cx.open_window(options, |window, cx| {
let workspace = cx.new(|cx| {
cx.new(|cx| {
let mut workspace =
Workspace::new(None, new_project.clone(), app_state.clone(), window, cx);
workspace.centered_layout = workspace_position.centered_layout;
workspace
});
cx.new(|cx| MultiWorkspace::new(workspace, cx))
})
})?
};
-2
View File
@@ -265,8 +265,6 @@ pub enum IconName {
UserRoundPen,
Warning,
WholeWord,
WorkspaceNavClosed,
WorkspaceNavOpen,
XCircle,
XCircleFilled,
ZedAgent,
+1
View File
@@ -18,6 +18,7 @@ editor.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
platform_title_bar.workspace = true
project.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
+3 -2
View File
@@ -1,7 +1,8 @@
use anyhow::{Context as _, anyhow};
use gpui::{App, DivInspectorState, Inspector, InspectorElementId, IntoElement, Window};
use platform_title_bar::PlatformTitleBar;
use std::{cell::OnceCell, path::Path, sync::Arc};
use ui::{Label, Tooltip, prelude::*, utils::platform_title_bar_height};
use ui::{Label, Tooltip, prelude::*};
use util::{ResultExt as _, command::new_smol_command};
use workspace::AppState;
@@ -60,7 +61,7 @@ fn render_inspector(
let ui_font = theme::setup_ui_font(window, cx);
let colors = cx.theme().colors();
let inspector_id = inspector.active_element_id();
let toolbar_height = platform_title_bar_height(window);
let toolbar_height = PlatformTitleBar::height(window);
v_flex()
.size_full()
+11 -14
View File
@@ -118,20 +118,17 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
})?
.await?;
new_workspace
.update(cx, |multi_workspace, window, cx| {
let workspace = multi_workspace.workspace().clone();
workspace.update(cx, |workspace, cx| {
workspace.open_paths(
vec![entry_path],
workspace::OpenOptions {
visible: Some(OpenVisible::All),
..Default::default()
},
None,
window,
cx,
)
})
.update(cx, |workspace, window, cx| {
workspace.open_paths(
vec![entry_path],
workspace::OpenOptions {
visible: Some(OpenVisible::All),
..Default::default()
},
None,
window,
cx,
)
})?
.await
} else {
+1 -1
View File
@@ -1319,7 +1319,7 @@ impl KeymapEditor {
cx.spawn(async move |_, _| {
remove_keybinding(to_remove, &fs, keyboard_mapper.as_ref()).await
})
.detach_and_notify_err(self.workspace.clone(), window, cx);
.detach_and_notify_err(window, cx);
}
fn copy_context_to_clipboard(
+15 -12
View File
@@ -8,7 +8,7 @@ use std::{
use gpui::{
App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement,
ParentElement as _, Render, SerializedTaskTiming, SharedString, StatefulInteractiveElement,
Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WeakEntity, WindowBounds,
Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WindowBounds, WindowHandle,
WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list,
};
use util::ResultExt;
@@ -22,10 +22,13 @@ use workspace::{
use zed_actions::OpenPerformanceProfiler;
pub fn init(startup_time: Instant, cx: &mut App) {
cx.observe_new(move |workspace: &mut workspace::Workspace, _, cx| {
let workspace_handle = cx.entity().downgrade();
workspace.register_action(move |_workspace, _: &OpenPerformanceProfiler, window, cx| {
open_performance_profiler(startup_time, workspace_handle.clone(), window, cx);
cx.observe_new(move |workspace: &mut workspace::Workspace, _, _| {
workspace.register_action(move |workspace, _: &OpenPerformanceProfiler, window, cx| {
let window_handle = window
.window_handle()
.downcast::<Workspace>()
.expect("Workspaces are root Windows");
open_performance_profiler(startup_time, workspace, window_handle, cx);
});
})
.detach();
@@ -33,8 +36,8 @@ pub fn init(startup_time: Instant, cx: &mut App) {
fn open_performance_profiler(
startup_time: Instant,
workspace_handle: WeakEntity<Workspace>,
_window: &mut gpui::Window,
_workspace: &mut workspace::Workspace,
workspace_handle: WindowHandle<Workspace>,
cx: &mut App,
) {
let existing_window = cx
@@ -45,7 +48,7 @@ fn open_performance_profiler(
if let Some(existing_window) = existing_window {
existing_window
.update(cx, |profiler_window, window, _cx| {
profiler_window.workspace = Some(workspace_handle.clone());
profiler_window.workspace = Some(workspace_handle);
window.activate_window();
})
.log_err();
@@ -94,14 +97,14 @@ pub struct ProfilerWindow {
include_self_timings: ToggleState,
autoscroll: bool,
scroll_handle: UniformListScrollHandle,
workspace: Option<WeakEntity<Workspace>>,
workspace: Option<WindowHandle<Workspace>>,
_refresh: Option<Task<()>>,
}
impl ProfilerWindow {
pub fn new(
startup_time: Instant,
workspace_handle: Option<WeakEntity<Workspace>>,
workspace_handle: Option<WindowHandle<Workspace>>,
cx: &mut App,
) -> Entity<Self> {
let entity = cx.new(|cx| ProfilerWindow {
@@ -277,7 +280,7 @@ impl Render for ProfilerWindow {
Button::new("export-data", "Save")
.style(ButtonStyle::Filled)
.on_click(cx.listener(|this, _, _window, cx| {
let Some(workspace) = this.workspace.as_ref() else {
let Some(workspace) = this.workspace else {
return;
};
@@ -294,7 +297,7 @@ impl Render for ProfilerWindow {
.log_err()
.flatten()
.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(
&active_path,
+5 -6
View File
@@ -238,16 +238,15 @@ impl Onboarding {
go_to_welcome_page(cx);
}
fn handle_sign_in(&mut self, _: &SignIn, window: &mut Window, cx: &mut Context<Self>) {
fn handle_sign_in(_: &SignIn, window: &mut Window, cx: &mut App) {
let client = Client::global(cx);
let workspace = self.workspace.clone();
window
.spawn(cx, async move |mut cx| {
.spawn(cx, async move |cx| {
client
.sign_in_with_optional_connect(true, &cx)
.sign_in_with_optional_connect(true, cx)
.await
.notify_workspace_async_err(workspace, &mut cx);
.notify_async_err(cx);
})
.detach();
}
@@ -275,7 +274,7 @@ impl Render for Onboarding {
.size_full()
.bg(cx.theme().colors().editor_background)
.on_action(Self::on_finish)
.on_action(cx.listener(Self::handle_sign_in))
.on_action(Self::handle_sign_in)
.on_action(Self::handle_open_account)
.on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| {
window.focus_next(cx);
+11 -13
View File
@@ -41,15 +41,15 @@ pub fn toggle(
window: &mut Window,
cx: &mut App,
) {
if let Some((workspace, outline)) = Workspace::for_window(window, cx).and_then(|workspace| {
editor
.read(cx)
.buffer()
.read(cx)
.snapshot(cx)
.outline(Some(cx.theme().syntax()))
.map(|outline| (workspace, outline))
}) {
let outline = editor
.read(cx)
.buffer()
.read(cx)
.snapshot(cx)
.outline(Some(cx.theme().syntax()));
let workspace = window.root::<Workspace>().flatten();
if let Some((workspace, outline)) = workspace.zip(outline) {
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
OutlineView::new(outline, editor, window, cx)
@@ -396,7 +396,7 @@ mod tests {
use project::{FakeFs, Project};
use serde_json::json;
use util::{path, rel_path::rel_path};
use workspace::{AppState, MultiWorkspace, Workspace};
use workspace::{AppState, Workspace};
#[gpui::test]
async fn test_outline_view_row_highlights(cx: &mut TestAppContext) {
@@ -424,9 +424,7 @@ mod tests {
});
let (workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = cx.read(|cx| workspace.read(cx).workspace().clone());
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
-1
View File
@@ -13,7 +13,6 @@ path = "src/platform_title_bar.rs"
doctest = false
[dependencies]
feature_flags.workspace = true
gpui.workspace = true
settings.workspace = true
smallvec.workspace = true
@@ -1,21 +1,16 @@
mod platforms;
mod system_window_tabs;
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
use gpui::{
AnyElement, App, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement,
MouseButton, ParentElement, StatefulInteractiveElement, Styled, Window, WindowControlArea, div,
px,
AnyElement, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px,
};
use smallvec::SmallVec;
use std::mem;
use ui::{
prelude::*,
utils::{TRAFFIC_LIGHT_PADDING, platform_title_bar_height},
};
use ui::prelude::*;
use crate::{
platforms::{platform_linux, platform_windows},
platforms::{platform_linux, platform_mac, platform_windows},
system_window_tabs::SystemWindowTabs,
};
@@ -29,8 +24,6 @@ pub struct PlatformTitleBar {
children: SmallVec<[AnyElement; 2]>,
should_move: bool,
system_window_tabs: Entity<SystemWindowTabs>,
workspace_sidebar_open: bool,
sidebar_has_notifications: bool,
}
impl PlatformTitleBar {
@@ -44,11 +37,20 @@ impl PlatformTitleBar {
children: SmallVec::new(),
should_move: false,
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 {
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
if window.is_window_active() && !self.should_move {
@@ -71,46 +73,17 @@ impl PlatformTitleBar {
pub fn init(cx: &mut App) {
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 {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let supported_controls = window.window_controls();
let decorations = window.window_decorations();
let height = platform_title_bar_height(window);
let height = Self::height(window);
let titlebar_color = self.title_bar_color(window, cx);
let close_action = Box::new(workspace::CloseWindow);
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()
.window_control_area(WindowControlArea::Drag)
.w_full()
@@ -159,10 +132,8 @@ impl Render for PlatformTitleBar {
.map(|this| {
if window.is_fullscreen() {
this.pl_2()
} else if self.platform_style == PlatformStyle::Mac
&& !is_multiworkspace_sidebar_open
{
this.pl(px(TRAFFIC_LIGHT_PADDING))
} else if self.platform_style == PlatformStyle::Mac {
this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING))
} else {
this.pl_2()
}
@@ -1,2 +1,3 @@
pub mod platform_linux;
pub mod platform_mac;
pub mod platform_windows;
@@ -0,0 +1,10 @@
// 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.;
+7 -16
View File
@@ -772,11 +772,7 @@ impl ProjectPanel {
{
match project_panel.confirm_edit(false, window, cx) {
Some(task) => {
task.detach_and_notify_err(
project_panel.workspace.clone(),
window,
cx,
);
task.detach_and_notify_err(window, cx);
}
None => {
project_panel.discard_edit_state(window, cx);
@@ -1652,7 +1648,7 @@ impl ProjectPanel {
fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
if let Some(task) = self.confirm_edit(true, window, cx) {
task.detach_and_notify_err(self.workspace.clone(), window, cx);
task.detach_and_notify_err(window, cx);
}
}
@@ -3037,25 +3033,20 @@ impl ProjectPanel {
}
let item_count = paste_tasks.len();
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |project_panel, mut cx| {
cx.spawn_in(window, async move |project_panel, cx| {
let mut last_succeed = None;
for task in paste_tasks {
match task {
PasteTask::Rename(task) => {
if let Some(CreatedEntry::Included(entry)) = task
.await
.notify_workspace_async_err(workspace.clone(), &mut cx)
if let Some(CreatedEntry::Included(entry)) =
task.await.notify_async_err(cx)
{
last_succeed = Some(entry);
}
}
PasteTask::Copy(task) => {
if let Some(Some(entry)) = task
.await
.notify_workspace_async_err(workspace.clone(), &mut cx)
{
if let Some(Some(entry)) = task.await.notify_async_err(cx) {
last_succeed = Some(entry);
}
}
@@ -3397,7 +3388,7 @@ impl ProjectPanel {
if let Some((file_path1, file_path2)) = selected_files {
self.workspace
.update(cx, |workspace, cx| {
FileDiffView::open(file_path1, file_path2, workspace.weak_handle(), window, cx)
FileDiffView::open(file_path1, file_path2, workspace, window, cx)
.detach_and_log_err(cx);
})
.ok();
-2
View File
@@ -23,7 +23,6 @@ db.workspace = true
dev_container.workspace = true
editor.workspace = true
extension_host.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
@@ -67,7 +66,6 @@ language = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
release_channel.workspace = true
remote = { workspace = true, features = ["test-support"] }
remote_connection = { workspace = true, features = ["test-support"] }
remote_server.workspace = true
serde_json.workspace = true
settings = { workspace = true, features = ["test-support"] }
@@ -7,9 +7,7 @@ use ui::{
HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal,
ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, Window, div, h_flex, rems,
};
use workspace::{
ModalView, MultiWorkspace, OpenOptions, Workspace, notifications::DetachAndPromptErr,
};
use workspace::{ModalView, OpenOptions, Workspace, notifications::DetachAndPromptErr};
use crate::open_remote_project;
@@ -111,7 +109,7 @@ impl DisconnectedOverlay {
return;
};
let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
let Some(window_handle) = window.window_handle().downcast::<Workspace>() else {
return;
};
+87 -123
View File
@@ -4,9 +4,7 @@ mod remote_connections;
mod remote_servers;
mod ssh_config;
use std::{path::PathBuf, sync::Arc};
use fs::Fs;
use std::path::PathBuf;
#[cfg(target_os = "windows")]
mod wsl_picker;
@@ -29,11 +27,11 @@ use picker::{
pub use remote_connections::RemoteSettings;
pub use remote_servers::RemoteServerProjects;
use settings::Settings;
use std::path::Path;
use std::{path::Path, sync::Arc};
use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container};
use util::{ResultExt, paths::PathExt};
use workspace::{
HistoryManager, ModalView, MultiWorkspace, OpenOptions, PathList, SerializedWorkspaceLocation,
CloseIntent, HistoryManager, ModalView, OpenOptions, PathList, SerializedWorkspaceLocation,
WORKSPACE_DB, Workspace, WorkspaceId, notifications::DetachAndPromptErr,
with_active_or_new_workspace,
};
@@ -50,10 +48,9 @@ pub struct RecentProjectEntry {
pub async fn get_recent_projects(
current_workspace_id: Option<WorkspaceId>,
limit: Option<usize>,
fs: Arc<dyn fs::Fs>,
) -> Vec<RecentProjectEntry> {
let workspaces = WORKSPACE_DB
.recent_workspaces_on_disk(fs.as_ref())
.recent_workspaces_on_disk()
.await
.unwrap_or_default();
@@ -179,7 +176,7 @@ pub fn init(cx: &mut App) {
let fs = workspace.project().read(cx).fs().clone();
add_wsl_distro(fs, &open_wsl.distro, cx);
let open_options = OpenOptions {
replace_window: window.window_handle().downcast::<MultiWorkspace>(),
replace_window: window.window_handle().downcast::<Workspace>(),
..Default::default()
};
@@ -235,8 +232,10 @@ pub fn init(cx: &mut App) {
cx.on_action(|_: &OpenDevContainer, cx| {
with_active_or_new_workspace(cx, move |workspace, window, cx| {
if !workspace.project().read(cx).is_local() {
cx.spawn_in(window, async move |_, cx| {
let is_local = workspace.project().read(cx).is_local();
cx.spawn_in(window, async move |_, cx| {
if !is_local {
cx.prompt(
gpui::PromptLevel::Critical,
"Cannot open Dev Container from remote project",
@@ -245,16 +244,21 @@ pub fn init(cx: &mut App) {
)
.await
.ok();
})
.detach();
return;
}
return;
}
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)
});
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();
});
});
@@ -330,7 +334,6 @@ impl ModalView for RecentProjects {}
impl RecentProjects {
fn new(
delegate: RecentProjectsDelegate,
fs: Option<Arc<dyn Fs>>,
rem_width: f32,
window: &mut Window,
cx: &mut Context<Self>,
@@ -347,9 +350,8 @@ impl RecentProjects {
// 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.
cx.spawn_in(window, async move |this, cx| {
let Some(fs) = fs else { return };
let workspaces = WORKSPACE_DB
.recent_workspaces_on_disk(fs.as_ref())
.recent_workspaces_on_disk()
.await
.log_err()
.unwrap_or_default();
@@ -359,7 +361,7 @@ impl RecentProjects {
picker.update_matches(picker.query(cx), window, cx)
})
})
.ok();
.ok()
})
.detach();
Self {
@@ -377,11 +379,10 @@ impl RecentProjects {
cx: &mut Context<Workspace>,
) {
let weak = cx.entity().downgrade();
let fs = Some(workspace.app_state().fs.clone());
workspace.toggle_modal(window, cx, |window, cx| {
let delegate = RecentProjectsDelegate::new(weak, create_new_window, true, focus_handle);
Self::new(delegate, fs, 34., window, cx)
Self::new(delegate, 34., window, cx)
})
}
@@ -392,13 +393,10 @@ impl RecentProjects {
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let fs = workspace
.upgrade()
.map(|ws| ws.read(cx).app_state().fs.clone());
cx.new(|cx| {
let delegate =
RecentProjectsDelegate::new(workspace, create_new_window, true, focus_handle);
let list = Self::new(delegate, fs, 34., window, cx);
let list = Self::new(delegate, 34., window, cx);
list.picker.focus_handle(cx).focus(window, cx);
list
})
@@ -582,21 +580,27 @@ impl PickerDelegate for RecentProjectsDelegate {
SerializedWorkspaceLocation::Local => {
let paths = candidate_workspace_paths.paths().to_vec();
if replace_current_window {
if let Some(handle) =
window.window_handle().downcast::<MultiWorkspace>()
{
cx.defer(move |cx| {
if let Some(task) = handle
.update(cx, |multi_workspace, window, cx| {
multi_workspace.open_project(paths, window, cx)
})
.log_err()
{
task.detach_and_log_err(cx);
}
});
}
return;
cx.spawn_in(window, async move |workspace, cx| {
let continue_replacing = workspace
.update_in(cx, |workspace, window, cx| {
workspace.prepare_to_close(
CloseIntent::ReplaceWindow,
window,
cx,
)
})?
.await?;
if continue_replacing {
workspace
.update_in(cx, |workspace, window, cx| {
workspace
.open_workspace_for_paths(true, paths, window, cx)
})?
.await
} else {
Ok(())
}
})
} else {
workspace.open_workspace_for_paths(false, paths, window, cx)
}
@@ -605,7 +609,7 @@ impl PickerDelegate for RecentProjectsDelegate {
let app_state = workspace.app_state().clone();
let replace_window = if replace_current_window {
window.window_handle().downcast::<MultiWorkspace>()
window.window_handle().downcast::<Workspace>()
} else {
None
};
@@ -880,18 +884,10 @@ impl RecentProjectsDelegate {
) {
if let Some(selected_match) = self.matches.get(ix) {
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| {
WORKSPACE_DB
.delete_workspace_by_id(workspace_id)
.await
.log_err();
let Some(fs) = fs else { return };
let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await;
let workspaces = WORKSPACE_DB
.recent_workspaces_on_disk(fs.as_ref())
.recent_workspaces_on_disk()
.await
.unwrap_or_default();
this.update_in(cx, move |picker, window, cx| {
@@ -908,7 +904,6 @@ impl RecentProjectsDelegate {
.update(cx, |this, cx| this.delete_history(workspace_id, cx));
}
})
.ok();
})
.detach();
}
@@ -956,7 +951,7 @@ mod tests {
use super::*;
#[gpui::test]
async fn test_dirty_workspace_survives_when_opening_recent_project(cx: &mut TestAppContext) {
async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) {
let app_state = init_test(cx);
cx.update(|cx| {
@@ -980,11 +975,6 @@ mod tests {
}),
)
.await;
app_state
.fs
.as_fake()
.insert_tree(path!("/test/path"), json!({}))
.await;
cx.update(|cx| {
open_paths(
&[PathBuf::from(path!("/dir/main.ts"))],
@@ -997,40 +987,31 @@ mod tests {
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
multi_workspace
.update(cx, |multi_workspace, _, cx| {
assert!(!multi_workspace.workspace().read(cx).is_edited())
})
let workspace = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
workspace
.update(cx, |workspace, _, _| assert!(!workspace.is_edited()))
.unwrap();
let editor = multi_workspace
.read_with(cx, |multi_workspace, cx| {
multi_workspace
.workspace()
.read(cx)
let editor = workspace
.read_with(cx, |workspace, cx| {
workspace
.active_item(cx)
.unwrap()
.downcast::<Editor>()
.unwrap()
})
.unwrap();
multi_workspace
workspace
.update(cx, |_, window, cx| {
editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
})
.unwrap();
multi_workspace
.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"
)
})
workspace
.update(cx, |workspace, _, _| assert!(workspace.is_edited(), "After inserting more text into the editor without saving, we should have a dirty project"))
.unwrap();
let recent_projects_picker = open_recent_projects(&multi_workspace, cx);
multi_workspace
let recent_projects_picker = open_recent_projects(&workspace, cx);
workspace
.update(cx, |_, _, cx| {
recent_projects_picker.update(cx, |picker, cx| {
assert_eq!(picker.query(cx), "");
@@ -1054,64 +1035,47 @@ mod tests {
!cx.has_pending_prompt(),
"Should have no pending prompt on dirty project before opening the new recent project"
);
let dirty_workspace = multi_workspace
.read_with(cx, |multi_workspace, _cx| {
multi_workspace.workspace().clone()
})
.unwrap();
cx.dispatch_action(*multi_workspace, menu::Confirm);
cx.run_until_parked();
multi_workspace
.update(cx, |multi_workspace, _, cx| {
cx.dispatch_action(*workspace, menu::Confirm);
workspace
.update(cx, |workspace, _, cx| {
assert!(
multi_workspace
.workspace()
.read(cx)
.active_modal::<RecentProjects>(cx)
.is_none(),
workspace.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!(
cx.has_pending_prompt(),
"Dirty workspace should prompt before opening the new recent project"
);
cx.simulate_prompt_answer("Cancel");
assert!(
!cx.has_pending_prompt(),
"No save prompt in multi-workspace mode — dirty workspace survives in background"
"Should have no pending prompt after cancelling"
);
workspace
.update(cx, |workspace, _, _| {
assert!(
workspace.is_edited(),
"Should be in the same dirty project after cancelling"
)
})
.unwrap();
}
fn open_recent_projects(
multi_workspace: &WindowHandle<MultiWorkspace>,
workspace: &WindowHandle<Workspace>,
cx: &mut TestAppContext,
) -> Entity<Picker<RecentProjectsDelegate>> {
cx.dispatch_action(
(*multi_workspace).into(),
(*workspace).into(),
OpenRecent {
create_new_window: false,
},
);
multi_workspace
.update(cx, |multi_workspace, _, cx| {
multi_workspace
.workspace()
.read(cx)
workspace
.update(cx, |workspace, _, cx| {
workspace
.active_modal::<RecentProjects>(cx)
.unwrap()
.read(cx)
@@ -19,7 +19,7 @@ use remote::{
pub use settings::SshConnection;
use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection};
use util::paths::PathWithPosition;
use workspace::{AppState, MultiWorkspace, Workspace};
use workspace::{AppState, Workspace};
pub use remote_connection::{
RemoteClientDelegate, RemoteConnectionModal, RemoteConnectionPrompt, SshConnectionHeader,
@@ -131,11 +131,8 @@ pub async fn open_remote_project(
cx: &mut AsyncApp,
) -> Result<()> {
let created_new_window = open_options.replace_window.is_none();
let (window, initial_workspace) = if let Some(window) = open_options.replace_window {
let workspace = window.update(cx, |multi_workspace, _, _| {
multi_workspace.workspace().clone()
})?;
(window, workspace)
let window = if let Some(window) = open_options.replace_window {
window
} else {
let workspace_position = cx
.update(|cx| {
@@ -148,7 +145,7 @@ pub async fn open_remote_project(
cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx));
options.window_bounds = workspace_position.window_bounds;
let window = cx.open_window(options, |window, cx| {
cx.open_window(options, |window, cx| {
let project = project::Project::local(
app_state.client.clone(),
app_state.node_runtime.clone(),
@@ -162,17 +159,12 @@ pub async fn open_remote_project(
},
cx,
);
let workspace = cx.new(|cx| {
cx.new(|cx| {
let mut workspace = Workspace::new(None, project, app_state.clone(), window, cx);
workspace.centered_layout = workspace_position.centered_layout;
workspace
});
cx.new(|cx| MultiWorkspace::new(workspace, cx))
})?;
let workspace = window.update(cx, |multi_workspace, _, _cx| {
multi_workspace.workspace().clone()
})?;
(window, workspace)
})
})?
};
loop {
@@ -180,38 +172,35 @@ pub async fn open_remote_project(
let delegate = window.update(cx, {
let paths = paths.clone();
let connection_options = connection_options.clone();
let initial_workspace = initial_workspace.clone();
move |_multi_workspace: &mut MultiWorkspace, window, cx| {
move |workspace, window, cx| {
window.activate_window();
initial_workspace.update(cx, |workspace, cx| {
workspace.hide_modal(window, cx);
workspace.toggle_modal(window, cx, |window, cx| {
RemoteConnectionModal::new(&connection_options, paths, window, cx)
});
workspace.hide_modal(window, cx);
workspace.toggle_modal(window, cx, |window, cx| {
RemoteConnectionModal::new(&connection_options, paths, window, cx)
});
let ui = workspace
.active_modal::<RemoteConnectionModal>(cx)?
.read(cx)
.prompt
.clone();
let ui = workspace
.active_modal::<RemoteConnectionModal>(cx)?
.read(cx)
.prompt
.clone();
ui.update(cx, |ui, _cx| {
ui.set_cancellation_tx(cancel_tx);
});
ui.update(cx, |ui, _cx| {
ui.set_cancellation_tx(cancel_tx);
});
Some(Arc::new(RemoteClientDelegate::new(
window.window_handle(),
ui.downgrade(),
if let RemoteConnectionOptions::Ssh(options) = &connection_options {
options
.password
.as_deref()
.and_then(|pw| EncryptedPassword::try_from(pw).ok())
} else {
None
},
)))
})
Some(Arc::new(RemoteClientDelegate::new(
window.window_handle(),
ui.downgrade(),
if let RemoteConnectionOptions::Ssh(options) = &connection_options {
options
.password
.as_deref()
.and_then(|pw| EncryptedPassword::try_from(pw).ok())
} else {
None
},
)))
}
})?;
@@ -220,11 +209,13 @@ pub async fn open_remote_project(
let connection = remote::connect(connection_options.clone(), delegate.clone(), cx);
let connection = select! {
_ = cancel_rx => {
initial_workspace.update(cx, |workspace, cx| {
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
ui.update(cx, |modal, cx| modal.finished(cx))
}
});
window
.update(cx, |workspace, _, cx| {
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
ui.update(cx, |modal, cx| modal.finished(cx))
}
})
.ok();
break;
},
@@ -233,11 +224,13 @@ pub async fn open_remote_project(
let remote_connection = match connection {
Ok(connection) => connection,
Err(e) => {
initial_workspace.update(cx, |workspace, cx| {
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
ui.update(cx, |modal, cx| modal.finished(cx))
}
});
window
.update(cx, |workspace, _, cx| {
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
ui.update(cx, |modal, cx| modal.finished(cx))
}
})
.ok();
log::error!("Failed to open project: {e:#}");
let response = window
.update(cx, |_, window, cx| {
@@ -291,11 +284,13 @@ pub async fn open_remote_project(
})
.await;
initial_workspace.update(cx, |workspace, cx| {
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
ui.update(cx, |modal, cx| modal.finished(cx))
}
});
window
.update(cx, |workspace, _, cx| {
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
ui.update(cx, |modal, cx| modal.finished(cx))
}
})
.ok();
match opened_items {
Err(e) => {
@@ -325,20 +320,20 @@ pub async fn open_remote_project(
continue;
}
if created_new_window {
window
.update(cx, |_, window, _| window.remove_window())
.ok();
}
initial_workspace.update(cx, |workspace, cx| {
trusted_worktrees::track_worktree_trust(
workspace.project().read(cx).worktree_store(),
None,
None,
None,
cx,
);
});
window
.update(cx, |workspace, window, cx| {
if created_new_window {
window.remove_window();
}
trusted_worktrees::track_worktree_trust(
workspace.project().read(cx).worktree_store(),
None,
None,
None,
cx,
);
})
.ok();
}
Ok(items) => {
@@ -371,20 +366,14 @@ pub async fn open_remote_project(
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
.update(cx, |multi_workspace: &mut MultiWorkspace, _, cx| {
let workspace = multi_workspace.workspace().clone();
workspace.update(cx, |workspace, cx| {
if let Some(client) = workspace.project().read(cx).remote_client() {
if let Some(extension_store) = ExtensionStore::try_global(cx) {
extension_store
.update(cx, |store, cx| store.register_remote_client(client, cx));
}
.update(cx, |workspace, _, cx| {
if let Some(client) = workspace.project().read(cx).remote_client() {
if let Some(extension_store) = ExtensionStore::try_global(cx) {
extension_store
.update(cx, |store, cx| store.register_remote_client(client, cx));
}
});
}
})
.ok();
Ok(())
@@ -511,16 +500,12 @@ mod tests {
let windows = cx.update(|cx| cx.windows().len());
assert_eq!(windows, 1, "Should have opened a window");
let multi_workspace_handle =
cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
let workspace_handle = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
multi_workspace_handle
.update(cx, |multi_workspace, _, cx| {
let workspace = multi_workspace.workspace().clone();
workspace.update(cx, |workspace, cx| {
let project = workspace.project().read(cx);
assert!(project.is_remote(), "Project should be a remote project");
});
workspace_handle
.update(cx, |workspace, _, cx| {
let project = workspace.project().read(cx);
assert!(project.is_remote(), "Project should be a remote project");
})
.unwrap();
}
+62 -57
View File
@@ -6,8 +6,7 @@ use crate::{
ssh_config::parse_ssh_config_hosts,
};
use dev_container::{
DevContainerConfig, DevContainerContext, find_devcontainer_configs,
start_dev_container_with_config,
DevContainerConfig, find_devcontainer_configs, start_dev_container_with_config,
};
use editor::Editor;
@@ -52,7 +51,7 @@ use util::{
rel_path::RelPath,
};
use workspace::{
ModalView, MultiWorkspace, OpenLog, OpenOptions, Toast, Workspace,
ModalView, OpenLog, OpenOptions, Toast, Workspace,
notifications::{DetachAndPromptErr, NotificationId},
open_remote_project_with_existing_connection,
};
@@ -479,11 +478,10 @@ impl ProjectPicker {
.log_err()?;
let window = cx
.open_window(options, |window, cx| {
let workspace = cx.new(|cx| {
cx.new(|cx| {
telemetry::event!("SSH Project Created");
Workspace::new(None, project.clone(), app_state.clone(), window, cx)
});
cx.new(|cx| MultiWorkspace::new(workspace, cx))
})
})
.log_err()?;
@@ -810,18 +808,11 @@ impl RemoteServerProjects {
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> Self {
let configs = workspace
.read_with(cx, |workspace, cx| find_devcontainer_configs(workspace, cx))
.unwrap_or_default();
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)),
let this = Self::new_inner(
Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
DevContainerCreationProgress::Creating,
cx,
)),
false,
fs,
window,
@@ -829,15 +820,35 @@ impl RemoteServerProjects {
cx,
);
if configs.len() > 1 {
let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
this.dev_container_picker =
Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
} else {
let config = configs.into_iter().next();
this.open_dev_container(config, window, cx);
this.view_in_progress_dev_container(window, cx);
}
// Spawn a task to scan for configs and then start the container
cx.spawn_in(window, async move |entity, cx| {
let configs = find_devcontainer_configs(cx);
entity
.update_in(cx, |this, window, cx| {
if configs.len() > 1 {
// Multiple configs found - show selection UI
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
}
@@ -1540,9 +1551,7 @@ impl RemoteServerProjects {
let replace_window = match (create_new_window, secondary_confirm) {
(true, false) | (false, true) => None,
(true, true) | (false, false) => {
window.window_handle().downcast::<MultiWorkspace>()
}
(true, true) | (false, false) => window.window_handle().downcast::<Workspace>(),
};
cx.spawn_in(window, async move |_, cx| {
@@ -1794,25 +1803,25 @@ impl RemoteServerProjects {
}
fn init_dev_container_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let configs = self
.workspace
.read_with(cx, |workspace, cx| find_devcontainer_configs(workspace, cx))
.unwrap_or_default();
cx.spawn_in(window, async move |entity, cx| {
let configs = find_devcontainer_configs(cx);
if configs.len() > 1 {
let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
self.dev_container_picker =
Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
entity
.update_in(cx, |this, 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);
self.mode = Mode::CreateRemoteDevContainer(state);
cx.notify();
} else {
let config = configs.into_iter().next();
self.open_dev_container(config, window, cx);
self.view_in_progress_dev_container(window, cx);
}
let state = CreateRemoteDevContainer::new(
DevContainerCreationProgress::SelectingConfig,
cx,
);
this.mode = Mode::CreateRemoteDevContainer(state);
cx.notify();
})
.log_err();
})
.detach();
}
fn open_dev_container(
@@ -1821,25 +1830,21 @@ impl RemoteServerProjects {
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some((app_state, context)) = self
let Some(app_state) = self
.workspace
.read_with(cx, |workspace, cx| {
let app_state = workspace.app_state().clone();
let context = DevContainerContext::from_workspace(workspace, cx)?;
Some((app_state, context))
})
.read_with(cx, |workspace, _| workspace.app_state().clone())
.log_err()
.flatten()
else {
log::error!("No active project directory for Dev Container");
return;
};
let replace_window = window.window_handle().downcast::<MultiWorkspace>();
let replace_window = window.window_handle().downcast::<Workspace>();
cx.spawn_in(window, async move |entity, cx| {
let (connection, starting_dir) =
match start_dev_container_with_config(context, config).await {
match start_dev_container_with_config(cx, app_state.node_runtime.clone(), config)
.await
{
Ok((c, s)) => (Connection::DevContainer(c), s),
Err(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,
};
use util::ResultExt as _;
use workspace::{ModalView, MultiWorkspace};
use workspace::{ModalView, Workspace};
use crate::open_remote_project;
@@ -249,7 +249,7 @@ impl WslOpenModal {
false => !secondary,
};
let replace_window = match replace_current_window {
true => window.window_handle().downcast::<MultiWorkspace>(),
true => window.window_handle().downcast::<Workspace>(),
false => None,
};
+3 -2
View File
@@ -78,8 +78,9 @@ pub fn init(cx: &mut App) {
return;
}
cx.defer_in(window, |editor, _window, cx| {
let project = editor.project().cloned();
cx.defer_in(window, |editor, window, cx| {
let workspace = Workspace::for_window(window, cx);
let project = workspace.map(|workspace| workspace.read(cx).project().clone());
let is_local_project = project
.as_ref()
+6 -8
View File
@@ -24,7 +24,7 @@ use theme::ThemeSettings;
use ui::{Divider, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
use ui_input::ErasedEditor;
use util::{ResultExt, TryFutureExt};
use workspace::{MultiWorkspace, Workspace, WorkspaceSettings, client_side_decorations};
use workspace::{Workspace, WorkspaceSettings, client_side_decorations};
use zed_actions::assistant::InlineAssist;
use prompt_store::*;
@@ -968,14 +968,12 @@ impl RulesLibrary {
.assist(rule_editor, initial_prompt, window, cx);
} else {
for window in cx.windows() {
if let Some(multi_workspace) = window.downcast::<MultiWorkspace>() {
let panel = multi_workspace
.update(cx, |multi_workspace, window, cx| {
if let Some(workspace) = window.downcast::<Workspace>() {
let panel = workspace
.update(cx, |workspace, window, cx| {
window.activate_window();
multi_workspace.workspace().update(cx, |workspace, cx| {
self.inline_assist_delegate
.focus_agent_panel(workspace, window, cx)
})
self.inline_assist_delegate
.focus_agent_panel(workspace, window, cx)
})
.ok();
if panel == Some(true) {
+1 -1
View File
@@ -191,7 +191,7 @@ pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) {
struct NotifType();
let notification_id = NotificationId::unique::<NotifType>();
let Some(workspace) = Workspace::for_window(window, cx) else {
let Some(workspace) = window.root::<Workspace>().flatten() else {
return;
};
workspace.update(cx, |workspace, cx| {
-14
View File
@@ -47,15 +47,6 @@ 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 {
&self.session_id
}
@@ -118,11 +109,6 @@ impl AppSession {
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>> {
self.session.old_window_ids.clone()
}
@@ -287,7 +287,7 @@ mod tests {
use serde_json::json;
use settings::Settings;
use theme::{self, ThemeSettings};
use workspace::{self, AppState, MultiWorkspace};
use workspace::{self, AppState};
use zed_actions::settings_profile_selector;
async fn init_test(
@@ -320,11 +320,8 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, ["/test".as_ref()], cx).await;
let window = cx.add_window(|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();
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
cx.update(|_, cx| {
assert!(!cx.has_global::<ActiveSettingsProfileName>());
+133 -150
View File
@@ -40,9 +40,7 @@ use ui::{
};
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
use workspace::{
AppState, MultiWorkspace, OpenOptions, OpenVisible, Workspace, client_side_decorations,
};
use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decorations};
use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt};
use crate::components::{
@@ -396,7 +394,7 @@ pub fn init(cx: &mut App) {
|workspace, OpenSettingsAt { path }: &OpenSettingsAt, window, cx| {
let window_handle = window
.window_handle()
.downcast::<MultiWorkspace>()
.downcast::<Workspace>()
.expect("Workspaces are root Windows");
open_settings_editor(workspace, Some(&path), false, window_handle, cx);
},
@@ -404,14 +402,14 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &OpenSettings, window, cx| {
let window_handle = window
.window_handle()
.downcast::<MultiWorkspace>()
.downcast::<Workspace>()
.expect("Workspaces are root Windows");
open_settings_editor(workspace, None, false, window_handle, cx);
})
.register_action(|workspace, _: &OpenProjectSettings, window, cx| {
let window_handle = window
.window_handle()
.downcast::<MultiWorkspace>()
.downcast::<Workspace>()
.expect("Workspaces are root Windows");
open_settings_editor(workspace, None, true, window_handle, cx);
});
@@ -549,7 +547,7 @@ pub fn open_settings_editor(
_workspace: &mut Workspace,
path: Option<&str>,
open_project_settings: bool,
workspace_handle: WindowHandle<MultiWorkspace>,
workspace_handle: WindowHandle<Workspace>,
cx: &mut App,
) {
telemetry::event!("Settings Viewed");
@@ -717,7 +715,7 @@ fn active_language_mut() -> Option<std::sync::RwLockWriteGuard<'static, Option<S
pub struct SettingsWindow {
title_bar: Option<Entity<PlatformTitleBar>>,
original_window: Option<WindowHandle<MultiWorkspace>>,
original_window: Option<WindowHandle<Workspace>>,
files: Vec<(SettingsUiFile, FocusHandle)>,
worktree_root_dirs: HashMap<WorktreeId, String>,
current_file: SettingsUiFile,
@@ -1449,7 +1447,7 @@ impl SettingsUiFile {
impl SettingsWindow {
fn new(
original_window: Option<WindowHandle<MultiWorkspace>>,
original_window: Option<WindowHandle<Workspace>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -1520,21 +1518,34 @@ impl SettingsWindow {
.detach();
if let Some(app_state) = AppState::global(cx).upgrade() {
let workspaces: Vec<Entity<Workspace>> = app_state
for project in app_state
.workspace_store
.read(cx)
.workspaces()
.filter_map(|weak| weak.upgrade())
.collect();
for workspace in workspaces {
let project = workspace.read(cx).project().clone();
.iter()
.filter_map(|space| {
space
.read(cx)
.ok()
.map(|workspace| workspace.project().clone())
})
.collect::<Vec<_>>()
{
cx.observe_release_in(&project, window, |this, _, window, cx| {
this.fetch_files(window, cx)
})
.detach();
cx.subscribe_in(&project, window, Self::handle_project_event)
.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| {
this.fetch_files(window, cx)
})
@@ -3309,19 +3320,56 @@ impl SettingsWindow {
return;
};
original_window
.update(cx, |multi_workspace, window, cx| {
multi_workspace
.workspace()
.clone()
.update(cx, |workspace, cx| {
workspace
.with_local_or_wsl_workspace(
window,
cx,
open_user_settings_in_workspace,
)
.detach();
});
.update(cx, |workspace, window, cx| {
workspace
.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
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();
})
.detach();
})
.ok();
@@ -3333,22 +3381,22 @@ impl SettingsWindow {
return;
};
let Some((workspace_window, worktree, corresponding_workspace)) = app_state
let Some((worktree, corresponding_workspace)) = app_state
.workspace_store
.read(cx)
.workspaces_with_windows()
.filter_map(|(window_handle, weak)| {
let workspace = weak.upgrade()?;
let window = window_handle.downcast::<MultiWorkspace>()?;
Some((window, workspace))
})
.find_map(|(window, workspace): (_, Entity<Workspace>)| {
.workspaces()
.iter()
.find_map(|workspace| {
workspace
.read(cx)
.project()
.read(cx)
.worktree_for_id(*worktree_id, cx)
.map(|worktree| (window, worktree, workspace))
.read_with(cx, |workspace, cx| {
workspace
.project()
.read(cx)
.worktree_for_id(*worktree_id, cx)
})
.ok()
.flatten()
.zip(Some(*workspace))
})
else {
log::error!(
@@ -3376,15 +3424,14 @@ impl SettingsWindow {
// TODO: move zed::open_local_file() APIs to this crate, and
// re-implement the "initial_contents" behavior
let workspace_weak = corresponding_workspace.downgrade();
workspace_window
corresponding_workspace
.update(cx, |_, window, cx| {
cx.spawn_in(window, async move |_, cx| {
cx.spawn_in(window, async move |workspace, cx| {
if let Some(create_task) = create_task {
create_task.await.ok()?;
};
workspace_weak
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path(
(worktree_id, settings_path.clone()),
@@ -3398,7 +3445,7 @@ impl SettingsWindow {
.await
.log_err()?;
workspace_weak
workspace
.update_in(cx, |_, window, cx| {
window.activate_window();
cx.notify();
@@ -3705,7 +3752,7 @@ impl Render for SettingsWindow {
}
fn all_projects(
window: Option<&WindowHandle<MultiWorkspace>>,
window: Option<&WindowHandle<Workspace>>,
cx: &App,
) -> impl Iterator<Item = Entity<Project>> {
let mut seen_project_ids = std::collections::HashSet::new();
@@ -3716,19 +3763,10 @@ fn all_projects(
.workspace_store
.read(cx)
.workspaces()
.filter_map(|weak| weak.upgrade())
.map(|workspace: Entity<Workspace>| workspace.read(cx).project().clone())
.iter()
.filter_map(|workspace| Some(workspace.read(cx).ok()?.project().clone()))
.chain(
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<_>>()
}),
window.and_then(|workspace| Some(workspace.read(cx).ok()?.project().clone())),
)
.filter(move |project| seen_project_ids.insert(project.entity_id()))
})
@@ -3736,51 +3774,6 @@ fn all_projects(
.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(
file: SettingsUiFile,
file_name: Option<&'static str>,
@@ -4761,33 +4754,29 @@ pub mod test {
.await
.expect("Failed to create worktree_c");
let (_multi_workspace1, cx) = cx.add_window_view(|window, cx| {
let workspace = cx.new(|cx| {
Workspace::new(
Default::default(),
project1.clone(),
app_state.clone(),
window,
cx,
)
});
MultiWorkspace::new(workspace, cx)
let (_workspace1, cx) = cx.add_window_view(|window, cx| {
Workspace::new(
Default::default(),
project1.clone(),
app_state.clone(),
window,
cx,
)
});
let (_multi_workspace2, cx) = cx.add_window_view(|window, cx| {
let workspace = cx.new(|cx| {
Workspace::new(
Default::default(),
project2.clone(),
app_state.clone(),
window,
cx,
)
});
MultiWorkspace::new(workspace, cx)
let _workspace1_handle = cx.window_handle().downcast::<Workspace>().unwrap();
let (_workspace2, cx) = cx.add_window_view(|window, cx| {
Workspace::new(
Default::default(),
project2.clone(),
app_state.clone(),
window,
cx,
)
});
let workspace2_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
let workspace2_handle = cx.window_handle().downcast::<Workspace>().unwrap();
cx.run_until_parked();
@@ -4906,20 +4895,17 @@ pub mod test {
.await
.expect("Failed to create worktree_a");
let (_multi_workspace1, cx) = cx.add_window_view(|window, cx| {
let workspace = cx.new(|cx| {
Workspace::new(
Default::default(),
project1.clone(),
app_state.clone(),
window,
cx,
)
});
MultiWorkspace::new(workspace, cx)
let (_workspace1, cx) = cx.add_window_view(|window, cx| {
Workspace::new(
Default::default(),
project1.clone(),
app_state.clone(),
window,
cx,
)
});
let workspace1_handle = cx.window_handle().downcast::<MultiWorkspace>().unwrap();
let workspace1_handle = cx.window_handle().downcast::<Workspace>().unwrap();
cx.run_until_parked();
@@ -4956,17 +4942,14 @@ pub mod test {
.await
.expect("Failed to create worktree_b");
let (_multi_workspace2, cx) = cx.add_window_view(|window, cx| {
let workspace = cx.new(|cx| {
Workspace::new(
Default::default(),
project2.clone(),
app_state.clone(),
window,
cx,
)
});
MultiWorkspace::new(workspace, cx)
let (_workspace2, cx) = cx.add_window_view(|window, cx| {
Workspace::new(
Default::default(),
project2.clone(),
app_state.clone(),
window,
cx,
)
});
cx.run_until_parked();
-43
View File
@@ -1,43 +0,0 @@
[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
@@ -1 +0,0 @@
../../LICENSE-GPL
File diff suppressed because it is too large. Load diff
-1
View File
@@ -38,7 +38,6 @@ chrono.workspace = true
client.workspace = true
cloud_api_types.workspace = true
db.workspace = true
feature_flags.workspace = true
git_ui.workspace = true
gpui = { workspace = true, features = ["screen-capture"] }
menu.workspace = true
+32 -32
View File
@@ -11,8 +11,7 @@ use project::{Project, Worktree, git_store::Repository};
use recent_projects::{RecentProjectEntry, delete_recent_project, get_recent_projects};
use settings::WorktreeId;
use ui::{ContextMenu, DocumentationAside, DocumentationSide, Tooltip, prelude::*};
use util::ResultExt as _;
use workspace::{MultiWorkspace, Workspace};
use workspace::{CloseIntent, Workspace};
actions!(project_dropdown, [RemoveSelectedFolder]);
@@ -67,12 +66,8 @@ impl ProjectDropdown {
let recent_projects_for_fetch = recent_projects.clone();
let menu_shell_for_fetch = menu_shell.clone();
let workspace_for_fetch = workspace.clone();
let fs = workspace
.upgrade()
.map(|ws| ws.read(cx).app_state().fs.clone());
cx.spawn_in(window, async move |_this, cx| {
let Some(fs) = fs else { return };
let current_workspace_id = cx
.update(|_, cx| {
workspace_for_fetch
@@ -82,7 +77,7 @@ impl ProjectDropdown {
.ok()
.flatten();
let projects = get_recent_projects(current_workspace_id, None, fs).await;
let projects = get_recent_projects(current_workspace_id, None).await;
cx.update(|window, cx| {
*recent_projects_for_fetch.borrow_mut() = projects;
@@ -93,7 +88,7 @@ impl ProjectDropdown {
});
}
})
.ok();
.ok()
})
.detach();
@@ -401,31 +396,36 @@ impl ProjectDropdown {
window: &mut Window,
cx: &mut App,
) {
if create_new_window {
let Some(workspace) = workspace.upgrade() else {
return;
};
workspace.update(cx, |workspace, cx| {
workspace
.open_workspace_for_paths(false, paths, window, cx)
.detach_and_log_err(cx);
});
} else {
let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() else {
return;
};
let Some(workspace) = workspace.upgrade() else {
return;
};
cx.defer(move |cx| {
if let Some(task) = handle
.update(cx, |multi_workspace, window, cx| {
multi_workspace.open_project(paths, window, cx)
})
.log_err()
{
task.detach_and_log_err(cx);
}
});
}
workspace.update(cx, |workspace, cx| {
if create_new_window {
workspace.open_workspace_for_paths(false, paths, window, cx)
} else {
cx.spawn_in(window, {
let paths = paths.clone();
async move |workspace, cx| {
let continue_replacing = workspace
.update_in(cx, |workspace, window, cx| {
workspace.prepare_to_close(CloseIntent::ReplaceWindow, window, cx)
})?
.await?;
if continue_replacing {
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_workspace_for_paths(true, paths, window, cx)
})?
.await
} else {
Ok(())
}
}
})
}
.detach_and_log_err(cx);
});
}
/// Get all projects sorted alphabetically with their branch info.
+8 -94
View File
@@ -22,7 +22,6 @@ use auto_update::AutoUpdateStatus;
use call::ActiveCall;
use client::{Client, UserStore, zed_urls};
use cloud_api_types::Plan;
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
use gpui::{
Action, AnyElement, App, Context, Corner, Element, Entity, FocusHandle, Focusable,
InteractiveElement, IntoElement, MouseButton, ParentElement, Render,
@@ -39,13 +38,10 @@ use theme::ActiveTheme;
use title_bar_settings::TitleBarSettings;
use ui::{
Avatar, ButtonLike, Chip, ContextMenu, IconWithIndicator, Indicator, PopoverMenu,
PopoverMenuHandle, TintColor, Tooltip, prelude::*, utils::platform_title_bar_height,
PopoverMenuHandle, TintColor, Tooltip, prelude::*,
};
use util::ResultExt;
use workspace::{
MultiWorkspace, SwitchProject, ToggleWorkspaceSidebar, ToggleWorktreeSecurity, Workspace,
notifications::NotifyResultExt,
};
use workspace::{SwitchProject, ToggleWorktreeSecurity, Workspace, notifications::NotifyResultExt};
use zed_actions::OpenRemote;
pub use onboarding_banner::restore_banner;
@@ -84,10 +80,8 @@ pub fn init(cx: &mut App) {
.titlebar_item()
.and_then(|item| item.downcast::<TitleBar>().ok())
{
window.defer(cx, move |window, cx| {
titlebar.update(cx, |titlebar, cx| {
titlebar.show_project_dropdown(window, cx);
})
titlebar.update(cx, |titlebar, cx| {
titlebar.show_project_dropdown(window, cx);
});
}
});
@@ -164,7 +158,7 @@ impl Render for TitleBar {
children.push(
h_flex()
.gap_0p5()
.gap_1()
.map(|title_bar| {
let mut render_project_items = title_bar_settings.show_branch_name
|| title_bar_settings.show_project_items;
@@ -177,7 +171,6 @@ impl Render for TitleBar {
title_bar.child(menu)
},
)
.children(self.render_workspace_sidebar_toggle(window, cx))
.children(self.render_restricted_mode(cx))
.when(render_project_items, |title_bar| {
title_bar
@@ -239,7 +232,7 @@ impl Render for TitleBar {
);
});
let height = platform_title_bar_height(window);
let height = PlatformTitleBar::height(window);
let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| {
platform_titlebar.title_bar_color(window, cx)
});
@@ -347,48 +340,6 @@ impl TitleBar {
let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx));
// Set up observer to sync sidebar state from MultiWorkspace to PlatformTitleBar.
{
let platform_titlebar = platform_titlebar.clone();
let window_handle = window.window_handle();
cx.spawn(async move |this: WeakEntity<TitleBar>, cx| {
let Some(multi_workspace_handle) = window_handle.downcast::<MultiWorkspace>()
else {
return;
};
let _ = cx.update(|cx| {
let Ok(multi_workspace) = multi_workspace_handle.entity(cx) else {
return;
};
let is_open = multi_workspace.read(cx).is_sidebar_open();
let has_notifications = multi_workspace.read(cx).sidebar_has_notifications(cx);
platform_titlebar.update(cx, |titlebar, cx| {
titlebar.set_workspace_sidebar_open(is_open, cx);
titlebar.set_sidebar_has_notifications(has_notifications, cx);
});
let platform_titlebar = platform_titlebar.clone();
let subscription = cx.observe(&multi_workspace, move |mw, cx| {
let is_open = mw.read(cx).is_sidebar_open();
let has_notifications = mw.read(cx).sidebar_has_notifications(cx);
platform_titlebar.update(cx, |titlebar, cx| {
titlebar.set_workspace_sidebar_open(is_open, cx);
titlebar.set_sidebar_has_notifications(has_notifications, cx);
});
});
if let Some(this) = this.upgrade() {
this.update(cx, |this, _| {
this._subscriptions.push(subscription);
});
}
});
})
.detach();
}
Self {
platform_titlebar,
application_menu,
@@ -676,41 +627,6 @@ impl TitleBar {
)
}
fn render_workspace_sidebar_toggle(
&self,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<AnyElement> {
if !cx.has_flag::<AgentV2FeatureFlag>() {
return None;
}
let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open();
if is_sidebar_open {
return None;
}
let has_notifications = self.platform_titlebar.read(cx).sidebar_has_notifications();
Some(
IconButton::new("toggle-workspace-sidebar", IconName::WorkspaceNavClosed)
.icon_size(IconSize::Small)
.when(has_notifications, |button| {
button
.indicator(Indicator::dot().color(Color::Accent))
.indicator_border_color(Some(cx.theme().colors().title_bar_background))
})
.tooltip(move |_, cx| {
Tooltip::for_action("Open Workspace Sidebar", &ToggleWorkspaceSidebar, cx)
})
.on_click(|_, window, cx| {
window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
})
.into_any_element(),
)
}
pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
let workspace = self.workspace.clone();
@@ -995,18 +911,16 @@ impl TitleBar {
pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
let client = self.client.clone();
let workspace = self.workspace.clone();
Button::new("sign_in", "Sign In")
.label_size(LabelSize::Small)
.on_click(move |_, window, cx| {
let client = client.clone();
let workspace = workspace.clone();
window
.spawn(cx, async move |mut cx| {
.spawn(cx, async move |cx| {
client
.sign_in_with_optional_connect(true, cx)
.await
.notify_workspace_async_err(workspace, &mut cx);
.notify_async_err(cx);
})
.detach();
})
+16 -40
View File
@@ -1,6 +1,5 @@
use crate::{
DecoratedIcon, DiffStat, HighlightedLabel, IconDecoration, IconDecorationKind, SpinnerLabel,
prelude::*,
Chip, DecoratedIcon, DiffStat, IconDecoration, IconDecorationKind, SpinnerLabel, prelude::*,
};
use gpui::{ClickEvent, SharedString};
@@ -9,7 +8,6 @@ pub struct ThreadItem {
id: ElementId,
icon: IconName,
title: SharedString,
highlight_positions: Vec<usize>,
timestamp: SharedString,
running: bool,
generation_done: bool,
@@ -26,7 +24,6 @@ impl ThreadItem {
id: id.into(),
icon: IconName::ZedAgent,
title: title.into(),
highlight_positions: Vec::new(),
timestamp: "".into(),
running: false,
generation_done: false,
@@ -78,11 +75,6 @@ impl ThreadItem {
self
}
pub fn highlight_positions(mut self, positions: Vec<usize>) -> Self {
self.highlight_positions = positions;
self
}
pub fn on_click(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
@@ -120,17 +112,7 @@ impl RenderOnce for ThreadItem {
agent_icon.into_any_element()
};
// let has_no_changes = self.added.is_none() && self.removed.is_none();
let title = self.title;
let highlight_positions = self.highlight_positions;
let title_label = if highlight_positions.is_empty() {
Label::new(title).truncate().into_any_element()
} else {
HighlightedLabel::new(title, highlight_positions)
.truncate()
.into_any_element()
};
let has_no_changes = self.added.is_none() && self.removed.is_none();
v_flex()
.id(self.id.clone())
@@ -145,7 +127,7 @@ impl RenderOnce for ThreadItem {
.w_full()
.gap_1p5()
.child(icon)
.child(title_label)
.child(Label::new(self.title).truncate())
.when(self.running, |this| {
this.child(icon_container().child(SpinnerLabel::new().color(Color::Accent)))
}),
@@ -155,32 +137,26 @@ impl RenderOnce for ThreadItem {
.gap_1p5()
.child(icon_container()) // Icon Spacing
.when_some(self.worktree, |this, name| {
this.child(Label::new(name).size(LabelSize::Small).color(Color::Muted))
this.child(Chip::new(name).label_size(LabelSize::XSmall))
})
.child(
Label::new(self.timestamp)
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Label::new("•")
.size(LabelSize::Small)
.color(Color::Muted)
.alpha(0.5),
)
.child(
Label::new(self.timestamp)
.size(LabelSize::Small)
.color(Color::Muted),
)
// .child(
// Label::new("•")
// .size(LabelSize::Small)
// .color(Color::Muted)
// .alpha(0.5),
// )
// .when(has_no_changes, |this| {
// this.child(
// Label::new("No Changes")
// .size(LabelSize::Small)
// .color(Color::Muted),
// )
// })
.when(has_no_changes, |this| {
this.child(
Label::new("No Changes")
.size(LabelSize::Small)
.color(Color::Muted),
)
})
.when(self.added.is_some() || self.removed.is_some(), |this| {
this.child(DiffStat::new(
self.id,
-2
View File
@@ -5,7 +5,6 @@ use theme::ActiveTheme;
mod apca_contrast;
mod color_contrast;
mod constants;
mod corner_solver;
mod format_distance;
mod search_input;
@@ -13,7 +12,6 @@ mod with_rem_size;
pub use apca_contrast::*;
pub use color_contrast::*;
pub use constants::*;
pub use corner_solver::{CornerSolver, inner_corner_radius};
pub use format_distance::*;
pub use search_input::*;
-27
View File
@@ -1,27 +0,0 @@
use gpui::{Pixels, Window, px};
// 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.;
/// Returns the platform-appropriate title bar height.
///
/// On Windows, this returns a fixed height of 32px.
/// On other platforms, it scales with the window's rem size (1.75x) with a minimum of 34px.
#[cfg(not(target_os = "windows"))]
pub fn platform_title_bar_height(window: &Window) -> Pixels {
(1.75 * window.rem_size()).max(px(34.))
}
#[cfg(target_os = "windows")]
pub fn platform_title_bar_height(_window: &Window) -> Pixels {
// todo(windows) instead of hard coded size report the actual size to the Windows platform API
px(32.)
}
+16 -16
View File
@@ -318,7 +318,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
}
});
Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| {
let Some(workspace) = vim.workspace(window, cx) else {
let Some(workspace) = vim.workspace(window) else {
return;
};
workspace.update(cx, |workspace, cx| {
@@ -327,7 +327,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
let Some(workspace) = vim.workspace(window, cx) else {
let Some(workspace) = vim.workspace(window) else {
return;
};
workspace.update(cx, |workspace, cx| {
@@ -346,7 +346,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {
let Some(workspace) = vim.workspace(window, cx) else {
let Some(workspace) = vim.workspace(window) else {
return;
};
workspace.update(cx, |workspace, cx| {
@@ -398,7 +398,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
if action.filename.is_empty() {
if whole_buffer {
if let Some(workspace) = vim.workspace(window, cx) {
if let Some(workspace) = vim.workspace(window) {
workspace.update(cx, |workspace, cx| {
workspace
.save_active_item(
@@ -472,7 +472,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
return;
}
if action.filename.is_empty() {
if let Some(workspace) = vim.workspace(window, cx) {
if let Some(workspace) = vim.workspace(window) {
workspace.update(cx, |workspace, cx| {
workspace
.save_active_item(
@@ -549,7 +549,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, action: &VimSplit, window, cx| {
let Some(workspace) = vim.workspace(window, cx) else {
let Some(workspace) = vim.workspace(window) else {
return;
};
@@ -647,7 +647,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| {
vim.update_editor(cx, |vim, editor, cx| {
let Some(workspace) = vim.workspace(window, cx) else {
let Some(workspace) = vim.workspace(window) else {
return;
};
let Some(project) = editor.project().cloned() else {
@@ -814,7 +814,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
}
};
let Some(workspace) = vim.workspace(window, cx) else {
let Some(workspace) = vim.workspace(window) else {
return;
};
let task = workspace.update(cx, |workspace, cx| {
@@ -855,7 +855,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| {
let Some(workspace) = vim.workspace(window, cx) else {
let Some(workspace) = vim.workspace(window) else {
return;
};
let count = Vim::take_count(cx).unwrap_or(1);
@@ -888,7 +888,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
anyhow::Ok(())
});
if let Some(e @ Err(_)) = result {
let Some(workspace) = vim.workspace(window, cx) else {
let Some(workspace) = vim.workspace(window) else {
return;
};
workspace.update(cx, |workspace, cx| {
@@ -932,7 +932,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
let range = match result {
None => return,
Some(e @ Err(_)) => {
let Some(workspace) = vim.workspace(window, cx) else {
let Some(workspace) = vim.workspace(window) else {
return;
};
workspace.update(cx, |workspace, cx| {
@@ -2132,7 +2132,7 @@ impl OnMatchingLines {
let range = match result {
None => return,
Some(e @ Err(_)) => {
let Some(workspace) = vim.workspace(window, cx) else {
let Some(workspace) = vim.workspace(window) else {
return;
};
workspace.update(cx, |workspace, cx| {
@@ -2149,7 +2149,7 @@ impl OnMatchingLines {
let mut regexes = match Regex::new(&self.search) {
Ok(regex) => vec![(regex, !self.invert)],
e @ Err(_) => {
let Some(workspace) = vim.workspace(window, cx) else {
let Some(workspace) = vim.workspace(window) else {
return;
};
workspace.update(cx, |workspace, cx| {
@@ -2347,7 +2347,7 @@ impl Vim {
cx: &mut Context<Vim>,
) {
self.stop_recording(cx);
let Some(workspace) = self.workspace(window, cx) else {
let Some(workspace) = self.workspace(window) else {
return;
};
let command = self.update_editor(cx, |_, editor, cx| {
@@ -2396,7 +2396,7 @@ impl Vim {
cx: &mut Context<Vim>,
) {
self.stop_recording(cx);
let Some(workspace) = self.workspace(window, cx) else {
let Some(workspace) = self.workspace(window) else {
return;
};
let command = self.update_editor(cx, |_, editor, cx| {
@@ -2448,7 +2448,7 @@ impl ShellExec {
}
pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
let Some(workspace) = vim.workspace(window, cx) else {
let Some(workspace) = vim.workspace(window) else {
return;
};
+5 -5
View File
@@ -81,7 +81,7 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace(window, cx) else {
let Some(workspace) = self.workspace(window) else {
return;
};
workspace.update(cx, |workspace, cx| {
@@ -133,7 +133,7 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace(window, cx) else {
let Some(workspace) = self.workspace(window) else {
return;
};
let task = workspace.update(cx, |workspace, cx| {
@@ -272,7 +272,7 @@ impl Vim {
window: &mut Window,
cx: &mut App,
) {
let Some(workspace) = self.workspace(window, cx) else {
let Some(workspace) = self.workspace(window) else {
return;
};
if name == "`" {
@@ -324,7 +324,7 @@ impl Vim {
return Some(Mark::Local(anchors));
}
VimGlobals::update_global(cx, |globals, cx| {
let workspace_id = self.workspace(window, cx)?.entity_id();
let workspace_id = self.workspace(window)?.entity_id();
globals
.marks
.get_mut(&workspace_id)?
@@ -339,7 +339,7 @@ impl Vim {
window: &mut Window,
cx: &mut App,
) {
let Some(workspace) = self.workspace(window, cx) else {
let Some(workspace) = self.workspace(window) else {
return;
};
if name == "`" || name == "'" {
+2 -2
View File
@@ -112,7 +112,7 @@ impl Replayer {
let this = self.clone();
window.defer(cx, move |window, cx| {
this.next(window, cx);
let Some(workspace) = Workspace::for_window(window, cx) else {
let Some(Some(workspace)) = window.root::<Workspace>() else {
return;
};
let Some(editor) = workspace
@@ -165,7 +165,7 @@ impl Replayer {
text,
utf16_range_to_replace,
} => {
let Some(workspace) = Workspace::for_window(window, cx) else {
let Some(Some(workspace)) = window.root::<Workspace>() else {
return;
};
let Some(editor) = workspace
+1 -1
View File
@@ -555,7 +555,7 @@ impl Vim {
let replacement = action.replacement.clone();
let Some(((pane, workspace), editor)) = self
.pane(window, cx)
.zip(self.workspace(window, cx))
.zip(self.workspace(window))
.zip(self.editor())
else {
return;
+7 -11
View File
@@ -36,7 +36,7 @@ use ui::{
use util::ResultExt;
use util::rel_path::RelPath;
use workspace::searchable::Direction;
use workspace::{MultiWorkspace, Workspace, WorkspaceDb, WorkspaceId};
use workspace::{Workspace, WorkspaceDb, WorkspaceId};
#[derive(Clone, Copy, Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum Mode {
@@ -731,16 +731,12 @@ impl VimGlobals {
});
GlobalCommandPaletteInterceptor::set(cx, command_interceptor);
for window in cx.windows() {
if let Some(multi_workspace) = window.downcast::<MultiWorkspace>() {
multi_workspace
.update(cx, |multi_workspace, _, cx| {
for workspace in multi_workspace.workspaces() {
workspace.update(cx, |workspace, cx| {
Vim::update_globals(cx, |globals, cx| {
globals.register_workspace(workspace, cx)
});
});
}
if let Some(workspace) = window.downcast::<Workspace>() {
workspace
.update(cx, |workspace, _, cx| {
Vim::update_globals(cx, |globals, cx| {
globals.register_workspace(workspace, cx)
});
})
.ok();
}
+4 -4
View File
@@ -1003,12 +1003,12 @@ impl Vim {
self.editor.upgrade()
}
pub fn workspace(&self, window: &Window, cx: &App) -> Option<Entity<Workspace>> {
Workspace::for_window(window, cx)
pub fn workspace(&self, window: &mut Window) -> Option<Entity<Workspace>> {
window.root::<Workspace>().flatten()
}
pub fn pane(&self, window: &Window, cx: &Context<Self>) -> Option<Entity<Pane>> {
self.workspace(window, cx)
pub fn pane(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Entity<Pane>> {
self.workspace(window)
.map(|workspace| workspace.read(cx).focused_pane(window, cx))
}
-1
View File
@@ -79,7 +79,6 @@ db = { workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
remote = { workspace = true, features = ["test-support"] }
session = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
+5 -6
View File
@@ -1,6 +1,5 @@
use std::{path::PathBuf, sync::Arc};
use std::path::PathBuf;
use fs::Fs;
use gpui::{AppContext, Entity, Global, MenuItem};
use smallvec::SmallVec;
use ui::{App, Context};
@@ -10,10 +9,10 @@ use crate::{
NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList,
};
pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
pub fn init(cx: &mut App) {
let manager = cx.new(|_| HistoryManager::new());
HistoryManager::set_global(manager.clone(), cx);
HistoryManager::init(manager, fs, cx);
HistoryManager::init(manager, cx);
}
pub struct HistoryManager {
@@ -39,10 +38,10 @@ impl HistoryManager {
}
}
fn init(this: Entity<HistoryManager>, fs: Arc<dyn Fs>, cx: &App) {
fn init(this: Entity<HistoryManager>, cx: &App) {
cx.spawn(async move |cx| {
let recent_folders = WORKSPACE_DB
.recent_workspaces_on_disk(fs.as_ref())
.recent_workspaces_on_disk()
.await
.unwrap_or_default()
.into_iter()
-513
View File
@@ -1,513 +0,0 @@
use anyhow::Result;
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
use gpui::{
AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, Focusable, ManagedView,
MouseButton, Pixels, Render, Subscription, Task, Window, actions, deferred, px,
};
use project::Project;
use std::path::PathBuf;
use ui::prelude::*;
const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
use crate::{
DockPosition, Item, ModalView, Panel, Workspace, WorkspaceId, client_side_decorations,
};
actions!(
multi_workspace,
[
/// Creates a new workspace within the current window.
NewWorkspaceInWindow,
/// Switches to the next workspace within the current window.
NextWorkspaceInWindow,
/// Switches to the previous workspace within the current window.
PreviousWorkspaceInWindow,
/// Toggles the workspace switcher sidebar.
ToggleWorkspaceSidebar,
]
);
pub enum SidebarEvent {
Open,
Close,
}
pub trait Sidebar: EventEmitter<SidebarEvent> + Focusable + Render + Sized {
fn width(&self, cx: &App) -> Pixels;
fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
fn has_notifications(&self, cx: &App) -> bool;
}
pub trait SidebarHandle: 'static + Send + Sync {
fn width(&self, cx: &App) -> Pixels;
fn set_width(&self, width: Option<Pixels>, cx: &mut App);
fn focus(&self, window: &mut Window, cx: &mut App);
fn has_notifications(&self, cx: &App) -> bool;
fn to_any(&self) -> AnyView;
fn entity_id(&self) -> EntityId;
}
#[derive(Clone)]
pub struct DraggedSidebar;
impl Render for DraggedSidebar {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
gpui::Empty
}
}
impl<T: Sidebar> SidebarHandle for Entity<T> {
fn width(&self, cx: &App) -> Pixels {
self.read(cx).width(cx)
}
fn set_width(&self, width: Option<Pixels>, cx: &mut App) {
self.update(cx, |this, cx| this.set_width(width, cx))
}
fn focus(&self, window: &mut Window, cx: &mut App) {
let handle = self.read(cx).focus_handle(cx);
window.focus(&handle, cx);
}
fn has_notifications(&self, cx: &App) -> bool {
self.read(cx).has_notifications(cx)
}
fn to_any(&self) -> AnyView {
self.clone().into()
}
fn entity_id(&self) -> EntityId {
Entity::entity_id(self)
}
}
pub struct MultiWorkspace {
workspaces: Vec<Entity<Workspace>>,
active_workspace_index: usize,
sidebar: Option<Box<dyn SidebarHandle>>,
sidebar_open: bool,
_sidebar_subscription: Option<Subscription>,
}
impl MultiWorkspace {
pub fn new(workspace: Entity<Workspace>, _cx: &mut Context<Self>) -> Self {
Self {
workspaces: vec![workspace],
active_workspace_index: 0,
sidebar: None,
sidebar_open: false,
_sidebar_subscription: None,
}
}
pub fn register_sidebar<T: Sidebar>(
&mut self,
sidebar: Entity<T>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let subscription =
cx.subscribe_in(&sidebar, window, |this, _, event, window, cx| match event {
SidebarEvent::Open => this.toggle_sidebar(window, cx),
SidebarEvent::Close => {
this.close_sidebar(window, cx);
}
});
self.sidebar = Some(Box::new(sidebar));
self._sidebar_subscription = Some(subscription);
}
pub fn sidebar(&self) -> Option<&dyn SidebarHandle> {
self.sidebar.as_deref()
}
pub fn sidebar_open(&self) -> bool {
self.sidebar_open && self.sidebar.is_some()
}
pub fn sidebar_has_notifications(&self, cx: &App) -> bool {
self.sidebar
.as_ref()
.map_or(false, |s| s.has_notifications(cx))
}
pub(crate) fn multi_workspace_enabled(&self, cx: &App) -> bool {
cx.has_flag::<AgentV2FeatureFlag>()
}
pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.multi_workspace_enabled(cx) {
return;
}
if self.sidebar_open {
self.close_sidebar(window, cx);
let pane = self.workspace().read(cx).active_pane().clone();
window.focus(&pane.read(cx).focus_handle(cx), cx);
} else {
self.open_sidebar(window, cx);
if let Some(sidebar) = &self.sidebar {
sidebar.focus(window, cx);
}
}
}
pub fn open_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.sidebar_open = true;
self.serialize(window, cx);
cx.notify();
}
fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.sidebar_open = false;
self.serialize(window, cx);
cx.notify();
}
pub fn is_sidebar_open(&self) -> bool {
self.sidebar_open
}
pub fn workspace(&self) -> &Entity<Workspace> {
&self.workspaces[self.active_workspace_index]
}
pub fn workspaces(&self) -> &[Entity<Workspace>] {
&self.workspaces
}
pub fn active_workspace_index(&self) -> usize {
self.active_workspace_index
}
pub fn activate(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) {
if !self.multi_workspace_enabled(cx) {
self.workspaces[0] = workspace;
self.active_workspace_index = 0;
cx.notify();
return;
}
let index = self.add_workspace(workspace, cx);
if self.active_workspace_index != index {
self.active_workspace_index = index;
cx.notify();
}
}
/// Adds a workspace to this window without changing which workspace is active.
/// Returns the index of the workspace (existing or newly inserted).
pub fn add_workspace(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
index
} else {
self.workspaces.push(workspace);
cx.notify();
self.workspaces.len() - 1
}
}
pub fn activate_index(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
debug_assert!(
index < self.workspaces.len(),
"workspace index out of bounds"
);
self.active_workspace_index = index;
self.serialize(window, cx);
self.focus_active_workspace(window, cx);
cx.notify();
}
pub fn activate_next_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.workspaces.len() > 1 {
let next_index = (self.active_workspace_index + 1) % self.workspaces.len();
self.activate_index(next_index, window, cx);
}
}
pub fn activate_previous_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.workspaces.len() > 1 {
let prev_index = if self.active_workspace_index == 0 {
self.workspaces.len() - 1
} else {
self.active_workspace_index - 1
};
self.activate_index(prev_index, window, cx);
}
}
fn serialize(&self, window: &mut Window, cx: &mut App) {
let window_id = window.window_handle().window_id();
let state = crate::persistence::model::MultiWorkspaceState {
active_workspace_id: self.workspace().read(cx).database_id(),
sidebar_open: self.sidebar_open,
};
cx.background_spawn(async move {
crate::persistence::write_multi_workspace_state(window_id, state).await;
})
.detach();
}
fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
let pane = self.workspace().read(cx).active_pane().clone();
let focus_handle = pane.read(cx).focus_handle(cx);
window.focus(&focus_handle, cx);
}
pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
self.workspace().read(cx).panel::<T>(cx)
}
pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
self.workspace().read(cx).active_modal::<V>(cx)
}
pub fn add_panel<T: Panel>(
&mut self,
panel: Entity<T>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.workspace().update(cx, |workspace, cx| {
workspace.add_panel(panel, window, cx);
});
}
pub fn focus_panel<T: Panel>(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<T>> {
self.workspace()
.update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
}
pub fn toggle_modal<V: ModalView, B>(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
build: B,
) where
B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
{
self.workspace().update(cx, |workspace, cx| {
workspace.toggle_modal(window, cx, build);
});
}
pub fn toggle_dock(
&mut self,
dock_side: DockPosition,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.workspace().update(cx, |workspace, cx| {
workspace.toggle_dock(dock_side, window, cx);
});
}
pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
self.workspace().read(cx).active_item_as::<I>(cx)
}
pub fn items_of_type<'a, T: Item>(
&'a self,
cx: &'a App,
) -> impl 'a + Iterator<Item = Entity<T>> {
self.workspace().read(cx).items_of_type::<T>(cx)
}
pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
self.workspace().read(cx).database_id()
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
self.workspace().update(cx, |workspace, _cx| {
workspace.set_random_database_id();
});
}
#[cfg(any(test, feature = "test-support"))]
pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
Self::new(workspace, cx)
}
pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.multi_workspace_enabled(cx) {
return;
}
let app_state = self.workspace().read(cx).app_state().clone();
let project = Project::local(
app_state.client.clone(),
app_state.node_runtime.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
None,
project::LocalProjectFlags::default(),
cx,
);
let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
self.activate(new_workspace, cx);
self.focus_active_workspace(window, cx);
}
pub fn remove_workspace(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
if self.workspaces.len() <= 1 || index >= self.workspaces.len() {
return;
}
self.workspaces.remove(index);
if self.active_workspace_index >= self.workspaces.len() {
self.active_workspace_index = self.workspaces.len() - 1;
} else if self.active_workspace_index > index {
self.active_workspace_index -= 1;
}
self.focus_active_workspace(window, cx);
cx.notify();
}
pub fn open_project(
&mut self,
paths: Vec<PathBuf>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let workspace = self.workspace().clone();
if self.multi_workspace_enabled(cx) {
workspace.update(cx, |workspace, cx| {
workspace.open_workspace_for_paths(true, paths, window, cx)
})
} else {
cx.spawn_in(window, async move |_this, cx| {
let should_continue = workspace
.update_in(cx, |workspace, window, cx| {
workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
})?
.await?;
if should_continue {
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_workspace_for_paths(true, paths, window, cx)
})?
.await
} else {
Ok(())
}
})
}
}
}
impl Render for MultiWorkspace {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let multi_workspace_enabled = self.multi_workspace_enabled(cx);
let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open {
self.sidebar.as_ref().map(|sidebar_handle| {
let weak = cx.weak_entity();
let sidebar_width = sidebar_handle.width(cx);
let resize_handle = deferred(
div()
.id("sidebar-resize-handle")
.absolute()
.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
.top(px(0.))
.h_full()
.w(SIDEBAR_RESIZE_HANDLE_SIZE)
.cursor_col_resize()
.on_drag(DraggedSidebar, |dragged, _, _, cx| {
cx.stop_propagation();
cx.new(|_| dragged.clone())
})
.on_mouse_down(MouseButton::Left, |_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(MouseButton::Left, move |event, _, cx| {
if event.click_count == 2 {
weak.update(cx, |this, cx| {
if let Some(sidebar) = this.sidebar.as_mut() {
sidebar.set_width(None, cx);
}
})
.ok();
cx.stop_propagation();
}
})
.occlude(),
);
div()
.id("sidebar-container")
.relative()
.h_full()
.w(sidebar_width)
.flex_shrink_0()
.child(sidebar_handle.to_any())
.child(resize_handle)
.into_any_element()
})
} else {
None
};
client_side_decorations(
h_flex()
.key_context("Workspace")
.size_full()
.on_action(
cx.listener(|this: &mut Self, _: &NewWorkspaceInWindow, window, cx| {
this.create_workspace(window, cx);
}),
)
.on_action(
cx.listener(|this: &mut Self, _: &NextWorkspaceInWindow, window, cx| {
this.activate_next_workspace(window, cx);
}),
)
.on_action(cx.listener(
|this: &mut Self, _: &PreviousWorkspaceInWindow, window, cx| {
this.activate_previous_workspace(window, cx);
},
))
.on_action(cx.listener(
|this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
this.toggle_sidebar(window, cx);
},
))
.when(
self.sidebar_open() && self.multi_workspace_enabled(cx),
|this| {
this.on_drag_move(cx.listener(
|this: &mut Self, e: &DragMoveEvent<DraggedSidebar>, _window, cx| {
if let Some(sidebar) = &this.sidebar {
let new_width = e.event.position.x;
sidebar.set_width(Some(new_width), cx);
}
},
))
.children(sidebar)
},
)
.child(
div()
.flex()
.flex_1()
.size_full()
.overflow_hidden()
.child(self.workspace().clone()),
),
window,
cx,
)
}
}
+26 -51
View File
@@ -1,9 +1,9 @@
use crate::{MultiWorkspace, SuppressNotification, Toast, Workspace};
use crate::{SuppressNotification, Toast, Workspace};
use anyhow::Context as _;
use gpui::{
AnyEntity, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, ClickEvent, Context,
AnyEntity, AnyView, App, AppContext as _, AsyncWindowContext, ClickEvent, Context,
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle,
Task, TextStyleRefinement, UnderlineStyle, WeakEntity, svg,
Task, TextStyleRefinement, UnderlineStyle, svg,
};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use parking_lot::Mutex;
@@ -1037,18 +1037,14 @@ pub fn show_app_notification<V: Notification + 'static>(
.insert(id.clone(), build_notification.clone());
for window in cx.windows() {
if let Some(multi_workspace) = window.downcast::<MultiWorkspace>() {
multi_workspace
.update(cx, |multi_workspace, _window, cx| {
for workspace in multi_workspace.workspaces() {
workspace.update(cx, |workspace, cx| {
workspace.show_notification_without_handling_dismiss_events(
&id,
cx,
|cx| build_notification(cx),
);
});
}
if let Some(workspace_window) = window.downcast::<Workspace>() {
workspace_window
.update(cx, |workspace, _window, cx| {
workspace.show_notification_without_handling_dismiss_events(
&id,
cx,
|cx| build_notification(cx),
);
})
.ok(); // Doesn't matter if the windows are dropped
}
@@ -1062,15 +1058,11 @@ pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) {
cx.defer(move |cx| {
GLOBAL_APP_NOTIFICATIONS.lock().remove(&id);
for window in cx.windows() {
if let Some(multi_workspace) = window.downcast::<MultiWorkspace>() {
if let Some(workspace_window) = window.downcast::<Workspace>() {
let id = id.clone();
multi_workspace
.update(cx, |multi_workspace, _window, cx| {
for workspace in multi_workspace.workspaces() {
workspace.update(cx, |workspace, cx| {
workspace.dismiss_notification(&id, cx)
});
}
workspace_window
.update(cx, |workspace, _window, cx| {
workspace.dismiss_notification(&id, cx)
})
.ok();
}
@@ -1084,11 +1076,7 @@ pub trait NotifyResultExt {
fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>)
-> Option<Self::Ok>;
fn notify_workspace_async_err(
self,
workspace: WeakEntity<Workspace>,
cx: &mut AsyncApp,
) -> Option<Self::Ok>;
fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
/// Notifies the active workspace if there is one, otherwise notifies all workspaces.
fn notify_app_err(self, cx: &mut App) -> Option<Self::Ok>;
@@ -1111,18 +1099,17 @@ where
}
}
fn notify_workspace_async_err(
self,
workspace: WeakEntity<Workspace>,
cx: &mut AsyncApp,
) -> Option<T> {
fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
match self {
Ok(value) => Some(value),
Err(err) => {
log::error!("{err:?}");
workspace
.update(cx, |workspace, cx| workspace.show_error(&err, cx))
.ok();
cx.update_root(|view, _, cx| {
if let Ok(workspace) = view.downcast::<Workspace>() {
workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
}
})
.ok();
None
}
}
@@ -1150,12 +1137,7 @@ where
}
pub trait NotifyTaskExt {
fn detach_and_notify_err(
self,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
);
fn detach_and_notify_err(self, window: &mut Window, cx: &mut App);
}
impl<R, E> NotifyTaskExt for Task<std::result::Result<R, E>>
@@ -1163,16 +1145,9 @@ where
E: std::fmt::Debug + std::fmt::Display + Sized + 'static,
R: 'static,
{
fn detach_and_notify_err(
self,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) {
fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) {
window
.spawn(cx, async move |mut cx| {
self.await.notify_workspace_async_err(workspace, &mut cx)
})
.spawn(cx, async move |cx| self.await.notify_async_err(cx))
.detach();
}
}
+3 -4
View File
@@ -3881,10 +3881,9 @@ impl Pane {
.path_for_entry(project_entry_id, cx)
{
let load_path_task = workspace.load_path(project_path.clone(), window, cx);
cx.spawn_in(window, async move |workspace, mut cx| {
if let Some((project_entry_id, build_item)) = load_path_task
.await
.notify_workspace_async_err(workspace.clone(), &mut cx)
cx.spawn_in(window, async move |workspace, cx| {
if let Some((project_entry_id, build_item)) =
load_path_task.await.notify_async_err(cx)
{
let (to_pane, new_item_handle) = workspace
.update_in(cx, |workspace, window, cx| {
+87 -370
View File
@@ -8,8 +8,6 @@ use std::{
sync::Arc,
};
use fs::Fs;
use anyhow::{Context as _, Result, bail};
use collections::{HashMap, HashSet, IndexSet};
use db::{
@@ -50,7 +48,7 @@ use model::{
SerializedPaneGroup, SerializedWorkspace,
};
use self::model::{DockStructure, SerializedWorkspaceLocation, SessionWorkspace};
use self::model::{DockStructure, SerializedWorkspaceLocation};
// https://www.sqlite.org/limits.html
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
@@ -283,64 +281,6 @@ impl From<WindowBoundsJson> for WindowBounds {
}
}
fn multi_workspace_states() -> db::kvp::ScopedKeyValueStore<'static> {
KEY_VALUE_STORE.scoped("multi_workspace_state")
}
fn read_multi_workspace_state(window_id: WindowId) -> model::MultiWorkspaceState {
multi_workspace_states()
.read(&window_id.as_u64().to_string())
.log_err()
.flatten()
.and_then(|json| serde_json::from_str(&json).ok())
.unwrap_or_default()
}
pub async fn write_multi_workspace_state(window_id: WindowId, state: model::MultiWorkspaceState) {
if let Ok(json_str) = serde_json::to_string(&state) {
multi_workspace_states()
.write(window_id.as_u64().to_string(), json_str)
.await
.log_err();
}
}
pub fn read_serialized_multi_workspaces(
session_workspaces: Vec<model::SessionWorkspace>,
) -> Vec<model::SerializedMultiWorkspace> {
let mut window_groups: Vec<Vec<model::SessionWorkspace>> = Vec::new();
let mut window_id_to_group: HashMap<WindowId, usize> = HashMap::default();
for session_workspace in session_workspaces {
match session_workspace.window_id {
Some(window_id) => {
let group_index = *window_id_to_group.entry(window_id).or_insert_with(|| {
window_groups.push(Vec::new());
window_groups.len() - 1
});
window_groups[group_index].push(session_workspace);
}
None => {
window_groups.push(vec![session_workspace]);
}
}
}
window_groups
.into_iter()
.map(|group| {
let window_id = group.first().and_then(|sw| sw.window_id);
let state = window_id
.map(read_multi_workspace_state)
.unwrap_or_default();
model::SerializedMultiWorkspace {
workspaces: group,
state,
}
})
.collect()
}
const DEFAULT_DOCK_STATE_KEY: &str = "default_dock_state";
pub fn read_default_dock_state() -> Option<DockStructure> {
@@ -1768,26 +1708,10 @@ impl WorkspaceDb {
}
}
async fn all_paths_exist_with_a_directory(paths: &[PathBuf], fs: &dyn Fs) -> bool {
let mut any_dir = false;
for path in paths {
match fs.metadata(path).await.ok().flatten() {
None => return false,
Some(meta) => {
if meta.is_dir {
any_dir = true;
}
}
}
}
any_dir
}
// Returns the recent locations which are still valid on disk and deletes ones which no longer
// exist.
pub async fn recent_workspaces_on_disk(
&self,
fs: &dyn Fs,
) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
let mut result = Vec::new();
let mut delete_tasks = Vec::new();
@@ -1820,8 +1744,11 @@ impl WorkspaceDb {
// If a local workspace points to WSL, this check will cause us to wait for the
// WSL VM and file server to boot up. This can block for many seconds.
// Supported scenarios use remote workspaces.
if !has_wsl_path && Self::all_paths_exist_with_a_directory(paths.paths(), fs).await {
result.push((id, SerializedWorkspaceLocation::Local, paths));
if !has_wsl_path && paths.paths().iter().all(|path| path.exists()) {
// Only show directories in recent projects
if paths.paths().iter().any(|path| path.is_dir()) {
result.push((id, SerializedWorkspaceLocation::Local, paths));
}
} else {
delete_tasks.push(self.delete_workspace_by_id(id));
}
@@ -1833,67 +1760,65 @@ impl WorkspaceDb {
pub async fn last_workspace(
&self,
fs: &dyn Fs,
) -> Result<Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
Ok(self.recent_workspaces_on_disk(fs).await?.into_iter().next())
Ok(self.recent_workspaces_on_disk().await?.into_iter().next())
}
// Returns the locations of the workspaces that were still opened when the last
// session was closed (i.e. when Zed was quit).
// If `last_session_window_order` is provided, the returned locations are ordered
// according to that.
pub async fn last_session_workspace_locations(
pub fn last_session_workspace_locations(
&self,
last_session_id: &str,
last_session_window_stack: Option<Vec<WindowId>>,
fs: &dyn Fs,
) -> Result<Vec<SessionWorkspace>> {
) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
let mut workspaces = Vec::new();
for (workspace_id, paths, window_id, remote_connection_id) in
self.session_workspaces(last_session_id.to_owned())?
{
let window_id = window_id.map(WindowId::from);
if let Some(remote_connection_id) = remote_connection_id {
workspaces.push(SessionWorkspace {
workspaces.push((
workspace_id,
location: SerializedWorkspaceLocation::Remote(
SerializedWorkspaceLocation::Remote(
self.remote_connection(remote_connection_id)?,
),
paths,
window_id,
});
window_id.map(WindowId::from),
));
} else if paths.is_empty() {
// Empty workspace with items (drafts, files) - include for restoration
workspaces.push(SessionWorkspace {
workspaces.push((
workspace_id,
location: SerializedWorkspaceLocation::Local,
SerializedWorkspaceLocation::Local,
paths,
window_id,
});
} else {
if Self::all_paths_exist_with_a_directory(paths.paths(), fs).await {
workspaces.push(SessionWorkspace {
workspace_id,
location: SerializedWorkspaceLocation::Local,
paths,
window_id,
});
}
window_id.map(WindowId::from),
));
} else if paths.paths().iter().all(|path| path.exists())
&& paths.paths().iter().any(|path| path.is_dir())
{
workspaces.push((
workspace_id,
SerializedWorkspaceLocation::Local,
paths,
window_id.map(WindowId::from),
));
}
}
if let Some(stack) = last_session_window_stack {
workspaces.sort_by_key(|workspace| {
workspace
.window_id
workspaces.sort_by_key(|(_, _, _, window_id)| {
window_id
.and_then(|id| stack.iter().position(|&order_id| order_id == id))
.unwrap_or(usize::MAX)
});
}
Ok(workspaces)
Ok(workspaces
.into_iter()
.map(|(workspace_id, location, paths, _)| (workspace_id, location, paths))
.collect::<Vec<_>>())
}
fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
@@ -2347,12 +2272,11 @@ pub fn delete_unloaded_items(
mod tests {
use super::*;
use crate::persistence::model::{
SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, SessionWorkspace,
SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
};
use gpui;
use pretty_assertions::assert_eq;
use remote::SshConnectionOptions;
use serde_json::json;
use std::{thread, time::Duration};
#[gpui::test]
@@ -3116,18 +3040,12 @@ mod tests {
}
#[gpui::test]
async fn test_last_session_workspace_locations(cx: &mut gpui::TestAppContext) {
async fn test_last_session_workspace_locations() {
let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
let fs = fs::FakeFs::new(cx.executor());
fs.insert_tree(dir1.path(), json!({})).await;
fs.insert_tree(dir2.path(), json!({})).await;
fs.insert_tree(dir3.path(), json!({})).await;
fs.insert_tree(dir4.path(), json!({})).await;
let db =
WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
@@ -3170,55 +3088,47 @@ mod tests {
]));
let locations = db
.last_session_workspace_locations("one-session", stack, fs.as_ref())
.await
.last_session_workspace_locations("one-session", stack)
.unwrap();
assert_eq!(
locations,
[
SessionWorkspace {
workspace_id: WorkspaceId(4),
location: SerializedWorkspaceLocation::Local,
paths: PathList::new(&[dir4.path()]),
window_id: Some(WindowId::from(2u64)),
},
SessionWorkspace {
workspace_id: WorkspaceId(3),
location: SerializedWorkspaceLocation::Local,
paths: PathList::new(&[dir3.path()]),
window_id: Some(WindowId::from(8u64)),
},
SessionWorkspace {
workspace_id: WorkspaceId(2),
location: SerializedWorkspaceLocation::Local,
paths: PathList::new(&[dir2.path()]),
window_id: Some(WindowId::from(5u64)),
},
SessionWorkspace {
workspace_id: WorkspaceId(1),
location: SerializedWorkspaceLocation::Local,
paths: PathList::new(&[dir1.path()]),
window_id: Some(WindowId::from(9u64)),
},
SessionWorkspace {
workspace_id: WorkspaceId(5),
location: SerializedWorkspaceLocation::Local,
paths: PathList::new(&[dir1.path(), dir2.path(), dir3.path()]),
window_id: Some(WindowId::from(3u64)),
},
SessionWorkspace {
workspace_id: WorkspaceId(6),
location: SerializedWorkspaceLocation::Local,
paths: PathList::new(&[dir4.path(), dir3.path(), dir2.path()]),
window_id: Some(WindowId::from(4u64)),
},
(
WorkspaceId(4),
SerializedWorkspaceLocation::Local,
PathList::new(&[dir4.path()])
),
(
WorkspaceId(3),
SerializedWorkspaceLocation::Local,
PathList::new(&[dir3.path()])
),
(
WorkspaceId(2),
SerializedWorkspaceLocation::Local,
PathList::new(&[dir2.path()])
),
(
WorkspaceId(1),
SerializedWorkspaceLocation::Local,
PathList::new(&[dir1.path()])
),
(
WorkspaceId(5),
SerializedWorkspaceLocation::Local,
PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
),
(
WorkspaceId(6),
SerializedWorkspaceLocation::Local,
PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
),
]
);
}
#[gpui::test]
async fn test_last_session_workspace_locations_remote(cx: &mut gpui::TestAppContext) {
let fs = fs::FakeFs::new(cx.executor());
async fn test_last_session_workspace_locations_remote() {
let db =
WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
.await;
@@ -3280,45 +3190,40 @@ mod tests {
]));
let have = db
.last_session_workspace_locations("one-session", stack, fs.as_ref())
.await
.last_session_workspace_locations("one-session", stack)
.unwrap();
assert_eq!(have.len(), 4);
assert_eq!(
have[0],
SessionWorkspace {
workspace_id: WorkspaceId(4),
location: SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
paths: PathList::default(),
window_id: Some(WindowId::from(2u64)),
}
(
WorkspaceId(4),
SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
PathList::default()
)
);
assert_eq!(
have[1],
SessionWorkspace {
workspace_id: WorkspaceId(3),
location: SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
paths: PathList::default(),
window_id: Some(WindowId::from(8u64)),
}
(
WorkspaceId(3),
SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
PathList::default()
)
);
assert_eq!(
have[2],
SessionWorkspace {
workspace_id: WorkspaceId(2),
location: SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
paths: PathList::default(),
window_id: Some(WindowId::from(5u64)),
}
(
WorkspaceId(2),
SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
PathList::default()
)
);
assert_eq!(
have[3],
SessionWorkspace {
workspace_id: WorkspaceId(1),
location: SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
paths: PathList::default(),
window_id: Some(WindowId::from(9u64)),
}
(
WorkspaceId(1),
SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
PathList::default()
)
);
}
@@ -3650,192 +3555,4 @@ mod tests {
assert!(retrieved.display.is_some());
assert_eq!(retrieved.display.unwrap(), display_uuid);
}
#[gpui::test]
async fn test_last_session_workspace_locations_groups_by_window_id(
cx: &mut gpui::TestAppContext,
) {
let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
let dir5 = tempfile::TempDir::with_prefix("dir5").unwrap();
let fs = fs::FakeFs::new(cx.executor());
fs.insert_tree(dir1.path(), json!({})).await;
fs.insert_tree(dir2.path(), json!({})).await;
fs.insert_tree(dir3.path(), json!({})).await;
fs.insert_tree(dir4.path(), json!({})).await;
fs.insert_tree(dir5.path(), json!({})).await;
let db =
WorkspaceDb::open_test_db("test_last_session_workspace_locations_groups_by_window_id")
.await;
// Simulate two MultiWorkspace windows each containing two workspaces,
// plus one single-workspace window:
// Window 10: workspace 1, workspace 2
// Window 20: workspace 3, workspace 4
// Window 30: workspace 5 (only one)
//
// On session restore, the caller should be able to group these by
// window_id to reconstruct the MultiWorkspace windows.
let workspaces_data: Vec<(i64, &Path, u64)> = vec![
(1, dir1.path(), 10),
(2, dir2.path(), 10),
(3, dir3.path(), 20),
(4, dir4.path(), 20),
(5, dir5.path(), 30),
];
for (id, dir, window_id) in &workspaces_data {
db.save_workspace(SerializedWorkspace {
id: WorkspaceId(*id),
paths: PathList::new(&[*dir]),
location: SerializedWorkspaceLocation::Local,
center_group: Default::default(),
window_bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
centered_layout: false,
session_id: Some("test-session".to_owned()),
breakpoints: Default::default(),
window_id: Some(*window_id),
user_toolchains: Default::default(),
})
.await;
}
let locations = db
.last_session_workspace_locations("test-session", None, fs.as_ref())
.await
.unwrap();
// All 5 workspaces should be returned with their window_ids.
assert_eq!(locations.len(), 5);
// Every entry should have a window_id so the caller can group them.
for session_workspace in &locations {
assert!(
session_workspace.window_id.is_some(),
"workspace {:?} missing window_id",
session_workspace.workspace_id
);
}
// Group by window_id, simulating what the restoration code should do.
let mut by_window: HashMap<WindowId, Vec<WorkspaceId>> = HashMap::default();
for session_workspace in &locations {
if let Some(window_id) = session_workspace.window_id {
by_window
.entry(window_id)
.or_default()
.push(session_workspace.workspace_id);
}
}
// Should produce 3 windows, not 5.
assert_eq!(
by_window.len(),
3,
"Expected 3 window groups, got {}: {:?}",
by_window.len(),
by_window
);
// Window 10 should contain workspaces 1 and 2.
let window_10 = by_window.get(&WindowId::from(10u64)).unwrap();
assert_eq!(window_10.len(), 2);
assert!(window_10.contains(&WorkspaceId(1)));
assert!(window_10.contains(&WorkspaceId(2)));
// Window 20 should contain workspaces 3 and 4.
let window_20 = by_window.get(&WindowId::from(20u64)).unwrap();
assert_eq!(window_20.len(), 2);
assert!(window_20.contains(&WorkspaceId(3)));
assert!(window_20.contains(&WorkspaceId(4)));
// Window 30 should contain only workspace 5.
let window_30 = by_window.get(&WindowId::from(30u64)).unwrap();
assert_eq!(window_30.len(), 1);
assert!(window_30.contains(&WorkspaceId(5)));
}
#[gpui::test]
async fn test_read_serialized_multi_workspaces_with_state() {
use crate::persistence::model::MultiWorkspaceState;
// Write multi-workspace state for two windows via the scoped KVP.
let window_10 = WindowId::from(10u64);
let window_20 = WindowId::from(20u64);
write_multi_workspace_state(
window_10,
MultiWorkspaceState {
active_workspace_id: Some(WorkspaceId(2)),
sidebar_open: true,
},
)
.await;
write_multi_workspace_state(
window_20,
MultiWorkspaceState {
active_workspace_id: Some(WorkspaceId(3)),
sidebar_open: false,
},
)
.await;
// Build session workspaces: two in window 10, one in window 20, one with no window.
let session_workspaces = vec![
SessionWorkspace {
workspace_id: WorkspaceId(1),
location: SerializedWorkspaceLocation::Local,
paths: PathList::new(&["/a"]),
window_id: Some(window_10),
},
SessionWorkspace {
workspace_id: WorkspaceId(2),
location: SerializedWorkspaceLocation::Local,
paths: PathList::new(&["/b"]),
window_id: Some(window_10),
},
SessionWorkspace {
workspace_id: WorkspaceId(3),
location: SerializedWorkspaceLocation::Local,
paths: PathList::new(&["/c"]),
window_id: Some(window_20),
},
SessionWorkspace {
workspace_id: WorkspaceId(4),
location: SerializedWorkspaceLocation::Local,
paths: PathList::new(&["/d"]),
window_id: None,
},
];
let results = read_serialized_multi_workspaces(session_workspaces);
// Should produce 3 groups: window 10, window 20, and the orphan.
assert_eq!(results.len(), 3);
// Window 10 group: 2 workspaces, active_workspace_id = 2, sidebar open.
let group_10 = &results[0];
assert_eq!(group_10.workspaces.len(), 2);
assert_eq!(group_10.state.active_workspace_id, Some(WorkspaceId(2)));
assert_eq!(group_10.state.sidebar_open, true);
// Window 20 group: 1 workspace, active_workspace_id = 3, sidebar closed.
let group_20 = &results[1];
assert_eq!(group_20.workspaces.len(), 1);
assert_eq!(group_20.state.active_workspace_id, Some(WorkspaceId(3)));
assert_eq!(group_20.state.sidebar_open, false);
// Orphan group: no window_id, so state is default.
let group_none = &results[2];
assert_eq!(group_none.workspaces.len(), 1);
assert_eq!(group_none.state.active_workspace_id, None);
assert_eq!(group_none.state.sidebar_open, false);
}
}
+1 -27
View File
@@ -10,7 +10,7 @@ use db::sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use gpui::{AsyncWindowContext, Entity, WeakEntity, WindowId};
use gpui::{AsyncWindowContext, Entity, WeakEntity};
use language::{Toolchain, ToolchainScope};
use project::{Project, debugger::breakpoint_store::SourceBreakpoint};
@@ -49,32 +49,6 @@ impl SerializedWorkspaceLocation {
}
}
/// A workspace entry from a previous session, containing all the info needed
/// to restore it including which window it belonged to (for MultiWorkspace grouping).
#[derive(Debug, PartialEq, Clone)]
pub struct SessionWorkspace {
pub workspace_id: WorkspaceId,
pub location: SerializedWorkspaceLocation,
pub paths: PathList,
pub window_id: Option<WindowId>,
}
/// Per-window state for a MultiWorkspace, persisted to KVP.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct MultiWorkspaceState {
pub active_workspace_id: Option<WorkspaceId>,
pub sidebar_open: bool,
}
/// The serialized state of a single MultiWorkspace window from a previous session:
/// all workspaces that shared the window, which one was active, and whether the
/// sidebar was open.
#[derive(Debug, Clone)]
pub struct SerializedMultiWorkspace {
pub workspaces: Vec<SessionWorkspace>,
pub state: MultiWorkspaceState,
}
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct SerializedWorkspace {
pub(crate) id: WorkspaceId,
+11 -14
View File
@@ -114,9 +114,7 @@ impl RenderOnce for SectionButton {
.size(rems_from_px(12.)),
),
)
.on_click(move |_, window, cx| {
self.focus_handle.dispatch_action(&*self.action, window, cx)
})
.on_click(move |_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx))
}
}
@@ -227,13 +225,9 @@ impl WelcomePage {
.detach();
if fallback_to_recent_projects {
let fs = workspace
.upgrade()
.map(|ws| ws.read(cx).app_state().fs.clone());
cx.spawn_in(window, async move |this: WeakEntity<Self>, cx| {
let Some(fs) = fs else { return };
let workspaces = WORKSPACE_DB
.recent_workspaces_on_disk(fs.as_ref())
.recent_workspaces_on_disk()
.await
.log_err()
.unwrap_or_default();
@@ -273,18 +267,21 @@ impl WelcomePage {
) {
if let Some(recent_workspaces) = &self.recent_workspaces {
if let Some((_workspace_id, location, paths)) = recent_workspaces.get(action.index) {
let paths = paths.clone();
let location = location.clone();
let is_local = matches!(location, SerializedWorkspaceLocation::Local);
let workspace = self.workspace.clone();
if is_local {
let paths = paths.clone();
let paths = paths.paths().to_vec();
self.workspace
.update(cx, |workspace, cx| {
cx.spawn_in(window, async move |_, cx| {
let _ = workspace.update_in(cx, |workspace, window, cx| {
workspace
.open_workspace_for_paths(true, paths, window, cx)
.detach_and_log_err(cx);
})
.log_err();
.detach();
});
})
.detach();
} else {
use zed_actions::OpenRecent;
window.dispatch_action(OpenRecent::default().boxed_clone(), cx);
File diff suppressed because it is too large. Load diff
-2
View File
@@ -49,7 +49,6 @@ visual-tests = [
"language_model/test-support",
"fs/test-support",
"recent_projects/test-support",
"sidebar/test-support",
"title_bar/test-support",
]
@@ -188,7 +187,6 @@ settings.workspace = true
settings_profile_selector.workspace = true
settings_ui.workspace = true
shellexpand.workspace = true
sidebar.workspace = true
smol.workspace = true
snippet_provider.workspace = true
snippets_ui.workspace = true
+135 -155
View File
@@ -54,8 +54,8 @@ use theme::{ActiveTheme, GlobalTheme, ThemeRegistry};
use util::{ResultExt, TryFutureExt, maybe};
use uuid::Uuid;
use workspace::{
AppState, MultiWorkspace, SerializedWorkspaceLocation, SessionWorkspace, Toast,
WorkspaceSettings, WorkspaceStore, notifications::NotificationId, restore_multiworkspace,
AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceId,
WorkspaceSettings, WorkspaceStore, notifications::NotificationId,
};
use zed::{
OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options,
@@ -511,13 +511,15 @@ fn main() {
let workspace_store = workspace_store.clone();
Arc::new(move |cx: &mut App| {
workspace_store.update(cx, |workspace_store, cx| {
Ok(workspace_store
workspace_store
.workspaces()
.filter_map(|weak| weak.upgrade())
.map(|workspace: gpui::Entity<workspace::Workspace>| {
workspace.read(cx).project().read(cx).lsp_store()
.iter()
.map(|workspace| {
workspace.update(cx, |workspace, _, cx| {
workspace.project().read(cx).lsp_store()
})
})
.collect())
.collect()
})
})
}),
@@ -847,7 +849,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
OpenRequestKind::Extension { extension_id } => {
cx.spawn(async move |cx| {
let workspace =
workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?;
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
workspace.update(cx, |_, window, cx| {
window.dispatch_action(
Box::new(zed_actions::Extensions {
@@ -862,40 +864,31 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
}
OpenRequestKind::AgentPanel { initial_prompt } => {
cx.spawn(async move |cx| {
let multi_workspace =
workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?;
multi_workspace.update(cx, |multi_workspace, window, cx| {
multi_workspace.workspace().update(cx, |workspace, cx| {
if let Some(panel) = workspace.focus_panel::<AgentPanel>(window, cx) {
panel.update(cx, |panel, cx| {
panel.new_external_thread_with_text(initial_prompt, window, cx);
});
}
});
let workspace =
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
workspace.update(cx, |workspace, window, cx| {
if let Some(panel) = workspace.focus_panel::<AgentPanel>(window, cx) {
panel.update(cx, |panel, cx| {
panel.new_external_thread_with_text(initial_prompt, window, cx);
});
}
})
})
.detach_and_log_err(cx);
}
OpenRequestKind::SharedAgentThread { session_id } => {
cx.spawn(async move |cx| {
let multi_workspace =
workspace::get_any_active_multi_workspace(app_state.clone(), cx.clone())
.await?;
let workspace =
multi_workspace.read_with(cx, |mw, _| mw.workspace().clone())?;
workspace::get_any_active_workspace(app_state.clone(), cx.clone()).await?;
let (client, thread_store) =
multi_workspace.update(cx, |_, _window, cx| {
workspace.update(cx, |workspace, cx| {
let client = workspace.project().read(cx).client();
let thread_store: Option<gpui::Entity<ThreadStore>> = workspace
.panel::<AgentPanel>(cx)
.map(|panel| panel.read(cx).thread_store().clone());
anyhow::Ok((client, thread_store))
})
})??;
workspace.update(cx, |workspace, _window, cx| {
let client = workspace.project().read(cx).client();
let thread_store: Option<gpui::Entity<ThreadStore>> = workspace
.panel::<AgentPanel>(cx)
.map(|panel| panel.read(cx).thread_store().clone());
(client, thread_store)
})?;
let Some(thread_store): Option<gpui::Entity<ThreadStore>> = thread_store else {
anyhow::bail!("Agent panel not available");
@@ -928,27 +921,25 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
meta: None,
};
let sharer_username = response.sharer_username.clone();
workspace.update(cx, |workspace, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.open_thread(thread_metadata, window, cx);
});
panel.focus_handle(cx).focus(window, cx);
}
})?;
multi_workspace.update(cx, |_, window, cx| {
workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.open_thread(thread_metadata, window, cx);
});
panel.focus_handle(cx).focus(window, cx);
}
struct ImportedThreadToast;
workspace.show_toast(
Toast::new(
NotificationId::unique::<ImportedThreadToast>(),
format!("Imported shared thread from {}", sharer_username),
)
.autohide(),
cx,
);
});
workspace.update(cx, |workspace, _window, cx| {
struct ImportedThreadToast;
workspace.show_toast(
Toast::new(
NotificationId::unique::<ImportedThreadToast>(),
format!("Imported shared thread from {}", response.sharer_username),
)
.autohide(),
cx,
);
})?;
anyhow::Ok(())
@@ -1023,7 +1014,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
// [ languages $(language) tab_size]
cx.spawn(async move |cx| {
let workspace =
workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?;
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
workspace.update(cx, |_, window, cx| match setting_path {
None => window.dispatch_action(Box::new(zed_actions::OpenSettings), cx),
@@ -1085,29 +1076,23 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
.await?;
workspace
.update(cx, |multi_workspace, window, cx| {
multi_workspace
.workspace()
.clone()
.update(cx, |workspace, cx| {
let Some(repo) =
workspace.project().read(cx).active_repository(cx)
else {
log::error!("no active repository found for commit view");
return Err(anyhow::anyhow!("no active repository found"));
};
.update(cx, |workspace, window, cx| {
let Some(repo) = workspace.project().read(cx).active_repository(cx)
else {
log::error!("no active repository found for commit view");
return Err(anyhow::anyhow!("no active repository found"));
};
git_ui::commit_view::CommitView::open(
sha,
repo.downgrade(),
workspace.weak_handle(),
None,
None,
window,
cx,
);
Ok(())
})
git_ui::commit_view::CommitView::open(
sha,
repo.downgrade(),
workspace.weak_handle(),
None,
None,
window,
cx,
);
Ok(())
})
.log_err();
@@ -1177,7 +1162,6 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
client::ChannelId(channel_id),
app_state.clone(),
None,
None,
cx,
)
})
@@ -1185,9 +1169,8 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
}
let workspace_window =
workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?;
let workspace = workspace_window.read_with(cx, |mw, _| mw.workspace().clone())?;
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
let workspace = workspace_window.entity(cx)?;
let mut promises = Vec::new();
for (channel_id, heading) in request.open_channel_notes {
@@ -1277,53 +1260,78 @@ async fn installation_id() -> Result<IdType> {
Ok(IdType::New(installation_id))
}
pub(crate) async fn restore_or_create_workspace(
app_state: Arc<AppState>,
cx: &mut AsyncApp,
) -> Result<()> {
if let Some((multi_workspaces, remote_workspaces)) = restorable_workspaces(cx, &app_state).await
{
async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp) -> Result<()> {
if let Some(locations) = restorable_workspace_locations(cx, &app_state).await {
let use_system_window_tabs =
cx.update(|cx| WorkspaceSettings::get_global(cx).use_system_window_tabs);
let mut results: Vec<Result<(), Error>> = Vec::new();
let mut tasks = Vec::new();
let mut local_results = Vec::new();
for multi_workspace in multi_workspaces {
local_results
.push(restore_multiworkspace(multi_workspace, app_state.clone(), cx).await);
}
for (index, (workspace_id, location, paths)) in locations.into_iter().enumerate() {
match location {
SerializedWorkspaceLocation::Local if paths.is_empty() => {
// Restore empty workspace by ID (has items like drafts but no folders)
let app_state = app_state.clone();
let task = cx.spawn(async move |cx| {
let open_task = cx.update(|cx| {
workspace::open_workspace_by_id(workspace_id, app_state, cx)
});
open_task.await.map(|_| ())
});
for result in local_results {
results.push(result.map(|_| ()));
}
if use_system_window_tabs && index == 0 {
results.push(task.await);
} else {
tasks.push(task);
}
}
SerializedWorkspaceLocation::Local => {
let app_state = app_state.clone();
let task = cx.spawn(async move |cx| {
let open_task = cx.update(|cx| {
workspace::open_paths(
&paths.paths(),
app_state,
workspace::OpenOptions::default(),
cx,
)
});
open_task.await.map(|_| ())
});
for session_workspace in remote_workspaces {
let app_state = app_state.clone();
let SerializedWorkspaceLocation::Remote(mut connection_options) =
session_workspace.location
else {
continue;
};
let paths = session_workspace.paths;
if let RemoteConnectionOptions::Ssh(options) = &mut connection_options {
cx.update(|cx| {
RemoteSettings::get_global(cx).fill_connection_options_from_settings(options)
});
// If we're using system window tabs and this is the first workspace,
// wait for it to finish so that the other windows can be added as tabs.
if use_system_window_tabs && index == 0 {
results.push(task.await);
} else {
tasks.push(task);
}
}
SerializedWorkspaceLocation::Remote(mut connection_options) => {
let app_state = app_state.clone();
if let RemoteConnectionOptions::Ssh(options) = &mut connection_options {
cx.update(|cx| {
RemoteSettings::get_global(cx)
.fill_connection_options_from_settings(options)
});
}
let task = cx.spawn(async move |cx| {
recent_projects::open_remote_project(
connection_options,
paths.paths().into_iter().map(PathBuf::from).collect(),
app_state,
workspace::OpenOptions::default(),
cx,
)
.await
.map_err(|e| anyhow::anyhow!(e))
});
tasks.push(task);
}
}
let task = cx.spawn(async move |cx| {
recent_projects::open_remote_project(
connection_options,
paths.paths().iter().map(PathBuf::from).collect(),
app_state,
workspace::OpenOptions::default(),
cx,
)
.await
.map_err(|e| anyhow::anyhow!(e))
});
tasks.push(task);
}
// Wait for all window groups and remote workspaces to open concurrently
// Wait for all workspaces to open concurrently
results.extend(future::join_all(tasks).await);
// Show notifications for any errors that occurred
@@ -1348,16 +1356,12 @@ pub(crate) async fn restore_or_create_workspace(
// Try to find an active workspace to show the toast
let toast_shown = cx.update(|cx| {
if let Some(window) = cx.active_window()
&& let Some(multi_workspace) = window.downcast::<MultiWorkspace>()
&& let Some(workspace) = window.downcast::<Workspace>()
{
multi_workspace
.update(cx, |multi_workspace, _, cx| {
multi_workspace.workspace().update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(NotificationId::unique::<()>(), message),
cx,
)
});
workspace
.update(cx, |workspace, _, cx| {
workspace
.show_toast(Toast::new(NotificationId::unique::<()>(), message), cx)
})
.ok();
return true;
@@ -1398,25 +1402,10 @@ pub(crate) async fn restore_or_create_workspace(
Ok(())
}
async fn restorable_workspaces(
cx: &mut AsyncApp,
app_state: &Arc<AppState>,
) -> Option<(
Vec<workspace::SerializedMultiWorkspace>,
Vec<SessionWorkspace>,
)> {
let locations = restorable_workspace_locations(cx, app_state).await?;
let (remote_workspaces, local_workspaces) = locations
.into_iter()
.partition(|sw| matches!(sw.location, SerializedWorkspaceLocation::Remote(_)));
let multi_workspaces = workspace::read_serialized_multi_workspaces(local_workspaces);
Some((multi_workspaces, remote_workspaces))
}
pub(crate) async fn restorable_workspace_locations(
cx: &mut AsyncApp,
app_state: &Arc<AppState>,
) -> Option<Vec<SessionWorkspace>> {
) -> Option<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
let mut restore_behavior = cx.update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup);
let session_handle = app_state.session.clone();
@@ -1440,16 +1429,9 @@ pub(crate) async fn restorable_workspace_locations(
match restore_behavior {
workspace::RestoreOnStartupBehavior::LastWorkspace => {
workspace::last_opened_workspace_location(app_state.fs.as_ref())
workspace::last_opened_workspace_location()
.await
.map(|(workspace_id, location, paths)| {
vec![SessionWorkspace {
workspace_id,
location,
paths,
window_id: None,
}]
})
.map(|location| vec![location])
}
workspace::RestoreOnStartupBehavior::LastSession => {
if let Some(last_session_id) = last_session_id {
@@ -1458,9 +1440,7 @@ pub(crate) async fn restorable_workspace_locations(
let mut locations = workspace::last_session_workspace_locations(
&last_session_id,
last_session_window_stack,
app_state.fs.as_ref(),
)
.await
.filter(|locations| !locations.is_empty());
// Since last_session_window_order returns the windows ordered front-to-back
+2 -317
View File
@@ -59,7 +59,6 @@ use {
},
image::RgbaImage,
project_panel::ProjectPanel,
recent_projects::RecentProjectEntry,
settings::{NotifyWhenAgentWaiting, Settings as _},
settings_ui::SettingsWindow,
std::{
@@ -71,7 +70,7 @@ use {
},
util::ResultExt as _,
watch,
workspace::{AppState, MultiWorkspace, Workspace, WorkspaceId},
workspace::{AppState, Workspace},
zed_actions::OpenSettingsAt,
};
@@ -436,24 +435,7 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()>
}
}
// Run Test 3: Multi-workspace sidebar visual tests
println!("\n--- Test 3: multi_workspace_sidebar ---");
match run_multi_workspace_sidebar_visual_tests(app_state.clone(), &mut cx, update_baseline) {
Ok(TestResult::Passed) => {
println!("✓ multi_workspace_sidebar: PASSED");
passed += 1;
}
Ok(TestResult::BaselineUpdated(_)) => {
println!("✓ multi_workspace_sidebar: Baselines updated");
updated += 1;
}
Err(e) => {
eprintln!("✗ multi_workspace_sidebar: FAILED - {}", e);
failed += 1;
}
}
// Run Test 4: Agent Thread View tests
// Run Test 3: Agent Thread View tests
#[cfg(feature = "visual-tests")]
{
println!("\n--- Test 3: agent_thread_with_image (collapsed + expanded) ---");
@@ -2799,300 +2781,3 @@ fn run_tool_permissions_visual_tests(
// Return success - we're just capturing screenshots, not comparing baselines
Ok(TestResult::Passed)
}
#[cfg(target_os = "macos")]
fn run_multi_workspace_sidebar_visual_tests(
app_state: Arc<AppState>,
cx: &mut VisualTestAppContext,
update_baseline: bool,
) -> Result<TestResult> {
// Create temporary directories to act as worktrees for active workspaces
let temp_dir = tempfile::tempdir()?;
let temp_path = temp_dir.keep();
let canonical_temp = temp_path.canonicalize()?;
let workspace1_dir = canonical_temp.join("private-test-remote");
let workspace2_dir = canonical_temp.join("zed");
std::fs::create_dir_all(&workspace1_dir)?;
std::fs::create_dir_all(&workspace2_dir)?;
// Create directories for recent projects (they must exist on disk for display)
let recent1_dir = canonical_temp.join("tiny-project");
let recent2_dir = canonical_temp.join("font-kit");
let recent3_dir = canonical_temp.join("ideas");
let recent4_dir = canonical_temp.join("tmp");
std::fs::create_dir_all(&recent1_dir)?;
std::fs::create_dir_all(&recent2_dir)?;
std::fs::create_dir_all(&recent3_dir)?;
std::fs::create_dir_all(&recent4_dir)?;
// Enable the agent-v2 feature flag so multi-workspace is active
cx.update(|cx| {
cx.update_flags(true, vec!["agent-v2".to_string()]);
});
// Create both projects upfront so we can build both workspaces during
// window creation, before the MultiWorkspace entity exists.
// This avoids a re-entrant read panic that occurs when Workspace::new
// tries to access the window root (MultiWorkspace) while it's being updated.
let project1 = cx.update(|cx| {
project::Project::local(
app_state.client.clone(),
app_state.node_runtime.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
None,
project::LocalProjectFlags {
init_worktree_trust: false,
..Default::default()
},
cx,
)
});
let project2 = cx.update(|cx| {
project::Project::local(
app_state.client.clone(),
app_state.node_runtime.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
None,
project::LocalProjectFlags {
init_worktree_trust: false,
..Default::default()
},
cx,
)
});
let window_size = size(px(1280.0), px(800.0));
let bounds = Bounds {
origin: point(px(0.0), px(0.0)),
size: window_size,
};
// Open a MultiWorkspace window with both workspaces created at construction time
let multi_workspace_window: WindowHandle<MultiWorkspace> = cx
.update(|cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
focus: false,
show: false,
..Default::default()
},
|window, cx| {
let workspace1 = cx.new(|cx| {
Workspace::new(None, project1.clone(), app_state.clone(), window, cx)
});
let workspace2 = cx.new(|cx| {
Workspace::new(None, project2.clone(), app_state.clone(), window, cx)
});
cx.new(|cx| {
let mut multi_workspace = MultiWorkspace::new(workspace1, cx);
multi_workspace.activate(workspace2, cx);
multi_workspace
})
},
)
})
.context("Failed to open MultiWorkspace window")?;
cx.run_until_parked();
// Add worktree to workspace 1 (index 0) so it shows as "private-test-remote"
let add_worktree1_task = multi_workspace_window
.update(cx, |multi_workspace, _window, cx| {
let workspace1 = &multi_workspace.workspaces()[0];
let project = workspace1.read(cx).project().clone();
project.update(cx, |project, cx| {
project.find_or_create_worktree(&workspace1_dir, true, cx)
})
})
.context("Failed to start adding worktree 1")?;
cx.background_executor.allow_parking();
cx.foreground_executor
.block_test(add_worktree1_task)
.context("Failed to add worktree 1")?;
cx.background_executor.forbid_parking();
cx.run_until_parked();
// Add worktree to workspace 2 (index 1) so it shows as "zed"
let add_worktree2_task = multi_workspace_window
.update(cx, |multi_workspace, _window, cx| {
let workspace2 = &multi_workspace.workspaces()[1];
let project = workspace2.read(cx).project().clone();
project.update(cx, |project, cx| {
project.find_or_create_worktree(&workspace2_dir, true, cx)
})
})
.context("Failed to start adding worktree 2")?;
cx.background_executor.allow_parking();
cx.foreground_executor
.block_test(add_worktree2_task)
.context("Failed to add worktree 2")?;
cx.background_executor.forbid_parking();
cx.run_until_parked();
// Switch to workspace 1 so it's highlighted as active (index 0)
multi_workspace_window
.update(cx, |multi_workspace, window, cx| {
multi_workspace.activate_index(0, window, cx);
})
.context("Failed to activate workspace 1")?;
cx.run_until_parked();
// Create the sidebar and register it on the MultiWorkspace
let sidebar = multi_workspace_window
.update(cx, |_multi_workspace, window, cx| {
let multi_workspace_handle = cx.entity();
cx.new(|cx| sidebar::Sidebar::new(multi_workspace_handle, window, cx))
})
.context("Failed to create sidebar")?;
multi_workspace_window
.update(cx, |multi_workspace, window, cx| {
multi_workspace.register_sidebar(sidebar.clone(), window, cx);
})
.context("Failed to register sidebar")?;
cx.run_until_parked();
// Inject recent project entries into the sidebar.
// We update the sidebar entity directly (not through the MultiWorkspace window update)
// to avoid a re-entrant read panic: rebuild_entries reads MultiWorkspace, so we can't
// be inside a MultiWorkspace update when that happens.
cx.update(|cx| {
sidebar.update(cx, |sidebar, cx| {
let recent_projects = vec![
RecentProjectEntry {
name: "tiny-project".into(),
full_path: recent1_dir.to_string_lossy().to_string().into(),
paths: vec![recent1_dir.clone()],
workspace_id: WorkspaceId::default(),
},
RecentProjectEntry {
name: "font-kit".into(),
full_path: recent2_dir.to_string_lossy().to_string().into(),
paths: vec![recent2_dir.clone()],
workspace_id: WorkspaceId::default(),
},
RecentProjectEntry {
name: "ideas".into(),
full_path: recent3_dir.to_string_lossy().to_string().into(),
paths: vec![recent3_dir.clone()],
workspace_id: WorkspaceId::default(),
},
RecentProjectEntry {
name: "tmp".into(),
full_path: recent4_dir.to_string_lossy().to_string().into(),
paths: vec![recent4_dir.clone()],
workspace_id: WorkspaceId::default(),
},
];
sidebar.set_test_recent_projects(recent_projects, cx);
});
});
// Set thread info directly on the sidebar for visual testing
cx.update(|cx| {
sidebar.update(cx, |sidebar, _cx| {
sidebar.set_test_thread_info(
0,
"Refine thread view scrolling behavior".into(),
sidebar::AgentThreadStatus::Completed,
);
sidebar.set_test_thread_info(
1,
"Add line numbers option to FileEditBlock".into(),
sidebar::AgentThreadStatus::Running,
);
});
});
// Set last-worked-on thread titles on some recent projects for visual testing
cx.update(|cx| {
sidebar.update(cx, |sidebar, cx| {
sidebar.set_test_recent_project_thread_title(
recent1_dir.to_string_lossy().to_string().into(),
"Fix flaky test in CI pipeline".into(),
cx,
);
sidebar.set_test_recent_project_thread_title(
recent2_dir.to_string_lossy().to_string().into(),
"Upgrade font rendering engine".into(),
cx,
);
});
});
cx.run_until_parked();
// Open the sidebar
multi_workspace_window
.update(cx, |multi_workspace, window, cx| {
multi_workspace.toggle_sidebar(window, cx);
})
.context("Failed to toggle sidebar")?;
// Let rendering settle
for _ in 0..10 {
cx.advance_clock(Duration::from_millis(100));
cx.run_until_parked();
}
// Refresh the window
cx.update_window(multi_workspace_window.into(), |_, window, _cx| {
window.refresh();
})?;
cx.run_until_parked();
// Capture: sidebar open with active workspaces and recent projects
let test_result = run_visual_test(
"multi_workspace_sidebar_open",
multi_workspace_window.into(),
cx,
update_baseline,
)?;
// Clean up worktrees
multi_workspace_window
.update(cx, |multi_workspace, _window, cx| {
for workspace in multi_workspace.workspaces() {
let project = workspace.read(cx).project().clone();
project.update(cx, |project, cx| {
let worktree_ids: Vec<_> =
project.worktrees(cx).map(|wt| wt.read(cx).id()).collect();
for id in worktree_ids {
project.remove_worktree(id, cx);
}
});
}
})
.log_err();
cx.run_until_parked();
// Close the window
cx.update_window(multi_workspace_window.into(), |_, window, _cx| {
window.remove_window();
})
.log_err();
cx.run_until_parked();
for _ in 0..15 {
cx.advance_clock(Duration::from_millis(100));
cx.run_until_parked();
}
Ok(test_result)
}
+152 -940
View File
File diff suppressed because it is too large. Load diff
+8 -11
View File
@@ -1,7 +1,6 @@
use anyhow::{Context as _, Result};
use editor::Editor;
use fs::Fs;
use gpui::WeakEntity;
use migrator::{migrate_keymap, migrate_settings};
use settings::{KeymapFile, Settings, SettingsStore};
use util::ResultExt;
@@ -23,7 +22,6 @@ pub enum MigrationType {
}
pub struct MigrationBanner {
workspace: WeakEntity<Workspace>,
migration_type: Option<MigrationType>,
should_migrate_task: Option<Task<()>>,
markdown: Option<Entity<Markdown>>,
@@ -56,7 +54,7 @@ struct GlobalMigrationNotification(Entity<MigrationNotification>);
impl Global for GlobalMigrationNotification {}
impl MigrationBanner {
pub fn new(workspace: WeakEntity<Workspace>, cx: &mut Context<Self>) -> Self {
pub fn new(_: &Workspace, cx: &mut Context<Self>) -> Self {
if let Some(notifier) = MigrationNotification::try_global(cx) {
cx.subscribe(
&notifier,
@@ -67,7 +65,6 @@ impl MigrationBanner {
.detach();
}
Self {
workspace,
migration_type: None,
should_migrate_task: None,
markdown: None,
@@ -238,22 +235,22 @@ impl Render for MigrationBanner {
),
)
.child(
Button::new("backup-and-migrate", "Backup and Update").on_click({
let workspace = self.workspace.clone();
Button::new("backup-and-migrate", "Backup and Update").on_click(
move |_, window, cx| {
let fs = <dyn Fs>::global(cx);
let task = match migration_type {
match migration_type {
Some(MigrationType::Keymap) => {
cx.background_spawn(write_keymap_migration(fs.clone()))
.detach_and_notify_err(window, cx);
}
Some(MigrationType::Settings) => {
cx.background_spawn(write_settings_migration(fs.clone()))
.detach_and_notify_err(window, cx);
}
None => unreachable!(),
};
task.detach_and_notify_err(workspace.clone(), window, cx);
}
}),
}
},
),
)
.into_any_element()
}
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff. Show more