1use anyhow::Result;
2use gpui::PathPromptOptions;
3use gpui::{
4 AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
5 ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId,
6 actions, deferred, px,
7};
8use project::{DirectoryLister, DisableAiSettings, Project, ProjectGroupKey};
9use remote::RemoteConnectionOptions;
10use settings::Settings;
11pub use settings::SidebarSide;
12use std::collections::{HashMap, HashSet};
13use std::future::Future;
14use std::path::Path;
15use std::path::PathBuf;
16use ui::prelude::*;
17use util::ResultExt;
18use util::path_list::PathList;
19use zed_actions::agents_sidebar::ToggleThreadSwitcher;
20
21use agent_settings::AgentSettings;
22use settings::SidebarDockPosition;
23use ui::{ContextMenu, right_click_menu};
24
25const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
26
27use crate::open_remote_project_with_existing_connection;
28use crate::{
29 CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, OpenMode,
30 Panel, Workspace, WorkspaceId, client_side_decorations,
31 persistence::model::MultiWorkspaceState,
32};
33
34actions!(
35 multi_workspace,
36 [
37 /// Toggles the workspace switcher sidebar.
38 ToggleWorkspaceSidebar,
39 /// Closes the workspace sidebar.
40 CloseWorkspaceSidebar,
41 /// Moves focus to or from the workspace sidebar without closing it.
42 FocusWorkspaceSidebar,
43 /// Activates the next project in the sidebar.
44 NextProject,
45 /// Activates the previous project in the sidebar.
46 PreviousProject,
47 /// Activates the next thread in sidebar order.
48 NextThread,
49 /// Activates the previous thread in sidebar order.
50 PreviousThread,
51 /// Expands the thread list for the current project to show more threads.
52 ShowMoreThreads,
53 /// Collapses the thread list for the current project to show fewer threads.
54 ShowFewerThreads,
55 /// Creates a new thread in the current workspace.
56 NewThread,
57 ]
58);
59
60#[derive(Default)]
61pub struct SidebarRenderState {
62 pub open: bool,
63 pub side: SidebarSide,
64}
65
66pub fn sidebar_side_context_menu(
67 id: impl Into<ElementId>,
68 cx: &App,
69) -> ui::RightClickMenu<ContextMenu> {
70 let current_position = AgentSettings::get_global(cx).sidebar_side;
71 right_click_menu(id).menu(move |window, cx| {
72 let fs = <dyn fs::Fs>::global(cx);
73 ContextMenu::build(window, cx, move |mut menu, _, _cx| {
74 let positions: [(SidebarDockPosition, &str); 2] = [
75 (SidebarDockPosition::Left, "Left"),
76 (SidebarDockPosition::Right, "Right"),
77 ];
78 for (position, label) in positions {
79 let fs = fs.clone();
80 menu = menu.toggleable_entry(
81 label,
82 position == current_position,
83 IconPosition::Start,
84 None,
85 move |_window, cx| {
86 settings::update_settings_file(fs.clone(), cx, move |settings, _cx| {
87 settings
88 .agent
89 .get_or_insert_default()
90 .set_sidebar_side(position);
91 });
92 },
93 );
94 }
95 menu
96 })
97 })
98}
99
100pub enum MultiWorkspaceEvent {
101 ActiveWorkspaceChanged,
102 WorkspaceAdded(Entity<Workspace>),
103 WorkspaceRemoved(EntityId),
104 ProjectGroupKeyChanged {
105 old_key: ProjectGroupKey,
106 new_key: ProjectGroupKey,
107 },
108}
109
110pub enum SidebarEvent {
111 SerializeNeeded,
112}
113
114pub trait Sidebar: Focusable + Render + EventEmitter<SidebarEvent> + Sized {
115 fn width(&self, cx: &App) -> Pixels;
116 fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
117 fn has_notifications(&self, cx: &App) -> bool;
118 fn side(&self, _cx: &App) -> SidebarSide;
119
120 fn is_threads_list_view_active(&self) -> bool {
121 true
122 }
123 /// Makes focus reset back to the search editor upon toggling the sidebar from outside
124 fn prepare_for_focus(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
125 /// Opens or cycles the thread switcher popup.
126 fn toggle_thread_switcher(
127 &mut self,
128 _select_last: bool,
129 _window: &mut Window,
130 _cx: &mut Context<Self>,
131 ) {
132 }
133
134 /// Activates the next or previous project.
135 fn cycle_project(&mut self, _forward: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
136
137 /// Activates the next or previous thread in sidebar order.
138 fn cycle_thread(&mut self, _forward: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
139
140 /// Return an opaque JSON blob of sidebar-specific state to persist.
141 fn serialized_state(&self, _cx: &App) -> Option<String> {
142 None
143 }
144
145 /// Restore sidebar state from a previously-serialized blob.
146 fn restore_serialized_state(
147 &mut self,
148 _state: &str,
149 _window: &mut Window,
150 _cx: &mut Context<Self>,
151 ) {
152 }
153}
154
155pub trait SidebarHandle: 'static + Send + Sync {
156 fn width(&self, cx: &App) -> Pixels;
157 fn set_width(&self, width: Option<Pixels>, cx: &mut App);
158 fn focus_handle(&self, cx: &App) -> FocusHandle;
159 fn focus(&self, window: &mut Window, cx: &mut App);
160 fn prepare_for_focus(&self, window: &mut Window, cx: &mut App);
161 fn has_notifications(&self, cx: &App) -> bool;
162 fn to_any(&self) -> AnyView;
163 fn entity_id(&self) -> EntityId;
164 fn toggle_thread_switcher(&self, select_last: bool, window: &mut Window, cx: &mut App);
165 fn cycle_project(&self, forward: bool, window: &mut Window, cx: &mut App);
166 fn cycle_thread(&self, forward: bool, window: &mut Window, cx: &mut App);
167
168 fn is_threads_list_view_active(&self, cx: &App) -> bool;
169
170 fn side(&self, cx: &App) -> SidebarSide;
171 fn serialized_state(&self, cx: &App) -> Option<String>;
172 fn restore_serialized_state(&self, state: &str, window: &mut Window, cx: &mut App);
173}
174
175#[derive(Clone)]
176pub struct DraggedSidebar;
177
178impl Render for DraggedSidebar {
179 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
180 gpui::Empty
181 }
182}
183
184impl<T: Sidebar> SidebarHandle for Entity<T> {
185 fn width(&self, cx: &App) -> Pixels {
186 self.read(cx).width(cx)
187 }
188
189 fn set_width(&self, width: Option<Pixels>, cx: &mut App) {
190 self.update(cx, |this, cx| this.set_width(width, cx))
191 }
192
193 fn focus_handle(&self, cx: &App) -> FocusHandle {
194 self.read(cx).focus_handle(cx)
195 }
196
197 fn focus(&self, window: &mut Window, cx: &mut App) {
198 let handle = self.read(cx).focus_handle(cx);
199 window.focus(&handle, cx);
200 }
201
202 fn prepare_for_focus(&self, window: &mut Window, cx: &mut App) {
203 self.update(cx, |this, cx| this.prepare_for_focus(window, cx));
204 }
205
206 fn has_notifications(&self, cx: &App) -> bool {
207 self.read(cx).has_notifications(cx)
208 }
209
210 fn to_any(&self) -> AnyView {
211 self.clone().into()
212 }
213
214 fn entity_id(&self) -> EntityId {
215 Entity::entity_id(self)
216 }
217
218 fn toggle_thread_switcher(&self, select_last: bool, window: &mut Window, cx: &mut App) {
219 let entity = self.clone();
220 window.defer(cx, move |window, cx| {
221 entity.update(cx, |this, cx| {
222 this.toggle_thread_switcher(select_last, window, cx);
223 });
224 });
225 }
226
227 fn cycle_project(&self, forward: bool, window: &mut Window, cx: &mut App) {
228 let entity = self.clone();
229 window.defer(cx, move |window, cx| {
230 entity.update(cx, |this, cx| {
231 this.cycle_project(forward, window, cx);
232 });
233 });
234 }
235
236 fn cycle_thread(&self, forward: bool, window: &mut Window, cx: &mut App) {
237 let entity = self.clone();
238 window.defer(cx, move |window, cx| {
239 entity.update(cx, |this, cx| {
240 this.cycle_thread(forward, window, cx);
241 });
242 });
243 }
244
245 fn is_threads_list_view_active(&self, cx: &App) -> bool {
246 self.read(cx).is_threads_list_view_active()
247 }
248
249 fn side(&self, cx: &App) -> SidebarSide {
250 self.read(cx).side(cx)
251 }
252
253 fn serialized_state(&self, cx: &App) -> Option<String> {
254 self.read(cx).serialized_state(cx)
255 }
256
257 fn restore_serialized_state(&self, state: &str, window: &mut Window, cx: &mut App) {
258 self.update(cx, |this, cx| {
259 this.restore_serialized_state(state, window, cx)
260 })
261 }
262}
263
264/// Tracks which workspace the user is currently looking at.
265///
266/// `Persistent` workspaces live in the `workspaces` vec and are shown in the
267/// sidebar. `Transient` workspaces exist outside the vec and are discarded
268/// when the user switches away.
269enum ActiveWorkspace {
270 /// A persistent workspace, identified by index into the `workspaces` vec.
271 Persistent(usize),
272 /// A workspace not in the `workspaces` vec that will be discarded on
273 /// switch or promoted to persistent when the sidebar is opened.
274 Transient(Entity<Workspace>),
275}
276
277impl ActiveWorkspace {
278 fn transient_workspace(&self) -> Option<&Entity<Workspace>> {
279 match self {
280 Self::Transient(workspace) => Some(workspace),
281 Self::Persistent(_) => None,
282 }
283 }
284
285 /// Sets the active workspace to transient, returning the previous
286 /// transient workspace (if any).
287 fn set_transient(&mut self, workspace: Entity<Workspace>) -> Option<Entity<Workspace>> {
288 match std::mem::replace(self, Self::Transient(workspace)) {
289 Self::Transient(old) => Some(old),
290 Self::Persistent(_) => None,
291 }
292 }
293
294 /// Sets the active workspace to persistent at the given index,
295 /// returning the previous transient workspace (if any).
296 fn set_persistent(&mut self, index: usize) -> Option<Entity<Workspace>> {
297 match std::mem::replace(self, Self::Persistent(index)) {
298 Self::Transient(workspace) => Some(workspace),
299 Self::Persistent(_) => None,
300 }
301 }
302}
303
304pub struct MultiWorkspace {
305 window_id: WindowId,
306 workspaces: Vec<Entity<Workspace>>,
307 active_workspace: ActiveWorkspace,
308 project_group_keys: Vec<ProjectGroupKey>,
309 workspace_group_keys: HashMap<EntityId, ProjectGroupKey>,
310 sidebar: Option<Box<dyn SidebarHandle>>,
311 sidebar_open: bool,
312 sidebar_overlay: Option<AnyView>,
313 pending_removal_tasks: Vec<Task<()>>,
314 _serialize_task: Option<Task<()>>,
315 _subscriptions: Vec<Subscription>,
316 previous_focus_handle: Option<FocusHandle>,
317}
318
319impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
320
321impl MultiWorkspace {
322 pub fn sidebar_side(&self, cx: &App) -> SidebarSide {
323 self.sidebar
324 .as_ref()
325 .map_or(SidebarSide::Left, |s| s.side(cx))
326 }
327
328 pub fn sidebar_render_state(&self, cx: &App) -> SidebarRenderState {
329 SidebarRenderState {
330 open: self.sidebar_open() && self.multi_workspace_enabled(cx),
331 side: self.sidebar_side(cx),
332 }
333 }
334
335 pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
336 let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
337 if let Some(task) = this._serialize_task.take() {
338 task.detach();
339 }
340 for task in std::mem::take(&mut this.pending_removal_tasks) {
341 task.detach();
342 }
343 });
344 let quit_subscription = cx.on_app_quit(Self::app_will_quit);
345 let settings_subscription = cx.observe_global_in::<settings::SettingsStore>(window, {
346 let mut previous_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
347 move |this, window, cx| {
348 if DisableAiSettings::get_global(cx).disable_ai != previous_disable_ai {
349 this.collapse_to_single_workspace(window, cx);
350 previous_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
351 }
352 }
353 });
354 Self::subscribe_to_workspace(&workspace, window, cx);
355 let weak_self = cx.weak_entity();
356 workspace.update(cx, |workspace, cx| {
357 workspace.set_multi_workspace(weak_self, cx);
358 });
359 Self {
360 window_id: window.window_handle().window_id(),
361 project_group_keys: Vec::new(),
362 workspace_group_keys: HashMap::default(),
363 workspaces: Vec::new(),
364 active_workspace: ActiveWorkspace::Transient(workspace),
365 sidebar: None,
366 sidebar_open: false,
367 sidebar_overlay: None,
368 pending_removal_tasks: Vec::new(),
369 _serialize_task: None,
370 _subscriptions: vec![
371 release_subscription,
372 quit_subscription,
373 settings_subscription,
374 ],
375 previous_focus_handle: None,
376 }
377 }
378
379 pub fn register_sidebar<T: Sidebar>(&mut self, sidebar: Entity<T>, cx: &mut Context<Self>) {
380 self._subscriptions
381 .push(cx.observe(&sidebar, |_this, _, cx| {
382 cx.notify();
383 }));
384 self._subscriptions
385 .push(cx.subscribe(&sidebar, |this, _, event, cx| match event {
386 SidebarEvent::SerializeNeeded => {
387 this.serialize(cx);
388 }
389 }));
390 self.sidebar = Some(Box::new(sidebar));
391 }
392
393 pub fn sidebar(&self) -> Option<&dyn SidebarHandle> {
394 self.sidebar.as_deref()
395 }
396
397 pub fn set_sidebar_overlay(&mut self, overlay: Option<AnyView>, cx: &mut Context<Self>) {
398 self.sidebar_overlay = overlay;
399 cx.notify();
400 }
401
402 pub fn sidebar_open(&self) -> bool {
403 self.sidebar_open
404 }
405
406 pub fn sidebar_has_notifications(&self, cx: &App) -> bool {
407 self.sidebar
408 .as_ref()
409 .map_or(false, |s| s.has_notifications(cx))
410 }
411
412 pub fn is_threads_list_view_active(&self, cx: &App) -> bool {
413 self.sidebar
414 .as_ref()
415 .map_or(false, |s| s.is_threads_list_view_active(cx))
416 }
417
418 pub fn multi_workspace_enabled(&self, cx: &App) -> bool {
419 !DisableAiSettings::get_global(cx).disable_ai
420 }
421
422 pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
423 if !self.multi_workspace_enabled(cx) {
424 return;
425 }
426
427 if self.sidebar_open() {
428 self.close_sidebar(window, cx);
429 } else {
430 self.previous_focus_handle = window.focused(cx);
431 self.open_sidebar(cx);
432 if let Some(sidebar) = &self.sidebar {
433 sidebar.prepare_for_focus(window, cx);
434 sidebar.focus(window, cx);
435 }
436 }
437 }
438
439 pub fn close_sidebar_action(&mut self, window: &mut Window, cx: &mut Context<Self>) {
440 if !self.multi_workspace_enabled(cx) {
441 return;
442 }
443
444 if self.sidebar_open() {
445 self.close_sidebar(window, cx);
446 }
447 }
448
449 pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
450 if !self.multi_workspace_enabled(cx) {
451 return;
452 }
453
454 if self.sidebar_open() {
455 let sidebar_is_focused = self
456 .sidebar
457 .as_ref()
458 .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
459
460 if sidebar_is_focused {
461 self.restore_previous_focus(false, window, cx);
462 } else {
463 self.previous_focus_handle = window.focused(cx);
464 if let Some(sidebar) = &self.sidebar {
465 sidebar.prepare_for_focus(window, cx);
466 sidebar.focus(window, cx);
467 }
468 }
469 } else {
470 self.previous_focus_handle = window.focused(cx);
471 self.open_sidebar(cx);
472 if let Some(sidebar) = &self.sidebar {
473 sidebar.prepare_for_focus(window, cx);
474 sidebar.focus(window, cx);
475 }
476 }
477 }
478
479 pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
480 self.sidebar_open = true;
481 if let ActiveWorkspace::Transient(workspace) = &self.active_workspace {
482 let workspace = workspace.clone();
483 let index = self.promote_transient(workspace, cx);
484 self.active_workspace = ActiveWorkspace::Persistent(index);
485 }
486 let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
487 for workspace in self.workspaces.iter() {
488 workspace.update(cx, |workspace, _cx| {
489 workspace.set_sidebar_focus_handle(sidebar_focus_handle.clone());
490 });
491 }
492 self.serialize(cx);
493 cx.notify();
494 }
495
496 pub fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
497 self.sidebar_open = false;
498 for workspace in self.workspaces.iter() {
499 workspace.update(cx, |workspace, _cx| {
500 workspace.set_sidebar_focus_handle(None);
501 });
502 }
503 let sidebar_has_focus = self
504 .sidebar
505 .as_ref()
506 .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
507 if sidebar_has_focus {
508 self.restore_previous_focus(true, window, cx);
509 } else {
510 self.previous_focus_handle.take();
511 }
512 self.serialize(cx);
513 cx.notify();
514 }
515
516 fn restore_previous_focus(&mut self, clear: bool, window: &mut Window, cx: &mut Context<Self>) {
517 let focus_handle = if clear {
518 self.previous_focus_handle.take()
519 } else {
520 self.previous_focus_handle.clone()
521 };
522
523 if let Some(previous_focus) = focus_handle {
524 previous_focus.focus(window, cx);
525 } else {
526 let pane = self.workspace().read(cx).active_pane().clone();
527 window.focus(&pane.read(cx).focus_handle(cx), cx);
528 }
529 }
530
531 pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
532 cx.spawn_in(window, async move |this, cx| {
533 let workspaces = this.update(cx, |multi_workspace, _cx| {
534 multi_workspace.workspaces().cloned().collect::<Vec<_>>()
535 })?;
536
537 for workspace in workspaces {
538 let should_continue = workspace
539 .update_in(cx, |workspace, window, cx| {
540 workspace.prepare_to_close(CloseIntent::CloseWindow, window, cx)
541 })?
542 .await?;
543 if !should_continue {
544 return anyhow::Ok(());
545 }
546 }
547
548 cx.update(|window, _cx| {
549 window.remove_window();
550 })?;
551
552 anyhow::Ok(())
553 })
554 .detach_and_log_err(cx);
555 }
556
557 fn subscribe_to_workspace(
558 workspace: &Entity<Workspace>,
559 window: &Window,
560 cx: &mut Context<Self>,
561 ) {
562 let project = workspace.read(cx).project().clone();
563 cx.subscribe_in(&project, window, {
564 let workspace = workspace.downgrade();
565 move |this, _project, event, _window, cx| match event {
566 project::Event::WorktreeAdded(_)
567 | project::Event::WorktreeRemoved(_)
568 | project::Event::WorktreeUpdatedRootRepoCommonDir(_) => {
569 if let Some(workspace) = workspace.upgrade() {
570 this.handle_workspace_key_change(&workspace, cx);
571 }
572 }
573 _ => {}
574 }
575 })
576 .detach();
577
578 cx.subscribe_in(workspace, window, |this, workspace, event, window, cx| {
579 if let WorkspaceEvent::Activate = event {
580 this.activate(workspace.clone(), window, cx);
581 }
582 })
583 .detach();
584 }
585
586 fn handle_workspace_key_change(
587 &mut self,
588 workspace: &Entity<Workspace>,
589 cx: &mut Context<Self>,
590 ) {
591 let workspace_id = workspace.entity_id();
592 let old_key = self.project_group_key_for_workspace(workspace, cx);
593 let new_key = workspace.read(cx).project_group_key(cx);
594
595 if new_key.path_list().paths().is_empty() || old_key == new_key {
596 return;
597 }
598
599 let active_workspace = self.workspace().clone();
600
601 self.set_workspace_group_key(workspace, new_key.clone());
602
603 let changed_root_paths = workspace.read(cx).root_paths(cx);
604 let old_paths = old_key.path_list().paths();
605 let new_paths = new_key.path_list().paths();
606
607 // Remove workspaces that already had the new key and have the same
608 // root paths (true duplicates that this workspace is replacing).
609 //
610 // NOTE: These are dropped without prompting for unsaved changes because
611 // the user explicitly added a folder that makes this workspace
612 // identical to the duplicate — they are intentionally overwriting it.
613 let duplicate_workspaces: Vec<Entity<Workspace>> = self
614 .workspaces
615 .iter()
616 .filter(|ws| {
617 ws.entity_id() != workspace_id
618 && self.project_group_key_for_workspace(ws, cx) == new_key
619 && ws.read(cx).root_paths(cx) == changed_root_paths
620 })
621 .cloned()
622 .collect();
623
624 for ws in &duplicate_workspaces {
625 self.detach_workspace(ws, cx);
626 self.workspaces.retain(|w| w != ws);
627 }
628
629 // Propagate folder adds/removes to linked worktree siblings
630 // (different root paths, same old key) so they stay in the group.
631 let group_workspaces: Vec<Entity<Workspace>> = self
632 .workspaces
633 .iter()
634 .filter(|ws| {
635 ws.entity_id() != workspace_id
636 && self.project_group_key_for_workspace(ws, cx) == old_key
637 })
638 .cloned()
639 .collect();
640
641 for workspace in &group_workspaces {
642 // Pre-set this to stop later WorktreeAdded events from triggering
643 self.set_workspace_group_key(&workspace, new_key.clone());
644
645 let project = workspace.read(cx).project().clone();
646
647 for added_path in new_paths.iter().filter(|p| !old_paths.contains(p)) {
648 project
649 .update(cx, |project, cx| {
650 project.find_or_create_worktree(added_path, true, cx)
651 })
652 .detach_and_log_err(cx);
653 }
654
655 for removed_path in old_paths.iter().filter(|p| !new_paths.contains(p)) {
656 project.update(cx, |project, cx| {
657 project.remove_worktree_for_main_worktree_path(removed_path, cx);
658 });
659 }
660 }
661
662 // Restore the active workspace after removals may have shifted
663 // the index. If the previously active workspace was removed,
664 // fall back to the workspace whose key just changed.
665 if let ActiveWorkspace::Persistent(_) = &self.active_workspace {
666 let target = if self.workspaces.contains(&active_workspace) {
667 &active_workspace
668 } else {
669 workspace
670 };
671 if let Some(new_index) = self.workspaces.iter().position(|ws| ws == target) {
672 self.active_workspace = ActiveWorkspace::Persistent(new_index);
673 }
674 }
675
676 self.remove_stale_project_group_keys(cx);
677
678 cx.emit(MultiWorkspaceEvent::ProjectGroupKeyChanged { old_key, new_key });
679 self.serialize(cx);
680 cx.notify();
681 }
682
683 fn add_project_group_key(&mut self, project_group_key: ProjectGroupKey) {
684 if project_group_key.path_list().paths().is_empty() {
685 return;
686 }
687 if self.project_group_keys.contains(&project_group_key) {
688 return;
689 }
690 // Store newest first so the vec is in "most recently added"
691 self.project_group_keys.insert(0, project_group_key);
692 }
693
694 pub(crate) fn set_workspace_group_key(
695 &mut self,
696 workspace: &Entity<Workspace>,
697 project_group_key: ProjectGroupKey,
698 ) {
699 self.workspace_group_keys
700 .insert(workspace.entity_id(), project_group_key.clone());
701 self.add_project_group_key(project_group_key);
702 }
703
704 pub fn project_group_key_for_workspace(
705 &self,
706 workspace: &Entity<Workspace>,
707 cx: &App,
708 ) -> ProjectGroupKey {
709 self.workspace_group_keys
710 .get(&workspace.entity_id())
711 .cloned()
712 .unwrap_or_else(|| workspace.read(cx).project_group_key(cx))
713 }
714
715 fn remove_stale_project_group_keys(&mut self, cx: &App) {
716 let workspace_keys: HashSet<ProjectGroupKey> = self
717 .workspaces
718 .iter()
719 .map(|workspace| self.project_group_key_for_workspace(workspace, cx))
720 .collect();
721 self.project_group_keys
722 .retain(|key| workspace_keys.contains(key));
723 }
724
725 pub fn restore_project_group_keys(&mut self, keys: Vec<ProjectGroupKey>) {
726 let mut restored: Vec<ProjectGroupKey> = Vec::with_capacity(keys.len());
727 for key in keys {
728 if key.path_list().paths().is_empty() {
729 continue;
730 }
731 if !restored.contains(&key) {
732 restored.push(key);
733 }
734 }
735 for existing_key in &self.project_group_keys {
736 if !restored.contains(existing_key) {
737 restored.push(existing_key.clone());
738 }
739 }
740 self.project_group_keys = restored;
741 }
742
743 pub fn project_group_keys(&self) -> impl Iterator<Item = &ProjectGroupKey> {
744 self.project_group_keys.iter()
745 }
746
747 /// Returns the project groups, ordered by most recently added.
748 pub fn project_groups(
749 &self,
750 cx: &App,
751 ) -> impl Iterator<Item = (ProjectGroupKey, Vec<Entity<Workspace>>)> {
752 let mut groups = self
753 .project_group_keys
754 .iter()
755 .map(|key| (key.clone(), Vec::new()))
756 .collect::<Vec<_>>();
757 for workspace in &self.workspaces {
758 let key = self.project_group_key_for_workspace(workspace, cx);
759 if let Some((_, workspaces)) = groups.iter_mut().find(|(k, _)| k == &key) {
760 workspaces.push(workspace.clone());
761 }
762 }
763 groups.into_iter()
764 }
765
766 pub fn workspaces_for_project_group(
767 &self,
768 project_group_key: &ProjectGroupKey,
769 cx: &App,
770 ) -> impl Iterator<Item = &Entity<Workspace>> {
771 self.workspaces.iter().filter(move |workspace| {
772 self.project_group_key_for_workspace(workspace, cx) == *project_group_key
773 })
774 }
775
776 pub fn remove_folder_from_project_group(
777 &mut self,
778 project_group_key: &ProjectGroupKey,
779 path: &Path,
780 cx: &mut Context<Self>,
781 ) {
782 let new_path_list = project_group_key.path_list().without_path(path);
783 if new_path_list.is_empty() {
784 return;
785 }
786
787 let new_key = ProjectGroupKey::new(project_group_key.host(), new_path_list);
788
789 let workspaces: Vec<_> = self
790 .workspaces_for_project_group(project_group_key, cx)
791 .cloned()
792 .collect();
793
794 self.add_project_group_key(new_key);
795
796 for workspace in workspaces {
797 let project = workspace.read(cx).project().clone();
798 project.update(cx, |project, cx| {
799 project.remove_worktree_for_main_worktree_path(path, cx);
800 });
801 }
802
803 self.serialize(cx);
804 cx.notify();
805 }
806
807 pub fn prompt_to_add_folders_to_project_group(
808 &mut self,
809 key: &ProjectGroupKey,
810 window: &mut Window,
811 cx: &mut Context<Self>,
812 ) {
813 let paths = self.workspace().update(cx, |workspace, cx| {
814 workspace.prompt_for_open_path(
815 PathPromptOptions {
816 files: false,
817 directories: true,
818 multiple: true,
819 prompt: None,
820 },
821 DirectoryLister::Project(workspace.project().clone()),
822 window,
823 cx,
824 )
825 });
826
827 let key = key.clone();
828 cx.spawn_in(window, async move |this, cx| {
829 if let Some(new_paths) = paths.await.ok().flatten() {
830 if !new_paths.is_empty() {
831 this.update(cx, |multi_workspace, cx| {
832 multi_workspace.add_folders_to_project_group(&key, new_paths, cx);
833 })?;
834 }
835 }
836 anyhow::Ok(())
837 })
838 .detach_and_log_err(cx);
839 }
840
841 pub fn add_folders_to_project_group(
842 &mut self,
843 project_group_key: &ProjectGroupKey,
844 new_paths: Vec<PathBuf>,
845 cx: &mut Context<Self>,
846 ) {
847 let mut all_paths: Vec<PathBuf> = project_group_key.path_list().paths().to_vec();
848 all_paths.extend(new_paths.iter().cloned());
849 let new_path_list = PathList::new(&all_paths);
850 let new_key = ProjectGroupKey::new(project_group_key.host(), new_path_list);
851
852 let workspaces: Vec<_> = self
853 .workspaces_for_project_group(project_group_key, cx)
854 .cloned()
855 .collect();
856
857 self.add_project_group_key(new_key);
858
859 for workspace in workspaces {
860 let project = workspace.read(cx).project().clone();
861 for path in &new_paths {
862 project
863 .update(cx, |project, cx| {
864 project.find_or_create_worktree(path, true, cx)
865 })
866 .detach_and_log_err(cx);
867 }
868 }
869
870 self.serialize(cx);
871 cx.notify();
872 }
873
874 pub fn remove_project_group(
875 &mut self,
876 key: &ProjectGroupKey,
877 window: &mut Window,
878 cx: &mut Context<Self>,
879 ) -> Task<Result<bool>> {
880 let workspaces: Vec<_> = self
881 .workspaces_for_project_group(key, cx)
882 .cloned()
883 .collect();
884
885 // Compute the neighbor while the key is still in the list.
886 let neighbor_key = {
887 let pos = self.project_group_keys.iter().position(|k| k == key);
888 pos.and_then(|pos| {
889 // Keys are in display order, so pos+1 is below
890 // and pos-1 is above. Try below first.
891 self.project_group_keys.get(pos + 1).or_else(|| {
892 pos.checked_sub(1)
893 .and_then(|i| self.project_group_keys.get(i))
894 })
895 })
896 .cloned()
897 };
898
899 // Now remove the key.
900 self.project_group_keys.retain(|k| k != key);
901
902 self.remove(
903 workspaces,
904 move |this, window, cx| {
905 if let Some(neighbor_key) = neighbor_key {
906 return this.find_or_create_local_workspace(
907 neighbor_key.path_list().clone(),
908 window,
909 cx,
910 );
911 }
912
913 // No other project groups remain — create an empty workspace.
914 let app_state = this.workspace().read(cx).app_state().clone();
915 let project = Project::local(
916 app_state.client.clone(),
917 app_state.node_runtime.clone(),
918 app_state.user_store.clone(),
919 app_state.languages.clone(),
920 app_state.fs.clone(),
921 None,
922 project::LocalProjectFlags::default(),
923 cx,
924 );
925 let new_workspace =
926 cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
927 Task::ready(Ok(new_workspace))
928 },
929 window,
930 cx,
931 )
932 }
933
934 /// Finds an existing workspace whose root paths and host exactly match.
935 pub fn workspace_for_paths(
936 &self,
937 path_list: &PathList,
938 host: Option<&RemoteConnectionOptions>,
939 cx: &App,
940 ) -> Option<Entity<Workspace>> {
941 self.workspaces
942 .iter()
943 .find(|ws| {
944 let key = ws.read(cx).project_group_key(cx);
945 key.host().as_ref() == host
946 && PathList::new(&ws.read(cx).root_paths(cx)) == *path_list
947 })
948 .cloned()
949 }
950
951 /// Finds an existing workspace whose paths match, or creates a new one.
952 ///
953 /// For local projects (`host` is `None`), this delegates to
954 /// [`Self::find_or_create_local_workspace`]. For remote projects, it
955 /// tries an exact path match and, if no existing workspace is found,
956 /// calls `connect_remote` to establish a connection and creates a new
957 /// remote workspace.
958 ///
959 /// The `connect_remote` closure is responsible for any user-facing
960 /// connection UI (e.g. password prompts). It receives the connection
961 /// options and should return a [`Task`] that resolves to the
962 /// [`RemoteClient`] session, or `None` if the connection was
963 /// cancelled.
964 pub fn find_or_create_workspace(
965 &mut self,
966 paths: PathList,
967 host: Option<RemoteConnectionOptions>,
968 provisional_project_group_key: Option<ProjectGroupKey>,
969 connect_remote: impl FnOnce(
970 RemoteConnectionOptions,
971 &mut Window,
972 &mut Context<Self>,
973 ) -> Task<Result<Option<Entity<remote::RemoteClient>>>>
974 + 'static,
975 window: &mut Window,
976 cx: &mut Context<Self>,
977 ) -> Task<Result<Entity<Workspace>>> {
978 if let Some(workspace) = self.workspace_for_paths(&paths, host.as_ref(), cx) {
979 self.activate(workspace.clone(), window, cx);
980 return Task::ready(Ok(workspace));
981 }
982
983 let Some(connection_options) = host else {
984 return self.find_or_create_local_workspace(paths, window, cx);
985 };
986
987 let app_state = self.workspace().read(cx).app_state().clone();
988 let window_handle = window.window_handle().downcast::<MultiWorkspace>();
989 let connect_task = connect_remote(connection_options.clone(), window, cx);
990 let paths_vec = paths.paths().to_vec();
991
992 cx.spawn(async move |_this, cx| {
993 let session = connect_task
994 .await?
995 .ok_or_else(|| anyhow::anyhow!("Remote connection was cancelled"))?;
996
997 let new_project = cx.update(|cx| {
998 Project::remote(
999 session,
1000 app_state.client.clone(),
1001 app_state.node_runtime.clone(),
1002 app_state.user_store.clone(),
1003 app_state.languages.clone(),
1004 app_state.fs.clone(),
1005 true,
1006 cx,
1007 )
1008 });
1009
1010 let window_handle =
1011 window_handle.ok_or_else(|| anyhow::anyhow!("Window is not a MultiWorkspace"))?;
1012
1013 open_remote_project_with_existing_connection(
1014 connection_options,
1015 new_project,
1016 paths_vec,
1017 app_state,
1018 window_handle,
1019 provisional_project_group_key,
1020 cx,
1021 )
1022 .await?;
1023
1024 window_handle.update(cx, |multi_workspace, window, cx| {
1025 let workspace = multi_workspace.workspace().clone();
1026 multi_workspace.add(workspace.clone(), window, cx);
1027 workspace
1028 })
1029 })
1030 }
1031
1032 /// Finds an existing workspace in this multi-workspace whose paths match,
1033 /// or creates a new one (deserializing its saved state from the database).
1034 /// Never searches other windows or matches workspaces with a superset of
1035 /// the requested paths.
1036 pub fn find_or_create_local_workspace(
1037 &mut self,
1038 path_list: PathList,
1039 window: &mut Window,
1040 cx: &mut Context<Self>,
1041 ) -> Task<Result<Entity<Workspace>>> {
1042 if let Some(workspace) = self.workspace_for_paths(&path_list, None, cx) {
1043 self.activate(workspace.clone(), window, cx);
1044 return Task::ready(Ok(workspace));
1045 }
1046
1047 if let Some(transient) = self.active_workspace.transient_workspace() {
1048 if transient.read(cx).project_group_key(cx).path_list() == &path_list {
1049 return Task::ready(Ok(transient.clone()));
1050 }
1051 }
1052
1053 let paths = path_list.paths().to_vec();
1054 let app_state = self.workspace().read(cx).app_state().clone();
1055 let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
1056
1057 cx.spawn(async move |_this, cx| {
1058 let result = cx
1059 .update(|cx| {
1060 Workspace::new_local(
1061 paths,
1062 app_state,
1063 requesting_window,
1064 None,
1065 None,
1066 OpenMode::Activate,
1067 cx,
1068 )
1069 })
1070 .await?;
1071 Ok(result.workspace)
1072 })
1073 }
1074
1075 pub fn workspace(&self) -> &Entity<Workspace> {
1076 match &self.active_workspace {
1077 ActiveWorkspace::Persistent(index) => &self.workspaces[*index],
1078 ActiveWorkspace::Transient(workspace) => workspace,
1079 }
1080 }
1081
1082 pub fn workspaces(&self) -> impl Iterator<Item = &Entity<Workspace>> {
1083 self.workspaces
1084 .iter()
1085 .chain(self.active_workspace.transient_workspace())
1086 }
1087
1088 /// Adds a workspace to this window as persistent without changing which
1089 /// workspace is active. Unlike `activate()`, this always inserts into the
1090 /// persistent list regardless of sidebar state — it's used for system-
1091 /// initiated additions like deserialization and worktree discovery.
1092 pub fn add(&mut self, workspace: Entity<Workspace>, window: &Window, cx: &mut Context<Self>) {
1093 self.insert_workspace(workspace, window, cx);
1094 }
1095
1096 /// Ensures the workspace is in the multiworkspace and makes it the active one.
1097 pub fn activate(
1098 &mut self,
1099 workspace: Entity<Workspace>,
1100 window: &mut Window,
1101 cx: &mut Context<Self>,
1102 ) {
1103 // Re-activating the current workspace is a no-op.
1104 if self.workspace() == &workspace {
1105 self.focus_active_workspace(window, cx);
1106 return;
1107 }
1108
1109 // Resolve where we're going.
1110 let new_index = if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
1111 Some(index)
1112 } else if self.sidebar_open {
1113 Some(self.insert_workspace(workspace.clone(), &*window, cx))
1114 } else {
1115 None
1116 };
1117
1118 // Transition the active workspace.
1119 if let Some(index) = new_index {
1120 if let Some(old) = self.active_workspace.set_persistent(index) {
1121 if self.sidebar_open {
1122 self.promote_transient(old, cx);
1123 } else {
1124 self.detach_workspace(&old, cx);
1125 }
1126 }
1127 } else {
1128 Self::subscribe_to_workspace(&workspace, window, cx);
1129 let weak_self = cx.weak_entity();
1130 workspace.update(cx, |workspace, cx| {
1131 workspace.set_multi_workspace(weak_self, cx);
1132 });
1133 if let Some(old) = self.active_workspace.set_transient(workspace) {
1134 self.detach_workspace(&old, cx);
1135 }
1136 }
1137
1138 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
1139 self.serialize(cx);
1140 self.focus_active_workspace(window, cx);
1141 cx.notify();
1142 }
1143
1144 /// Promotes the currently active workspace to persistent if it is
1145 /// transient, so it is retained across workspace switches even when
1146 /// the sidebar is closed. No-op if the workspace is already persistent.
1147 pub fn retain_active_workspace(&mut self, cx: &mut Context<Self>) {
1148 if let ActiveWorkspace::Transient(workspace) = &self.active_workspace {
1149 let workspace = workspace.clone();
1150 let index = self.promote_transient(workspace, cx);
1151 self.active_workspace = ActiveWorkspace::Persistent(index);
1152 self.serialize(cx);
1153 cx.notify();
1154 }
1155 }
1156
1157 /// Promotes a former transient workspace into the persistent list.
1158 /// Returns the index of the newly inserted workspace.
1159 fn promote_transient(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
1160 let project_group_key = self.project_group_key_for_workspace(&workspace, cx);
1161 self.set_workspace_group_key(&workspace, project_group_key);
1162 self.workspaces.push(workspace.clone());
1163 cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
1164 self.workspaces.len() - 1
1165 }
1166
1167 /// Collapses to a single transient workspace, discarding all persistent
1168 /// workspaces. Used when multi-workspace is disabled (e.g. disable_ai).
1169 fn collapse_to_single_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1170 if self.sidebar_open {
1171 self.close_sidebar(window, cx);
1172 }
1173 let active = self.workspace().clone();
1174 for workspace in std::mem::take(&mut self.workspaces) {
1175 if workspace != active {
1176 self.detach_workspace(&workspace, cx);
1177 }
1178 }
1179 self.project_group_keys.clear();
1180 self.workspace_group_keys.clear();
1181 self.active_workspace = ActiveWorkspace::Transient(active);
1182 cx.notify();
1183 }
1184
1185 /// Inserts a workspace into the list if not already present. Returns the
1186 /// index of the workspace (existing or newly inserted). Does not change
1187 /// the active workspace index.
1188 fn insert_workspace(
1189 &mut self,
1190 workspace: Entity<Workspace>,
1191 window: &Window,
1192 cx: &mut Context<Self>,
1193 ) -> usize {
1194 if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
1195 index
1196 } else {
1197 let project_group_key = self.project_group_key_for_workspace(&workspace, cx);
1198
1199 Self::subscribe_to_workspace(&workspace, window, cx);
1200 self.sync_sidebar_to_workspace(&workspace, cx);
1201 let weak_self = cx.weak_entity();
1202 workspace.update(cx, |workspace, cx| {
1203 workspace.set_multi_workspace(weak_self, cx);
1204 });
1205
1206 self.set_workspace_group_key(&workspace, project_group_key);
1207 self.workspaces.push(workspace.clone());
1208 cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
1209 cx.notify();
1210 self.workspaces.len() - 1
1211 }
1212 }
1213
1214 /// Detaches a workspace: clears session state, DB binding, cached
1215 /// group key, and emits `WorkspaceRemoved`. The DB row is preserved
1216 /// so the workspace still appears in the recent-projects list.
1217 fn detach_workspace(&mut self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
1218 self.workspace_group_keys.remove(&workspace.entity_id());
1219 cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
1220 workspace.update(cx, |workspace, _cx| {
1221 workspace.session_id.take();
1222 workspace._schedule_serialize_workspace.take();
1223 workspace._serialize_workspace_task.take();
1224 });
1225
1226 if let Some(workspace_id) = workspace.read(cx).database_id() {
1227 let db = crate::persistence::WorkspaceDb::global(cx);
1228 self.pending_removal_tasks.retain(|task| !task.is_ready());
1229 self.pending_removal_tasks
1230 .push(cx.background_spawn(async move {
1231 db.set_session_binding(workspace_id, None, None)
1232 .await
1233 .log_err();
1234 }));
1235 }
1236 }
1237
1238 fn sync_sidebar_to_workspace(&self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
1239 if self.sidebar_open() {
1240 let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
1241 workspace.update(cx, |workspace, _| {
1242 workspace.set_sidebar_focus_handle(sidebar_focus_handle);
1243 });
1244 }
1245 }
1246
1247 pub(crate) fn serialize(&mut self, cx: &mut Context<Self>) {
1248 self._serialize_task = Some(cx.spawn(async move |this, cx| {
1249 let Some((window_id, state)) = this
1250 .read_with(cx, |this, cx| {
1251 let state = MultiWorkspaceState {
1252 active_workspace_id: this.workspace().read(cx).database_id(),
1253 project_group_keys: this
1254 .project_group_keys()
1255 .cloned()
1256 .map(Into::into)
1257 .collect::<Vec<_>>(),
1258 sidebar_open: this.sidebar_open,
1259 sidebar_state: this.sidebar.as_ref().and_then(|s| s.serialized_state(cx)),
1260 };
1261 (this.window_id, state)
1262 })
1263 .ok()
1264 else {
1265 return;
1266 };
1267 let kvp = cx.update(|cx| db::kvp::KeyValueStore::global(cx));
1268 crate::persistence::write_multi_workspace_state(&kvp, window_id, state).await;
1269 }));
1270 }
1271
1272 /// Returns the in-flight serialization task (if any) so the caller can
1273 /// await it. Used by the quit handler to ensure pending DB writes
1274 /// complete before the process exits.
1275 pub fn flush_serialization(&mut self) -> Task<()> {
1276 self._serialize_task.take().unwrap_or(Task::ready(()))
1277 }
1278
1279 fn app_will_quit(&mut self, _cx: &mut Context<Self>) -> impl Future<Output = ()> + use<> {
1280 let mut tasks: Vec<Task<()>> = Vec::new();
1281 if let Some(task) = self._serialize_task.take() {
1282 tasks.push(task);
1283 }
1284 tasks.extend(std::mem::take(&mut self.pending_removal_tasks));
1285
1286 async move {
1287 futures::future::join_all(tasks).await;
1288 }
1289 }
1290
1291 pub fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
1292 // If a dock panel is zoomed, focus it instead of the center pane.
1293 // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
1294 // which closes the zoomed dock.
1295 let focus_handle = {
1296 let workspace = self.workspace().read(cx);
1297 let mut target = None;
1298 for dock in workspace.all_docks() {
1299 let dock = dock.read(cx);
1300 if dock.is_open() {
1301 if let Some(panel) = dock.active_panel() {
1302 if panel.is_zoomed(window, cx) {
1303 target = Some(panel.panel_focus_handle(cx));
1304 break;
1305 }
1306 }
1307 }
1308 }
1309 target.unwrap_or_else(|| {
1310 let pane = workspace.active_pane().clone();
1311 pane.read(cx).focus_handle(cx)
1312 })
1313 };
1314 window.focus(&focus_handle, cx);
1315 }
1316
1317 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
1318 self.workspace().read(cx).panel::<T>(cx)
1319 }
1320
1321 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
1322 self.workspace().read(cx).active_modal::<V>(cx)
1323 }
1324
1325 pub fn add_panel<T: Panel>(
1326 &mut self,
1327 panel: Entity<T>,
1328 window: &mut Window,
1329 cx: &mut Context<Self>,
1330 ) {
1331 self.workspace().update(cx, |workspace, cx| {
1332 workspace.add_panel(panel, window, cx);
1333 });
1334 }
1335
1336 pub fn focus_panel<T: Panel>(
1337 &mut self,
1338 window: &mut Window,
1339 cx: &mut Context<Self>,
1340 ) -> Option<Entity<T>> {
1341 self.workspace()
1342 .update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
1343 }
1344
1345 // used in a test
1346 pub fn toggle_modal<V: ModalView, B>(
1347 &mut self,
1348 window: &mut Window,
1349 cx: &mut Context<Self>,
1350 build: B,
1351 ) where
1352 B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
1353 {
1354 self.workspace().update(cx, |workspace, cx| {
1355 workspace.toggle_modal(window, cx, build);
1356 });
1357 }
1358
1359 pub fn toggle_dock(
1360 &mut self,
1361 dock_side: DockPosition,
1362 window: &mut Window,
1363 cx: &mut Context<Self>,
1364 ) {
1365 self.workspace().update(cx, |workspace, cx| {
1366 workspace.toggle_dock(dock_side, window, cx);
1367 });
1368 }
1369
1370 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
1371 self.workspace().read(cx).active_item_as::<I>(cx)
1372 }
1373
1374 pub fn items_of_type<'a, T: Item>(
1375 &'a self,
1376 cx: &'a App,
1377 ) -> impl 'a + Iterator<Item = Entity<T>> {
1378 self.workspace().read(cx).items_of_type::<T>(cx)
1379 }
1380
1381 pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
1382 self.workspace().read(cx).database_id()
1383 }
1384
1385 pub fn take_pending_removal_tasks(&mut self) -> Vec<Task<()>> {
1386 let tasks: Vec<Task<()>> = std::mem::take(&mut self.pending_removal_tasks)
1387 .into_iter()
1388 .filter(|task| !task.is_ready())
1389 .collect();
1390 tasks
1391 }
1392
1393 #[cfg(any(test, feature = "test-support"))]
1394 pub fn assert_project_group_key_integrity(&self, cx: &App) -> anyhow::Result<()> {
1395 let stored_keys: HashSet<&ProjectGroupKey> = self.project_group_keys().collect();
1396
1397 let workspace_group_keys: HashSet<&ProjectGroupKey> =
1398 self.workspace_group_keys.values().collect();
1399 let extra_keys = &workspace_group_keys - &stored_keys;
1400 anyhow::ensure!(
1401 extra_keys.is_empty(),
1402 "workspace_group_keys values not in project_group_keys: {:?}",
1403 extra_keys,
1404 );
1405
1406 let cached_ids: HashSet<EntityId> = self.workspace_group_keys.keys().copied().collect();
1407 let workspace_ids: HashSet<EntityId> =
1408 self.workspaces.iter().map(|ws| ws.entity_id()).collect();
1409 anyhow::ensure!(
1410 cached_ids == workspace_ids,
1411 "workspace_group_keys entity IDs don't match workspaces.\n\
1412 only in cache: {:?}\n\
1413 only in workspaces: {:?}",
1414 &cached_ids - &workspace_ids,
1415 &workspace_ids - &cached_ids,
1416 );
1417
1418 for workspace in self.workspaces() {
1419 let live_key = workspace.read(cx).project_group_key(cx);
1420 let cached_key = &self.workspace_group_keys[&workspace.entity_id()];
1421 anyhow::ensure!(
1422 *cached_key == live_key,
1423 "workspace {:?} has live key {:?} but cached key {:?}",
1424 workspace.entity_id(),
1425 live_key,
1426 cached_key,
1427 );
1428 }
1429
1430 Ok(())
1431 }
1432
1433 #[cfg(any(test, feature = "test-support"))]
1434 pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
1435 self.workspace().update(cx, |workspace, _cx| {
1436 workspace.set_random_database_id();
1437 });
1438 }
1439
1440 #[cfg(any(test, feature = "test-support"))]
1441 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
1442 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
1443 Self::new(workspace, window, cx)
1444 }
1445
1446 #[cfg(any(test, feature = "test-support"))]
1447 pub fn test_add_workspace(
1448 &mut self,
1449 project: Entity<Project>,
1450 window: &mut Window,
1451 cx: &mut Context<Self>,
1452 ) -> Entity<Workspace> {
1453 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
1454 self.activate(workspace.clone(), window, cx);
1455 workspace
1456 }
1457
1458 #[cfg(any(test, feature = "test-support"))]
1459 pub fn create_test_workspace(
1460 &mut self,
1461 window: &mut Window,
1462 cx: &mut Context<Self>,
1463 ) -> Task<()> {
1464 let app_state = self.workspace().read(cx).app_state().clone();
1465 let project = Project::local(
1466 app_state.client.clone(),
1467 app_state.node_runtime.clone(),
1468 app_state.user_store.clone(),
1469 app_state.languages.clone(),
1470 app_state.fs.clone(),
1471 None,
1472 project::LocalProjectFlags::default(),
1473 cx,
1474 );
1475 let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
1476 self.activate(new_workspace.clone(), window, cx);
1477
1478 let weak_workspace = new_workspace.downgrade();
1479 let db = crate::persistence::WorkspaceDb::global(cx);
1480 cx.spawn_in(window, async move |this, cx| {
1481 let workspace_id = db.next_id().await.unwrap();
1482 let workspace = weak_workspace.upgrade().unwrap();
1483 let task: Task<()> = this
1484 .update_in(cx, |this, window, cx| {
1485 let session_id = workspace.read(cx).session_id();
1486 let window_id = window.window_handle().window_id().as_u64();
1487 workspace.update(cx, |workspace, _cx| {
1488 workspace.set_database_id(workspace_id);
1489 });
1490 this.serialize(cx);
1491 let db = db.clone();
1492 cx.background_spawn(async move {
1493 db.set_session_binding(workspace_id, session_id, Some(window_id))
1494 .await
1495 .log_err();
1496 })
1497 })
1498 .unwrap();
1499 task.await
1500 })
1501 }
1502
1503 /// Removes one or more workspaces from this multi-workspace.
1504 ///
1505 /// If the active workspace is among those being removed,
1506 /// `fallback_workspace` is called **synchronously before the removal
1507 /// begins** to produce a `Task` that resolves to the workspace that
1508 /// should become active. The fallback must not be one of the
1509 /// workspaces being removed.
1510 ///
1511 /// Returns `true` if any workspaces were actually removed.
1512 pub fn remove(
1513 &mut self,
1514 workspaces: impl IntoIterator<Item = Entity<Workspace>>,
1515 fallback_workspace: impl FnOnce(
1516 &mut Self,
1517 &mut Window,
1518 &mut Context<Self>,
1519 ) -> Task<Result<Entity<Workspace>>>,
1520 window: &mut Window,
1521 cx: &mut Context<Self>,
1522 ) -> Task<Result<bool>> {
1523 let workspaces: Vec<_> = workspaces.into_iter().collect();
1524
1525 if workspaces.is_empty() {
1526 return Task::ready(Ok(false));
1527 }
1528
1529 let removing_active = workspaces.iter().any(|ws| ws == self.workspace());
1530 let original_active = self.workspace().clone();
1531
1532 let fallback_task = removing_active.then(|| fallback_workspace(self, window, cx));
1533
1534 cx.spawn_in(window, async move |this, cx| {
1535 // Prompt each workspace for unsaved changes. If any workspace
1536 // has dirty buffers, save_all_internal will emit Activate to
1537 // bring it into view before showing the save dialog.
1538 for workspace in &workspaces {
1539 let should_continue = workspace
1540 .update_in(cx, |workspace, window, cx| {
1541 workspace.save_all_internal(crate::SaveIntent::Close, window, cx)
1542 })?
1543 .await?;
1544
1545 if !should_continue {
1546 return Ok(false);
1547 }
1548 }
1549
1550 // If we're removing the active workspace, await the
1551 // fallback and switch to it before tearing anything down.
1552 // Otherwise restore the original active workspace in case
1553 // prompting switched away from it.
1554 if let Some(fallback_task) = fallback_task {
1555 let new_active = fallback_task.await?;
1556
1557 this.update_in(cx, |this, window, cx| {
1558 assert!(
1559 !workspaces.contains(&new_active),
1560 "fallback workspace must not be one of the workspaces being removed"
1561 );
1562 this.activate(new_active, window, cx);
1563 })?;
1564 } else {
1565 this.update_in(cx, |this, window, cx| {
1566 if *this.workspace() != original_active {
1567 this.activate(original_active, window, cx);
1568 }
1569 })?;
1570 }
1571
1572 // Actually remove the workspaces.
1573 this.update_in(cx, |this, _, cx| {
1574 // Save a handle to the active workspace so we can restore
1575 // its index after the removals shift the vec around.
1576 let active_workspace = this.workspace().clone();
1577
1578 let mut removed_workspaces: Vec<Entity<Workspace>> = Vec::new();
1579
1580 this.workspaces.retain(|ws| {
1581 if workspaces.contains(ws) {
1582 removed_workspaces.push(ws.clone());
1583 false
1584 } else {
1585 true
1586 }
1587 });
1588
1589 for workspace in &removed_workspaces {
1590 this.detach_workspace(workspace, cx);
1591 }
1592
1593 let removed_any = !removed_workspaces.is_empty();
1594
1595 if removed_any {
1596 // Restore the active workspace index after removals.
1597 if let Some(new_index) = this
1598 .workspaces
1599 .iter()
1600 .position(|ws| ws == &active_workspace)
1601 {
1602 this.active_workspace = ActiveWorkspace::Persistent(new_index);
1603 }
1604
1605 this.serialize(cx);
1606 cx.notify();
1607 }
1608
1609 Ok(removed_any)
1610 })?
1611 })
1612 }
1613
1614 pub fn open_project(
1615 &mut self,
1616 paths: Vec<PathBuf>,
1617 open_mode: OpenMode,
1618 window: &mut Window,
1619 cx: &mut Context<Self>,
1620 ) -> Task<Result<Entity<Workspace>>> {
1621 if self.multi_workspace_enabled(cx) {
1622 self.find_or_create_local_workspace(PathList::new(&paths), window, cx)
1623 } else {
1624 let workspace = self.workspace().clone();
1625 cx.spawn_in(window, async move |_this, cx| {
1626 let should_continue = workspace
1627 .update_in(cx, |workspace, window, cx| {
1628 workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
1629 })?
1630 .await?;
1631 if should_continue {
1632 workspace
1633 .update_in(cx, |workspace, window, cx| {
1634 workspace.open_workspace_for_paths(open_mode, paths, window, cx)
1635 })?
1636 .await
1637 } else {
1638 Ok(workspace)
1639 }
1640 })
1641 }
1642 }
1643}
1644
1645impl Render for MultiWorkspace {
1646 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1647 let multi_workspace_enabled = self.multi_workspace_enabled(cx);
1648 let sidebar_side = self.sidebar_side(cx);
1649 let sidebar_on_right = sidebar_side == SidebarSide::Right;
1650
1651 let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open() {
1652 self.sidebar.as_ref().map(|sidebar_handle| {
1653 let weak = cx.weak_entity();
1654
1655 let sidebar_width = sidebar_handle.width(cx);
1656 let resize_handle = deferred(
1657 div()
1658 .id("sidebar-resize-handle")
1659 .absolute()
1660 .when(!sidebar_on_right, |el| {
1661 el.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
1662 })
1663 .when(sidebar_on_right, |el| {
1664 el.left(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
1665 })
1666 .top(px(0.))
1667 .h_full()
1668 .w(SIDEBAR_RESIZE_HANDLE_SIZE)
1669 .cursor_col_resize()
1670 .on_drag(DraggedSidebar, |dragged, _, _, cx| {
1671 cx.stop_propagation();
1672 cx.new(|_| dragged.clone())
1673 })
1674 .on_mouse_down(MouseButton::Left, |_, _, cx| {
1675 cx.stop_propagation();
1676 })
1677 .on_mouse_up(MouseButton::Left, move |event, _, cx| {
1678 if event.click_count == 2 {
1679 weak.update(cx, |this, cx| {
1680 if let Some(sidebar) = this.sidebar.as_mut() {
1681 sidebar.set_width(None, cx);
1682 }
1683 this.serialize(cx);
1684 })
1685 .ok();
1686 cx.stop_propagation();
1687 } else {
1688 weak.update(cx, |this, cx| {
1689 this.serialize(cx);
1690 })
1691 .ok();
1692 }
1693 })
1694 .occlude(),
1695 );
1696
1697 div()
1698 .id("sidebar-container")
1699 .relative()
1700 .h_full()
1701 .w(sidebar_width)
1702 .flex_shrink_0()
1703 .child(sidebar_handle.to_any())
1704 .child(resize_handle)
1705 .into_any_element()
1706 })
1707 } else {
1708 None
1709 };
1710
1711 let (left_sidebar, right_sidebar) = if sidebar_on_right {
1712 (None, sidebar)
1713 } else {
1714 (sidebar, None)
1715 };
1716
1717 let ui_font = theme_settings::setup_ui_font(window, cx);
1718 let text_color = cx.theme().colors().text;
1719
1720 let workspace = self.workspace().clone();
1721 let workspace_key_context = workspace.update(cx, |workspace, cx| workspace.key_context(cx));
1722 let root = workspace.update(cx, |workspace, cx| workspace.actions(h_flex(), window, cx));
1723
1724 client_side_decorations(
1725 root.key_context(workspace_key_context)
1726 .relative()
1727 .size_full()
1728 .font(ui_font)
1729 .text_color(text_color)
1730 .on_action(cx.listener(Self::close_window))
1731 .when(self.multi_workspace_enabled(cx), |this| {
1732 this.on_action(cx.listener(
1733 |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
1734 this.toggle_sidebar(window, cx);
1735 },
1736 ))
1737 .on_action(cx.listener(
1738 |this: &mut Self, _: &CloseWorkspaceSidebar, window, cx| {
1739 this.close_sidebar_action(window, cx);
1740 },
1741 ))
1742 .on_action(cx.listener(
1743 |this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| {
1744 this.focus_sidebar(window, cx);
1745 },
1746 ))
1747 .on_action(cx.listener(
1748 |this: &mut Self, action: &ToggleThreadSwitcher, window, cx| {
1749 if let Some(sidebar) = &this.sidebar {
1750 sidebar.toggle_thread_switcher(action.select_last, window, cx);
1751 }
1752 },
1753 ))
1754 .on_action(cx.listener(|this: &mut Self, _: &NextProject, window, cx| {
1755 if let Some(sidebar) = &this.sidebar {
1756 sidebar.cycle_project(true, window, cx);
1757 }
1758 }))
1759 .on_action(
1760 cx.listener(|this: &mut Self, _: &PreviousProject, window, cx| {
1761 if let Some(sidebar) = &this.sidebar {
1762 sidebar.cycle_project(false, window, cx);
1763 }
1764 }),
1765 )
1766 .on_action(cx.listener(|this: &mut Self, _: &NextThread, window, cx| {
1767 if let Some(sidebar) = &this.sidebar {
1768 sidebar.cycle_thread(true, window, cx);
1769 }
1770 }))
1771 .on_action(cx.listener(
1772 |this: &mut Self, _: &PreviousThread, window, cx| {
1773 if let Some(sidebar) = &this.sidebar {
1774 sidebar.cycle_thread(false, window, cx);
1775 }
1776 },
1777 ))
1778 })
1779 .when(
1780 self.sidebar_open() && self.multi_workspace_enabled(cx),
1781 |this| {
1782 this.on_drag_move(cx.listener(
1783 move |this: &mut Self,
1784 e: &DragMoveEvent<DraggedSidebar>,
1785 window,
1786 cx| {
1787 if let Some(sidebar) = &this.sidebar {
1788 let new_width = if sidebar_on_right {
1789 window.bounds().size.width - e.event.position.x
1790 } else {
1791 e.event.position.x
1792 };
1793 sidebar.set_width(Some(new_width), cx);
1794 }
1795 },
1796 ))
1797 },
1798 )
1799 .children(left_sidebar)
1800 .child(
1801 div()
1802 .flex()
1803 .flex_1()
1804 .size_full()
1805 .overflow_hidden()
1806 .child(self.workspace().clone()),
1807 )
1808 .children(right_sidebar)
1809 .child(self.workspace().read(cx).modal_layer.clone())
1810 .children(self.sidebar_overlay.as_ref().map(|view| {
1811 deferred(div().absolute().size_full().inset_0().occlude().child(
1812 v_flex().h(px(0.0)).top_20().items_center().child(
1813 h_flex().occlude().child(view.clone()).on_mouse_down(
1814 MouseButton::Left,
1815 |_, _, cx| {
1816 cx.stop_propagation();
1817 },
1818 ),
1819 ),
1820 ))
1821 .with_priority(2)
1822 })),
1823 window,
1824 cx,
1825 Tiling {
1826 left: !sidebar_on_right && multi_workspace_enabled && self.sidebar_open(),
1827 right: sidebar_on_right && multi_workspace_enabled && self.sidebar_open(),
1828 ..Tiling::default()
1829 },
1830 )
1831 }
1832}