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