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