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