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 = keys;
609 for existing_key in &self.project_group_keys {
610 if !restored.contains(existing_key) {
611 restored.push(existing_key.clone());
612 }
613 }
614 self.project_group_keys = restored;
615 }
616
617 pub fn project_group_keys(&self) -> impl Iterator<Item = &ProjectGroupKey> {
618 self.project_group_keys.iter()
619 }
620
621 /// Returns the project groups, ordered by most recently added.
622 pub fn project_groups(
623 &self,
624 cx: &App,
625 ) -> impl Iterator<Item = (ProjectGroupKey, Vec<Entity<Workspace>>)> {
626 let mut groups = self
627 .project_group_keys
628 .iter()
629 .rev()
630 .map(|key| (key.clone(), Vec::new()))
631 .collect::<Vec<_>>();
632 for workspace in &self.workspaces {
633 let key = workspace.read(cx).project_group_key(cx);
634 if let Some((_, workspaces)) = groups.iter_mut().find(|(k, _)| k == &key) {
635 workspaces.push(workspace.clone());
636 }
637 }
638 groups.into_iter()
639 }
640
641 pub fn workspaces_for_project_group(
642 &self,
643 project_group_key: &ProjectGroupKey,
644 cx: &App,
645 ) -> impl Iterator<Item = &Entity<Workspace>> {
646 self.workspaces
647 .iter()
648 .filter(move |ws| ws.read(cx).project_group_key(cx) == *project_group_key)
649 }
650
651 pub fn remove_folder_from_project_group(
652 &mut self,
653 project_group_key: &ProjectGroupKey,
654 path: &Path,
655 cx: &mut Context<Self>,
656 ) {
657 let new_path_list = project_group_key.path_list().without_path(path);
658 if new_path_list.is_empty() {
659 return;
660 }
661
662 let new_key = ProjectGroupKey::new(project_group_key.host(), new_path_list);
663
664 let workspaces: Vec<_> = self
665 .workspaces_for_project_group(project_group_key, cx)
666 .cloned()
667 .collect();
668
669 self.add_project_group_key(new_key);
670
671 for workspace in workspaces {
672 let project = workspace.read(cx).project().clone();
673 project.update(cx, |project, cx| {
674 project.remove_worktree_for_main_worktree_path(path, cx);
675 });
676 }
677
678 self.serialize(cx);
679 cx.notify();
680 }
681
682 pub fn prompt_to_add_folders_to_project_group(
683 &mut self,
684 key: &ProjectGroupKey,
685 window: &mut Window,
686 cx: &mut Context<Self>,
687 ) {
688 let paths = self.workspace().update(cx, |workspace, cx| {
689 workspace.prompt_for_open_path(
690 PathPromptOptions {
691 files: false,
692 directories: true,
693 multiple: true,
694 prompt: None,
695 },
696 DirectoryLister::Project(workspace.project().clone()),
697 window,
698 cx,
699 )
700 });
701
702 let key = key.clone();
703 cx.spawn_in(window, async move |this, cx| {
704 if let Some(new_paths) = paths.await.ok().flatten() {
705 if !new_paths.is_empty() {
706 this.update(cx, |multi_workspace, cx| {
707 multi_workspace.add_folders_to_project_group(&key, new_paths, cx);
708 })?;
709 }
710 }
711 anyhow::Ok(())
712 })
713 .detach_and_log_err(cx);
714 }
715
716 pub fn add_folders_to_project_group(
717 &mut self,
718 project_group_key: &ProjectGroupKey,
719 new_paths: Vec<PathBuf>,
720 cx: &mut Context<Self>,
721 ) {
722 let mut all_paths: Vec<PathBuf> = project_group_key.path_list().paths().to_vec();
723 all_paths.extend(new_paths.iter().cloned());
724 let new_path_list = PathList::new(&all_paths);
725 let new_key = ProjectGroupKey::new(project_group_key.host(), new_path_list);
726
727 let workspaces: Vec<_> = self
728 .workspaces_for_project_group(project_group_key, cx)
729 .cloned()
730 .collect();
731
732 self.add_project_group_key(new_key);
733
734 for workspace in workspaces {
735 let project = workspace.read(cx).project().clone();
736 for path in &new_paths {
737 project
738 .update(cx, |project, cx| {
739 project.find_or_create_worktree(path, true, cx)
740 })
741 .detach_and_log_err(cx);
742 }
743 }
744
745 self.serialize(cx);
746 cx.notify();
747 }
748
749 pub fn remove_project_group(
750 &mut self,
751 key: &ProjectGroupKey,
752 window: &mut Window,
753 cx: &mut Context<Self>,
754 ) {
755 self.project_group_keys.retain(|k| k != key);
756
757 let workspaces: Vec<_> = self
758 .workspaces_for_project_group(key, cx)
759 .cloned()
760 .collect();
761 for workspace in workspaces {
762 self.remove(&workspace, window, cx);
763 }
764
765 self.serialize(cx);
766 cx.notify();
767 }
768
769 /// Finds an existing workspace in this multi-workspace whose paths match,
770 /// or creates a new one (deserializing its saved state from the database).
771 /// Never searches other windows or matches workspaces with a superset of
772 /// the requested paths.
773 pub fn find_or_create_local_workspace(
774 &mut self,
775 path_list: PathList,
776 window: &mut Window,
777 cx: &mut Context<Self>,
778 ) -> Task<Result<Entity<Workspace>>> {
779 if let Some(workspace) = self
780 .workspaces
781 .iter()
782 .find(|ws| PathList::new(&ws.read(cx).root_paths(cx)) == path_list)
783 .cloned()
784 {
785 self.activate(workspace.clone(), window, cx);
786 return Task::ready(Ok(workspace));
787 }
788
789 if let Some(transient) = self.active_workspace.transient_workspace() {
790 if transient.read(cx).project_group_key(cx).path_list() == &path_list {
791 return Task::ready(Ok(transient.clone()));
792 }
793 }
794
795 let paths = path_list.paths().to_vec();
796 let app_state = self.workspace().read(cx).app_state().clone();
797 let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
798
799 cx.spawn(async move |_this, cx| {
800 let result = cx
801 .update(|cx| {
802 Workspace::new_local(
803 paths,
804 app_state,
805 requesting_window,
806 None,
807 None,
808 OpenMode::Activate,
809 cx,
810 )
811 })
812 .await?;
813 Ok(result.workspace)
814 })
815 }
816
817 pub fn workspace(&self) -> &Entity<Workspace> {
818 match &self.active_workspace {
819 ActiveWorkspace::Persistent(index) => &self.workspaces[*index],
820 ActiveWorkspace::Transient(workspace) => workspace,
821 }
822 }
823
824 pub fn workspaces(&self) -> impl Iterator<Item = &Entity<Workspace>> {
825 self.workspaces
826 .iter()
827 .chain(self.active_workspace.transient_workspace())
828 }
829
830 /// Adds a workspace to this window as persistent without changing which
831 /// workspace is active. Unlike `activate()`, this always inserts into the
832 /// persistent list regardless of sidebar state — it's used for system-
833 /// initiated additions like deserialization and worktree discovery.
834 pub fn add(&mut self, workspace: Entity<Workspace>, window: &Window, cx: &mut Context<Self>) {
835 self.insert_workspace(workspace, window, cx);
836 }
837
838 /// Ensures the workspace is in the multiworkspace and makes it the active one.
839 pub fn activate(
840 &mut self,
841 workspace: Entity<Workspace>,
842 window: &mut Window,
843 cx: &mut Context<Self>,
844 ) {
845 // Re-activating the current workspace is a no-op.
846 if self.workspace() == &workspace {
847 self.focus_active_workspace(window, cx);
848 return;
849 }
850
851 // Resolve where we're going.
852 let new_index = if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
853 Some(index)
854 } else if self.sidebar_open {
855 Some(self.insert_workspace(workspace.clone(), &*window, cx))
856 } else {
857 None
858 };
859
860 // Transition the active workspace.
861 if let Some(index) = new_index {
862 if let Some(old) = self.active_workspace.set_persistent(index) {
863 if self.sidebar_open {
864 self.promote_transient(old, cx);
865 } else {
866 self.detach_workspace(&old, cx);
867 cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old.entity_id()));
868 }
869 }
870 } else {
871 Self::subscribe_to_workspace(&workspace, window, cx);
872 let weak_self = cx.weak_entity();
873 workspace.update(cx, |workspace, cx| {
874 workspace.set_multi_workspace(weak_self, cx);
875 });
876 if let Some(old) = self.active_workspace.set_transient(workspace) {
877 self.detach_workspace(&old, cx);
878 cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old.entity_id()));
879 }
880 }
881
882 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
883 self.serialize(cx);
884 self.focus_active_workspace(window, cx);
885 cx.notify();
886 }
887
888 /// Promotes the currently active workspace to persistent if it is
889 /// transient, so it is retained across workspace switches even when
890 /// the sidebar is closed. No-op if the workspace is already persistent.
891 pub fn retain_active_workspace(&mut self, cx: &mut Context<Self>) {
892 if let ActiveWorkspace::Transient(workspace) = &self.active_workspace {
893 let workspace = workspace.clone();
894 let index = self.promote_transient(workspace, cx);
895 self.active_workspace = ActiveWorkspace::Persistent(index);
896 self.serialize(cx);
897 cx.notify();
898 }
899 }
900
901 /// Promotes a former transient workspace into the persistent list.
902 /// Returns the index of the newly inserted workspace.
903 fn promote_transient(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
904 let project_group_key = workspace.read(cx).project().read(cx).project_group_key(cx);
905 self.add_project_group_key(project_group_key);
906 self.workspaces.push(workspace.clone());
907 cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
908 self.workspaces.len() - 1
909 }
910
911 /// Collapses to a single transient workspace, discarding all persistent
912 /// workspaces. Used when multi-workspace is disabled (e.g. disable_ai).
913 fn collapse_to_single_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
914 if self.sidebar_open {
915 self.close_sidebar(window, cx);
916 }
917 let active = self.workspace().clone();
918 for workspace in std::mem::take(&mut self.workspaces) {
919 if workspace != active {
920 self.detach_workspace(&workspace, cx);
921 cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
922 }
923 }
924 self.project_group_keys.clear();
925 self.active_workspace = ActiveWorkspace::Transient(active);
926 cx.notify();
927 }
928
929 /// Inserts a workspace into the list if not already present. Returns the
930 /// index of the workspace (existing or newly inserted). Does not change
931 /// the active workspace index.
932 fn insert_workspace(
933 &mut self,
934 workspace: Entity<Workspace>,
935 window: &Window,
936 cx: &mut Context<Self>,
937 ) -> usize {
938 if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
939 index
940 } else {
941 let project_group_key = workspace.read(cx).project().read(cx).project_group_key(cx);
942
943 Self::subscribe_to_workspace(&workspace, window, cx);
944 self.sync_sidebar_to_workspace(&workspace, cx);
945 let weak_self = cx.weak_entity();
946 workspace.update(cx, |workspace, cx| {
947 workspace.set_multi_workspace(weak_self, cx);
948 });
949
950 self.add_project_group_key(project_group_key);
951 self.workspaces.push(workspace.clone());
952 cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
953 cx.notify();
954 self.workspaces.len() - 1
955 }
956 }
957
958 /// Clears session state and DB binding for a workspace that is being
959 /// removed or replaced. The DB row is preserved so the workspace still
960 /// appears in the recent-projects list.
961 fn detach_workspace(&mut self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
962 workspace.update(cx, |workspace, _cx| {
963 workspace.session_id.take();
964 workspace._schedule_serialize_workspace.take();
965 workspace._serialize_workspace_task.take();
966 });
967
968 if let Some(workspace_id) = workspace.read(cx).database_id() {
969 let db = crate::persistence::WorkspaceDb::global(cx);
970 self.pending_removal_tasks.retain(|task| !task.is_ready());
971 self.pending_removal_tasks
972 .push(cx.background_spawn(async move {
973 db.set_session_binding(workspace_id, None, None)
974 .await
975 .log_err();
976 }));
977 }
978 }
979
980 fn sync_sidebar_to_workspace(&self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
981 if self.sidebar_open() {
982 let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
983 workspace.update(cx, |workspace, _| {
984 workspace.set_sidebar_focus_handle(sidebar_focus_handle);
985 });
986 }
987 }
988
989 pub(crate) fn serialize(&mut self, cx: &mut Context<Self>) {
990 self._serialize_task = Some(cx.spawn(async move |this, cx| {
991 let Some((window_id, state)) = this
992 .read_with(cx, |this, cx| {
993 let state = MultiWorkspaceState {
994 active_workspace_id: this.workspace().read(cx).database_id(),
995 project_group_keys: this
996 .project_group_keys()
997 .cloned()
998 .map(Into::into)
999 .collect::<Vec<_>>(),
1000 sidebar_open: this.sidebar_open,
1001 sidebar_state: this.sidebar.as_ref().and_then(|s| s.serialized_state(cx)),
1002 };
1003 (this.window_id, state)
1004 })
1005 .ok()
1006 else {
1007 return;
1008 };
1009 let kvp = cx.update(|cx| db::kvp::KeyValueStore::global(cx));
1010 crate::persistence::write_multi_workspace_state(&kvp, window_id, state).await;
1011 }));
1012 }
1013
1014 /// Returns the in-flight serialization task (if any) so the caller can
1015 /// await it. Used by the quit handler to ensure pending DB writes
1016 /// complete before the process exits.
1017 pub fn flush_serialization(&mut self) -> Task<()> {
1018 self._serialize_task.take().unwrap_or(Task::ready(()))
1019 }
1020
1021 fn app_will_quit(&mut self, _cx: &mut Context<Self>) -> impl Future<Output = ()> + use<> {
1022 let mut tasks: Vec<Task<()>> = Vec::new();
1023 if let Some(task) = self._serialize_task.take() {
1024 tasks.push(task);
1025 }
1026 tasks.extend(std::mem::take(&mut self.pending_removal_tasks));
1027
1028 async move {
1029 futures::future::join_all(tasks).await;
1030 }
1031 }
1032
1033 pub fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
1034 // If a dock panel is zoomed, focus it instead of the center pane.
1035 // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
1036 // which closes the zoomed dock.
1037 let focus_handle = {
1038 let workspace = self.workspace().read(cx);
1039 let mut target = None;
1040 for dock in workspace.all_docks() {
1041 let dock = dock.read(cx);
1042 if dock.is_open() {
1043 if let Some(panel) = dock.active_panel() {
1044 if panel.is_zoomed(window, cx) {
1045 target = Some(panel.panel_focus_handle(cx));
1046 break;
1047 }
1048 }
1049 }
1050 }
1051 target.unwrap_or_else(|| {
1052 let pane = workspace.active_pane().clone();
1053 pane.read(cx).focus_handle(cx)
1054 })
1055 };
1056 window.focus(&focus_handle, cx);
1057 }
1058
1059 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
1060 self.workspace().read(cx).panel::<T>(cx)
1061 }
1062
1063 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
1064 self.workspace().read(cx).active_modal::<V>(cx)
1065 }
1066
1067 pub fn add_panel<T: Panel>(
1068 &mut self,
1069 panel: Entity<T>,
1070 window: &mut Window,
1071 cx: &mut Context<Self>,
1072 ) {
1073 self.workspace().update(cx, |workspace, cx| {
1074 workspace.add_panel(panel, window, cx);
1075 });
1076 }
1077
1078 pub fn focus_panel<T: Panel>(
1079 &mut self,
1080 window: &mut Window,
1081 cx: &mut Context<Self>,
1082 ) -> Option<Entity<T>> {
1083 self.workspace()
1084 .update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
1085 }
1086
1087 // used in a test
1088 pub fn toggle_modal<V: ModalView, B>(
1089 &mut self,
1090 window: &mut Window,
1091 cx: &mut Context<Self>,
1092 build: B,
1093 ) where
1094 B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
1095 {
1096 self.workspace().update(cx, |workspace, cx| {
1097 workspace.toggle_modal(window, cx, build);
1098 });
1099 }
1100
1101 pub fn toggle_dock(
1102 &mut self,
1103 dock_side: DockPosition,
1104 window: &mut Window,
1105 cx: &mut Context<Self>,
1106 ) {
1107 self.workspace().update(cx, |workspace, cx| {
1108 workspace.toggle_dock(dock_side, window, cx);
1109 });
1110 }
1111
1112 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
1113 self.workspace().read(cx).active_item_as::<I>(cx)
1114 }
1115
1116 pub fn items_of_type<'a, T: Item>(
1117 &'a self,
1118 cx: &'a App,
1119 ) -> impl 'a + Iterator<Item = Entity<T>> {
1120 self.workspace().read(cx).items_of_type::<T>(cx)
1121 }
1122
1123 pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
1124 self.workspace().read(cx).database_id()
1125 }
1126
1127 pub fn take_pending_removal_tasks(&mut self) -> Vec<Task<()>> {
1128 let tasks: Vec<Task<()>> = std::mem::take(&mut self.pending_removal_tasks)
1129 .into_iter()
1130 .filter(|task| !task.is_ready())
1131 .collect();
1132 tasks
1133 }
1134
1135 #[cfg(any(test, feature = "test-support"))]
1136 pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
1137 self.workspace().update(cx, |workspace, _cx| {
1138 workspace.set_random_database_id();
1139 });
1140 }
1141
1142 #[cfg(any(test, feature = "test-support"))]
1143 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
1144 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
1145 Self::new(workspace, window, cx)
1146 }
1147
1148 #[cfg(any(test, feature = "test-support"))]
1149 pub fn test_add_workspace(
1150 &mut self,
1151 project: Entity<Project>,
1152 window: &mut Window,
1153 cx: &mut Context<Self>,
1154 ) -> Entity<Workspace> {
1155 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
1156 self.activate(workspace.clone(), window, cx);
1157 workspace
1158 }
1159
1160 #[cfg(any(test, feature = "test-support"))]
1161 pub fn create_test_workspace(
1162 &mut self,
1163 window: &mut Window,
1164 cx: &mut Context<Self>,
1165 ) -> Task<()> {
1166 let app_state = self.workspace().read(cx).app_state().clone();
1167 let project = Project::local(
1168 app_state.client.clone(),
1169 app_state.node_runtime.clone(),
1170 app_state.user_store.clone(),
1171 app_state.languages.clone(),
1172 app_state.fs.clone(),
1173 None,
1174 project::LocalProjectFlags::default(),
1175 cx,
1176 );
1177 let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
1178 self.activate(new_workspace.clone(), window, cx);
1179
1180 let weak_workspace = new_workspace.downgrade();
1181 let db = crate::persistence::WorkspaceDb::global(cx);
1182 cx.spawn_in(window, async move |this, cx| {
1183 let workspace_id = db.next_id().await.unwrap();
1184 let workspace = weak_workspace.upgrade().unwrap();
1185 let task: Task<()> = this
1186 .update_in(cx, |this, window, cx| {
1187 let session_id = workspace.read(cx).session_id();
1188 let window_id = window.window_handle().window_id().as_u64();
1189 workspace.update(cx, |workspace, _cx| {
1190 workspace.set_database_id(workspace_id);
1191 });
1192 this.serialize(cx);
1193 let db = db.clone();
1194 cx.background_spawn(async move {
1195 db.set_session_binding(workspace_id, session_id, Some(window_id))
1196 .await
1197 .log_err();
1198 })
1199 })
1200 .unwrap();
1201 task.await
1202 })
1203 }
1204
1205 pub fn remove(
1206 &mut self,
1207 workspace: &Entity<Workspace>,
1208 window: &mut Window,
1209 cx: &mut Context<Self>,
1210 ) -> bool {
1211 let Some(index) = self.workspaces.iter().position(|w| w == workspace) else {
1212 return false;
1213 };
1214
1215 let old_key = workspace.read(cx).project_group_key(cx);
1216
1217 if self.workspaces.len() <= 1 {
1218 let has_worktrees = workspace.read(cx).visible_worktrees(cx).next().is_some();
1219
1220 if !has_worktrees {
1221 return false;
1222 }
1223
1224 let old_workspace = workspace.clone();
1225 let old_entity_id = old_workspace.entity_id();
1226
1227 let app_state = old_workspace.read(cx).app_state().clone();
1228
1229 let project = Project::local(
1230 app_state.client.clone(),
1231 app_state.node_runtime.clone(),
1232 app_state.user_store.clone(),
1233 app_state.languages.clone(),
1234 app_state.fs.clone(),
1235 None,
1236 project::LocalProjectFlags::default(),
1237 cx,
1238 );
1239
1240 let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
1241
1242 self.workspaces[0] = new_workspace.clone();
1243 self.active_workspace = ActiveWorkspace::Persistent(0);
1244
1245 Self::subscribe_to_workspace(&new_workspace, window, cx);
1246
1247 self.sync_sidebar_to_workspace(&new_workspace, cx);
1248
1249 let weak_self = cx.weak_entity();
1250
1251 new_workspace.update(cx, |workspace, cx| {
1252 workspace.set_multi_workspace(weak_self, cx);
1253 });
1254
1255 self.detach_workspace(&old_workspace, cx);
1256
1257 cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(old_entity_id));
1258 cx.emit(MultiWorkspaceEvent::WorkspaceAdded(new_workspace));
1259 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
1260 } else {
1261 let removed_workspace = self.workspaces.remove(index);
1262
1263 if let Some(active_index) = self.active_workspace.persistent_index() {
1264 if active_index >= self.workspaces.len() {
1265 self.active_workspace = ActiveWorkspace::Persistent(self.workspaces.len() - 1);
1266 } else if active_index > index {
1267 self.active_workspace = ActiveWorkspace::Persistent(active_index - 1);
1268 }
1269 }
1270
1271 self.detach_workspace(&removed_workspace, cx);
1272
1273 cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(
1274 removed_workspace.entity_id(),
1275 ));
1276 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
1277 }
1278
1279 let key_still_in_use = self
1280 .workspaces
1281 .iter()
1282 .any(|ws| ws.read(cx).project_group_key(cx) == old_key);
1283
1284 if !key_still_in_use {
1285 self.project_group_keys.retain(|k| k != &old_key);
1286 }
1287
1288 self.serialize(cx);
1289 self.focus_active_workspace(window, cx);
1290 cx.notify();
1291
1292 true
1293 }
1294
1295 pub fn move_workspace_to_new_window(
1296 &mut self,
1297 workspace: &Entity<Workspace>,
1298 window: &mut Window,
1299 cx: &mut Context<Self>,
1300 ) {
1301 let workspace = workspace.clone();
1302 if !self.remove(&workspace, window, cx) {
1303 return;
1304 }
1305
1306 let app_state: Arc<AppState> = workspace.read(cx).app_state().clone();
1307
1308 cx.defer(move |cx| {
1309 let options = (app_state.build_window_options)(None, cx);
1310
1311 let Ok(window) = cx.open_window(options, |window, cx| {
1312 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
1313 }) else {
1314 return;
1315 };
1316
1317 let _ = window.update(cx, |_, window, _| {
1318 window.activate_window();
1319 });
1320 });
1321 }
1322
1323 pub fn move_project_group_to_new_window(
1324 &mut self,
1325 key: &ProjectGroupKey,
1326 window: &mut Window,
1327 cx: &mut Context<Self>,
1328 ) {
1329 let workspaces: Vec<_> = self
1330 .workspaces_for_project_group(key, cx)
1331 .cloned()
1332 .collect();
1333 if workspaces.is_empty() {
1334 return;
1335 }
1336
1337 self.project_group_keys.retain(|k| k != key);
1338
1339 let mut removed = Vec::new();
1340 for workspace in &workspaces {
1341 if self.remove(workspace, window, cx) {
1342 removed.push(workspace.clone());
1343 }
1344 }
1345
1346 if removed.is_empty() {
1347 return;
1348 }
1349
1350 let app_state = removed[0].read(cx).app_state().clone();
1351
1352 cx.defer(move |cx| {
1353 let options = (app_state.build_window_options)(None, cx);
1354
1355 let first = removed[0].clone();
1356 let rest = removed[1..].to_vec();
1357
1358 let Ok(new_window) = cx.open_window(options, |window, cx| {
1359 cx.new(|cx| MultiWorkspace::new(first, window, cx))
1360 }) else {
1361 return;
1362 };
1363
1364 new_window
1365 .update(cx, |mw, window, cx| {
1366 for workspace in rest {
1367 mw.activate(workspace, window, cx);
1368 }
1369 window.activate_window();
1370 })
1371 .log_err();
1372 });
1373 }
1374
1375 pub fn open_project(
1376 &mut self,
1377 paths: Vec<PathBuf>,
1378 open_mode: OpenMode,
1379 window: &mut Window,
1380 cx: &mut Context<Self>,
1381 ) -> Task<Result<Entity<Workspace>>> {
1382 if self.multi_workspace_enabled(cx) {
1383 self.find_or_create_local_workspace(PathList::new(&paths), window, cx)
1384 } else {
1385 let workspace = self.workspace().clone();
1386 cx.spawn_in(window, async move |_this, cx| {
1387 let should_continue = workspace
1388 .update_in(cx, |workspace, window, cx| {
1389 workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
1390 })?
1391 .await?;
1392 if should_continue {
1393 workspace
1394 .update_in(cx, |workspace, window, cx| {
1395 workspace.open_workspace_for_paths(open_mode, paths, window, cx)
1396 })?
1397 .await
1398 } else {
1399 Ok(workspace)
1400 }
1401 })
1402 }
1403 }
1404}
1405
1406impl Render for MultiWorkspace {
1407 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1408 let multi_workspace_enabled = self.multi_workspace_enabled(cx);
1409 let sidebar_side = self.sidebar_side(cx);
1410 let sidebar_on_right = sidebar_side == SidebarSide::Right;
1411
1412 let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open() {
1413 self.sidebar.as_ref().map(|sidebar_handle| {
1414 let weak = cx.weak_entity();
1415
1416 let sidebar_width = sidebar_handle.width(cx);
1417 let resize_handle = deferred(
1418 div()
1419 .id("sidebar-resize-handle")
1420 .absolute()
1421 .when(!sidebar_on_right, |el| {
1422 el.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
1423 })
1424 .when(sidebar_on_right, |el| {
1425 el.left(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
1426 })
1427 .top(px(0.))
1428 .h_full()
1429 .w(SIDEBAR_RESIZE_HANDLE_SIZE)
1430 .cursor_col_resize()
1431 .on_drag(DraggedSidebar, |dragged, _, _, cx| {
1432 cx.stop_propagation();
1433 cx.new(|_| dragged.clone())
1434 })
1435 .on_mouse_down(MouseButton::Left, |_, _, cx| {
1436 cx.stop_propagation();
1437 })
1438 .on_mouse_up(MouseButton::Left, move |event, _, cx| {
1439 if event.click_count == 2 {
1440 weak.update(cx, |this, cx| {
1441 if let Some(sidebar) = this.sidebar.as_mut() {
1442 sidebar.set_width(None, cx);
1443 }
1444 this.serialize(cx);
1445 })
1446 .ok();
1447 cx.stop_propagation();
1448 } else {
1449 weak.update(cx, |this, cx| {
1450 this.serialize(cx);
1451 })
1452 .ok();
1453 }
1454 })
1455 .occlude(),
1456 );
1457
1458 div()
1459 .id("sidebar-container")
1460 .relative()
1461 .h_full()
1462 .w(sidebar_width)
1463 .flex_shrink_0()
1464 .child(sidebar_handle.to_any())
1465 .child(resize_handle)
1466 .into_any_element()
1467 })
1468 } else {
1469 None
1470 };
1471
1472 let (left_sidebar, right_sidebar) = if sidebar_on_right {
1473 (None, sidebar)
1474 } else {
1475 (sidebar, None)
1476 };
1477
1478 let ui_font = theme_settings::setup_ui_font(window, cx);
1479 let text_color = cx.theme().colors().text;
1480
1481 let workspace = self.workspace().clone();
1482 let workspace_key_context = workspace.update(cx, |workspace, cx| workspace.key_context(cx));
1483 let root = workspace.update(cx, |workspace, cx| workspace.actions(h_flex(), window, cx));
1484
1485 client_side_decorations(
1486 root.key_context(workspace_key_context)
1487 .relative()
1488 .size_full()
1489 .font(ui_font)
1490 .text_color(text_color)
1491 .on_action(cx.listener(Self::close_window))
1492 .when(self.multi_workspace_enabled(cx), |this| {
1493 this.on_action(cx.listener(
1494 |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
1495 this.toggle_sidebar(window, cx);
1496 },
1497 ))
1498 .on_action(cx.listener(
1499 |this: &mut Self, _: &CloseWorkspaceSidebar, window, cx| {
1500 this.close_sidebar_action(window, cx);
1501 },
1502 ))
1503 .on_action(cx.listener(
1504 |this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| {
1505 this.focus_sidebar(window, cx);
1506 },
1507 ))
1508 .on_action(cx.listener(
1509 |this: &mut Self, action: &ToggleThreadSwitcher, window, cx| {
1510 if let Some(sidebar) = &this.sidebar {
1511 sidebar.toggle_thread_switcher(action.select_last, window, cx);
1512 }
1513 },
1514 ))
1515 .on_action(
1516 cx.listener(|this: &mut Self, _: &NextProjectGroup, window, cx| {
1517 if let Some(sidebar) = &this.sidebar {
1518 sidebar.cycle_project_group(true, window, cx);
1519 }
1520 }),
1521 )
1522 .on_action(cx.listener(
1523 |this: &mut Self, _: &PreviousProjectGroup, window, cx| {
1524 if let Some(sidebar) = &this.sidebar {
1525 sidebar.cycle_project_group(false, window, cx);
1526 }
1527 },
1528 ))
1529 .on_action(cx.listener(|this: &mut Self, _: &NextThread, window, cx| {
1530 if let Some(sidebar) = &this.sidebar {
1531 sidebar.cycle_thread(true, window, cx);
1532 }
1533 }))
1534 .on_action(
1535 cx.listener(|this: &mut Self, _: &PreviousThread, window, cx| {
1536 if let Some(sidebar) = &this.sidebar {
1537 sidebar.cycle_thread(false, window, cx);
1538 }
1539 }),
1540 )
1541 .on_action(cx.listener(
1542 |this: &mut Self, _: &MoveWorkspaceToNewWindow, window, cx| {
1543 if let Some(sidebar) = &this.sidebar {
1544 sidebar.move_workspace_to_new_window(window, cx);
1545 }
1546 },
1547 ))
1548 })
1549 .when(
1550 self.sidebar_open() && self.multi_workspace_enabled(cx),
1551 |this| {
1552 this.on_drag_move(cx.listener(
1553 move |this: &mut Self,
1554 e: &DragMoveEvent<DraggedSidebar>,
1555 window,
1556 cx| {
1557 if let Some(sidebar) = &this.sidebar {
1558 let new_width = if sidebar_on_right {
1559 window.bounds().size.width - e.event.position.x
1560 } else {
1561 e.event.position.x
1562 };
1563 sidebar.set_width(Some(new_width), cx);
1564 }
1565 },
1566 ))
1567 },
1568 )
1569 .children(left_sidebar)
1570 .child(
1571 div()
1572 .flex()
1573 .flex_1()
1574 .size_full()
1575 .overflow_hidden()
1576 .child(self.workspace().clone()),
1577 )
1578 .children(right_sidebar)
1579 .child(self.workspace().read(cx).modal_layer.clone())
1580 .children(self.sidebar_overlay.as_ref().map(|view| {
1581 deferred(div().absolute().size_full().inset_0().occlude().child(
1582 v_flex().h(px(0.0)).top_20().items_center().child(
1583 h_flex().occlude().child(view.clone()).on_mouse_down(
1584 MouseButton::Left,
1585 |_, _, cx| {
1586 cx.stop_propagation();
1587 },
1588 ),
1589 ),
1590 ))
1591 .with_priority(2)
1592 })),
1593 window,
1594 cx,
1595 Tiling {
1596 left: !sidebar_on_right && multi_workspace_enabled && self.sidebar_open(),
1597 right: sidebar_on_right && multi_workspace_enabled && self.sidebar_open(),
1598 ..Tiling::default()
1599 },
1600 )
1601 }
1602}