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